Refactor and redesign Location Sharing Screen (#5155)

* refactored the static location screen to the location sharing screen

# Conflicts:
#	UnitTests/Sources/LocationSharingScreenViewModelTests.swift

# Conflicts:
#	ElementX.xcodeproj/project.pbxproj

* implemented a custom pin with an overlayable view

* implemented the render of the user when the location is sender instead of the pin type

* removed description and body they are not used at all.

* reimplemented single button for sharing this or user location + implemented an experimental way to update annotations

* removed unnecessary @Suite description

* implemented a way to display the alert on top of the sheet and added a spinner to the center user location button

* fixed alerts strings

* fixed a failing test

* improved preview tests
This commit is contained in:
Mauro
2026-03-06 12:51:16 +01:00
committed by GitHub
parent 0c79b5404d
commit bbbb2e9dee
85 changed files with 917 additions and 626 deletions
@@ -303,6 +303,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "LocationRoomTimelineView_Previews")
}
func testLocationSharingScreen() async throws {
try await performAccessibilityAudit(named: "LocationSharingScreen_Previews")
}
func testLoginScreen() async throws {
try await performAccessibilityAudit(named: "LoginScreen_Previews")
}
@@ -695,10 +699,6 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "StateRoomTimelineView_Previews")
}
func testStaticLocationScreenViewer() async throws {
try await performAccessibilityAudit(named: "StaticLocationScreenViewer_Previews")
}
func testStickerRoomTimelineView() async throws {
try await performAccessibilityAudit(named: "StickerRoomTimelineView_Previews")
}
+26 -22
View File
@@ -46,6 +46,7 @@
053B8BD2496207838878C6C9 /* PinnedItemsBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C9BAE9F9436B14E4E22E8F /* PinnedItemsBannerView.swift */; };
059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; };
059A6BEDE9BADF3C81AC4146 /* SpaceAddRoomsScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB44FFCF7DA5300D943169 /* SpaceAddRoomsScreenSelectedItem.swift */; };
05B02B9508E922677F0E9451 /* LocationSharingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052A8C0B00FE8AD7C972F2A4 /* LocationSharingScreenViewModel.swift */; };
05BAB510CBC2ED35C154ADD0 /* AnalyticsPromptScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */; };
05E797C4E0048BB487E5C4D6 /* AppLockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */; };
05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
@@ -168,7 +169,6 @@
1A3B073568D1DC8F76F1F3A0 /* UserProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE69982BBA18C6D51AD08E /* UserProfileScreen.swift */; };
1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; };
1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */; };
1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */; };
1AE4532D92E6ED42BFE87064 /* JoinRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A566F8F2C27B99E1FB80C69B /* JoinRule.swift */; };
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; };
1AF4A82B4332CAD2DB3EB5DA /* TopBannerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */; };
@@ -308,6 +308,7 @@
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; };
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; };
3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; };
358290C2E4574456FE0C2DF6 /* LocationSharingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B73675A21580971E9ADF66A /* LocationSharingScreenViewModelProtocol.swift */; };
35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; };
36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; };
3684AD01C5FCB7616B28F629 /* TimelineMediaPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDE60FEE95039CCCEEEE3B0 /* TimelineMediaPreviewController.swift */; };
@@ -521,7 +522,6 @@
5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; };
5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; };
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; };
5C33976A720B64094CBC56B1 /* VideoMediaEventsTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B1FB56520A847DD2532D5C8 /* VideoMediaEventsTimelineView.swift */; };
5C61810ED7B7CB48346B1B9D /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */; };
5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */; };
@@ -702,6 +702,7 @@
7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; };
7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; };
7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */; };
7A37EC9D7164319587539E1D /* LocationSharingScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA4F7FFF0704D1C5471BF6E /* LocationSharingScreenCoordinator.swift */; };
7A495A5F3E5522DD7928CF8F /* UserIdentityProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */; };
7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; };
7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; };
@@ -744,6 +745,7 @@
80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.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 */; };
8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */; };
828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; };
832A4EA1094B8FE423A08700 /* RoomChangeRolesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2A421198FD20AAAED20004 /* RoomChangeRolesScreen.swift */; };
@@ -898,7 +900,6 @@
9B3589276CA008E38FAAAE91 /* SpacesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358528B29FA72ACFD0D9644B /* SpacesScreenCoordinator.swift */; };
9B84F55288AB98783C11CC49 /* SpaceRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0E7B059E84E7E374D3322A2 /* SpaceRoomCell.swift */; };
9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */; };
9BB91CABB10D8FE90C491BCD /* StaticLocationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */; };
9C11138F7D8C291494BB0B20 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
9C4EC28A921486B1775D7F8C /* IdentityConfirmedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */; };
9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1627F2D56477BD331F6D732C /* RoomHeaderView.swift */; };
@@ -1017,7 +1018,6 @@
AFE2AB612A1460E49578D746 /* JoinRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */; };
B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */; };
B0CB16349B96262AA65A04AF /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; };
B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */; };
B1088417801A962BA861C13F /* RoomPowerLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E247E29C7A0E421DB839D7 /* RoomPowerLevel.swift */; };
B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */; };
B13774779EA19FDD7A35A4A8 /* RoomRolesAndPermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C28B70BEFD3676F11D5D51F /* RoomRolesAndPermissionsScreenCoordinator.swift */; };
@@ -1133,6 +1133,7 @@
C5A07E2D88BE7D51DCECD166 /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */; };
C5E3A4A678B4F8900830B76A /* NavigationTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C39B1D3FC8CF41D6C3B054F /* NavigationTabCoordinator.swift */; };
C646CFE86CA46495D6BB7448 /* ManageAuthorizedSpacesScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74AE4F02A507882743973214 /* ManageAuthorizedSpacesScreenViewModelProtocol.swift */; };
C67156445600FAE6430DE41E /* LocationSharingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F56E6E41C6DFE8054787D57 /* LocationSharingScreen.swift */; };
C67FCC854F3A6FC7A2EC04D0 /* MediaUploadPreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */; };
C6AC34B731F4F853E9D15146 /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2397174D0DC3918A7A8A7B /* AnalyticsConfiguration.swift */; };
C6C06DDA8881260303FBA3A0 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; };
@@ -1268,7 +1269,6 @@
DF40CDBEFE1D02B5F9C4ACB1 /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = 810133CF215075C285FC6F3A /* test_image.png */; };
DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */; };
DF8F1211F2B0B56F0FCCA5C2 /* CertificateValidatorHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */; };
DFD5AA8688A34C72D48AF3B1 /* StaticLocationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5311C989EC15B4C2D699025 /* StaticLocationScreenViewModel.swift */; };
DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; };
E010DDE938032D3B8E84CC35 /* TracingHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A95C9B8299A36A6495DECA6 /* TracingHook.swift */; };
E02DAD9FD8D62587049FFFEC /* LinkNewDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F04B12FA231E797B7151A8 /* LinkNewDeviceTests.swift */; };
@@ -1411,6 +1411,7 @@
F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */; };
F6767F17D538578B370DD805 /* TimelineItemBubbleBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE739A6836E86E3780748477 /* TimelineItemBubbleBackground.swift */; };
F6BF52CB027393EE03CEC523 /* TimelineMediaPreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */; };
F6D07D834279BE17D18A33E9 /* LocationSharingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EED6D8956E554CEDFD4FE00D /* LocationSharingScreenViewModelTests.swift */; };
F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; };
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
F71C2B24AFB566119ACCDDA1 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
@@ -1583,6 +1584,7 @@
04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = "<group>"; };
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = "<group>"; };
052A8C0B00FE8AD7C972F2A4 /* LocationSharingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenViewModel.swift; sourceTree = "<group>"; };
052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; };
0545AC444BEEA89FF8C509FD /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = "<group>"; };
@@ -1975,10 +1977,11 @@
4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = "<group>"; };
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; };
4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = "<group>"; };
4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; path = AppIcon.icon; sourceTree = "<group>"; };
4B2B564CA6570E1487A7C7CC /* SpaceRoomListProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxy.swift; sourceTree = "<group>"; };
4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = "<group>"; };
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
4B73675A21580971E9ADF66A /* LocationSharingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenViewModelProtocol.swift; sourceTree = "<group>"; };
4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
4C8D988E82A8DFA13BE46F7C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pl; path = pl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1993,6 +1996,7 @@
4F5F0662483ED69791D63B16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = et; path = et.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; };
4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorTests.swift; sourceTree = "<group>"; };
4FA4F7FFF0704D1C5471BF6E /* LocationSharingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenCoordinator.swift; sourceTree = "<group>"; };
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = "<group>"; };
4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenModels.swift; sourceTree = "<group>"; };
@@ -2144,6 +2148,7 @@
6F054DE7D47849687662C9D9 /* BootDetectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootDetectionManager.swift; sourceTree = "<group>"; };
6F1C3CBBC62C566DDF5E84C1 /* TimelineItemMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuAction.swift; sourceTree = "<group>"; };
6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = "<group>"; };
6F56E6E41C6DFE8054787D57 /* LocationSharingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreen.swift; sourceTree = "<group>"; };
6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderMock.swift; sourceTree = "<group>"; };
6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelTests.swift; sourceTree = "<group>"; };
6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsBannerStateTests.swift; sourceTree = "<group>"; };
@@ -2392,7 +2397,6 @@
9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModel.swift; sourceTree = "<group>"; };
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; };
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactoryTests.swift; sourceTree = "<group>"; };
9B06663F7858E45882E63471 /* StaticLocationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreen.swift; sourceTree = "<group>"; };
9B6572E6EF5D5F4B0C338A40 /* PinnedEventsTimelineScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineScreenModels.swift; sourceTree = "<group>"; };
9B663BE498BB39EADC24025D /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = "<group>"; };
9B67DF223EEB8DCAF178A1D4 /* AnalyticsPromptScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -2612,7 +2616,6 @@
C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = "<group>"; };
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = "<group>"; };
C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenCoordinator.swift; sourceTree = "<group>"; };
C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAccountSettingsPresenter.swift; sourceTree = "<group>"; };
C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeView.swift; sourceTree = "<group>"; };
C258C9C815272911A5B132C3 /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
@@ -2635,7 +2638,6 @@
C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyProtocol.swift; sourceTree = "<group>"; };
C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenCoordinator.swift; sourceTree = "<group>"; };
C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = "<group>"; };
C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
@@ -2645,7 +2647,6 @@
C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewRedactConfirmationView.swift; sourceTree = "<group>"; };
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = "<group>"; };
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelTests.swift; sourceTree = "<group>"; };
C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = "<group>"; };
C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = "<group>"; };
@@ -2696,6 +2697,7 @@
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationBadge.swift; sourceTree = "<group>"; };
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.swift; sourceTree = "<group>"; };
D2C513A6CD99E6C3C163DA1E /* RowDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowDivider.swift; sourceTree = "<group>"; };
D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelProtocol.swift; sourceTree = "<group>"; };
D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D3557ACB95D0F666EF5AF0CE /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
@@ -2849,6 +2851,7 @@
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; };
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
EED6D8956E554CEDFD4FE00D /* LocationSharingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenViewModelTests.swift; sourceTree = "<group>"; };
EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = "<group>"; };
EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; };
EF36FC3DE25B20B7FA91F1FD /* SpaceServiceProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyMock.swift; sourceTree = "<group>"; };
@@ -2891,7 +2894,6 @@
F4CEB4590CCF70F0E3C0B171 /* GeneratedAccessibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedAccessibilityTests.swift; sourceTree = "<group>"; };
F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F51D674A5B5F1FE1B878E20F /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = "<group>"; };
F5311C989EC15B4C2D699025 /* StaticLocationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModel.swift; sourceTree = "<group>"; };
F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorToastView.swift; sourceTree = "<group>"; };
F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyMock.swift; sourceTree = "<group>"; };
F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = "<group>"; };
@@ -3118,6 +3120,7 @@
children = (
76A46ABD27628CB5FC402541 /* Backports.swift */,
F03AEAA2F66E796C365EFD58 /* ElementNavigationStack.swift */,
D2C513A6CD99E6C3C163DA1E /* RowDivider.swift */,
693E16574C6F7F9FA1015A8C /* Search.swift */,
832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */,
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */,
@@ -4777,6 +4780,7 @@
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */,
C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */,
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
EED6D8956E554CEDFD4FE00D /* LocationSharingScreenViewModelTests.swift */,
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */,
@@ -4836,7 +4840,6 @@
5557DDA438841AF5DC003D0B /* SpaceListScreenViewModelTests.swift */,
18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */,
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */,
C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */,
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */,
2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */,
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */,
@@ -5395,10 +5398,10 @@
948DD12A5533BE1BC260E437 /* LocationSharing */ = {
isa = PBXGroup;
children = (
4FA4F7FFF0704D1C5471BF6E /* LocationSharingScreenCoordinator.swift */,
8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */,
C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */,
F5311C989EC15B4C2D699025 /* StaticLocationScreenViewModel.swift */,
C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */,
052A8C0B00FE8AD7C972F2A4 /* LocationSharingScreenViewModel.swift */,
4B73675A21580971E9ADF66A /* LocationSharingScreenViewModelProtocol.swift */,
9FD8D798D879069243A7E7F7 /* View */,
);
path = LocationSharing;
@@ -5555,7 +5558,7 @@
9FD8D798D879069243A7E7F7 /* View */ = {
isa = PBXGroup;
children = (
9B06663F7858E45882E63471 /* StaticLocationScreen.swift */,
6F56E6E41C6DFE8054787D57 /* LocationSharingScreen.swift */,
);
path = View;
sourceTree = "<group>";
@@ -7158,6 +7161,7 @@
};
};
buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@@ -7233,7 +7237,6 @@
C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 681566846AF307E9BA4C72C6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
@@ -7652,6 +7655,7 @@
BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */,
CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */,
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
F6D07D834279BE17D18A33E9 /* LocationSharingScreenViewModelTests.swift in Sources */,
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */,
@@ -7718,7 +7722,6 @@
920DC020F18ABC88175114D3 /* SpaceListScreenViewModelTests.swift in Sources */,
72D2298DE695A6797CDA1A2A /* SpaceScreenViewModelTests.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 */,
@@ -8217,7 +8220,11 @@
D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */,
854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */,
973C48F9E4EFB808F61BE401 /* LocationRoomTimelineView.swift in Sources */,
C67156445600FAE6430DE41E /* LocationSharingScreen.swift in Sources */,
7A37EC9D7164319587539E1D /* LocationSharingScreenCoordinator.swift in Sources */,
29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */,
05B02B9508E922677F0E9451 /* LocationSharingScreenViewModel.swift in Sources */,
358290C2E4574456FE0C2DF6 /* LocationSharingScreenViewModelProtocol.swift in Sources */,
5EDBDE802761B5ECB54E6787 /* LogLevel.swift in Sources */,
36DE961B784087D5E18EF9BA /* LogViewerScreen.swift in Sources */,
CF38B70D8C6DD42C00A56A27 /* LogViewerScreenCoordinator.swift in Sources */,
@@ -8546,6 +8553,7 @@
AD55E245FE686D7DB4C86406 /* RoomTimelineItemView.swift in Sources */,
41CE5E1289C8768FC5B6490C /* RoomTimelineItemViewState.swift in Sources */,
B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */,
82434593648CB74121F1A821 /* RowDivider.swift in Sources */,
3F55721B5C08E8D9F1295592 /* SDKListener.swift in Sources */,
88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */,
FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */,
@@ -8670,10 +8678,6 @@
3B277D9538090766DA6C4566 /* StateRoomTimelineView.swift in Sources */,
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
CF638B8C6FDCE920AE061FAE /* StateStoreViewModelV2.swift in Sources */,
B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */,
1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */,
DFD5AA8688A34C72D48AF3B1 /* StaticLocationScreenViewModel.swift in Sources */,
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */,
C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */,
197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */,
2F94054F50E312AF30BE07F3 /* String.swift in Sources */,
@@ -36,6 +36,7 @@
"a11y_session_verification_time_limited_action_required" = "Time limited action required, you have one minute to verify";
"a11y_show_password" = "Show password";
"a11y_start_call" = "Start a call";
"a11y_start_voice_call" = "Start a voice call";
"a11y_tombstoned_room" = "Tombstoned room";
"a11y_user_menu" = "User menu";
"a11y_view_avatar" = "View avatar";
@@ -262,6 +263,7 @@
"common_open_source_licenses" = "Open source licenses";
"common_optic_id_ios" = "Optic ID";
"common_or" = "or";
"common_other_options" = "Other options";
"common_password" = "Password";
"common_people" = "People";
"common_permalink" = "Permalink";
@@ -396,7 +398,7 @@
"dialog_file_too_large_to_upload_title" = "The file size is too large to upload";
"dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings.";
"dialog_permission_generic" = "Please grant the permission in the system settings.";
"dialog_permission_location_description_ios" = "To share your current location, Element needs location access. Go to Settings > Location.";
"dialog_permission_location_description_ios" = "To share your current location, %1$@ needs location access. Go to Settings > Location.";
"dialog_permission_location_title_ios" = "%1$@ does not have access to your location.";
"dialog_permission_microphone" = "In order to let the application use the microphone, please grant the permission in the system settings.";
"dialog_permission_microphone_description_ios" = "Grant access so you can record and send messages with audio.";
@@ -458,6 +460,7 @@
"notification_channel_ringing_calls" = "Ringing calls";
"notification_channel_silent" = "Silent notifications";
"notification_fallback_content" = "You have new messages.";
"notification_incoming_audio_call" = "📞 Incoming call";
"notification_incoming_call" = "📹 Incoming call";
"notification_inline_reply_failed" = "** Failed to send - please open room";
"notification_invite_body" = "Invited you to chat";
@@ -665,6 +668,7 @@
"screen_media_upload_preview_item_count" = "Item %1$d of %2$d";
"screen_media_upload_preview_optimize_image_quality_title" = "Optimize image quality";
"screen_media_upload_preview_processing" = "Processing...";
"screen_onboarding_welcome_back" = "Welcome back";
"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here.";
"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered";
"screen_push_history_title" = "Push history";
@@ -1267,7 +1271,7 @@
"screen_share_open_apple_maps" = "Open in Apple Maps";
"screen_share_open_google_maps" = "Open in Google Maps";
"screen_share_open_osm_maps" = "Open in OpenStreetMap";
"screen_share_this_location_action" = "Share this location";
"screen_share_this_location_action" = "Share pinned location";
"screen_signed_out_reason_1" = "You’ve changed your password on another session";
"screen_signed_out_reason_2" = "You have deleted the session from another session";
"screen_signed_out_reason_3" = "Your server’s administrator has invalidated your access";
@@ -36,6 +36,7 @@
"a11y_session_verification_time_limited_action_required" = "Time limited action required, you have one minute to verify";
"a11y_show_password" = "Show password";
"a11y_start_call" = "Start a call";
"a11y_start_voice_call" = "Start a voice call";
"a11y_tombstoned_room" = "Tombstoned room";
"a11y_user_menu" = "User menu";
"a11y_view_avatar" = "View avatar";
@@ -262,6 +263,7 @@
"common_open_source_licenses" = "Open source licenses";
"common_optic_id_ios" = "Optic ID";
"common_or" = "or";
"common_other_options" = "Other options";
"common_password" = "Password";
"common_people" = "People";
"common_permalink" = "Permalink";
@@ -396,7 +398,7 @@
"dialog_file_too_large_to_upload_title" = "The file size is too large to upload";
"dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings.";
"dialog_permission_generic" = "Please grant the permission in the system settings.";
"dialog_permission_location_description_ios" = "To share your current location, Element needs location access. Go to Settings > Location.";
"dialog_permission_location_description_ios" = "To share your current location, %1$@ needs location access. Go to Settings > Location.";
"dialog_permission_location_title_ios" = "%1$@ does not have access to your location.";
"dialog_permission_microphone" = "In order to let the application use the microphone, please grant the permission in the system settings.";
"dialog_permission_microphone_description_ios" = "Grant access so you can record and send messages with audio.";
@@ -458,6 +460,7 @@
"notification_channel_ringing_calls" = "Ringing calls";
"notification_channel_silent" = "Silent notifications";
"notification_fallback_content" = "You have new messages.";
"notification_incoming_audio_call" = "📞 Incoming call";
"notification_incoming_call" = "📹 Incoming call";
"notification_inline_reply_failed" = "** Failed to send - please open room";
"notification_invite_body" = "Invited you to chat";
@@ -665,6 +668,7 @@
"screen_media_upload_preview_item_count" = "Item %1$d of %2$d";
"screen_media_upload_preview_optimize_image_quality_title" = "Optimise image quality";
"screen_media_upload_preview_processing" = "Processing...";
"screen_onboarding_welcome_back" = "Welcome back";
"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here.";
"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered";
"screen_push_history_title" = "Push history";
@@ -1267,7 +1271,7 @@
"screen_share_open_apple_maps" = "Open in Apple Maps";
"screen_share_open_google_maps" = "Open in Google Maps";
"screen_share_open_osm_maps" = "Open in OpenStreetMap";
"screen_share_this_location_action" = "Share this location";
"screen_share_this_location_action" = "Share pinned location";
"screen_signed_out_reason_1" = "You’ve changed your password on another session";
"screen_signed_out_reason_2" = "You have deleted the session from another session";
"screen_signed_out_reason_3" = "Your server’s administrator has invalidated your access";
@@ -64,6 +64,8 @@ final class AppSettings {
case elementCallBaseURLOverride
case voiceMessagePlaybackSpeed
// Feature flags
case publicSearchEnabled
case fuzzyRoomListSearchEnabled
@@ -76,9 +78,8 @@ final class AppSettings {
case linkPreviewsEnabled
case focusEventOnNotificationTap
case linkNewDeviceEnabled
case liveLocationSharingEnabled
case voiceMessagePlaybackSpeed
// Doug's tweaks 🔧
case hideUnreadMessagesBadge
case hideQuietNotificationAlerts
@@ -432,6 +433,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.linkNewDeviceEnabled, defaultValue: false, storageType: .userDefaults(store))
var linkNewDeviceEnabled
@UserPreference(key: UserDefaultsKeys.liveLocationSharingEnabled, defaultValue: false, storageType: .userDefaults(store))
var liveLocationSharingEnabled
@UserPreference(key: UserDefaultsKeys.developerOptionsEnabled, defaultValue: appBuildType == .debug, storageType: .userDefaults(store))
var developerOptionsEnabled
}
@@ -85,8 +85,10 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
actionsSubject.send(.finished)
case .displayUser(let userID):
actionsSubject.send(.displayUser(userID: userID))
case .presentLocationViewer(let geoURI, let description):
presentMapNavigator(geoURI: geoURI, description: description, timelineController: timelineController)
case .presentLocationViewer(let senderID, let geoURI):
presentMapNavigator(senderID: senderID,
geoURI: geoURI,
timelineController: timelineController)
case .displayMessageForwarding(let forwardingItem):
presentMessageForwarding(with: forwardingItem)
case .displayRoomScreenWithFocussedPin(let eventID, let threadRootEventID):
@@ -98,16 +100,22 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.setRootCoordinator(coordinator)
}
private func presentMapNavigator(geoURI: GeoURI, description: String?, timelineController: TimelineControllerProtocol) {
private func presentMapNavigator(senderID: String?,
geoURI: GeoURI,
timelineController: TimelineControllerProtocol) {
let stackCoordinator = NavigationStackCoordinator()
let params = StaticLocationScreenCoordinatorParameters(interactionMode: .viewOnly(geoURI: geoURI, description: description),
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
timelineController: timelineController,
appMediator: flowParameters.appMediator,
analytics: flowParameters.analytics,
userIndicatorController: flowParameters.userIndicatorController)
let coordinator = StaticLocationScreenCoordinator(parameters: params)
let params = LocationSharingScreenCoordinatorParameters(interactionMode: .viewStatic(senderID: senderID,
geoURI: geoURI),
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
liveLocationSharingEnabled: flowParameters.appSettings.liveLocationSharingEnabled,
roomProxy: roomProxy,
timelineController: timelineController,
appMediator: flowParameters.appMediator,
analytics: flowParameters.analytics,
userIndicatorController: flowParameters.userIndicatorController,
mediaProvider: flowParameters.userSession.mediaProvider)
let coordinator = LocationSharingScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
@@ -703,8 +703,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationViewer(_, let geoURI, let description):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, description: description)),
case .presentLocationViewer(let senderID, let geoURI):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewStatic(senderID: senderID, geoURI: geoURI)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentRoomMemberDetails(userID: let userID):
stateMachine.tryEvent(.startMembersFlow(entryPoint: .roomMember(userID: userID)))
@@ -788,10 +788,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationViewer(_, let geoURI, let description):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI,
description: description)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationViewer(let senderID, let geoURI):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewStatic(senderID: senderID,
geoURI: geoURI)),
userInfo: EventUserInfo(animated: animated,
timelineController: timelineController))
case .presentEmojiPicker(let itemID, let selectedEmojis):
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
@@ -1128,18 +1129,21 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentMapNavigator(interactionMode: StaticLocationInteractionMode,
private func presentMapNavigator(interactionMode: LocationSharingInteractionMode,
timelineController: TimelineControllerProtocol,
animated: Bool) {
let stackCoordinator = NavigationStackCoordinator()
let params = StaticLocationScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
timelineController: timelineController,
appMediator: flowParameters.appMediator,
analytics: flowParameters.analytics,
userIndicatorController: flowParameters.userIndicatorController)
let coordinator = StaticLocationScreenCoordinator(parameters: params)
let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
liveLocationSharingEnabled: flowParameters.appSettings.liveLocationSharingEnabled,
roomProxy: roomProxy,
timelineController: timelineController,
appMediator: flowParameters.appMediator,
analytics: flowParameters.analytics,
userIndicatorController: flowParameters.userIndicatorController,
mediaProvider: flowParameters.userSession.mediaProvider)
let coordinator = LocationSharingScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
@@ -144,7 +144,7 @@ extension RoomFlowCoordinator {
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case dismissEmojiPicker
case presentMapNavigator(interactionMode: StaticLocationInteractionMode)
case presentMapNavigator(interactionMode: LocationSharingInteractionMode)
case dismissMapNavigator
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
+13 -3
View File
@@ -116,6 +116,8 @@ internal enum L10n {
internal static var a11yShowPassword: String { return L10n.tr("Localizable", "a11y_show_password") }
/// Start a call
internal static var a11yStartCall: String { return L10n.tr("Localizable", "a11y_start_call") }
/// Start a voice call
internal static var a11yStartVoiceCall: String { return L10n.tr("Localizable", "a11y_start_voice_call") }
/// Tombstoned room
internal static var a11yTombstonedRoom: String { return L10n.tr("Localizable", "a11y_tombstoned_room") }
/// User menu
@@ -584,6 +586,8 @@ internal enum L10n {
internal static var commonOpticIdIos: String { return L10n.tr("Localizable", "common_optic_id_ios") }
/// or
internal static var commonOr: String { return L10n.tr("Localizable", "common_or") }
/// Other options
internal static var commonOtherOptions: String { return L10n.tr("Localizable", "common_other_options") }
/// Password
internal static var commonPassword: String { return L10n.tr("Localizable", "common_password") }
/// People
@@ -908,8 +912,10 @@ internal enum L10n {
internal static var dialogPermissionCamera: String { return L10n.tr("Localizable", "dialog_permission_camera") }
/// Please grant the permission in the system settings.
internal static var dialogPermissionGeneric: String { return L10n.tr("Localizable", "dialog_permission_generic") }
/// To share your current location, Element needs location access. Go to Settings > Location.
internal static var dialogPermissionLocationDescriptionIos: String { return L10n.tr("Localizable", "dialog_permission_location_description_ios") }
/// To share your current location, %1$@ needs location access. Go to Settings > Location.
internal static func dialogPermissionLocationDescriptionIos(_ p1: Any) -> String {
return L10n.tr("Localizable", "dialog_permission_location_description_ios", String(describing: p1))
}
/// %1$@ does not have access to your location.
internal static func dialogPermissionLocationTitleIos(_ p1: Any) -> String {
return L10n.tr("Localizable", "dialog_permission_location_title_ios", String(describing: p1))
@@ -1062,6 +1068,8 @@ internal enum L10n {
internal static func notificationFallbackNContent(_ p1: Int) -> String {
return L10n.tr("Localizable", "notification_fallback_n_content", p1)
}
/// 📞 Incoming call
internal static var notificationIncomingAudioCall: String { return L10n.tr("Localizable", "notification_incoming_audio_call") }
/// 📹 Incoming call
internal static var notificationIncomingCall: String { return L10n.tr("Localizable", "notification_incoming_call") }
/// ** Failed to send - please open room
@@ -2238,6 +2246,8 @@ internal enum L10n {
internal static var screenOnboardingSignInWithQrCode: String { return L10n.tr("Localizable", "screen_onboarding_sign_in_with_qr_code") }
/// Create account
internal static var screenOnboardingSignUp: String { return L10n.tr("Localizable", "screen_onboarding_sign_up") }
/// Welcome back
internal static var screenOnboardingWelcomeBack: String { return L10n.tr("Localizable", "screen_onboarding_welcome_back") }
/// Welcome to the fastest %1$@ ever. Supercharged for speed and simplicity.
internal static func screenOnboardingWelcomeMessage(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_onboarding_welcome_message", String(describing: p1))
@@ -3157,7 +3167,7 @@ internal enum L10n {
internal static var screenShareOpenGoogleMaps: String { return L10n.tr("Localizable", "screen_share_open_google_maps") }
/// Open in OpenStreetMap
internal static var screenShareOpenOsmMaps: String { return L10n.tr("Localizable", "screen_share_open_osm_maps") }
/// Share this location
/// Share pinned location
internal static var screenShareThisLocationAction: String { return L10n.tr("Localizable", "screen_share_this_location_action") }
/// You’ve changed your password on another session
internal static var screenSignedOutReason1: String { return L10n.tr("Localizable", "screen_signed_out_reason_1") }
+38 -57
View File
@@ -84,55 +84,38 @@ enum UserAvatarSizeOnScreen {
case sendInviteConfirmation
case sessionVerification
case threadSummary
case map
var value: CGFloat {
switch self {
case .chats, .spaces:
return 32
case .timeline:
return 32
case .readReceipt:
return 16
case .readReceiptSheet:
return 32
16
case .spaceHeader:
return 20
case .completionSuggestions:
return 32
case .blockedUsers:
return 32
case .settings:
return 52
case .roomDetails:
return 44
case .roomMembersList:
return 32
case .roomChangeRoles:
return 56
case .startChat:
return 36
case .memberDetails:
return 96
case .inviteUsers:
return 52
case .editUserDetails:
return 96
case .dmDetails:
return 75
case .knockingUsersBannerStack:
return 28
case .knockingUserBanner:
return 32
case .knockingUserList:
return 52
case .mediaPreviewDetails:
return 32
case .sendInviteConfirmation:
return 64
case .sessionVerification:
return 52
20
case .threadSummary:
return 24
24
case .knockingUsersBannerStack:
28
case .chats, .spaces, .map,
.timeline, .readReceiptSheet, .completionSuggestions,
.blockedUsers, .roomMembersList, .knockingUserBanner,
.mediaPreviewDetails:
32
case .startChat:
36
case .roomDetails:
44
case .inviteUsers, .knockingUserList, .sessionVerification,
.settings:
52
case .roomChangeRoles:
56
case .sendInviteConfirmation:
64
case .dmDetails:
75
case .memberDetails, .editUserDetails:
96
}
}
}
@@ -160,24 +143,22 @@ enum RoomAvatarSizeOnScreen {
var value: CGFloat {
switch self {
case .chats, .spaces, .spaceSettings:
return 52
case .notificationSettings:
30
case .timeline, .leaveSpace, .roomDirectorySearch,
.completionSuggestions, .authorizedSpaces, .createRoomSelectSpace,
.spaceFilters:
return 32
case .notificationSettings:
return 30
case .messageForwarding, .globalSearch, .roomSelection, .spaceAddRooms:
return 36
case .spaceAddRoomsSelected:
return 52
32
case .messageForwarding, .globalSearch, .roomSelection,
.spaceAddRooms:
36
case .chats, .spaces, .spaceSettings,
.spaceAddRoomsSelected:
52
case .joinRoom, .spaceHeader:
64
case .details:
return 96
case .joinRoom:
return 64
case .spaceHeader:
return 64
96
}
}
}
@@ -11,23 +11,32 @@ import MapLibre
import SwiftUI
final class LocationAnnotation: NSObject, MLNAnnotation {
let coordinate: CLLocationCoordinate2D
let id: String
var coordinate: CLLocationCoordinate2D
let anchorPoint: CGPoint
let view: AnyView
var view: AnyView
// MARK: - Setup
init(coordinate: CLLocationCoordinate2D,
init(id: String = UUID().uuidString,
coordinate: CLLocationCoordinate2D,
anchorPoint: CGPoint = .init(x: 0.5, y: 0.5),
@ViewBuilder label: () -> some View) {
self.id = id
self.coordinate = coordinate
self.anchorPoint = anchorPoint
view = AnyView(label())
super.init()
}
func updateView(@ViewBuilder label: () -> some View) {
view = AnyView(label())
}
}
final class LocationAnnotationView: MLNUserLocationAnnotationView {
private var hostingController: UIHostingController<AnyView>?
// MARK: - Setup
override init(annotation: MLNAnnotation?, reuseIdentifier: String?) {
@@ -37,12 +46,21 @@ final class LocationAnnotationView: MLNUserLocationAnnotationView {
convenience init(annotation: LocationAnnotation) {
self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
let view: UIView = UIHostingController(rootView: annotation.view).view
let hostingController = UIHostingController(rootView: annotation.view)
self.hostingController = hostingController
let view: UIView = hostingController.view
view.backgroundColor = .clear
view.anchorPoint = annotation.anchorPoint
addSubview(view)
view.bounds.size = view.intrinsicContentSize
}
func updateContent(with view: AnyView) {
hostingController?.rootView = view
if let hostedView = hostingController?.view {
hostedView.bounds.size = hostedView.intrinsicContentSize
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
@@ -20,10 +20,10 @@ struct MapLibreMapView: UIViewRepresentable {
/// The initial map center
let mapCenter: CLLocationCoordinate2D
/// Map annotations
let annotations: [LocationAnnotation]
/// Map annotations keyed by a stable identifier (e.g. sender userID for user pins, UUID string for generic pins)
let annotations: [String: LocationAnnotation]
init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) {
init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [String: LocationAnnotation] = [:]) {
self.zoomLevel = zoomLevel
self.initialZoomLevel = initialZoomLevel
self.mapCenter = mapCenter
@@ -45,6 +45,7 @@ struct MapLibreMapView: UIViewRepresentable {
@Binding var error: MapLibreError?
/// Coordinate of the center of the map
@Binding var mapCenterCoordinate: CLLocationCoordinate2D?
@Binding var hasLoadedUserLocation: Bool
@Binding var isLocationAuthorized: Bool?
/// The radius of uncertainty for the location, measured in meters.
@Binding var geolocationUncertainty: CLLocationAccuracy?
@@ -70,6 +71,11 @@ struct MapLibreMapView: UIViewRepresentable {
mapView.styleURL = dynamicMapURL
}
// Update existing annotation views with fresh SwiftUI content.
// This handles the case where the annotation's view data changes after
// the annotation was initially placed (e.g. user avatar loads asynchronously).
updateAnnotations(in: mapView)
showUserLocation(in: mapView)
}
@@ -80,17 +86,52 @@ struct MapLibreMapView: UIViewRepresentable {
// MARK: - Private
private func setupMap(mapView: MLNMapView, with options: Options) {
mapView.addAnnotations(options.annotations)
mapView.addAnnotations(Array(options.annotations.values))
mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel
mapView.centerCoordinate = options.mapCenter
}
private func updateAnnotations(in mapView: MLNMapView) {
let existingByID = Dictionary(uniqueKeysWithValues:
(mapView.annotations ?? []).compactMap { $0 as? LocationAnnotation }.map { ($0.id, $0) })
let existingIDs = Set(existingByID.keys)
let updatedIDs = Set(options.annotations.keys)
// Remove annotations that are no longer present
let removedIDs = existingIDs.subtracting(updatedIDs)
if !removedIDs.isEmpty {
let toRemove = removedIDs.compactMap { existingByID[$0] }
mapView.removeAnnotations(toRemove)
}
// Add new annotations
let addedIDs = updatedIDs.subtracting(existingIDs)
if !addedIDs.isEmpty {
let toAdd = addedIDs.compactMap { options.annotations[$0] }
mapView.addAnnotations(toAdd)
}
// Update existing annotations that are still present
let keptIDs = existingIDs.intersection(updatedIDs)
for id in keptIDs {
guard let existingAnnotation = existingByID[id],
let updatedAnnotation = options.annotations[id] else {
continue
}
existingAnnotation.coordinate = updatedAnnotation.coordinate
if let annotationView = mapView.view(for: existingAnnotation) as? LocationAnnotationView {
annotationView.updateContent(with: updatedAnnotation.view)
}
}
}
private func makeMapView() -> MLNMapView {
let mapView = MLNMapView(frame: .zero, styleURL: mapURLBuilder.interactiveMapURL(for: colorScheme == .dark ? .dark : .light))
mapView.logoViewPosition = .topLeft
mapView.attributionButtonPosition = .topLeft
mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2)
mapView.tintColor = .black
mapView.tintColor = .compound.iconAccentPrimary
mapView.allowsRotating = false
mapView.allowsTilting = false
return mapView
@@ -149,7 +190,8 @@ extension MapLibreMapView {
func mapView(_ mapView: MLNMapView, didUpdate userLocation: MLNUserLocation?) {
guard let userLocation else { return }
mapLibreView.hasLoadedUserLocation = true
if previousUserLocation == nil, mapLibreView.options.annotations.isEmpty {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.options.zoomLevel, animated: true)
@@ -18,12 +18,12 @@ final class ShareToMapsAppActivity: UIActivity {
private let type: MapsAppType
private let location: CLLocationCoordinate2D
private let locationDescription: String?
private let senderName: String?
init(type: MapsAppType, location: CLLocationCoordinate2D, locationDescription: String?) {
init(type: MapsAppType, location: CLLocationCoordinate2D, senderName: String?) {
self.type = type
self.location = location
self.locationDescription = locationDescription
self.senderName = senderName
super.init()
}
@@ -44,20 +44,20 @@ final class ShareToMapsAppActivity: UIActivity {
}
override func prepare(withActivityItems activityItems: [Any]) {
UIApplication.shared.open(type.activityURL(for: location, locationDescription: locationDescription), options: [:]) { [weak self] result in
UIApplication.shared.open(type.activityURL(for: location, senderName: senderName), options: [:]) { [weak self] result in
self?.activityDidFinish(result)
}
}
}
extension ShareToMapsAppActivity.MapsAppType {
func activityURL(for location: CLLocationCoordinate2D, locationDescription: String?) -> URL {
func activityURL(for location: CLLocationCoordinate2D, senderName: String?) -> URL {
switch self {
case .apple:
var url: URL = "https://maps.apple.com/"
url.append(queryItems: [
.init(name: "ll", value: "\(location.latitude),\(location.longitude)"),
.init(name: "q", value: locationDescription ?? "Pin")
.init(name: "q", value: senderName ?? "Pin") // We need to provide a value or no marker is displayed.
])
return url
case .google:
@@ -0,0 +1,30 @@
//
// 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 SwiftUI
struct RowDivider: ViewModifier {
@Environment(\.pixelLength) var pixelLength: CGFloat
let alignment: Alignment
let horizontalInsets: CGFloat
func body(content: Content) -> some View {
content
.overlay(alignment: alignment) {
Rectangle()
.fill(Color.compound.borderDisabled)
.frame(height: pixelLength)
.padding(.trailing, -horizontalInsets)
}
}
}
extension View {
func rowDivider(alignment: Alignment = .bottom, horizontalInsets: CGFloat) -> some View {
modifier(RowDivider(alignment: alignment, horizontalInsets: horizontalInsets))
}
}
@@ -10,28 +10,84 @@ import Compound
import SwiftUI
struct LocationMarkerView: View {
private let pinColor: Color = .compound.iconOnSolidPrimary
private let pinInsets = EdgeInsets(top: 13, leading: 12, bottom: 15, trailing: 12)
var userProfile: UserProfileProxy?
var fillColor = Color.compound.bgCanvasDefault
var strokeColor = Color.compound.iconSecondaryAlpha
var dotColor = Color.compound.iconPrimary
@ScaledMetric var size: CGFloat = 42
var mediaProvider: MediaProviderProtocol?
private let circleCenter = CGPoint(x: 21, y: 21) // in SVG space
private let circleRadius: CGFloat = 6 // in SVG space
var body: some View {
CompoundIcon(\.locationPinSolid)
.dynamicTypeSize(.large)
.foregroundStyle(pinColor)
.padding(pinInsets)
.background {
backgroundShape
.shadow(color: .black.opacity(0.2), radius: 4.1129, x: 0, y: 4.93548)
// Generated from the SVG
Canvas { context, canvasSize in
let scaleX = canvasSize.width / 42
let scaleY = canvasSize.height / 50
let pinPath = Path { p in
p.move(to: CGPoint(x: 21 * scaleX, y: 49 * scaleY))
p.addCurve(to: CGPoint(x: 17.6875 * scaleX, y: 47.7581 * scaleY),
control1: CGPoint(x: 19.25 * scaleX, y: 49 * scaleY),
control2: CGPoint(x: 18.1458 * scaleX, y: 48.4825 * scaleY))
p.addCurve(to: CGPoint(x: 5.28125 * scaleX, y: 33.6313 * scaleY),
control1: CGPoint(x: 12.5833 * scaleX, y: 42.8525 * scaleY),
control2: CGPoint(x: 8.41667 * scaleX, y: 38.1332 * scaleY))
p.addCurve(to: CGPoint(x: 1 * scaleX, y: 21.3674 * scaleY),
control1: CGPoint(x: 2.82292 * scaleX, y: 29.2846 * scaleY),
control2: CGPoint(x: 1 * scaleX, y: 25.1863 * scaleY))
p.addCurve(to: CGPoint(x: 21 * scaleX, y: 1 * scaleY),
control1: CGPoint(x: 1 * scaleX, y: 10.2109 * scaleY),
control2: CGPoint(x: 9.5 * scaleX, y: 1 * scaleY))
p.addCurve(to: CGPoint(x: 41 * scaleX, y: 21.3674 * scaleY),
control1: CGPoint(x: 32.5 * scaleX, y: 1 * scaleY),
control2: CGPoint(x: 41 * scaleX, y: 10.2109 * scaleY))
p.addCurve(to: CGPoint(x: 36.7188 * scaleX, y: 33.6313 * scaleY),
control1: CGPoint(x: 41 * scaleX, y: 25.1863 * scaleY),
control2: CGPoint(x: 39.1771 * scaleX, y: 29.2846 * scaleY))
p.addCurve(to: CGPoint(x: 24.3125 * scaleX, y: 47.7581 * scaleY),
control1: CGPoint(x: 33.5833 * scaleX, y: 38.1332 * scaleY),
control2: CGPoint(x: 29.4167 * scaleX, y: 42.8525 * scaleY))
p.addCurve(to: CGPoint(x: 21 * scaleX, y: 49 * scaleY),
control1: CGPoint(x: 23.3333 * scaleX, y: 48.4825 * scaleY),
control2: CGPoint(x: 21.5833 * scaleX, y: 49 * scaleY))
p.closeSubpath()
}
.alignmentGuide(VerticalAlignment.center) { dimensions in
dimensions[.bottom]
context.stroke(pinPath, with: .color(strokeColor), lineWidth: 2 * scaleX)
context.fill(pinPath, with: .color(fillColor))
// Dot
let dotPath = Path(ellipseIn: CGRect(x: (circleCenter.x - circleRadius) * scaleX,
y: (circleCenter.y - circleRadius) * scaleY,
width: circleRadius * 2 * scaleX,
height: circleRadius * 2 * scaleY))
context.fill(dotPath, with: .color(dotColor))
// Draw resolved symbol centered on the circle
if userProfile != nil, let symbol = context.resolveSymbol(id: 0) {
let center = CGPoint(x: circleCenter.x * scaleX,
y: circleCenter.y * scaleY)
context.draw(symbol, at: center, anchor: .center)
}
}
var backgroundShape: some View {
Image(asset: Asset.Images.locationMarkerShape)
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundStyle(.compound.iconPrimary)
} symbols: {
if let userProfile {
LoadableAvatarImage(url: userProfile.avatarURL,
name: userProfile.displayName,
contentID: userProfile.userID,
avatarSize: .user(on: .map),
mediaProvider: mediaProvider)
.overlay {
Circle().inset(by: 0.5).stroke(strokeColor)
}
.tag(0)
}
}
.frame(width: size, height: size * 50 / 42)
.alignmentGuide(VerticalAlignment.center) { dimensions in
dimensions[.bottom]
}
}
}
@@ -39,9 +95,19 @@ struct LocationMarkerView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 30) {
LocationMarkerView()
LocationMarkerView()
.colorScheme(.dark)
LocationMarkerView(userProfile: UserProfileProxy.mockDan,
mediaProvider: MediaProviderMock(configuration: .init()))
LocationMarkerView(userProfile: UserProfileProxy.mockDan,
mediaProvider: MediaProviderMock(configuration: .init()))
.colorScheme(.dark)
}
.padding(16)
.background(Color(red: 0.9, green: 0.85, blue: 0.8))
.previewLayout(.sizeThatFits)
}
}
@@ -14,6 +14,7 @@ struct ToolbarButton: View {
static let cancel = Role.cancel(title: L10n.actionCancel)
static let done = Role.confirm(title: L10n.actionDone)
static let save = Role.confirm(title: L10n.actionSave)
static let close = Role.cancel(title: L10n.actionClose)
case cancel(title: String)
case confirm(title: String)
@@ -83,6 +83,7 @@ enum TestablePreviewsDictionary {
"LoadableImage_Previews" : LoadableImage_Previews.self,
"LocationMarkerView_Previews" : LocationMarkerView_Previews.self,
"LocationRoomTimelineView_Previews" : LocationRoomTimelineView_Previews.self,
"LocationSharingScreen_Previews" : LocationSharingScreen_Previews.self,
"LoginScreen_Previews" : LoginScreen_Previews.self,
"LongPressWithFeedback_Previews" : LongPressWithFeedback_Previews.self,
"ManageAuthorizedSpacesScreen_Previews" : ManageAuthorizedSpacesScreen_Previews.self,
@@ -181,7 +182,6 @@ enum TestablePreviewsDictionary {
"StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self,
"StartChatScreen_Previews" : StartChatScreen_Previews.self,
"StateRoomTimelineView_Previews" : StateRoomTimelineView_Previews.self,
"StaticLocationScreenViewer_Previews" : StaticLocationScreenViewer_Previews.self,
"StickerRoomTimelineView_Previews" : StickerRoomTimelineView_Previews.self,
"SwipeRightAction_Previews" : SwipeRightAction_Previews.self,
"SwipeToReplyView_Previews" : SwipeToReplyView_Previews.self,
@@ -33,12 +33,7 @@ struct HomeScreenRoomCell: View {
content
.padding(.vertical, verticalInsets)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.compound.borderDisabled)
.frame(height: 1 / UIScreen.main.scale)
.padding(.trailing, -horizontalInsets)
}
.rowDivider(horizontalInsets: horizontalInsets)
}
.padding(.horizontal, horizontalInsets)
.accessibilityElement(children: .combine)
@@ -0,0 +1,70 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
struct LocationSharingScreenCoordinatorParameters {
let interactionMode: LocationSharingInteractionMode
let mapURLBuilder: MapTilerURLBuilderProtocol
let liveLocationSharingEnabled: Bool
let roomProxy: JoinedRoomProxyProtocol
let timelineController: TimelineControllerProtocol
let appMediator: AppMediatorProtocol
let analytics: AnalyticsService
let userIndicatorController: UserIndicatorControllerProtocol
let mediaProvider: MediaProviderProtocol
}
enum LocationSharingScreenCoordinatorAction {
case close
}
final class LocationSharingScreenCoordinator: CoordinatorProtocol {
private let parameters: LocationSharingScreenCoordinatorParameters
private let viewModel: LocationSharingScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<LocationSharingScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<LocationSharingScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: LocationSharingScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = LocationSharingScreenViewModel(interactionMode: parameters.interactionMode,
mapURLBuilder: parameters.mapURLBuilder,
liveLocationSharingEnabled: parameters.liveLocationSharingEnabled,
roomProxy: parameters.roomProxy,
timelineController: parameters.timelineController,
analytics: parameters.analytics,
userIndicatorController: parameters.userIndicatorController,
mediaProvider: parameters.mediaProvider)
}
// MARK: - Public
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .close:
actionsSubject.send(.close)
case .openSystemSettings:
parameters.appMediator.openAppSettings()
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(LocationSharingScreen(context: viewModel.context))
}
}
@@ -14,31 +14,44 @@ enum LocationSharingViewError: Error, Hashable {
case mapError(MapLibreError)
}
enum StaticLocationScreenViewModelAction {
enum LocationSharingScreenViewModelAction {
case close
case openSystemSettings
}
enum StaticLocationInteractionMode: Hashable {
enum LocationSharingInteractionMode: Hashable {
case picker
case viewOnly(geoURI: GeoURI, description: String? = nil)
case viewStatic(senderID: String?, geoURI: GeoURI)
var canShowAvatar: Bool {
switch self {
case .picker, .viewStatic(.some(_), _):
true
default:
false
}
}
}
struct StaticLocationScreenViewState: BindableState {
init(interactionMode: StaticLocationInteractionMode, mapURLBuilder: MapTilerURLBuilderProtocol) {
struct LocationSharingScreenViewState: BindableState {
init(interactionMode: LocationSharingInteractionMode,
mapURLBuilder: MapTilerURLBuilderProtocol,
showLiveLocationSharingButton: Bool) {
self.interactionMode = interactionMode
self.mapURLBuilder = mapURLBuilder
self.showLiveLocationSharingButton = showLiveLocationSharingButton
bindings.showsUserLocationMode = switch interactionMode {
case .picker: .showAndFollow
case .viewOnly: .show
case .viewStatic: .show
}
}
let interactionMode: StaticLocationInteractionMode
let interactionMode: LocationSharingInteractionMode
let mapURLBuilder: MapTilerURLBuilderProtocol
let showLiveLocationSharingButton: Bool
var bindings = StaticLocationScreenBindings(showsUserLocationMode: .hide)
var bindings = LocationSharingScreenBindings(showsUserLocationMode: .hide)
/// Indicates whether the user is sharing his current location
var isSharingUserLocation: Bool {
@@ -50,21 +63,17 @@ struct StaticLocationScreenViewState: BindableState {
case .picker:
// middle point in Europe, to be used if the users location is not yet known
return .init(latitude: 49.843, longitude: 9.902056)
case .viewOnly(let geoURI, _):
case .viewStatic(_, let geoURI):
return .init(latitude: geoURI.latitude, longitude: geoURI.longitude)
}
}
var isLocationPickerMode: Bool {
interactionMode == .picker
}
var navigationTitle: String {
switch interactionMode {
case .picker:
return L10n.screenShareLocationTitle
case .viewOnly:
return L10n.screenViewLocationTitle
true
default:
false
}
}
@@ -72,10 +81,14 @@ struct StaticLocationScreenViewState: BindableState {
switch interactionMode {
case .picker:
return false
case .viewOnly:
case .viewStatic:
return true
}
}
var isLocationLoading: Bool {
!bindings.hasLoadedUserLocation && bindings.isLocationAuthorized != false
}
var zoomLevel: Double {
15.0
@@ -85,27 +98,28 @@ struct StaticLocationScreenViewState: BindableState {
switch interactionMode {
case .picker:
return 2.7
case .viewOnly:
case .viewStatic:
return 15.0
}
}
var locationDescription: String? {
var userProfile: UserProfileProxy?
var shownUserProfile: UserProfileProxy? {
switch interactionMode {
case .picker:
return nil
case .viewOnly(_, let description):
return description
isSharingUserLocation ? userProfile : nil
case .viewStatic:
userProfile
}
}
}
struct StaticLocationScreenBindings {
struct LocationSharingScreenBindings {
var mapCenterLocation: CLLocationCoordinate2D?
var geolocationUncertainty: CLLocationAccuracy?
var showsUserLocationMode: ShowUserLocationMode
var hasLoadedUserLocation = false
var isLocationAuthorized: Bool?
/// Information describing the currently displayed alert.
@@ -127,7 +141,7 @@ struct StaticLocationScreenBindings {
var showShareSheet = false
}
enum StaticLocationScreenViewAction {
enum LocationSharingScreenViewAction {
case close
case selectLocation
case centerToUser
@@ -141,20 +155,18 @@ extension AlertInfo where T == LocationSharingViewError {
switch error {
case .missingAuthorization:
self.init(id: error,
title: L10n.dialogPermissionLocationTitleIos(InfoPlistReader.main.bundleDisplayName),
message: L10n.dialogPermissionLocationDescriptionIos,
title: L10n.dialogAllowAccess,
message: L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
case .mapError(.failedLoadingMap):
self.init(id: error,
title: "",
message: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName),
title: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
case .mapError(.failedLocatingUser):
self.init(id: error,
title: "",
message: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName),
title: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
}
@@ -9,31 +9,44 @@
import Combine
import Foundation
typealias StaticLocationScreenViewModelType = StateStoreViewModelV2<StaticLocationScreenViewState, StaticLocationScreenViewAction>
typealias LocationSharingScreenViewModelType = StateStoreViewModelV2<LocationSharingScreenViewState, LocationSharingScreenViewAction>
class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLocationScreenViewModelProtocol {
class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, LocationSharingScreenViewModelProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let timelineController: TimelineControllerProtocol
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private let actionsSubject: PassthroughSubject<StaticLocationScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<StaticLocationScreenViewModelAction, Never> {
private let actionsSubject: PassthroughSubject<LocationSharingScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<LocationSharingScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(interactionMode: StaticLocationInteractionMode,
init(interactionMode: LocationSharingInteractionMode,
mapURLBuilder: MapTilerURLBuilderProtocol,
liveLocationSharingEnabled: Bool,
roomProxy: JoinedRoomProxyProtocol,
timelineController: TimelineControllerProtocol,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
mediaProvider: MediaProviderProtocol) {
self.roomProxy = roomProxy
self.timelineController = timelineController
self.analytics = analytics
self.userIndicatorController = userIndicatorController
super.init(initialViewState: .init(interactionMode: interactionMode, mapURLBuilder: mapURLBuilder))
super.init(initialViewState: .init(interactionMode: interactionMode,
mapURLBuilder: mapURLBuilder,
showLiveLocationSharingButton: liveLocationSharingEnabled),
mediaProvider: mediaProvider)
if interactionMode.canShowAvatar {
updateShownUserProfile(members: roomProxy.membersPublisher.value)
setupSubscriptions()
}
}
override func process(viewAction: StaticLocationScreenViewAction) {
override func process(viewAction: LocationSharingScreenViewAction) {
switch viewAction {
case .close:
actionsSubject.send(.close)
@@ -58,6 +71,32 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
// MARK: - Private
private func setupSubscriptions() {
roomProxy.membersPublisher.sink { [weak self] members in
self?.updateShownUserProfile(members: members)
}
.store(in: &cancellables)
}
private func updateShownUserProfile(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)
}
case .viewStatic(.some(let senderID), _):
if let sender = members.first(where: { $0.userID == senderID }).map(UserProfileProxy.init) {
state.userProfile = sender
} else {
state.userProfile = .init(userID: senderID)
}
default:
state.userProfile = nil
}
}
private func sendLocation(_ geoURI: GeoURI, isUserLocation: Bool) async {
guard case .success = await timelineController.sendLocation(body: geoURI.bodyMessage,
geoURI: geoURI,
@@ -9,7 +9,7 @@
import Combine
@MainActor
protocol StaticLocationScreenViewModelProtocol {
var actions: AnyPublisher<StaticLocationScreenViewModelAction, Never> { get }
var context: StaticLocationScreenViewModelType.Context { get }
protocol LocationSharingScreenViewModelProtocol {
var actions: AnyPublisher<LocationSharingScreenViewModelAction, Never> { get }
var context: LocationSharingScreenViewModelType.Context { get }
}
@@ -1,64 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
struct StaticLocationScreenCoordinatorParameters {
let interactionMode: StaticLocationInteractionMode
let mapURLBuilder: MapTilerURLBuilderProtocol
let timelineController: TimelineControllerProtocol
let appMediator: AppMediatorProtocol
let analytics: AnalyticsService
let userIndicatorController: UserIndicatorControllerProtocol
}
enum StaticLocationScreenCoordinatorAction {
case close
}
final class StaticLocationScreenCoordinator: CoordinatorProtocol {
private let parameters: StaticLocationScreenCoordinatorParameters
private let viewModel: StaticLocationScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<StaticLocationScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<StaticLocationScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: StaticLocationScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = StaticLocationScreenViewModel(interactionMode: parameters.interactionMode,
mapURLBuilder: parameters.mapURLBuilder,
timelineController: parameters.timelineController,
analytics: parameters.analytics,
userIndicatorController: parameters.userIndicatorController)
}
// MARK: - Public
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .close:
actionsSubject.send(.close)
case .openSystemSettings:
parameters.appMediator.openAppSettings()
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(StaticLocationScreen(context: viewModel.context))
}
}
@@ -0,0 +1,264 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct LocationSharingScreen: View {
@Bindable var context: LocationSharingScreenViewModel.Context
var body: some View {
if context.viewState.isLocationPickerMode {
mainContent
.sheet(isPresented: .constant(true)) {
sharingOptionsSheet
.alert(item: $context.alertInfo)
}
} else {
mainContent
.alert(item: $context.alertInfo)
}
}
// MARK: - Private
private var mainContent: some View {
mapView
.ignoresSafeArea(edges: .bottom)
.track(screen: context.viewState.isLocationPickerMode ? .LocationSend : .LocationView)
.navigationTitle(L10n.screenViewLocationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
}
private var mapView: some View {
ZStack(alignment: .center) {
MapLibreMapView(mapURLBuilder: context.viewState.mapURLBuilder,
options: mapOptions,
showsUserLocationMode: $context.showsUserLocationMode,
error: $context.mapError,
mapCenterCoordinate: $context.mapCenterLocation,
hasLoadedUserLocation: $context.hasLoadedUserLocation,
isLocationAuthorized: $context.isLocationAuthorized,
geolocationUncertainty: $context.geolocationUncertainty) {
context.send(viewAction: .userDidPan)
}
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
if context.viewState.isLocationPickerMode {
LocationMarkerView(userProfile: context.viewState.shownUserProfile, mediaProvider: context.mediaProvider)
}
}
.overlay(alignment: .topTrailing) {
centerToUserLocationButton
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: context.viewState.showShareAction ? .topBarLeading : .topBarTrailing) {
ToolbarButton(role: .close) {
context.send(viewAction: .close)
}
}
if context.viewState.showShareAction {
ToolbarItem(placement: .topBarTrailing) {
Button {
context.showShareSheet = true
} label: {
Image(systemName: "square.and.arrow.up")
}
.popover(isPresented: $context.showShareSheet) { shareSheet }
}
}
}
private var mapOptions: MapLibreMapView.Options {
var annotations: [String: LocationAnnotation] = [:]
if !context.viewState.isLocationPickerMode {
let id = context.viewState.shownUserProfile?.userID ?? UUID().uuidString
let annotation = LocationAnnotation(id: id,
coordinate: context.viewState.initialMapCenter,
anchorPoint: .bottomCenter) {
LocationMarkerView(userProfile: context.viewState.shownUserProfile, mediaProvider: context.mediaProvider)
}
annotations[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]
}
@ViewBuilder
private var centerToUseIcon: some View {
if context.viewState.isLocationLoading {
ProgressView()
.tint(.compound.iconPrimary)
.padding(13)
} else {
CompoundIcon(context.viewState.isSharingUserLocation ? \.locationNavigatorCentred : \.locationNavigator)
.foregroundStyle(.compound.iconPrimary)
.padding(13)
}
}
private var centerToUserLocationButton: some View {
Button {
context.send(viewAction: .centerToUser)
} label: {
if #available(iOS 26.0, *) {
centerToUseIcon
.glassEffect(.regular.interactive(), in: Circle())
.tint(.compound.bgCanvasDefault)
} else {
centerToUseIcon
.background(.compound.bgCanvasDefault, in: RoundedRectangle(cornerRadius: 6))
}
}
.disabled(context.viewState.isLocationLoading)
.dynamicTypeSize(.large)
.padding(13)
}
@ViewBuilder
private var shareSheet: some View {
let location = context.viewState.initialMapCenter
let senderName = context.viewState.shownUserProfile?.displayName
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, senderName: senderName)],
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, senderName: senderName) })
.edgesIgnoringSafeArea(.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
}
}
@State private var sharingOptionsSheetHeight: CGFloat = .zero
private var sharingOptionsSheet: some View {
VStack(alignment: .leading, spacing: 0) {
Button {
context.send(viewAction: .selectLocation)
} label: {
if context.viewState.isSharingUserLocation {
LocationSharingLabel(text: L10n.screenShareMyLocationAction,
icon: \.locationNavigatorCentred,
iconColor: .compound.iconSecondary)
} else {
LocationSharingLabel(text: L10n.screenShareThisLocationAction,
icon: \.locationNavigator,
iconColor: .compound.iconSecondary)
}
}
if context.viewState.showLiveLocationSharingButton {
Button { } label: {
LocationSharingLabel(text: L10n.actionShareLiveLocation,
icon: \.locationPinSolid,
iconColor: .compound.iconAccentPrimary)
}
}
}
.font(.compound.bodyLG)
.foregroundStyle(.compound.textPrimary)
.padding(.top, 38)
.readHeight($sharingOptionsSheetHeight)
.interactiveDismissDisabled()
.presentationBackground(.compound.bgCanvasDefault)
.presentationBackgroundInteraction(.enabled)
.presentationDragIndicator(.hidden)
.presentationDetents([.height(sharingOptionsSheetHeight)])
}
}
private struct LocationSharingLabel: View {
let text: String
let icon: KeyPath<CompoundIcons, Image>
let iconColor: Color
var body: some View {
Label {
Text(text)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 14)
.rowDivider(alignment: .top, horizontalInsets: 16.0)
} icon: {
CompoundIcon(icon)
.foregroundStyle(iconColor)
}
.padding(.leading, 16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// MARK: - Previews
struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = LocationSharingScreenViewModel(interactionMode: .viewStatic(senderID: "@dan:matrix.org", geoURI: .init(latitude: 41.9027835,
longitude: 12.4963655)),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
liveLocationSharingEnabled: true,
roomProxy: JoinedRoomProxyMock(.init()),
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init()))
static let pinViewModel = LocationSharingScreenViewModel(interactionMode: .viewStatic(senderID: nil, geoURI: .init(latitude: 41.9027835,
longitude: 12.4963655)),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
liveLocationSharingEnabled: true,
roomProxy: JoinedRoomProxyMock(.init()),
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init()))
static let pickerViewModel = LocationSharingScreenViewModel(interactionMode: .picker,
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
liveLocationSharingEnabled: true,
roomProxy: JoinedRoomProxyMock(.init()),
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init()))
static var previews: some View {
ElementNavigationStack {
LocationSharingScreen(context: pickerViewModel.context)
}
.previewDisplayName("Picker")
ElementNavigationStack {
LocationSharingScreen(context: viewModel.context)
}
.previewDisplayName("User Static Location")
ElementNavigationStack {
LocationSharingScreen(context: pinViewModel.context)
}
.previewDisplayName("Pin Static Location")
}
}
private extension CGPoint {
static let bottomCenter: Self = .init(x: 0.5, y: 1)
}
@@ -1,198 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct StaticLocationScreen: View {
@Bindable var context: StaticLocationScreenViewModel.Context
var body: some View {
VStack(spacing: 0) {
if let locationDescription = context.viewState.locationDescription {
Text(locationDescription)
.lineLimit(2)
.foregroundColor(Color.compound.textPrimary)
.font(.compound.bodyMD)
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
mapView
}
.track(screen: context.viewState.isLocationPickerMode ? .LocationSend : .LocationView)
.navigationTitle(context.viewState.navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.alert(item: $context.alertInfo)
}
private var mapView: some View {
ZStack(alignment: .center) {
MapLibreMapView(mapURLBuilder: context.viewState.mapURLBuilder,
options: mapOptions,
showsUserLocationMode: $context.showsUserLocationMode,
error: $context.mapError,
mapCenterCoordinate: $context.mapCenterLocation,
isLocationAuthorized: $context.isLocationAuthorized,
geolocationUncertainty: $context.geolocationUncertainty) {
context.send(viewAction: .userDidPan)
}
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
if context.viewState.isLocationPickerMode {
LocationMarkerView()
}
}
.overlay(alignment: .bottomTrailing) {
centerToUserLocationButton
}
}
// MARK: - Private
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
closeButton
}
if context.viewState.showShareAction {
ToolbarItem(placement: .navigationBarTrailing) {
shareButton
.popover(isPresented: $context.showShareSheet) { shareSheet }
}
}
if context.viewState.isLocationPickerMode {
ToolbarItemGroup(placement: .bottomBar) {
selectLocationButton
Spacer()
}
}
}
private var mapOptions: MapLibreMapView.Options {
var annotations: [LocationAnnotation] = []
if context.viewState.isLocationPickerMode == false {
let annotation = LocationAnnotation(coordinate: context.viewState.initialMapCenter, anchorPoint: .bottomCenter) {
LocationMarkerView()
}
annotations.append(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]
}
private var selectLocationButton: some View {
Button {
context.send(viewAction: .selectLocation)
} label: {
HStack(spacing: 8) {
CompoundIcon(\.shareIos)
Text(context.viewState.isSharingUserLocation ? L10n.screenShareMyLocationAction : L10n.screenShareThisLocationAction)
}
}
}
private var centerToUserLocationButton: some View {
Button {
context.send(viewAction: .centerToUser)
} label: {
CompoundIcon(context.viewState.isSharingUserLocation ? \.locationNavigatorCentred : \.locationNavigator)
.padding(8)
.background(.compound.bgCanvasDefault, in: RoundedRectangle(cornerRadius: 6))
}
.dynamicTypeSize(.large)
.padding(16)
}
private var closeButton: some View {
Button(L10n.actionCancel) {
context.send(viewAction: .close)
}
}
private var shareButton: some View {
Button {
context.showShareSheet = true
} label: {
Image(systemName: "square.and.arrow.up")
}
}
@ViewBuilder
private var shareSheet: some View {
let location = context.viewState.initialMapCenter
let locationDescription = context.viewState.locationDescription
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)],
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) })
.edgesIgnoringSafeArea(.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
struct StaticLocationScreenViewer_Previews: PreviewProvider, TestablePreview {
static let viewModel = StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835,
longitude: 12.4963655)),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
static let pickerViewModel = StaticLocationScreenViewModel(interactionMode: .picker,
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
static let descriptionViewModel = StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835,
longitude: 12.4963655),
description: "Cool position"),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
static var previews: some View {
ElementNavigationStack {
StaticLocationScreen(context: pickerViewModel.context)
}
.previewDisplayName("Picker")
ElementNavigationStack {
StaticLocationScreen(context: viewModel.context)
}
.previewDisplayName("View Only")
ElementNavigationStack {
StaticLocationScreen(context: descriptionViewModel.context)
}
.previewDisplayName("View Only (with description)")
}
}
private extension CGPoint {
static let bottomCenter: Self = .init(x: 0.5, y: 1)
}
@@ -26,7 +26,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters {
enum PinnedEventsTimelineScreenCoordinatorAction {
case dismiss
case displayUser(userID: String)
case presentLocationViewer(geoURI: GeoURI, description: String?)
case presentLocationViewer(senderID: String?, geoURI: GeoURI)
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?)
}
@@ -87,8 +87,9 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
case .displayMediaPreview(let mediaPreviewViewModel):
viewModel.displayMediaPreview(mediaPreviewViewModel)
case .displayLocation(_, let geoURI, let description):
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
case .displayLocation(let senderID, let geoURI):
actionsSubject.send(.presentLocationViewer(senderID: senderID,
geoURI: geoURI))
case .viewInRoomTimeline(let eventID, let threadRootEventID):
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
// These other actions will not be handled in this view
@@ -39,7 +39,7 @@ enum RoomScreenCoordinatorAction {
case presentRoomDetails
case presentLocationPicker
case presentPollForm(mode: PollFormMode)
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentLocationViewer(senderID: String?, geoURI: GeoURI)
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case presentRoomMemberDetails(userID: String)
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
@@ -137,8 +137,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
case .displayLocation(let body, let geoURI, let description):
actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description))
case .displayLocation(let senderID, let geoURI):
actionsSubject.send(.presentLocationViewer(senderID: senderID, geoURI: geoURI))
case .displayResolveSendFailure(let failure, let sendHandle):
actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
case .displayThread(let itemID):
@@ -67,6 +67,8 @@ protocol DeveloperOptionsProtocol: AnyObject {
var linkPreviewsEnabled: Bool { get set }
var linkNewDeviceEnabled: Bool { get set }
var liveLocationSharingEnabled: Bool { get set }
}
extension AppSettings: DeveloperOptionsProtocol { }
@@ -76,6 +76,10 @@ struct DeveloperOptionsScreen: View {
Text("Can leak the device IP address when loading link metadata.")
.foregroundStyle(.compound.textCriticalPrimary)
}
Toggle(isOn: $context.liveLocationSharingEnabled) {
Text("Live location sharing")
}
}
Section("Join rules") {
@@ -33,7 +33,7 @@ enum ThreadTimelineScreenCoordinatorAction {
case presentMediaUploadPicker(mode: MediaPickerScreenMode)
case presentMediaUploadPreviewScreen(mediaURLs: [URL])
case presentLocationPicker
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentLocationViewer(senderID: String?, geoURI: GeoURI)
case presentPollForm(mode: PollFormMode)
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case presentRoomMemberDetails(userID: String)
@@ -118,10 +118,9 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
viewModel.displayMediaPreview(mediaPreviewViewModel)
case .displayLocationPicker:
actionsSubject.send(.presentLocationPicker)
case .displayLocation(let body, let geoURI, let description):
actionsSubject.send(.presentLocationViewer(body: body,
geoURI: geoURI,
description: description))
case .displayLocation(let senderID, let geoURI):
actionsSubject.send(.presentLocationViewer(senderID: senderID,
geoURI: geoURI))
case .displayPollForm(let mode):
actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let mediaURLs):
@@ -536,7 +536,8 @@ class TimelineInteractionHandler {
switch timelineItem {
case let item as LocationRoomTimelineItem:
guard let geoURI = item.content.geoURI else { return .none }
return .displayLocation(body: item.content.body, geoURI: geoURI, description: item.content.description)
return .displayLocation(senderID: item.content.kind == .sender ? item.sender.id : nil,
geoURI: geoURI)
case is ImageRoomTimelineItem,
is VideoRoomTimelineItem:
return await mediaPreviewAction(for: timelineItem, messageTypes: [.image, .video])
@@ -23,7 +23,7 @@ enum TimelineViewModelAction {
case displaySenderDetails(userID: String)
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayMediaPreview(TimelineMediaPreviewViewModel)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
case displayLocation(senderID: String?, geoURI: GeoURI)
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case displayThread(itemID: TimelineItemIdentifier)
case composer(action: TimelineComposerAction)
@@ -655,8 +655,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
let mediaPreviewViewModel = makeMediaPreviewViewModel(item: item, timelineViewModelKind: timelineViewModelKind)
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
case .displayLocation(let body, let geoURI, let description):
actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description))
case .displayLocation(let senderID, let geoURI):
actionsSubject.send(.displayLocation(senderID: senderID, geoURI: geoURI))
case .none:
break
}
@@ -705,20 +705,7 @@ private struct MockTimelineContent: View {
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782,
longitude: 12.496366),
description: "Location description description description description description description description description"),
properties: .init(replyDetails: replyDetails,
isThreaded: isThreaded,
threadSummary: threadSummary)))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
longitude: 12.496366)),
properties: .init(replyDetails: replyDetails,
isThreaded: isThreaded,
threadSummary: threadSummary)))
@@ -16,7 +16,7 @@ struct LocationRoomTimelineView: View {
TimelineStyler(timelineItem: timelineItem) {
mainContent
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
.accessibilityLabel(L10n.commonSharedLocation)
.onTapGesture {
guard context.viewState.mapTilerConfiguration.isEnabled else { return }
context.send(viewAction: .mediaTapped(itemID: timelineItem.id))
@@ -27,41 +27,20 @@ struct LocationRoomTimelineView: View {
@ViewBuilder
private var mainContent: some View {
if let geoURI = timelineItem.content.geoURI {
VStack(alignment: .leading, spacing: 0) {
descriptionView
.frame(maxWidth: mapAspectRatio * mapMaxHeight, alignment: .leading)
MapLibreStaticMapView(geoURI: geoURI,
mapURLBuilder: context.viewState.mapTilerConfiguration,
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
LocationMarkerView()
}
.frame(maxHeight: mapMaxHeight)
.aspectRatio(mapAspectRatio, contentMode: .fit)
.clipped()
MapLibreStaticMapView(geoURI: geoURI,
mapURLBuilder: context.viewState.mapTilerConfiguration,
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
LocationMarkerView(userProfile: timelineItem.content.kind == .sender ? .init(sender: timelineItem.sender) : nil, mediaProvider: context.mediaProvider)
}
.frame(maxHeight: mapMaxHeight)
.aspectRatio(mapAspectRatio, contentMode: .fit)
.clipped()
} else {
FormattedBodyText(text: timelineItem.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
}
}
// MARK: - Private
private var accessibilityLabel: String {
if let description = timelineItem.content.description {
return "\(L10n.commonSharedLocation), \(description)"
}
return L10n.commonSharedLocation
}
@ViewBuilder
private var descriptionView: some View {
if let description = timelineItem.content.description, !description.isEmpty {
FormattedBodyText(text: description)
.padding(8)
}
}
private let mapAspectRatio: Double = 3 / 2
private let mapMaxHeight: Double = 300
@@ -107,9 +86,9 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
sender: .init(id: "Bob"),
sender: .init(id: "@bob:matrix.org", displayName: "Bob", avatarURL: .mockMXCUserAvatar),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description")))
geoURI: .init(latitude: 41.902782, longitude: 12.496366))))
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: .mock,
isOutgoing: false,
@@ -117,7 +96,8 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
canBeRepliedTo: true,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description"),
geoURI: .init(latitude: 41.902782, longitude: 12.496366),
kind: .pin),
properties: .init(replyDetails: .loaded(sender: .init(id: "Someone"),
eventID: "123",
eventContent: .message(.text(.init(body: "The thread content goes 'ere.")))),
@@ -25,7 +25,7 @@ enum TimelineControllerAction {
}
case displayMediaPreview(item: EventBasedMessageTimelineItemProtocol, timelineViewModel: TimelineViewModelKind)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
case displayLocation(senderID: String?, geoURI: GeoURI)
case none
}
@@ -6,16 +6,32 @@
// Please see LICENSE files in the repository root for full details.
//
import MatrixRustSDK
struct LocationRoomTimelineItemContent: Hashable {
enum Kind {
case sender
case pin
init(from asset: AssetType?) {
self = switch asset {
case .pin:
.pin
case .sender, .none:
.sender
}
}
}
let body: String
let geoURI: GeoURI?
let description: String?
let kind: Kind
init(body: String,
geoURI: GeoURI? = nil,
description: String? = nil) {
kind: Kind = .sender) {
self.body = body
self.geoURI = geoURI
self.description = description
self.kind = kind
}
}
@@ -583,7 +583,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildLocationTimelineItemContent(_ locationContent: LocationContent) -> LocationRoomTimelineItemContent {
LocationRoomTimelineItemContent(body: locationContent.body,
geoURI: .init(string: locationContent.geoUri),
description: locationContent.description)
kind: .init(from: locationContent.asset))
}
private func buildFileTimelineItemContent(_ messageContent: FileMessageContent) -> FileRoomTimelineItemContent {
@@ -44,6 +44,10 @@ struct UserProfileProxy: Equatable, Hashable {
avatarURL = sdkRoomHero.avatarUrl.flatMap(URL.init(string:))
}
init(member: RoomMemberProxyProtocol) {
self.init(member: RoomMemberDetails(withProxy: member))
}
/// A user is meant to be "verified" when the GET profile returns back either the display name or the avatar
/// If isn't we aren't sure that the related matrix id really exists.
var isVerified: Bool {
@@ -603,6 +603,14 @@ extension PreviewTests {
}
}
@Test
func locationSharingScreen() async throws {
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
for (index, preview) in LocationSharingScreen_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
@Test
func loginScreen() async throws {
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
@@ -1387,14 +1395,6 @@ extension PreviewTests {
}
}
@Test
func staticLocationScreenViewer() async throws {
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
for (index, preview) in StaticLocationScreenViewer_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
@Test
func stickerRoomTimelineView() async throws {
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43c010abad7474002e5830bf5332ae068c82deaa789ba26f0aa3cb26d61edb25
size 82843
oid sha256:c00139208711b767a8f19c563bc7cef80314c3382ce5ce7d481a71d5ef70df1a
size 69042
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43c010abad7474002e5830bf5332ae068c82deaa789ba26f0aa3cb26d61edb25
size 82843
oid sha256:c00139208711b767a8f19c563bc7cef80314c3382ce5ce7d481a71d5ef70df1a
size 69042
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6275a174efe2615503282742972c952b8a66e0d00bfbb806b8eea3f2ffee9b27
size 40724
oid sha256:5dcaa6876ed9b9ef600f3c8327aed5a1b5e87cfc3990994d2b44d89973597c2f
size 51341
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6275a174efe2615503282742972c952b8a66e0d00bfbb806b8eea3f2ffee9b27
size 40724
oid sha256:5dcaa6876ed9b9ef600f3c8327aed5a1b5e87cfc3990994d2b44d89973597c2f
size 51341
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fb62383066a45ae514f8736f6ea08b8a5c0bab0c231836613ff57d1ddb95975
size 1027619
oid sha256:71845ef91726086a8b88816e8535cbf5bbdd82e0de0d5a0ca9ebca75c376944b
size 1227270
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8575f49f01e82cbca9366dab9b31954df18e0c8a2fc267225fc3ca9d93286695
size 1029185
oid sha256:9a8b87ca66a0447bf808b4ef2d67569f1a88fbc798073eefd56fcb39d4f126d3
size 1228934
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:667d2b653d460a351e8c9b1adf708532d215b465ecfe3548409a1de3abc12ba5
size 607504
oid sha256:edeeccdfbfb21821ad3eb56836d92e78e2f36516ffbf8cdd9ab7b068a9b2b68c
size 580022
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:547c133de43262b6e4a3a91231af8ed39af98357dd7962df14dc8011379100a7
size 580526
oid sha256:410475fefb08e8e59d5f5e6c3b25a6ce8d220dd819a51708ead024754f4ece73
size 580183
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64737509400fe27cdb017b8f66cffe8c0807f958827cff84a9ce5484db0ca3fc
size 80109
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64737509400fe27cdb017b8f66cffe8c0807f958827cff84a9ce5484db0ca3fc
size 80109
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08e72ae0c7d34108690371b29952267b1ac06bb62f203515f3c5e4143efd2ca8
size 38511
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08e72ae0c7d34108690371b29952267b1ac06bb62f203515f3c5e4143efd2ca8
size 38511
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbf4a51ed98c176336fc2e5be99311342bdae0d931d5d78dabca0ace3dd3a85b
size 80783
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbf4a51ed98c176336fc2e5be99311342bdae0d931d5d78dabca0ace3dd3a85b
size 80783
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:117ab2273ccfba15cee5546e521a85b87c8f7b915b869a299234c7d1a0caecd2
size 41622
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:117ab2273ccfba15cee5546e521a85b87c8f7b915b869a299234c7d1a0caecd2
size 41622
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4635f42e9d912d20f4963c5c3627f543cb00f95183fa0e902c9cc61b1afc921
size 93073
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4635f42e9d912d20f4963c5c3627f543cb00f95183fa0e902c9cc61b1afc921
size 93073
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c68d58d06123afaa7e02fc798b2360e1818c450fd277b275308d5704203e5547
size 53102
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c68d58d06123afaa7e02fc798b2360e1818c450fd277b275308d5704203e5547
size 53102
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fe3be6ac7144e77a64deea2ec9abe990b7066df18bf693018e3cbbac95f7b19
size 92909
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29136dedce23b9175a539c68f190f339972cfeedd51714b2d7dbcd487c0918c1
size 94512
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd7467f8fecb694a44d88e404999373428dd5b0909f9a821a8cbc9961f132b59
size 46905
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3cd2e0746b0f78394304c614d9a80cd15392986a58b4b64eaa36757997aff42
size 48123
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1fed42a97d568cc0d024afb9be2c7c8ae2359a88f1335b307ab5b8cbe0929f59
size 88451
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79e7bdc97b95baf9ec65240a09543d1efa7a6f0ee8178a0ad24ec7fa25f4fbcf
size 88813
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8dc6d6f2dcdac244d56349c4d432e43c281e539a83769e4711ebfa43d5b62462
size 42173
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8dc6d6f2dcdac244d56349c4d432e43c281e539a83769e4711ebfa43d5b62462
size 42173
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ec7064466b5a91fd490c9aebdcfb36efbc2a585b86998725bcd90e2c280c726
size 91675
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:241dd732058072774947a9aa0dbd7c6bf410608ba8b4e1733942161feb7d725e
size 92048
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:763523520732699428fcf758b36119ff450d13b6f69b250e5c977651ae06d05e
size 45091
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:763523520732699428fcf758b36119ff450d13b6f69b250e5c977651ae06d05e
size 45091
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4c2e14e3362efa21a236c61fa17fe94c14f08c35965cc68cc7375e837b63e6e
size 2459797
oid sha256:7905d4da975ca2f8c8ba13afc3112081360e1c2586e780e0bf7f63e79f11e907
size 1816558
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3bed8adcdeed91791208203243bec9edab8c6cf233538382a6775e5b997f2970
size 2458950
oid sha256:6f225a517befd7486ca940036f0b32b051471e6179dddc1afec04027f0e5a34b
size 1815989
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:39e3c3ea4d8f07c295f6c716e568e4900c4fe3977ac008728d63a5679f8f1099
size 1032059
oid sha256:711bdb8bba6cf0b1148695e599599a34b137bfb41577ccf53f70402c188f7336
size 762444
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1aadd13737eb2768c20d5cd48fcb503120275ce73ef27ee3ec151ac5629be78c
size 1029398
oid sha256:bc8677193d2595d5d6993d2e39c40b74fd033e02ac952da52dfc320824b857b0
size 760909
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:89b62418f7c716f317fb3d0faee0ed5271cc99166740f517bb9082639465c618
size 2574127
oid sha256:8a3e77518c1d7225d0d89ac5d94f3a050721abb0320ac94c7768a64d9f8064c0
size 1912810
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb2a87744f36cd3317bfa0e79a8f0bc3f2a5077751b4da2e039c0c9edb23c149
size 2585620
oid sha256:648eb3d7089c0ce01bc4d20cddd0f0092421d78f2a0f483135b30f4d1bc5e665
size 1919591
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a122378791c1f44057926bf054374e6f31eb3b4644438a7f3df988f62e8422aa
size 1154014
oid sha256:6ec385fa56fdea70874f4ec43623561cba05f1b2c13633a46d560974e8487cb0
size 865130
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1752a98b3ddfa33cd8ee71fe2a49146b9a87d222fbad6fc41b4615d112948032
size 1157827
oid sha256:918a059719a7ff6f3974664904e9ce6473241a20907ebefda0c40561e6235823
size 868790
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:68bbaa372d7ef0f92176d530311c8484370aa38c7227a3a1f2dd7ed4d45240fc
size 2575915
oid sha256:f1a2e2ed1e99b2ff6b92fe5ea4cf70af039ed09c542ede3f471e88f3d8883a2c
size 1916887
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b8cecf8be430f6efbe9451a6a1b4e6e2df8e3140e7a4fcea3c0a22e85e846d41
size 2601396
oid sha256:ad75f5eb536ef43505a9713b17d64424451b5f4136795ff72e6318f2cc74998b
size 1939145
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a86e9f58d2cde2339de39905762d92f57f8447896e16f24afa1adbd8329fd40
size 1259715
oid sha256:7f33ce5a5b646d0de6f7d57a86ec2cd4ffa2c0a88c99f0607b4388aff3ba7099
size 944690
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:09fb516ddcf8ab30b8ded663a15dec446cc932f9173ecdeb09a7b68d9d7515e6
size 1259289
oid sha256:44fb917f17f99ea73143c6f81c4a935e5f5d6de51ff6dccb61eb44e6a006b2f9
size 945021
@@ -11,20 +11,23 @@ import Combine
import Testing
@MainActor
struct StaticLocationScreenViewModelTests {
struct LocationSharingScreenViewModelTests {
private let timelineProxy = TimelineProxyMock(.init())
private var viewModel: StaticLocationScreenViewModelProtocol
private var viewModel: LocationSharingScreenViewModelProtocol
private var context: StaticLocationScreenViewModel.Context {
private var context: LocationSharingScreenViewModel.Context {
viewModel.context
}
init() {
let viewModel = StaticLocationScreenViewModel(interactionMode: .picker,
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
timelineController: MockTimelineController(timelineProxy: timelineProxy),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
let viewModel = LocationSharingScreenViewModel(interactionMode: .picker,
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
liveLocationSharingEnabled: true,
roomProxy: JoinedRoomProxyMock(.init()),
timelineController: MockTimelineController(timelineProxy: timelineProxy),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init()))
viewModel.state.bindings.isLocationAuthorized = true
self.viewModel = viewModel
}
@@ -68,11 +71,11 @@ struct StaticLocationScreenViewModelTests {
@Test
func errorMapping() {
let mapError = AlertInfo(locationSharingViewError: .mapError(.failedLoadingMap))
#expect(mapError.message == L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName))
#expect(mapError.title == L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName))
let locationError = AlertInfo(locationSharingViewError: .mapError(.failedLocatingUser))
#expect(locationError.message == L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName))
#expect(locationError.title == L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName))
let authorizationError = AlertInfo(locationSharingViewError: .missingAuthorization)
#expect(authorizationError.message == L10n.dialogPermissionLocationDescriptionIos)
#expect(authorizationError.message == L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName))
}
@Test