diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 9d14b91a9..c48db4466 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -415,6 +415,7 @@ 9AFEE46B03B7E995B3E1A53D /* WaitlistScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C4927D09099497233E9980 /* WaitlistScreen.swift */; }; 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */; }; 9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */; }; + 9BB91CABB10D8FE90C491BCD /* StaticLocationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; 9C5A07E7C33F3F40287D7861 /* SettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */; }; @@ -496,6 +497,7 @@ B45F20A1C3F1CE19D5B8BA74 /* InvitesScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */; }; B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; + B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; }; B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; }; B5BD05558DC2C3091905E14A /* FilePreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714977AF906461C8F6F16ABA /* FilePreviewScreenCoordinator.swift */; }; B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; }; @@ -580,7 +582,6 @@ CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; - D04B93C644A0BE4A4C5D0A1B /* LocationPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA10BE264552E23946FF0499 /* LocationPinView.swift */; }; D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; @@ -1270,6 +1271,7 @@ C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelTests.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; + C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelTests.swift; sourceTree = ""; }; C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; @@ -1313,7 +1315,6 @@ D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = ""; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = ""; }; - DA10BE264552E23946FF0499 /* LocationPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPinView.swift; sourceTree = ""; }; DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = ""; }; DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = ""; }; DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsKeys.swift; sourceTree = ""; }; @@ -1402,6 +1403,7 @@ FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsScreen.swift; sourceTree = ""; }; + FFECCE59967018204876D0A5 /* LocationMarkerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMarkerView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1795,7 +1797,7 @@ 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, C352359663A0E52BA20761EE /* LoadableImage.swift */, - DA10BE264552E23946FF0499 /* LocationPinView.swift */, + FFECCE59967018204876D0A5 /* LocationMarkerView.swift */, 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */, 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, @@ -2400,6 +2402,7 @@ 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */, 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */, + C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */, 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */, 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */, 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */, @@ -3944,6 +3947,7 @@ 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */, 6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */, + 9BB91CABB10D8FE90C491BCD /* StaticLocationScreenViewModelTests.swift in Sources */, 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */, E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */, 3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */, @@ -4151,7 +4155,7 @@ 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */, D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */, 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */, - D04B93C644A0BE4A4C5D0A1B /* LocationPinView.swift in Sources */, + B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */, D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */, 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */, D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */, diff --git a/ElementX/Resources/Assets.xcassets/Images/Location/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Location/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/Location/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/Contents.json new file mode 100644 index 000000000..7932c7978 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "location-marker.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "location-marker-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/location-pin.imageset/location-pin-dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/location-marker-dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/images/location-pin.imageset/location-pin-dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/location-marker-dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/images/location-pin.imageset/location-pin.pdf b/ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/location-marker.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/images/location-pin.imageset/location-pin.pdf rename to ElementX/Resources/Assets.xcassets/Images/Location/location-marker.imageset/location-marker.pdf diff --git a/ElementX/Resources/Assets.xcassets/images/location-pin.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/images/location-pin.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/location-pin-dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/location-pin-dark.pdf new file mode 100644 index 000000000..08fbe3006 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/location-pin-dark.pdf @@ -0,0 +1,78 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 -0.123291 cm +0.921569 0.933333 0.949020 scn +7.000000 20.123291 m +3.130000 20.123291 0.000000 16.908089 0.000000 12.932741 c +0.000000 8.649229 4.420000 2.742706 6.240000 0.493092 c +6.640000 0.000025 7.370000 0.000025 7.770000 0.493092 c +9.580000 2.742706 14.000000 8.649229 14.000000 12.932741 c +14.000000 16.908089 10.870000 20.123291 7.000000 20.123291 c +h +7.000000 10.364689 m +5.620000 10.364689 4.500000 11.515176 4.500000 12.932741 c +4.500000 14.350307 5.620000 15.500795 7.000000 15.500795 c +8.380000 15.500795 9.500000 14.350307 9.500000 12.932741 c +9.500000 11.515176 8.380000 10.364689 7.000000 10.364689 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 699 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 14.000000 20.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000789 00000 n +0000000811 00000 n +0000000984 00000 n +0000001058 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1117 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/location-pin.pdf b/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/location-pin.pdf new file mode 100644 index 000000000..146075ae1 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/Location/location-pin.imageset/location-pin.pdf @@ -0,0 +1,147 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 20.000000 20.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.000000 -0.123291 cm +0.105882 0.113725 0.133333 scn +7.000000 20.123291 m +3.130000 20.123291 0.000000 16.908089 0.000000 12.932741 c +0.000000 8.649229 4.420000 2.742706 6.240000 0.493092 c +6.640000 0.000025 7.370000 0.000025 7.770000 0.493092 c +9.580000 2.742706 14.000000 8.649229 14.000000 12.932741 c +14.000000 16.908089 10.870000 20.123291 7.000000 20.123291 c +h +7.000000 10.364689 m +5.620000 10.364689 4.500000 11.515176 4.500000 12.932741 c +4.500000 14.350307 5.620000 15.500795 7.000000 15.500795 c +8.380000 15.500795 9.500000 14.350307 9.500000 12.932741 c +9.500000 11.515176 8.380000 10.364689 7.000000 10.364689 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 699 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 20.000000 20.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 20.000000 m +20.000000 20.000000 l +20.000000 0.000000 l +0.000000 0.000000 l +0.000000 20.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 232 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 20.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000957 00000 n +0000000979 00000 n +0000001459 00000 n +0000001481 00000 n +0000001779 00000 n +0000001881 00000 n +0000001902 00000 n +0000002075 00000 n +0000002149 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2209 +%%EOF \ No newline at end of file diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 46cf06930..f6896998d 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -30,6 +30,7 @@ final class AppSettings { case userSuggestionsEnabled case readReceiptsEnabled case locationEventsEnabled + case shareLocationEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -194,4 +195,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.locationEventsEnabled, defaultValue: false, storageType: .userDefaults(store)) var locationEventsEnabled + + @UserPreference(key: UserDefaultsKeys.shareLocationEnabled, defaultValue: false, storageType: .userDefaults(store)) + var shareLocationEnabled } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 430430230..4af6b2004 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -150,7 +150,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .messageForwarding(roomID: roomID, itemID: itemID) case (.dismissMessageForwarding, .messageForwarding(let roomID, _)): return .room(roomID: roomID) - + case (.presentLocationPicker, .room(let roomID)): + return .locationPicker(roomID: roomID) + case (.dismissLocationPicker, .locationPicker(let roomID)): + return .room(roomID: roomID) default: return nil } @@ -218,7 +221,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentMessageForwarding(for: eventID) case (.messageForwarding, .dismissMessageForwarding, .room): break - + case (.room, .presentLocationPicker, .locationPicker): + presentLocationPicker() + case (.locationPicker, .dismissLocationPicker, .room): + break default: fatalError("Unknown transition: \(context)") } @@ -312,6 +318,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url)) case .presentEmojiPicker(let itemID): stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID)) + case .presentLocationPicker: + stateMachine.tryEvent(.presentLocationPicker) case .presentRoomMemberDetails(member: let member): stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member))) case .presentMessageForwarding(let itemID): @@ -519,6 +527,33 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } + private func presentLocationPicker() { + let locationPickerNavigationStackCoordinator = NavigationStackCoordinator() + + let params = StaticLocationScreenCoordinatorParameters() + let coordinator = StaticLocationScreenCoordinator(parameters: params) + + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .selectedLocation(let geoURI): + Task { + _ = await self.roomProxy?.sendLocation(body: geoURI.bodyMessage, geoURI: geoURI) + self.navigationSplitCoordinator.setSheetCoordinator(nil) + } + case .close: + self.navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + locationPickerNavigationStackCoordinator.setRootCoordinator(coordinator) + + navigationStackCoordinator.setSheetCoordinator(locationPickerNavigationStackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissLocationPicker) + } + } + private func presentRoomMemberDetails(member: RoomMemberProxyProtocol) { let params = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: userSession.mediaProvider) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params) @@ -611,6 +646,7 @@ private extension RoomFlowCoordinator { case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource) case mediaUploadPreview(roomID: String, fileURL: URL) case emojiPicker(roomID: String, itemID: String) + case locationPicker(roomID: String) case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper) case messageForwarding(roomID: String, itemID: String) } @@ -642,6 +678,9 @@ private extension RoomFlowCoordinator { case presentEmojiPicker(itemID: String) case dismissEmojiPicker + case presentLocationPicker + case dismissLocationPicker + case presentRoomMemberDetails(member: HashableRoomMemberWrapper) case dismissRoomMemberDetails @@ -649,3 +688,9 @@ private extension RoomFlowCoordinator { case dismissMessageForwarding } } + +private extension GeoURI { + var bodyMessage: String { + String(format: "Location was shared at %@ as of %@", string, Date().description) + } +} diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index 8b530d591..3e28a24d3 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -29,6 +29,8 @@ internal enum Asset { internal static let backgroundColor = ColorAsset(name: "colors/background-color") } internal enum Images { + internal static let locationMarker = ImageAsset(name: "images/location-marker") + internal static let locationPin = ImageAsset(name: "images/location-pin") internal static let appLogo = ImageAsset(name: "images/app-logo") internal static let serverSelectionIcon = ImageAsset(name: "images/server-selection-icon") internal static let closeCircle = ImageAsset(name: "images/close-circle") @@ -37,7 +39,6 @@ internal enum Asset { internal static let encryptionWarning = ImageAsset(name: "images/encryption-warning") internal static let launchBackground = ImageAsset(name: "images/launch-background") internal static let launchLogo = ImageAsset(name: "images/launch-logo") - internal static let locationPin = ImageAsset(name: "images/location-pin") internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message") } } diff --git a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift index cc8b7b7f5..13e90747b 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift @@ -23,6 +23,7 @@ struct MapLibreMapView: UIViewRepresentable { private enum Constants { static let mapZoomLevel = 15.0 + static let mapZoomLevelWithoutPermission = 5.0 } // MARK: - Properties @@ -36,12 +37,22 @@ struct MapLibreMapView: UIViewRepresentable { /// Bind view errors if any let error: Binding + + /// Coordinate of the center of the map + @Binding var mapCenterCoordinate: CLLocationCoordinate2D? + /// Called when the user pan on the map + var userDidPan: (() -> Void)? + // MARK: - UIViewRepresentable func makeUIView(context: Context) -> MGLMapView { let mapView = makeMapView() mapView.delegate = context.coordinator + let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan)) + panGesture.delegate = context.coordinator + mapView.addGestureRecognizer(panGesture) + mapView.zoomLevel = Constants.mapZoomLevelWithoutPermission return mapView } @@ -65,12 +76,9 @@ struct MapLibreMapView: UIViewRepresentable { private func makeMapView() -> MGLMapView { let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light)) - - mapView.logoView.isHidden = true - mapView.attributionButton.isHidden = true - mapView.zoomLevel = Constants.mapZoomLevel showUserLocation(in: mapView) + mapView.attributionButton.isHidden = true return mapView } @@ -134,7 +142,9 @@ extension MapLibreMapView { } } - func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) { } + func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) { + mapLibreView.mapCenterCoordinate = mapView.centerCoordinate + } // MARK: Callout @@ -147,6 +157,11 @@ extension MapLibreMapView { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { gestureRecognizer is UIPanGestureRecognizer } + + @objc + func didPan() { + mapLibreView.userDidPan?() + } } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift b/ElementX/Sources/Other/SwiftUI/Views/LocationMarkerView.swift similarity index 82% rename from ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift rename to ElementX/Sources/Other/SwiftUI/Views/LocationMarkerView.swift index 8a62da02a..ac093e740 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LocationMarkerView.swift @@ -16,9 +16,9 @@ import SwiftUI -struct LocationPinView: View { +struct LocationMarkerView: View { var body: some View { - Image(Asset.Images.locationPin.name) + Image(Asset.Images.locationMarker.name) .alignmentGuide(VerticalAlignment.center) { dimensions in dimensions[.bottom] } @@ -26,12 +26,12 @@ struct LocationPinView: View { } } -struct LocationPinView_Previews: PreviewProvider { +struct LocationMarkerView_Previews: PreviewProvider { static var previews: some View { VStack(spacing: 30) { - LocationPinView() + LocationMarkerView() - LocationPinView() + LocationMarkerView() .colorScheme(.dark) } } diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift index c58b1dca6..654d2b23a 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import CoreLocation import Foundation enum LocationSharingViewError: Error, Hashable { @@ -21,13 +22,23 @@ enum LocationSharingViewError: Error, Hashable { case mapError(MapLibreError) } -enum StaticLocationScreenViewModelAction { } +enum StaticLocationScreenViewModelAction { + case close + case sendLocation(GeoURI) +} struct StaticLocationScreenViewState: BindableState { + /// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location + var isPinDropSharing = true + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide + var bindings = StaticLocationScreenBindings() } struct StaticLocationScreenBindings { + var mapCenterLocation: CLLocationCoordinate2D? + /// Information describing the currently displayed alert. var mapError: MapLibreError? { get { @@ -45,4 +56,8 @@ struct StaticLocationScreenBindings { var alertInfo: AlertInfo? } -enum StaticLocationScreenViewAction { } +enum StaticLocationScreenViewAction { + case close + case selectLocation + case userDidPan +} diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift index ac7372498..d5a0cbc9a 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift @@ -19,12 +19,22 @@ import SwiftUI struct StaticLocationScreenCoordinatorParameters { } -enum StaticLocationScreenCoordinatorAction { } +enum StaticLocationScreenCoordinatorAction { + case close + case selectedLocation(GeoURI) +} final class StaticLocationScreenCoordinator: CoordinatorProtocol { let parameters: StaticLocationScreenCoordinatorParameters let viewModel: StaticLocationScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + init(parameters: StaticLocationScreenCoordinatorParameters) { self.parameters = parameters @@ -33,6 +43,19 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol { // MARK: - Public + func start() { + viewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .close: + actionsSubject.send(.close) + case .sendLocation(let geoURI): + actionsSubject.send(.selectedLocation(geoURI)) + } + } + .store(in: &cancellables) + } + func toPresentable() -> AnyView { AnyView(StaticLocationScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift index 5d4fbf786..14fcc5193 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift @@ -17,7 +17,7 @@ import Combine import Foundation -typealias StaticLocationScreenViewModelType = StateStoreViewModel +typealias StaticLocationScreenViewModelType = StateStoreViewModel class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLocationScreenViewModelProtocol { private let actionsSubject: PassthroughSubject = .init() @@ -29,4 +29,17 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo init() { super.init(initialViewState: StaticLocationScreenViewState()) } + + override func process(viewAction: StaticLocationScreenViewAction) { + switch viewAction { + case .close: + actionsSubject.send(.close) + case .selectLocation: + guard let coordinate = state.bindings.mapCenterLocation else { return } + actionsSubject.send(.sendLocation(.init(coordinate: coordinate))) + case .userDidPan: + state.showsUserLocationMode = .hide + state.isPinDropSharing = true + } + } } diff --git a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift index d13d74082..3b1a7609c 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift @@ -22,18 +22,62 @@ struct StaticLocationScreen: View { private let builder = MapTilerStyleBuilder(appSettings: ServiceLocator.shared.settings) var body: some View { - NavigationView { - mapView - .ignoresSafeArea(.all, edges: [.bottom]) - .navigationBarTitleDisplayMode(.inline) - .alert(item: $context.alertInfo) + mapView + .ignoresSafeArea(.all, edges: .horizontal) + .navigationTitle(L10n.screenShareLocationTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } + .alert(item: $context.alertInfo) + } + + private var mapView: some View { + ZStack(alignment: .center) { + MapLibreMapView(builder: builder, + showsUserLocationMode: .hide, + error: $context.mapError, + mapCenterCoordinate: $context.mapCenterLocation, + userDidPan: { + context.send(viewAction: .userDidPan) + }) + if context.viewState.isPinDropSharing { + LocationMarkerView() + } } } - var mapView: MapLibreMapView { - MapLibreMapView(builder: builder, - showsUserLocationMode: .follow, - error: $context.mapError) + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .navigationBarLeading) { + closeButton + } + + ToolbarItemGroup(placement: .bottomBar) { + shareLocationButton + Spacer() + } + } + + @ScaledMetric private var shareMarkerSize: CGFloat = 28 + private var shareLocationButton: some View { + Button { + context.send(viewAction: .selectLocation) + } label: { + HStack(spacing: 8) { + Image(asset: Asset.Images.locationMarker) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: shareMarkerSize, height: shareMarkerSize) + Text(context.viewState.isPinDropSharing ? L10n.screenShareThisLocationAction : L10n.screenShareMyLocationAction) + } + } + } + + private var closeButton: some View { + Button(L10n.actionCancel, action: close) + } + + private func close() { + context.send(viewAction: .close) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index e494e775d..82aade9a8 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -30,6 +30,7 @@ enum RoomScreenCoordinatorAction { case presentMediaUploadPicker(MediaPickerScreenSource) case presentMediaUploadPreviewScreen(URL) case presentRoomDetails + case presentLocationPicker case presentEmojiPicker(itemID: String) case presentRoomMemberDetails(member: RoomMemberProxyProtocol) case presentMessageForwarding(itemID: String) @@ -78,6 +79,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentMediaUploadPicker(.photoLibrary)) case .displayDocumentPicker: actionsSubject.send(.presentMediaUploadPicker(.documents)) + case .displayLocationPicker: + actionsSubject.send(.presentLocationPicker) case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.presentMediaUploadPreviewScreen(url)) case .displayRoomMemberDetails(let member): diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 9d5cc8f65..ea76ae893 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -26,6 +26,7 @@ enum RoomScreenViewModelAction { case displayCameraPicker case displayMediaPicker case displayDocumentPicker + case displayLocationPicker case displayMediaUploadPreviewScreen(url: URL) case displayRoomMemberDetails(member: RoomMemberProxyProtocol) case displayMessageForwarding(itemID: String) @@ -68,6 +69,7 @@ enum RoomScreenViewAction { case displayCameraPicker case displayMediaPicker case displayDocumentPicker + case displayLocationPicker case handlePasteOrDrop(provider: NSItemProvider) case tappedOnUser(userID: String) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index a67d6909e..5d4e09c02 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -109,6 +109,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol callback?(.displayMediaPicker) case .displayDocumentPicker: callback?(.displayDocumentPicker) + case .displayLocationPicker: + callback?(.displayLocationPicker) case .handlePasteOrDrop(let provider): handlePasteOrDrop(provider) case .tappedOnUser(userID: let userID): diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift index bdde38654..77483644b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift @@ -66,7 +66,7 @@ struct TimelineReplyView: View { ReplyView(sender: sender, plainBody: L10n.commonSharedLocation, formattedBody: nil, - icon: .init(kind: .icon(Asset.Images.locationPin.name), cornerRadii: iconCornerRadii)) + icon: .init(kind: .icon(Asset.Images.locationMarker.name), cornerRadii: iconCornerRadii)) } default: LoadingReplyView() diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift index 7a1982e58..4a16da378 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift @@ -37,21 +37,30 @@ struct RoomAttachmentPicker: View { context.showAttachmentPopover = false context.send(viewAction: .displayMediaPicker) } label: { - PickerLabel(title: L10n.screenRoomAttachmentSourceGallery, systemImageName: "photo.fill") + PickerLabel(title: L10n.screenRoomAttachmentSourceGallery, icon: Image(systemName: "photo.fill")) } Button { context.showAttachmentPopover = false context.send(viewAction: .displayDocumentPicker) } label: { - PickerLabel(title: L10n.screenRoomAttachmentSourceFiles, systemImageName: "paperclip") + PickerLabel(title: L10n.screenRoomAttachmentSourceFiles, icon: Image(systemName: "paperclip")) } Button { context.showAttachmentPopover = false context.send(viewAction: .displayCameraPicker) } label: { - PickerLabel(title: L10n.screenRoomAttachmentSourceCamera, systemImageName: "camera.fill") + PickerLabel(title: L10n.screenRoomAttachmentSourceCamera, icon: Image(systemName: "camera.fill")) + } + + if ServiceLocator.shared.settings.shareLocationEnabled { + Button { + context.showAttachmentPopover = false + context.send(viewAction: .displayLocationPicker) + } label: { + PickerLabel(title: L10n.screenRoomAttachmentSourceLocation, icon: Image(asset: Asset.Images.locationPin)) + } } } .padding(.top, isPresented ? 20 : 0) @@ -73,14 +82,23 @@ struct RoomAttachmentPicker: View { private struct PickerLabel: View { let title: String - let systemImageName: String + let icon: Image + + init(title: String, icon: Image) { + self.title = title + self.icon = icon + } var body: some View { - Label(title, systemImage: systemImageName) - .labelStyle(FixedIconSizeLabelStyle()) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(16) + Label { + Text(title) + } icon: { + icon + } + .labelStyle(FixedIconSizeLabelStyle()) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift index 6791dd31a..a2c653358 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift @@ -25,7 +25,7 @@ struct LocationRoomTimelineView: View { if let geoURI = timelineItem.content.geoURI { let mapSize: CGSize = .init(width: 292, height: 188) MapLibreStaticMapView(geoURI: geoURI, size: mapSize) { - LocationPinView() + LocationMarkerView() } .frame(width: mapSize.width, height: mapSize.height) } else { diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index bd23483a4..66927fa8b 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -30,6 +30,7 @@ struct DeveloperOptionsScreenViewStateBindings { var readReceiptsEnabled: Bool var isEncryptionSyncEnabled: Bool var locationEventsEnabled: Bool + var shareLocationEnabled: Bool } enum DeveloperOptionsScreenViewAction { @@ -38,5 +39,6 @@ enum DeveloperOptionsScreenViewAction { case changedReadReceiptsEnabled case changedIsEncryptionSyncEnabled case changedLocationEventsEnabled + case changedShareLocationEnabled case clearCache } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 8d0fda818..23fcf059d 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -30,7 +30,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve userSuggestionsEnabled: appSettings.userSuggestionsEnabled, readReceiptsEnabled: appSettings.readReceiptsEnabled, isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled, - locationEventsEnabled: appSettings.locationEventsEnabled) + locationEventsEnabled: appSettings.locationEventsEnabled, + shareLocationEnabled: appSettings.shareLocationEnabled) let state = DeveloperOptionsScreenViewState(bindings: bindings) super.init(initialViewState: state) @@ -52,6 +53,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings.isEncryptionSyncEnabled = state.bindings.isEncryptionSyncEnabled case .changedLocationEventsEnabled: appSettings.locationEventsEnabled = state.bindings.locationEventsEnabled + case .changedShareLocationEnabled: + appSettings.shareLocationEnabled = state.bindings.shareLocationEnabled case .clearCache: callback?(.clearCache) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index e88174342..b8e0935a4 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -59,6 +59,13 @@ struct DeveloperOptionsScreen: View { .onChange(of: context.locationEventsEnabled) { _ in context.send(viewAction: .changedLocationEventsEnabled) } + + Toggle(isOn: $context.shareLocationEnabled) { + Text("Show share location action") + } + .onChange(of: context.shareLocationEnabled) { _ in + context.send(viewAction: .changedShareLocationEnabled) + } } Section { diff --git a/ElementX/Sources/Services/Timeline/GeoURI.swift b/ElementX/Sources/Services/Timeline/GeoURI.swift index c5e02a805..aa2ed94b4 100644 --- a/ElementX/Sources/Services/Timeline/GeoURI.swift +++ b/ElementX/Sources/Services/Timeline/GeoURI.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import CoreLocation import Foundation /// A structure that parses a geo URI (i.e. geo:53.99803101552848,-8.25347900390625;u=10) and constructs their constituent parts. @@ -71,3 +72,9 @@ private typealias RegexGeoURI = Regex<(Substring, latitude: Substring, longitude private extension RegexGeoURI { static let standard: Self = /geo:(?-?\d+(?:\.\d+)?),(?-?\d+(?:\.\d+)?)(?:;u=(?\d+(?:\.\d+)?))?/ } + +extension GeoURI { + init(coordinate: CLLocationCoordinate2D) { + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) + } +} diff --git a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift new file mode 100644 index 000000000..ae70221a8 --- /dev/null +++ b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift @@ -0,0 +1,44 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import XCTest + +@testable import ElementX + +@MainActor +class StaticLocationScreenViewModelTests: XCTestCase { + var viewModel: StaticLocationScreenViewModelProtocol! + + private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) + private var cancellables: Set = [] + + var context: StaticLocationScreenViewModel.Context { + viewModel.context + } + + override func setUpWithError() throws { + let viewModel = StaticLocationScreenViewModel() + viewModel.state.isPinDropSharing = false + self.viewModel = viewModel + } + + func testUserDidPan() async throws { + XCTAssertFalse(context.viewState.isPinDropSharing) + context.send(viewAction: .userDidPan) + XCTAssertTrue(context.viewState.isPinDropSharing) + } +} diff --git a/changelog.d/1179.feature b/changelog.d/1179.feature new file mode 100644 index 000000000..bc84550e1 --- /dev/null +++ b/changelog.d/1179.feature @@ -0,0 +1 @@ +Send pin-drop location \ No newline at end of file