diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index f80df6864..dc0c7371d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -161,11 +161,13 @@ 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 18978C9438206828C1D5AF2A /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */; }; 18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; }; + 18FD4EA36456910FD9CB1B95 /* RoomLiveLocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118892B93A8CFDD691D9B5E3 /* RoomLiveLocationService.swift */; }; 18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */; }; 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; }; 194585F6CD77242B36D4ADF1 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADECBBB672497BCD4822468 /* Result.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; }; + 19946B8EEE158356EB5E1107 /* LocationShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9406BD5F99685C5ABDEDD93 /* LocationShareSheet.swift */; }; 19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; }; 19DF5600A7F547B22DD7872A /* CompletionSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; @@ -740,7 +742,6 @@ 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; 7C545FFEC9930F7247352593 /* SecurityAndPrivacyScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978092B01BEAB39F2C4389AE /* SecurityAndPrivacyScreenViewModel.swift */; }; 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; }; - 7C9A62022717060DFC1878D7 /* RoomLiveLocationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C9C6EFC19EBB79E35806ED /* RoomLiveLocationServiceProtocol.swift */; }; 7C9BDF1FC7BD46C4676536AB /* AuthenticationStartScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682BC7BAF0EFEF512A8C5140 /* AuthenticationStartScreenBackgroundImage.swift */; }; 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; }; 7D249465ED00988EEEC14E05 /* JoinedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */; }; @@ -765,6 +766,7 @@ 805D16A15BDF97B4EA8D3EC6 /* RoomThreadListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14FD296A75F5F5637EDC365 /* RoomThreadListScreenCoordinator.swift */; }; 80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */; }; 80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */; }; + 811D9AA152443037BEDA6F22 /* RoomLiveLocationServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCDB4B1DAB0FE01CC874E8DA /* RoomLiveLocationServiceProtocol.swift */; }; 81CFE6FE42DF26BBCEDC7FF2 /* JoinCallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */; }; 81D4E550668B230A63B26CFB /* SpacesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */; }; 82434593648CB74121F1A821 /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C513A6CD99E6C3C163DA1E /* RowDivider.swift */; }; @@ -774,6 +776,7 @@ 8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; }; 83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */; }; 83D519C509F0F76EDBB60455 /* KnockRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */; }; + 84196A8F1963D55726261879 /* RoomLiveLocationServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79C2C6E7F66646D1D254927 /* RoomLiveLocationServiceMock.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; @@ -1183,7 +1186,6 @@ C8E1E4E06B7C7A3A8246FC9B /* MediaEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */; }; C900127318820AD04D6C90B8 /* LabsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E43D8784B0054C048060FEB /* LabsScreenModels.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; - C9169AB88A0953C0B3D8601B /* RoomLiveLocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89EE3CE58040FD2DF63DC23 /* RoomLiveLocationService.swift */; }; C960BACE42A9D8C535E8CB34 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1912062B53CE95E6F700DA60 /* Pagination.swift */; }; C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; @@ -1478,7 +1480,6 @@ FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */; }; FA53FA227FFBE469AFF32F71 /* TimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C585CE1F721A2770C70D47 /* TimelineControllerProtocol.swift */; }; FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; - FA5FD4910EA871ACCED8D47B /* RoomLiveLocationServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4609B3576A5E612A95352EC1 /* RoomLiveLocationServiceMock.swift */; }; FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; @@ -1623,7 +1624,7 @@ 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; 045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryTests.swift; sourceTree = ""; }; - 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = ""; }; 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 04EB6035C1F33F25F1EBFB7D /* RoomThreadListServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListServiceProxyProtocol.swift; sourceTree = ""; }; @@ -1703,6 +1704,7 @@ 111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; 113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; 113873B97F27394ABE41BCFD /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; + 118892B93A8CFDD691D9B5E3 /* RoomLiveLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationService.swift; sourceTree = ""; }; 11FCAE847556719BBE7A0882 /* ReportRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenModels.swift; sourceTree = ""; }; 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; @@ -1712,7 +1714,7 @@ 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 12B09A94C519227264A41208 /* RoomMembershipDetailsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxy.swift; sourceTree = ""; }; 12FD5280AF55AB7F50F8E47D /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1733,7 +1735,7 @@ 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = ""; }; 16D353E10A64172D863769BF /* TombstonedAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TombstonedAvatarImage.swift; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; - 174E4AEF3DED300AA81046EC /* compound-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "compound-ios"; sourceTree = SOURCE_ROOT; }; + 174E4AEF3DED300AA81046EC /* compound-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "compound-ios"; path = "compound-ios"; sourceTree = SOURCE_ROOT; }; 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyMock.swift; sourceTree = ""; }; 17BAE25A0E9E9F2F1BBA8930 /* DeactivateAccountScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModel.swift; sourceTree = ""; }; 181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreferenceTests.swift; sourceTree = ""; }; @@ -1760,7 +1762,7 @@ 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenModels.swift; sourceTree = ""; }; 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = ""; }; 1BA8082E26C77A2C587B34B3 /* MockTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTimelineController.swift; sourceTree = ""; }; - 1BC752C2A4606C4C2D1ADB41 /* 94 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = 94; sourceTree = ""; }; + 1BC752C2A4606C4C2D1ADB41 /* 94 */ = {isa = PBXFileReference; path = 94; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; 1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProvider.swift; sourceTree = ""; }; @@ -1825,7 +1827,7 @@ 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModelTests.swift; sourceTree = ""; }; 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; - 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = ""; }; + 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = ""; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = ""; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; 2711E5996016ABD6EAAEB58A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; @@ -1850,7 +1852,7 @@ 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; - 2A7BE2B89310058659E6F459 /* accountsV2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = accountsV2; sourceTree = ""; }; + 2A7BE2B89310058659E6F459 /* accountsV2 */ = {isa = PBXFileReference; path = accountsV2; sourceTree = ""; }; 2A95C9B8299A36A6495DECA6 /* TracingHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingHook.swift; sourceTree = ""; }; 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = ""; }; 2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelTests.swift; sourceTree = ""; }; @@ -1910,7 +1912,7 @@ 358528B29FA72ACFD0D9644B /* SpacesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesScreenCoordinator.swift; sourceTree = ""; }; 35A057BA9BE0F079784CD061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; @@ -2002,7 +2004,6 @@ 45A4B934BA41D6C255900265 /* preview_video.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_video.jpg; sourceTree = ""; }; 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelProtocol.swift; sourceTree = ""; }; 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = ""; }; - 4609B3576A5E612A95352EC1 /* RoomLiveLocationServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationServiceMock.swift; sourceTree = ""; }; 4629710C0337ADD9C8909542 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/Localizable.strings; sourceTree = ""; }; 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenModels.swift; sourceTree = ""; }; 467498BEA681758BE2F80826 /* TimelineMediaPreviewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewDetailsView.swift; sourceTree = ""; }; @@ -2392,7 +2393,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; 8E97CF050B0168F3D605F0E9 /* InviteUsersConfirmationSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersConfirmationSheetView.swift; sourceTree = ""; }; 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = ""; }; @@ -2551,7 +2552,7 @@ AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationAnnotation.swift; sourceTree = ""; }; AB07F03461023BC39C730922 /* PhishingDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetector.swift; sourceTree = ""; }; AB26D5444A4A7E095222DE8B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = ""; }; - AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AccessibilityTests.xctestplan; sourceTree = ""; }; + AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; path = AccessibilityTests.xctestplan; sourceTree = ""; }; ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; ABF84AA68B2B7584D9275769 /* VoiceMessageTrashButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageTrashButton.swift; sourceTree = ""; }; AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = ""; }; @@ -2620,7 +2621,7 @@ B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5D829FD8958376614504B18 /* TargetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetConfiguration.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; B65DDCF8E41759890355ACBC /* AuthenticationStartScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenViewModelProtocol.swift; sourceTree = ""; }; B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextButton.swift; sourceTree = ""; }; @@ -2647,13 +2648,14 @@ B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredFulfillment.swift; sourceTree = ""; }; + B9406BD5F99685C5ABDEDD93 /* LocationShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationShareSheet.swift; sourceTree = ""; }; B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenModels.swift; sourceTree = ""; }; BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferenceTests.swift; sourceTree = ""; }; BA257D747DD7E6FFA5C2BE2D /* LinkNewDeviceServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceServiceMock.swift; sourceTree = ""; }; BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; - BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_apple_image.heic; sourceTree = ""; }; + BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = ""; }; BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; @@ -2722,8 +2724,8 @@ C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewRedactConfirmationView.swift; sourceTree = ""; }; C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = ""; }; + C79C2C6E7F66646D1D254927 /* RoomLiveLocationServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationServiceMock.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; - C89EE3CE58040FD2DF63DC23 /* RoomLiveLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationService.swift; sourceTree = ""; }; C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = ""; }; @@ -2760,7 +2762,7 @@ CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenViewModel.swift; sourceTree = ""; }; CF847A34FC4C8C937CD39E08 /* LabsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreenViewModelProtocol.swift; sourceTree = ""; }; CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxy.swift; sourceTree = ""; }; @@ -2834,7 +2836,7 @@ DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; - DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; + DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = ""; }; DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenTests.swift; sourceTree = ""; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; @@ -2880,7 +2882,7 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; - E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; + E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModel.swift; sourceTree = ""; }; @@ -2933,7 +2935,7 @@ ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED25719E19B205B668FDACFF /* UserToInvite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserToInvite.swift; sourceTree = ""; }; ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = ""; }; @@ -2963,7 +2965,6 @@ F229480685F30BCB96C439EC /* AdvancedSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreen.swift; sourceTree = ""; }; F276F31C1AEC19E52B951B62 /* SendInviteConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendInviteConfirmationView.swift; sourceTree = ""; }; F2B94F1B0B5D9D42B15AA6E8 /* ChatsTabFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsTabFlowCoordinatorStateMachine.swift; sourceTree = ""; }; - F2C9C6EFC19EBB79E35806ED /* RoomLiveLocationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationServiceProtocol.swift; sourceTree = ""; }; F2DC502B1A566E99969D34DD /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreen.swift; sourceTree = ""; }; F3082001D373607455CB08A1 /* QRCodeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeErrorView.swift; sourceTree = ""; }; @@ -3012,6 +3013,7 @@ FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceRoomDetailsCell.swift; sourceTree = ""; }; FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = ""; }; FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = ""; }; + FCDB4B1DAB0FE01CC874E8DA /* RoomLiveLocationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationServiceProtocol.swift; sourceTree = ""; }; FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = ""; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; @@ -3756,7 +3758,6 @@ 7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */, BA257D747DD7E6FFA5C2BE2D /* LinkNewDeviceServiceMock.swift */, 0B5DF0E888F66652F8C4CEC5 /* LiveLocationManagerMock.swift */, - 4609B3576A5E612A95352EC1 /* RoomLiveLocationServiceMock.swift */, 6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */, 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */, 840182D7A61402D5947DE094 /* NotificationItemProxyMock.swift */, @@ -3766,6 +3767,7 @@ DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */, D38391154120264910D19528 /* PollMock.swift */, 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */, + C79C2C6E7F66646D1D254927 /* RoomLiveLocationServiceMock.swift */, 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */, F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */, E944F717FC10A428D027074D /* RoomPowerLevelsProxyMock.swift */, @@ -5478,8 +5480,8 @@ D17F49E39CC38DAB7B305701 /* LiveLocationManager.swift */, 33752AE856E93CE62412B7A1 /* LiveLocationManagerProtocol.swift */, AA12A7F5EF5C6D0B992869ED /* LiveLocationShareProxy.swift */, - C89EE3CE58040FD2DF63DC23 /* RoomLiveLocationService.swift */, - F2C9C6EFC19EBB79E35806ED /* RoomLiveLocationServiceProtocol.swift */, + 118892B93A8CFDD691D9B5E3 /* RoomLiveLocationService.swift */, + FCDB4B1DAB0FE01CC874E8DA /* RoomLiveLocationServiceProtocol.swift */, ); path = Location; sourceTree = ""; @@ -5703,6 +5705,7 @@ isa = PBXGroup; children = ( 408ACC0D28656F82A5EB6A7E /* LocationPickerSheet.swift */, + B9406BD5F99685C5ABDEDD93 /* LocationShareSheet.swift */, 6F56E6E41C6DFE8054787D57 /* LocationSharingScreen.swift */, ECE03E834CC8C2721899E6AC /* StaticLocationSheet.swift */, ); @@ -7343,7 +7346,6 @@ }; }; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; - compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -7421,6 +7423,7 @@ C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */, ); preferredProjectObjectVersion = 77; + productRefGroup = 681566846AF307E9BA4C72C6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( @@ -8420,9 +8423,6 @@ C8D0AC22E03F652118A2BB73 /* LiveLocationRoomTimelineItem.swift in Sources */, F7977C53B2B1D73030C69761 /* LiveLocationRoomTimelineView.swift in Sources */, 633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */, - FA5FD4910EA871ACCED8D47B /* RoomLiveLocationServiceMock.swift in Sources */, - C9169AB88A0953C0B3D8601B /* RoomLiveLocationService.swift in Sources */, - 7C9A62022717060DFC1878D7 /* RoomLiveLocationServiceProtocol.swift in Sources */, 9223E5F2A2CE0AFFDFF0AFFB /* LiveLocationSharingBannerView.swift in Sources */, 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */, D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */, @@ -8432,6 +8432,7 @@ D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */, 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */, 973C48F9E4EFB808F61BE401 /* LocationRoomTimelineView.swift in Sources */, + 19946B8EEE158356EB5E1107 /* LocationShareSheet.swift in Sources */, C67156445600FAE6430DE41E /* LocationSharingScreen.swift in Sources */, 7A37EC9D7164319587539E1D /* LocationSharingScreenCoordinator.swift in Sources */, 29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */, @@ -8690,6 +8691,9 @@ 4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */, BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */, 33F1FB19F222BA9930AB1A00 /* RoomListFiltersView.swift in Sources */, + 18FD4EA36456910FD9CB1B95 /* RoomLiveLocationService.swift in Sources */, + 84196A8F1963D55726261879 /* RoomLiveLocationServiceMock.swift in Sources */, + 811D9AA152443037BEDA6F22 /* RoomLiveLocationServiceProtocol.swift in Sources */, 8DC176CC5ABA24138EB443DD /* RoomMemberDetails.swift in Sources */, 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */, 899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */, @@ -9383,7 +9387,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = "-DRELEASE"; + OTHER_SWIFT_FLAGS = ( + "-DRELEASE", + ); PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.accessibility.tests"; PRODUCT_NAME = AccessibilityTests; SDKROOT = iphoneos; @@ -9402,7 +9408,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = "-DDEBUG"; + OTHER_SWIFT_FLAGS = ( + "-DDEBUG", + ); PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.accessibility.tests"; PRODUCT_NAME = AccessibilityTests; SDKROOT = iphoneos; @@ -9424,7 +9432,9 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = "-DIS_NSE"; + OTHER_SWIFT_FLAGS = ( + "-DIS_NSE", + ); PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -9491,7 +9501,9 @@ "$(inherited)", "-ObjC", ); - OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; + OTHER_SWIFT_FLAGS = ( + "-DIS_MAIN_APP", + ); PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -9521,7 +9533,9 @@ "$(inherited)", "-ObjC", ); - OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; + OTHER_SWIFT_FLAGS = ( + "-DIS_MAIN_APP", + ); PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -9754,7 +9768,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = "-DDEBUG"; + OTHER_SWIFT_FLAGS = ( + "-DDEBUG", + ); PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -9773,7 +9789,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = "-DRELEASE"; + OTHER_SWIFT_FLAGS = ( + "-DRELEASE", + ); PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -9795,7 +9813,9 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = "-DIS_NSE"; + OTHER_SWIFT_FLAGS = ( + "-DIS_NSE", + ); PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index ca797364a..40b1ac6fd 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -86,7 +86,10 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { case .displayUser(let userID): actionsSubject.send(.displayUser(userID: userID)) case .presentLocationViewer(let location): - presentMapNavigator(location: location, + presentMapNavigator(interactionMode: .viewStatic(location), + timelineController: timelineController) + case .presentLiveLocationViewer(let sender, let initialLiveLocationShare): + presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare), timelineController: timelineController) case .displayMessageForwarding(let forwardingItem): presentMessageForwarding(with: forwardingItem) @@ -99,11 +102,11 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.setRootCoordinator(coordinator) } - private func presentMapNavigator(location: StaticLocationData, + private func presentMapNavigator(interactionMode: LocationSharingInteractionMode, timelineController: TimelineControllerProtocol) { let stackCoordinator = NavigationStackCoordinator() - let params = LocationSharingScreenCoordinatorParameters(interactionMode: .viewStatic(location), + let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode, mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration, liveLocationSharingEnabled: flowParameters.appSettings.liveLocationSharingEnabled, roomProxy: roomProxy, diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 7cd6ebeeb..8596f6677 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -712,6 +712,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewStatic(location)), userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) + case .presentLiveLocationViewer(let sender, let initialLiveLocationShare): + stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)), + userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) case .presentRoomMemberDetails(userID: let userID): stateMachine.tryEvent(.startMembersFlow(entryPoint: .roomMember(userID: userID))) case .presentMessageForwarding(let forwardingItem): @@ -811,6 +814,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .presentLocationPicker: stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker), userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) + case .presentLiveLocationViewer(let sender, let initialLiveLocationShare): + stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)), + userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) case .presentPollForm(let mode): stateMachine.tryEvent(.presentPollForm(mode: mode), userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) diff --git a/ElementX/Sources/Other/MapLibre/LocationAnnotation.swift b/ElementX/Sources/Other/MapLibre/LocationAnnotation.swift index 30b573aa9..c66e62d82 100644 --- a/ElementX/Sources/Other/MapLibre/LocationAnnotation.swift +++ b/ElementX/Sources/Other/MapLibre/LocationAnnotation.swift @@ -6,32 +6,24 @@ // Please see LICENSE files in the repository root for full details. // +import CoreLocation import Foundation import MapLibre import SwiftUI -final class LocationAnnotation: NSObject, MLNAnnotation { +final class LocationAnnotation: NSObject, MLNAnnotation, Identifiable { let id: String var coordinate: CLLocationCoordinate2D - let anchorPoint: CGPoint - var view: AnyView + var kind: LocationMarkerKind // MARK: - Setup - init(id: String = UUID().uuidString, - coordinate: CLLocationCoordinate2D, - anchorPoint: CGPoint = .init(x: 0.5, y: 0.5), - @ViewBuilder label: () -> some View) { + init(id: String, coordinate: CLLocationCoordinate2D, kind: LocationMarkerKind) { self.id = id self.coordinate = coordinate - self.anchorPoint = anchorPoint - view = AnyView(label()) + self.kind = kind super.init() } - - func updateView(@ViewBuilder label: () -> some View) { - view = AnyView(label()) - } } final class LocationAnnotationView: MLNUserLocationAnnotationView { @@ -44,19 +36,21 @@ final class LocationAnnotationView: MLNUserLocationAnnotationView { reuseIdentifier) } - convenience init(annotation: LocationAnnotation) { + convenience init(annotation: LocationAnnotation, mediaProvider: MediaProviderProtocol?) { self.init(annotation: annotation, reuseIdentifier: "\(Self.self)") - let hostingController = UIHostingController(rootView: annotation.view) + let markerView = LocationMarkerView(kind: annotation.kind, mediaProvider: mediaProvider) + let hostingController = UIHostingController(rootView: AnyView(markerView)) self.hostingController = hostingController let view: UIView = hostingController.view view.backgroundColor = .clear - view.anchorPoint = annotation.anchorPoint + view.anchorPoint = .init(x: 0.5, y: 1.0) addSubview(view) view.bounds.size = view.intrinsicContentSize } - - func updateContent(with view: AnyView) { - hostingController?.rootView = view + + func updateContent(with kind: LocationMarkerKind, mediaProvider: MediaProviderProtocol?) { + let markerView = LocationMarkerView(kind: kind, mediaProvider: mediaProvider) + hostingController?.rootView = AnyView(markerView) if let hostedView = hostingController?.view { hostedView.bounds.size = hostedView.intrinsicContentSize } diff --git a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift index f0f46801d..92a8b6f8b 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift @@ -20,10 +20,10 @@ struct MapLibreMapView: UIViewRepresentable { /// The initial map center let mapCenter: CLLocationCoordinate2D - /// Map annotations keyed by a stable identifier (e.g. sender userID for user pins, UUID string for generic pins) - let annotations: [String: LocationAnnotation] + /// Map annotations + let annotations: [LocationAnnotation] - init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [String: LocationAnnotation] = [:]) { + init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) { self.zoomLevel = zoomLevel self.initialZoomLevel = initialZoomLevel self.mapCenter = mapCenter @@ -39,6 +39,8 @@ struct MapLibreMapView: UIViewRepresentable { let options: Options + let mediaProvider: MediaProviderProtocol? + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user @Binding var showsUserLocationMode: ShowUserLocationMode /// Bind view errors if any @@ -86,7 +88,7 @@ struct MapLibreMapView: UIViewRepresentable { // MARK: - Private private func setupMap(mapView: MLNMapView, with options: Options) { - mapView.addAnnotations(Array(options.annotations.values)) + mapView.addAnnotations(options.annotations) mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel mapView.centerCoordinate = options.mapCenter } @@ -94,9 +96,10 @@ struct MapLibreMapView: UIViewRepresentable { private func updateAnnotations(in mapView: MLNMapView) { let existingByID = Dictionary(uniqueKeysWithValues: (mapView.annotations ?? []).compactMap { $0 as? LocationAnnotation }.map { ($0.id, $0) }) + let updatedByID = Dictionary(uniqueKeysWithValues: options.annotations.map { ($0.id, $0) }) let existingIDs = Set(existingByID.keys) - let updatedIDs = Set(options.annotations.keys) + let updatedIDs = Set(updatedByID.keys) // Remove annotations that are no longer present let removedIDs = existingIDs.subtracting(updatedIDs) @@ -108,7 +111,7 @@ struct MapLibreMapView: UIViewRepresentable { // Add new annotations let addedIDs = updatedIDs.subtracting(existingIDs) if !addedIDs.isEmpty { - let toAdd = addedIDs.compactMap { options.annotations[$0] } + let toAdd = addedIDs.compactMap { updatedByID[$0] } mapView.addAnnotations(toAdd) } @@ -116,12 +119,12 @@ struct MapLibreMapView: UIViewRepresentable { let keptIDs = existingIDs.intersection(updatedIDs) for id in keptIDs { guard let existingAnnotation = existingByID[id], - let updatedAnnotation = options.annotations[id] else { + let updatedAnnotation = updatedByID[id] else { continue } existingAnnotation.coordinate = updatedAnnotation.coordinate if let annotationView = mapView.view(for: existingAnnotation) as? LocationAnnotationView { - annotationView.updateContent(with: updatedAnnotation.view) + annotationView.updateContent(with: updatedAnnotation.kind, mediaProvider: mediaProvider) } } } @@ -179,7 +182,7 @@ extension MapLibreMapView { guard let annotation = annotation as? LocationAnnotation else { return nil } - return LocationAnnotationView(annotation: annotation) + return LocationAnnotationView(annotation: annotation, mediaProvider: mapLibreView.mediaProvider) } func mapViewDidFailLoadingMap(_ mapView: MLNMapView, withError error: Error) { diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift index 2dbd8706f..581bd0e2a 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift @@ -26,6 +26,7 @@ enum LocationSharingScreenViewModelAction { enum LocationSharingInteractionMode: Hashable { case picker case viewStatic(StaticLocationData) + case viewLive(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare) } struct LocationSharingScreenViewState: BindableState { @@ -38,16 +39,23 @@ struct LocationSharingScreenViewState: BindableState { self.showLiveLocationSharingButton = showLiveLocationSharingButton self.ownUserID = ownUserID - userProfile = switch interactionMode { + let initialProfile: UserProfileProxy = switch interactionMode { case .viewStatic(let locationData): .init(sender: locationData.sender) + case .viewLive(let sender, _): + .init(sender: sender) case .picker: .init(userID: ownUserID) } + userProfiles = [initialProfile.userID: initialProfile] + + if case .viewLive(_, let initialLiveLocationShare) = interactionMode { + liveLocationShares = [initialLiveLocationShare] + } bindings.showsUserLocationMode = switch interactionMode { case .picker: .showAndFollow - case .viewStatic: .show + case .viewStatic, .viewLive: .show } } @@ -55,10 +63,35 @@ struct LocationSharingScreenViewState: BindableState { let mapURLBuilder: MapTilerURLBuilderProtocol let showLiveLocationSharingButton: Bool let ownUserID: String - var userProfile: UserProfileProxy + var userProfiles: [String: UserProfileProxy] + var liveLocationShares: [LiveLocationShare] = [] - var isOwnUser: Bool { - userProfile.userID == ownUserID + var annotations: [LocationAnnotation] { + switch interactionMode { + case .viewStatic(let location): + let profile = userProfiles.values.first + let kind: LocationMarkerKind = if location.kind == .sender, let profile { + .staticUser(profile) + } else { + .pin + } + let coordinate = CLLocationCoordinate2D(latitude: location.geoURI.latitude, longitude: location.geoURI.longitude) + return [LocationAnnotation(id: kind.id, coordinate: coordinate, kind: kind)] + case .viewLive: + return liveLocationShares.compactMap { share in + guard let geoURI = share.geoURI else { return nil } + let profile = userProfiles[share.userID] ?? UserProfileProxy(userID: share.userID) + let kind = LocationMarkerKind.liveUser(profile) + let coordinate = CLLocationCoordinate2D(latitude: geoURI.latitude, longitude: geoURI.longitude) + return LocationAnnotation(id: profile.userID, coordinate: coordinate, kind: kind) + } + case .picker: + return [] + } + } + + func isOwnUser(_ userID: String) -> Bool { + userID == ownUserID } var bindings = LocationSharingScreenBindings(showsUserLocationMode: .hide) @@ -75,15 +108,9 @@ struct LocationSharingScreenViewState: BindableState { .init(latitude: 49.843, longitude: 9.902056) case .viewStatic(let location): .init(latitude: location.geoURI.latitude, longitude: location.geoURI.longitude) - } - } - - var isLocationPickerMode: Bool { - switch interactionMode { - case .picker: - true - default: - false + case .viewLive(_, let initialLiveLocationShare): + .init(latitude: initialLiveLocationShare.geoURI?.latitude ?? 0, + longitude: initialLiveLocationShare.geoURI?.longitude ?? 0) } } @@ -101,18 +128,18 @@ struct LocationSharingScreenViewState: BindableState { switch interactionMode { case .picker: return 2.7 - case .viewStatic: + case .viewStatic, .viewLive: return 15.0 } } - var locationMarkerKind: LocationMarkerKind { - switch interactionMode { - case .picker: - isSharingUserLocation ? .staticUser(userProfile) : .pin - case .viewStatic(let location): - location.kind == .sender ? .staticUser(userProfile) : .pin + /// The marker kind used for the picker overlay (not a map annotation). + var pickerMarkerKind: LocationMarkerKind? { + guard case .picker = interactionMode else { return nil } + if let profile = userProfiles.values.first { + return isSharingUserLocation ? .staticUser(profile) : .pin } + return .pin } } @@ -139,7 +166,7 @@ struct LocationSharingScreenBindings { /// Information describing the currently displayed alert. var alertInfo: AlertInfo? - var showShareSheet = false + var sharedAnnotation: LocationAnnotation? } enum LocationSharingScreenViewAction { @@ -193,7 +220,7 @@ extension AlertInfo where T == LocationSharingViewAlert { } } -enum LocationMarkerKind { +enum LocationMarkerKind: Equatable { case pin case staticUser(UserProfileProxy) case liveUser(UserProfileProxy) diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift index 848a36d24..028929bb0 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift @@ -7,6 +7,7 @@ // import Combine +import CoreLocation import Foundation import SwiftUI @@ -26,6 +27,8 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati } private var authorizationStatusSubscription: AnyCancellable? + // periphery:ignore - keep alive to keep receiving updates. + private var liveLocationService: RoomLiveLocationServiceProtocol? init(interactionMode: LocationSharingInteractionMode, mapURLBuilder: MapTilerURLBuilderProtocol, @@ -50,8 +53,12 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati ownUserID: roomProxy.ownUserID), mediaProvider: mediaProvider) - updateShownUserProfile(members: roomProxy.membersPublisher.value) + updateUserProfiles(members: roomProxy.membersPublisher.value) setupSubscriptions() + + if case .viewLive = interactionMode { + Task { await setupLiveLocationSubscription() } + } } override func process(viewAction: LocationSharingScreenViewAction) { @@ -81,9 +88,23 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati // MARK: - Private + private func setupLiveLocationSubscription() async { + let liveLocationService = await roomProxy.makeLiveLocationService() + self.liveLocationService = liveLocationService + + liveLocationService.liveLocationsPublisher + .sink { [weak self] liveLocationsShares in + guard let self else { return } + MXLog.info("Received live location shares update: \(liveLocationsShares.count) share(s)") + state.liveLocationShares = liveLocationsShares + updateUserProfiles(members: roomProxy.membersPublisher.value) + } + .store(in: &cancellables) + } + private func setupSubscriptions() { roomProxy.membersPublisher.sink { [weak self] members in - self?.updateShownUserProfile(members: members) + self?.updateUserProfiles(members: members) } .store(in: &cancellables) @@ -95,19 +116,23 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati .store(in: &cancellables) } - private func updateShownUserProfile(members: [RoomMemberProxyProtocol]) { + private func updateUserProfiles(members: [RoomMemberProxyProtocol]) { switch state.interactionMode { case .picker: - if let ownUser = members.first(where: { $0.userID == roomProxy.ownUserID }).map(UserProfileProxy.init) { - state.userProfile = ownUser - } else { - state.userProfile = .init(userID: roomProxy.ownUserID) - } + let ownUser = members.first(where: { $0.userID == roomProxy.ownUserID }).map(UserProfileProxy.init) ?? .init(userID: roomProxy.ownUserID) + state.userProfiles = [ownUser.userID: ownUser] case .viewStatic(let location): - if let sender = members.first(where: { $0.userID == location.sender.id }).map(UserProfileProxy.init) { - state.userProfile = sender - } else { - state.userProfile = .init(sender: location.sender) + let sender = members.first(where: { $0.userID == location.sender.id }).map(UserProfileProxy.init) ?? .init(sender: location.sender) + state.userProfiles = [sender.userID: sender] + case .viewLive(let sender, _): + var userIDs = Set(state.liveLocationShares.map(\.userID)) + userIDs.insert(sender.id) + state.userProfiles = userIDs.reduce(into: [:]) { dict, userID in + if let member = members.first(where: { $0.userID == userID }) { + dict[userID] = UserProfileProxy(member: member) + } else { + dict[userID] = UserProfileProxy(userID: userID) + } } } } diff --git a/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift b/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift new file mode 100644 index 000000000..3fe57d53f --- /dev/null +++ b/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift @@ -0,0 +1,52 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import CoreLocation +import SwiftUI + +struct LocationShareSheet: View { + let annotation: LocationAnnotation + + private var location: CLLocationCoordinate2D { + annotation.coordinate + } + + private var senderName: String? { + annotation.kind.displayName ?? annotation.kind.userProfile?.userID + } + + var body: some View { + AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, senderName: senderName)], + applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, senderName: senderName) }) + .ignoresSafeArea(edges: .bottom) + .presentationDetents([.medium, .large]) + .presentationCompactAdaptation(compactPresentation) + .presentationDragIndicator(.hidden) + } + + private var compactPresentation: PresentationAdaptation { + if #available(iOS 26.0, *) { + .none // ShareLinks use a popover presentation on iOS 26, let it match that. + } else { + .sheet + } + } +} + +// MARK: - Previews + +struct LocationShareSheet_Previews: PreviewProvider { + static let profile = UserProfileProxy(userID: "@alice:example.com", displayName: "Alice") + static let annotation = LocationAnnotation(id: profile.userID, + coordinate: .init(latitude: 51.509865, longitude: -0.118092), + kind: .liveUser(profile)) + + static var previews: some View { + LocationShareSheet(annotation: annotation) + .previewDisplayName("Live location share sheet") + } +} diff --git a/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift b/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift index 90dc21bb3..a900949e1 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift @@ -25,8 +25,12 @@ struct LocationSharingScreen: View { .sheet(isPresented: .constant(true)) { StaticLocationSheet(context: context) .alert(item: $context.alertInfo) - .popover(isPresented: $context.showShareSheet) { shareSheet } + .popover(item: $context.sharedAnnotation) { annotation in + LocationShareSheet(annotation: annotation) + } } + case .viewLive: + mainContent } } @@ -35,7 +39,7 @@ struct LocationSharingScreen: View { private var mainContent: some View { mapView .ignoresSafeArea(edges: .bottom) - .track(screen: context.viewState.isLocationPickerMode ? .LocationSend : .LocationView) + .track(screen: context.viewState.interactionMode == .picker ? .LocationSend : .LocationView) .navigationTitle(L10n.screenViewLocationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } @@ -45,6 +49,7 @@ struct LocationSharingScreen: View { ZStack(alignment: .center) { MapLibreMapView(mapURLBuilder: context.viewState.mapURLBuilder, options: mapOptions, + mediaProvider: context.mediaProvider, showsUserLocationMode: $context.showsUserLocationMode, error: $context.mapError, mapCenterCoordinate: $context.mapCenterLocation, @@ -53,10 +58,10 @@ struct LocationSharingScreen: View { geolocationUncertainty: $context.geolocationUncertainty) { context.send(viewAction: .userDidPan) } - .ignoresSafeArea(.all, edges: mapSafeAreaEdges) + .ignoresSafeArea(edges: mapSafeAreaEdges) - if context.viewState.isLocationPickerMode { - LocationMarkerView(kind: context.viewState.locationMarkerKind, mediaProvider: context.mediaProvider) + if let pickerMarkerKind = context.viewState.pickerMarkerKind { + LocationMarkerView(kind: pickerMarkerKind, mediaProvider: context.mediaProvider) } } .overlay(alignment: .topTrailing) { @@ -64,6 +69,10 @@ struct LocationSharingScreen: View { } } + private var mapSafeAreaEdges: Edge.Set { + context.viewState.interactionMode == .picker ? .horizontal : [.horizontal, .bottom] + } + @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .primaryAction) { @@ -74,25 +83,10 @@ struct LocationSharingScreen: View { } private var mapOptions: MapLibreMapView.Options { - var annotations: [String: LocationAnnotation] = [:] - if !context.viewState.isLocationPickerMode { - let kind = context.viewState.locationMarkerKind - let annotation = LocationAnnotation(id: kind.id, - coordinate: context.viewState.initialMapCenter, - anchorPoint: .bottomCenter) { - LocationMarkerView(kind: kind, mediaProvider: context.mediaProvider) - } - annotations[kind.id] = annotation - } - - return .init(zoomLevel: context.viewState.zoomLevel, - initialZoomLevel: context.viewState.initialZoomLevel, - mapCenter: context.viewState.initialMapCenter, - annotations: annotations) - } - - private var mapSafeAreaEdges: Edge.Set { - context.viewState.isLocationPickerMode ? .horizontal : [.horizontal, .bottom] + .init(zoomLevel: context.viewState.zoomLevel, + initialZoomLevel: context.viewState.initialZoomLevel, + mapCenter: context.viewState.initialMapCenter, + annotations: context.viewState.annotations) } @ViewBuilder @@ -125,26 +119,6 @@ struct LocationSharingScreen: View { .dynamicTypeSize(.large) .padding(13) } - - @ViewBuilder - private var shareSheet: some View { - let location = context.viewState.initialMapCenter - let senderName = context.viewState.locationMarkerKind.displayName ?? context.viewState.locationMarkerKind.userProfile?.userID - AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, senderName: senderName)], - applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, senderName: senderName) }) - .ignoresSafeArea(edges: .bottom) - .presentationDetents([.medium, .large]) - .presentationCompactAdaptation(shareSheetCompactPresentation) - .presentationDragIndicator(.hidden) - } - - private var shareSheetCompactPresentation: PresentationAdaptation { - if #available(iOS 26.0, *) { - .none // ShareLinks use a popover presentation on iOS 26, let it match that. - } else { - .sheet - } - } } // MARK: - Previews @@ -180,7 +154,3 @@ struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview { .previewDisplayName("Pin Static Location") } } - -private extension CGPoint { - static let bottomCenter: Self = .init(x: 0.5, y: 1) -} diff --git a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationSheet.swift b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationSheet.swift index f67ddf9b2..a43952c46 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationSheet.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationSheet.swift @@ -34,12 +34,13 @@ struct StaticLocationSheet: View { .font(.compound.bodyLGSemibold) .padding(.bottom, 25) .padding(.top, 29) - if case let .viewStatic(location) = context.viewState.interactionMode { + if case let .viewStatic(location) = context.viewState.interactionMode, + let profile = context.viewState.userProfiles.values.first { Button { - context.showShareSheet = true + context.sharedAnnotation = context.viewState.annotations.first } label: { - UserLocationCell(profile: context.viewState.userProfile, - isOwnUser: context.viewState.isOwnUser, + UserLocationCell(profile: profile, + isOwnUser: context.viewState.isOwnUser(profile.userID), isUserLocation: location.kind == .sender, timestamp: location.timestamp, mediaProvider: context.mediaProvider) diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift index 33a91676d..45fdb2b04 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift @@ -72,7 +72,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType case .displayMediaDetails(item: let item): displayMediaPreviewSheet(for: item) case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, - .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, + .displayDocumentPicker, .displayLocationPicker, .displayLiveLocation, .displayPollForm, .displayMediaUploadPreviewScreen, .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .displayThread, .composer, .hasScrolled, .viewInRoomTimeline, .displayRoom: break @@ -97,7 +97,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType case .displayMediaDetails(item: let item): displayMediaPreviewSheet(for: item) case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, - .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, + .displayDocumentPicker, .displayLocationPicker, .displayLiveLocation, .displayPollForm, .displayMediaUploadPreviewScreen, .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .displayThread, .composer, .hasScrolled, .viewInRoomTimeline, .displayRoom: break diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 392222d8e..e34e872b3 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -27,6 +27,7 @@ enum PinnedEventsTimelineScreenCoordinatorAction { case dismiss case displayUser(userID: String) case presentLocationViewer(StaticLocationData) + case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare) case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?) } @@ -89,6 +90,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { viewModel.displayMediaPreview(mediaPreviewViewModel) case .displayLocation(let location): actionsSubject.send(.presentLocationViewer(location)) + case .displayLiveLocation(let sender, let initialLiveLocationShare): + actionsSubject.send(.presentLiveLocationViewer(sender: sender, initialLiveLocationShare: initialLiveLocationShare)) case .viewInRoomTimeline(let eventID, let threadRootEventID): actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID)) // These other actions will not be handled in this view diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index dceae5f49..d7af45926 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -40,6 +40,7 @@ enum RoomScreenCoordinatorAction { case presentLocationPicker case presentPollForm(mode: PollFormMode) case presentLocationViewer(StaticLocationData) + case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare) case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) case presentRoomMemberDetails(userID: String) case presentMessageForwarding(forwardingItem: MessageForwardingItem) @@ -140,6 +141,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem)) case .displayLocation(let location): actionsSubject.send(.presentLocationViewer(location)) + case .displayLiveLocation(let sender, let initialLiveLocationShare): + actionsSubject.send(.presentLiveLocationViewer(sender: sender, initialLiveLocationShare: initialLiveLocationShare)) case .displayResolveSendFailure(let failure, let sendHandle): actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle)) case .displayThread(let itemID): diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift index d803cb764..fd8413303 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift @@ -34,6 +34,7 @@ enum ThreadTimelineScreenCoordinatorAction { case presentMediaUploadPreviewScreen(mediaURLs: [URL]) case presentLocationPicker case presentLocationViewer(StaticLocationData) + case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare) case presentPollForm(mode: PollFormMode) case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) case presentRoomMemberDetails(userID: String) @@ -120,6 +121,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentLocationPicker) case .displayLocation(let location): actionsSubject.send(.presentLocationViewer(location)) + case .displayLiveLocation(let sender, let initialLiveLocationShare): + actionsSubject.send(.presentLiveLocationViewer(sender: sender, initialLiveLocationShare: initialLiveLocationShare)) case .displayPollForm(let mode): actionsSubject.send(.presentPollForm(mode: mode)) case .displayMediaUploadPreviewScreen(let mediaURLs): diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 07410e3b4..9c55fdd9c 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -529,7 +529,7 @@ class TimelineInteractionHandler { } func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction { - guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) as? EventBasedMessageTimelineItemProtocol else { + guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) as? EventBasedTimelineItemProtocol else { return .none } @@ -540,12 +540,21 @@ class TimelineInteractionHandler { geoURI: geoURI, kind: item.content.kind, timestamp: item.timestamp)) - case is ImageRoomTimelineItem, - is VideoRoomTimelineItem: - return await mediaPreviewAction(for: timelineItem, messageTypes: [.image, .video]) - case is AudioRoomTimelineItem, - is FileRoomTimelineItem: - return await mediaPreviewAction(for: timelineItem, messageTypes: [.audio, .file]) + case let item as LiveLocationRoomTimelineItem: + guard let geoURI = item.content.lastGeoURI else { return .none } + let initialLiveLocationShare = LiveLocationShare(userID: item.sender.id, + geoURI: geoURI, + timestamp: item.timestamp, + timeoutDate: item.content.timeoutDate) + return .displayLiveLocation(sender: item.sender, initialLiveLocationShare: initialLiveLocationShare) + case let item as ImageRoomTimelineItem: + return await mediaPreviewAction(for: item, messageTypes: [.image, .video]) + case let item as VideoRoomTimelineItem: + return await mediaPreviewAction(for: item, messageTypes: [.image, .video]) + case let item as AudioRoomTimelineItem: + return await mediaPreviewAction(for: item, messageTypes: [.audio, .file]) + case let item as FileRoomTimelineItem: + return await mediaPreviewAction(for: item, messageTypes: [.audio, .file]) default: return .none } diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 930e240eb..bbf9c4261 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -24,6 +24,7 @@ enum TimelineViewModelAction { case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayMediaPreview(TimelineMediaPreviewViewModel) case displayLocation(StaticLocationData) + case displayLiveLocation(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare) case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case displayThread(itemID: TimelineItemIdentifier) case composer(action: TimelineComposerAction) diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 6413c4377..d08aca665 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -666,6 +666,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel)) case .displayLocation(let location): actionsSubject.send(.displayLocation(location)) + case .displayLiveLocation(let sender, let initialLiveLocationShare): + actionsSubject.send(.displayLiveLocation(sender: sender, initialLiveLocationShare: initialLiveLocationShare)) case .none: break } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index 3907ea5c4..22ef7c532 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -26,6 +26,7 @@ enum TimelineControllerAction { case displayMediaPreview(item: EventBasedMessageTimelineItemProtocol, timelineViewModel: TimelineViewModelKind) case displayLocation(StaticLocationData) + case displayLiveLocation(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare) case none } diff --git a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift index b1dc97acb..c54b36fee 100644 --- a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift +++ b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift @@ -257,6 +257,72 @@ final class LocationSharingScreenViewModelTests { try await deferredFailure.fulfill() } + // MARK: - Live Location Share Update Tests + + @Test + func viewLiveInitialSenderShownCorrectly() { + let aliceShare = makeLiveLocationShare(userID: "@alice:matrix.org", latitude: 51.5, longitude: -0.1) + let sender = TimelineItemSender(id: "@alice:matrix.org", displayName: "Alice") + let liveLocationsSubject = CurrentValueSubject<[LiveLocationShare], Never>([aliceShare]) + + setupViewModelForViewLive(sender: sender, initialShare: aliceShare, liveLocationsSubject: liveLocationsSubject) + + // Initial state is synchronously set from the interaction mode before the async subscription runs. + let annotations = context.viewState.annotations + #expect(annotations.count == 1) + let annotation = annotations.first + #expect(annotation?.id == "@alice:matrix.org") + #expect(annotation?.coordinate.latitude == 51.5) + #expect(annotation?.coordinate.longitude == -0.1) + #expect(annotation?.kind == .liveUser(.init(userID: "@alice:matrix.org", displayName: "Alice"))) + } + + @Test + func viewLiveReceivesAdditionalLocationUpdates() async throws { + let aliceShare = makeLiveLocationShare(userID: "@alice:matrix.org", latitude: 51.5, longitude: -0.1) + let sender = TimelineItemSender(id: "@alice:matrix.org", displayName: "Alice") + let liveLocationsSubject = CurrentValueSubject<[LiveLocationShare], Never>([aliceShare]) + + setupViewModelForViewLive(sender: sender, initialShare: aliceShare, liveLocationsSubject: liveLocationsSubject) + + let bobShare = makeLiveLocationShare(userID: "@bob:matrix.org", latitude: 48.8, longitude: 2.3) + let charlieShare = makeLiveLocationShare(userID: "@charlie:matrix.org", latitude: 40.7, longitude: -74.0) + + let deferred = deferFulfillment(context.observe(\.viewState.annotations)) { $0.count == 3 } + liveLocationsSubject.send([aliceShare, bobShare, charlieShare]) + try await deferred.fulfill() + + let annotations = context.viewState.annotations + #expect(annotations.count == 3) + let annotationIDs = Set(annotations.map(\.id)) + #expect(annotationIDs == ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"]) + #expect(annotations.first { $0.id == "@alice:matrix.org" }?.coordinate.latitude == 51.5) + #expect(annotations.first { $0.id == "@bob:matrix.org" }?.coordinate.latitude == 48.8) + #expect(annotations.first { $0.id == "@charlie:matrix.org" }?.coordinate.latitude == 40.7) + } + + @Test + func viewLiveProfilesResolvedFromRoomMembers() async throws { + let aliceShare = makeLiveLocationShare(userID: "@alice:matrix.org", latitude: 51.5, longitude: -0.1) + let sender = TimelineItemSender(id: "@alice:matrix.org", displayName: "Alice") + let liveLocationsSubject = CurrentValueSubject<[LiveLocationShare], Never>([aliceShare]) + + setupViewModelForViewLive(sender: sender, initialShare: aliceShare, liveLocationsSubject: liveLocationsSubject) + + let bobShare = makeLiveLocationShare(userID: "@bob:matrix.org", latitude: 48.8, longitude: 2.3) + let charlieShare = makeLiveLocationShare(userID: "@charlie:matrix.org", latitude: 40.7, longitude: -74.0) + + let deferred = deferFulfillment(context.observe(\.viewState.annotations)) { $0.count == 3 } + liveLocationsSubject.send([aliceShare, bobShare, charlieShare]) + try await deferred.fulfill() + + // Annotation marker kinds should carry profiles resolved from room members. + let annotations = context.viewState.annotations + #expect(annotations.first { $0.id == "@alice:matrix.org" }?.kind == .liveUser(.init(userID: "@alice:matrix.org", displayName: "Alice"))) + #expect(annotations.first { $0.id == "@bob:matrix.org" }?.kind == .liveUser(.init(userID: "@bob:matrix.org", displayName: "Bob"))) + #expect(annotations.first { $0.id == "@charlie:matrix.org" }?.kind == .liveUser(.init(userID: "@charlie:matrix.org", displayName: "Charlie"))) + } + // MARK: - Private private func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init()) { @@ -286,4 +352,32 @@ final class LocationSharingScreenViewModelTests { mediaProvider: MediaProviderMock(configuration: .init())) viewModel.state.bindings.isLocationAuthorized = true } + + private func setupViewModelForViewLive(sender: TimelineItemSender, + initialShare: LiveLocationShare, + liveLocationsSubject: CurrentValueSubject<[LiveLocationShare], Never>, + members: [RoomMemberProxyMock] = .allMembers) { + let liveLocationServiceMock = RoomLiveLocationServiceMock() + liveLocationServiceMock.liveLocationsPublisher = liveLocationsSubject.eraseToAnyPublisher() + + let roomProxyMock = JoinedRoomProxyMock(.init(members: members)) + roomProxyMock.makeLiveLocationServiceReturnValue = liveLocationServiceMock + + viewModel = LocationSharingScreenViewModel(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialShare), + mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration, + liveLocationSharingEnabled: true, + roomProxy: roomProxyMock, + timelineController: MockTimelineController(timelineProxy: TimelineProxyMock(.init())), + liveLocationManager: LiveLocationManagerMock(.init()), + analytics: ServiceLocator.shared.analytics, + userIndicatorController: UserIndicatorControllerMock(), + mediaProvider: MediaProviderMock(configuration: .init())) + } + + private func makeLiveLocationShare(userID: String, latitude: Double = 0.0, longitude: Double = 0.0) -> LiveLocationShare { + LiveLocationShare(userID: userID, + geoURI: .init(latitude: latitude, longitude: longitude), + timestamp: .distantPast, + timeoutDate: .distantFuture) + } }