diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index cd2d7b3b0..5b5ada4e1 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; + 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 08248D02BACA75CDC3B39A96 /* UserNotificationCenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */; }; @@ -166,6 +167,7 @@ 41DFDD212D1BE57CA50D783B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; }; 4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */; }; + 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */; }; 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */; }; 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */; }; 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */; }; @@ -336,6 +338,7 @@ 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 84C0CF78BCE085C08CB94D86 /* TimelineEventProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B62EE933FC3D5651AF4607 /* TimelineEventProxy.swift */; }; 84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; + 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */; }; 85813D87DDD7F67A46BD9AF7 /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A8047B50E3607ACD354E /* ImageProviderProtocol.swift */; }; 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */; }; 858C04B62166B5BAFCD20F2D /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */; }; @@ -571,6 +574,7 @@ CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; + D04B93C644A0BE4A4C5D0A1B /* LocationPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA10BE264552E23946FF0499 /* LocationPinView.swift */; }; D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; @@ -579,6 +583,7 @@ D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; }; D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; }; D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; + D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; D4ACF3276F5D0DA28D4028C9 /* AnalyticsPromptScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8196D64EB9CF2AF1F43E4ED1 /* AnalyticsPromptScreenViewModelProtocol.swift */; }; D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */; }; D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; @@ -590,6 +595,7 @@ D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; + D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; }; @@ -810,13 +816,16 @@ 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReadReceiptsView.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = ""; }; + 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = ""; }; 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = ""; }; 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = ""; }; + 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = ""; }; 1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyMock.swift; sourceTree = ""; }; 1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = ""; }; + 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = ""; }; 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = ""; }; 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelProtocol.swift; sourceTree = ""; }; @@ -1038,6 +1047,7 @@ 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItemContent.swift; sourceTree = ""; }; 748AE77AC3B0A01223033B87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenCoordinator.swift; sourceTree = ""; }; + 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = ""; }; 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = ""; }; 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = ""; }; @@ -1256,6 +1266,7 @@ CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = ""; }; CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; + CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = ""; }; CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; @@ -1282,6 +1293,7 @@ D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = ""; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = ""; }; + DA10BE264552E23946FF0499 /* LocationPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPinView.swift; sourceTree = ""; }; DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = ""; }; DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = ""; }; DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsKeys.swift; sourceTree = ""; }; @@ -1761,6 +1773,7 @@ 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, C352359663A0E52BA20761EE /* LoadableImage.swift */, + DA10BE264552E23946FF0499 /* LocationPinView.swift */, 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */, 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, @@ -2330,6 +2343,7 @@ 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */, 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */, DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */, + 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */, 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */, CC14E5209C262530E19BC4C1 /* InvitesScreenViewModelTests.swift */, 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */, @@ -2541,6 +2555,8 @@ 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */, 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */, B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */, + 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */, + CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */, 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */, 1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */, 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */, @@ -2931,6 +2947,7 @@ E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */, F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */, D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */, + 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */, B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */, 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */, B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */, @@ -3351,6 +3368,7 @@ FCDF06BDB123505F0334B4F9 /* Timeline */ = { isa = PBXGroup; children = ( + 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */, 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */, 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */, 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */, @@ -3832,6 +3850,7 @@ 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */, 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */, CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */, + 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */, A216C83ADCF32BA5EF8A6FBC /* InviteUsersViewModelTests.swift in Sources */, @@ -4036,6 +4055,7 @@ 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */, 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */, F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */, + 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */, @@ -4077,6 +4097,10 @@ 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */, D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */, 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */, + D04B93C644A0BE4A4C5D0A1B /* LocationPinView.swift in Sources */, + D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */, + 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */, + D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */, 29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */, 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */, CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */, diff --git a/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/Contents.json new file mode 100644 index 000000000..bb52dd91a --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "location-pin.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "location-pin-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin-dark.pdf b/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin-dark.pdf new file mode 100644 index 000000000..48c62e9d6 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin-dark.pdf @@ -0,0 +1,170 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 50.000000 54.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 16.600098 16.852539 cm +0.062745 0.074510 0.090196 scn +8.400001 24.147461 m +3.756000 24.147461 0.000000 20.289217 0.000000 15.518802 c +0.000000 10.378586 5.304000 3.290758 7.488000 0.591221 c +7.968000 -0.000460 8.844001 -0.000460 9.324001 0.591221 c +11.496001 3.290758 16.800001 10.378586 16.800001 15.518802 c +16.800001 20.289217 13.044001 24.147461 8.400001 24.147461 c +h +8.400001 12.437138 m +6.744000 12.437138 5.400000 13.817722 5.400000 15.518802 c +5.400000 17.219879 6.744000 18.600466 8.400001 18.600466 c +10.056001 18.600466 11.400001 17.219879 11.400001 15.518802 c +11.400001 13.817722 10.056001 12.437138 8.400001 12.437138 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 710 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 50.000000 54.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 13.000000 17.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 24.000000 m +24.000000 24.000000 l +24.000000 0.000000 l +0.000000 0.000000 l +0.000000 24.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 234 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +-1.000000 0.000000 -0.000000 -1.000000 32.000000 6.000000 cm +0.921569 0.933333 0.949020 scn +7.000000 6.000000 m +13.062179 0.000001 l +0.937822 0.000001 l +7.000000 6.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 0.000000 4.000000 cm +0.921569 0.933333 0.949020 scn +50.000000 25.000000 m +50.000000 11.192883 38.807117 0.000000 25.000000 0.000000 c +11.192882 0.000000 0.000000 11.192883 0.000000 25.000000 c +0.000000 38.807117 11.192882 50.000000 25.000000 50.000000 c +38.807117 50.000000 50.000000 38.807117 50.000000 25.000000 c +h +f +n +Q +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 592 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 50.000000 54.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000968 00000 n +0000000990 00000 n +0000001472 00000 n +0000001494 00000 n +0000001792 00000 n +0000002440 00000 n +0000002462 00000 n +0000002635 00000 n +0000002709 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2769 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin.pdf b/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin.pdf new file mode 100644 index 000000000..7b7bc8b56 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin.pdf @@ -0,0 +1,170 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 50.000000 54.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 16.600098 16.852539 cm +1.000000 1.000000 1.000000 scn +8.400001 24.147461 m +3.756000 24.147461 0.000000 20.289217 0.000000 15.518802 c +0.000000 10.378586 5.304000 3.290758 7.488000 0.591221 c +7.968000 -0.000460 8.844001 -0.000460 9.324001 0.591221 c +11.496001 3.290758 16.800001 10.378586 16.800001 15.518802 c +16.800001 20.289217 13.044001 24.147461 8.400001 24.147461 c +h +8.400001 12.437138 m +6.744000 12.437138 5.400000 13.817722 5.400000 15.518802 c +5.400000 17.219879 6.744000 18.600466 8.400001 18.600466 c +10.056001 18.600466 11.400001 17.219879 11.400001 15.518802 c +11.400001 13.817722 10.056001 12.437138 8.400001 12.437138 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 710 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 50.000000 54.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 13.000000 17.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 24.000000 m +24.000000 24.000000 l +24.000000 0.000000 l +0.000000 0.000000 l +0.000000 24.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 234 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +-1.000000 0.000000 -0.000000 -1.000000 32.000000 6.000000 cm +0.105882 0.113725 0.133333 scn +7.000000 6.000000 m +13.062179 0.000001 l +0.937822 0.000001 l +7.000000 6.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 0.000000 4.000000 cm +0.105882 0.113725 0.133333 scn +50.000000 25.000000 m +50.000000 11.192883 38.807117 0.000000 25.000000 0.000000 c +11.192882 0.000000 0.000000 11.192883 0.000000 25.000000 c +0.000000 38.807117 11.192882 50.000000 25.000000 50.000000 c +38.807117 50.000000 50.000000 38.807117 50.000000 25.000000 c +h +f +n +Q +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 592 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 50.000000 54.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000968 00000 n +0000000990 00000 n +0000001472 00000 n +0000001494 00000 n +0000001792 00000 n +0000002440 00000 n +0000002462 00000 n +0000002635 00000 n +0000002709 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2769 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 303aa32c0..0a6367d28 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -107,6 +107,7 @@ "common_server_not_supported" = "Server not supported"; "common_server_url" = "Server URL"; "common_settings" = "Settings"; +"common_shared_location" = "Shared location"; "common_starting_chat" = "Starting chat…"; "common_sticker" = "Sticker"; "common_success" = "Success"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 1f25c9668..46cf06930 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -29,6 +29,7 @@ final class AppSettings { case shouldCollapseRoomStateEvents case userSuggestionsEnabled case readReceiptsEnabled + case locationEventsEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -185,13 +186,12 @@ final class AppSettings { // MARK: - Feature Flags - // MARK: Start Chat - @UserPreference(key: UserDefaultsKeys.userSuggestionsEnabled, defaultValue: false, storageType: .volatile) var userSuggestionsEnabled - // MARK: Receipts - @UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: false, storageType: .userDefaults(store)) var readReceiptsEnabled + + @UserPreference(key: UserDefaultsKeys.locationEventsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var locationEventsEnabled } diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index e0a31cfbc..e03d53072 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -38,6 +38,7 @@ internal enum Asset { internal static let launchLogo = ImageAsset(name: "Images/LaunchLogo") internal static let appLogo = ImageAsset(name: "Images/app-logo") internal static let closeCircle = ImageAsset(name: "Images/close_circle") + internal static let locationPin = ImageAsset(name: "Images/location-pin") internal static let timelineComposerSendMessage = ImageAsset(name: "Images/timelineComposerSendMessage") } } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 4e948997f..f94f4f990 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -238,6 +238,8 @@ public enum L10n { public static var commonServerUrl: String { return L10n.tr("Localizable", "common_server_url") } /// Settings public static var commonSettings: String { return L10n.tr("Localizable", "common_settings") } + /// Shared location + public static var commonSharedLocation: String { return L10n.tr("Localizable", "common_shared_location") } /// Starting chat… public static var commonStartingChat: String { return L10n.tr("Localizable", "common_starting_chat") } /// Sticker diff --git a/ElementX/Sources/Other/MapLibre/MapLibreModels.swift b/ElementX/Sources/Other/MapLibre/MapLibreModels.swift index 21bcd9de6..21f0b2069 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreModels.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreModels.swift @@ -38,3 +38,11 @@ enum MapLibreError: Error { case failedLocatingUser case invalidLocationAuthorization } + +enum MapTilerAttributionPlacement: String { + case bottomRight = "bottomright" + case bottomLeft = "bottomleft" + case topLeft = "topleft" + case topRight = "topright" + case hidden = "false" +} diff --git a/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift b/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift index 834715449..2f44c5dfe 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift @@ -21,27 +21,34 @@ struct MapLibreStaticMapView: View { private let coordinates: CLLocationCoordinate2D private let zoomLevel: Double private let mapTilerStatic: MapTilerStaticMapProtocol + private let mapTilerAttributionPlacement: MapTilerAttributionPlacement private let pinAnnotationView: PinAnnotation @Environment(\.colorScheme) private var colorScheme - @ScaledMetric private var height: CGFloat - @ScaledMetric private var width: CGFloat - @State private var attempt = 0 + private let imageSize: CGSize + @State private var fetchAttempt = 0 - init(coordinates: CLLocationCoordinate2D, zoomLevel: Double, mapTilerStatic: MapTilerStaticMapProtocol, height: CGFloat, width: CGFloat, @ViewBuilder pinAnnotationView: () -> PinAnnotation) { + init(coordinates: CLLocationCoordinate2D, + zoomLevel: Double, + imageSize: CGSize, + attributionPlacement: MapTilerAttributionPlacement, + mapTilerStatic: MapTilerStaticMapProtocol, + @ViewBuilder pinAnnotationView: () -> PinAnnotation) { self.coordinates = coordinates self.zoomLevel = zoomLevel self.mapTilerStatic = mapTilerStatic - _height = .init(wrappedValue: height) - _width = .init(wrappedValue: width) + self.imageSize = imageSize + mapTilerAttributionPlacement = attributionPlacement self.pinAnnotationView = pinAnnotationView() } var body: some View { - if let url = mapTilerStatic.staticMapURL(for: colorScheme.mapStyle, coordinates: coordinates, zoomLevel: zoomLevel, size: .init(width: width, height: height)) { + if let url = mapTilerStatic.staticMapURL(for: colorScheme.mapStyle, coordinates: coordinates, zoomLevel: zoomLevel, size: imageSize, attribution: mapTilerAttributionPlacement) { AsyncImage(url: url) { phase in switch phase { case .empty: Image("mapBlurred") + .resizable() + .aspectRatio(contentMode: .fill) case .success(let image): ZStack { image @@ -55,17 +62,16 @@ struct MapLibreStaticMapView: View { EmptyView() } } - .id(attempt) - .frame(width: width, height: height) + .id(fetchAttempt) .clipped() } else { Image("mapBlurred") } } - var errorView: some View { + private var errorView: some View { Button { - attempt += 1 + fetchAttempt += 1 } label: { ZStack { Image("mapBlurred") @@ -95,8 +101,9 @@ struct MapLibreStaticMapView_Previews: PreviewProvider { static var previews: some View { MapLibreStaticMapView(coordinates: CLLocationCoordinate2D(), zoomLevel: 15, - mapTilerStatic: MapTilerStaticMapMock(), - height: 150, width: 300) { + imageSize: .init(width: 300, height: 200), + attributionPlacement: .bottomLeft, + mapTilerStatic: MapTilerStaticMapMock()) { Image(systemName: "mappin.circle.fill") .padding(.bottom, 35) } @@ -104,7 +111,7 @@ struct MapLibreStaticMapView_Previews: PreviewProvider { } private struct MapTilerStaticMapMock: MapTilerStaticMapProtocol { - func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize) -> URL? { + func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize, attribution: MapTilerAttributionPlacement) -> URL? { switch style { case .light: return URL(string: "https://www.maptiler.com/img/cloud/home/map5.webp") diff --git a/ElementX/Sources/Other/MapLibre/MapTilerStaticMap.swift b/ElementX/Sources/Other/MapLibre/MapTilerStaticMap.swift index 3a3906928..d06c916b9 100644 --- a/ElementX/Sources/Other/MapLibre/MapTilerStaticMap.swift +++ b/ElementX/Sources/Other/MapLibre/MapTilerStaticMap.swift @@ -27,7 +27,7 @@ struct MapTilerStaticMap: MapTilerStaticMapProtocol { self.key = key } - func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize) -> URL? { + func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize, attribution: MapTilerAttributionPlacement) -> URL? { var path: String switch style { case .light: @@ -38,7 +38,8 @@ struct MapTilerStaticMap: MapTilerStaticMapProtocol { path.append(String(format: "/static/%f,%f,%f/%dx%d@2x.png", coordinates.longitude, coordinates.latitude, zoomLevel, Int(size.width), Int(size.height))) - guard let url = URL(string: path) else { return nil } + guard var url = URL(string: path) else { return nil } + url.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)]) let authorization = MapTilerAuthorization(key: key) return authorization.authorizeURL(url) } diff --git a/ElementX/Sources/Other/MapLibre/MapTilerStaticMapProtocol.swift b/ElementX/Sources/Other/MapLibre/MapTilerStaticMapProtocol.swift index ad0010af8..790fdc70b 100644 --- a/ElementX/Sources/Other/MapLibre/MapTilerStaticMapProtocol.swift +++ b/ElementX/Sources/Other/MapLibre/MapTilerStaticMapProtocol.swift @@ -17,5 +17,9 @@ import CoreLocation protocol MapTilerStaticMapProtocol { - func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize) -> URL? + func staticMapURL(for style: MapTilerStyle, + coordinates: CLLocationCoordinate2D, + zoomLevel: Double, + size: CGSize, + attribution: MapTilerAttributionPlacement) -> URL? } diff --git a/ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift b/ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift new file mode 100644 index 000000000..8a62da02a --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift @@ -0,0 +1,38 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct LocationPinView: View { + var body: some View { + Image(Asset.Images.locationPin.name) + .alignmentGuide(VerticalAlignment.center) { dimensions in + dimensions[.bottom] + } + .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 5) + } +} + +struct LocationPinView_Previews: PreviewProvider { + static var previews: some View { + VStack(spacing: 30) { + LocationPinView() + + LocationPinView() + .colorScheme(.dark) + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift index 30e99a16f..bdde38654 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift @@ -34,7 +34,7 @@ struct TimelineReplyView: View { ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, - icon: .init(systemIconName: "waveform", cornerRadii: iconCornerRadii)) + icon: .init(kind: .systemIcon("waveform"), cornerRadii: iconCornerRadii)) case .emote(let content): ReplyView(sender: sender, plainBody: content.body, @@ -43,12 +43,12 @@ struct TimelineReplyView: View { ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, - icon: .init(systemIconName: "doc.text.fill", cornerRadii: iconCornerRadii)) + icon: .init(kind: .systemIcon("waveform"), cornerRadii: iconCornerRadii)) case .image(let content): ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, - icon: .init(mediaSource: content.thumbnailSource ?? content.source, cornerRadii: iconCornerRadii)) + icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii)) case .notice(let content): ReplyView(sender: sender, plainBody: content.body, @@ -61,7 +61,12 @@ struct TimelineReplyView: View { ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, - icon: .init(mediaSource: content.thumbnailSource, cornerRadii: iconCornerRadii)) + icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) }) + case .location: + ReplyView(sender: sender, + plainBody: L10n.commonSharedLocation, + formattedBody: nil, + icon: .init(kind: .icon(Asset.Images.locationPin.name), cornerRadii: iconCornerRadii)) } default: LoadingReplyView() @@ -87,8 +92,13 @@ struct TimelineReplyView: View { private struct ReplyView: View { struct Icon { - var mediaSource: MediaSourceProxy? - var systemIconName: String? + enum Kind { + case mediaSource(MediaSourceProxy) + case systemIcon(String) + case icon(String) + } + + let kind: Kind let cornerRadii: Double } @@ -102,7 +112,7 @@ struct TimelineReplyView: View { var icon: Icon? var isTextOnly: Bool { - icon?.mediaSource == nil && icon?.systemIconName == nil + icon == nil } /// The string shown as the message preview. @@ -141,22 +151,28 @@ struct TimelineReplyView: View { @ViewBuilder private var iconView: some View { - if let mediaSource = icon?.mediaSource { - LoadableImage(mediaSource: mediaSource, - size: .init(width: imageContainerSize, - height: imageContainerSize), - imageProvider: context.imageProvider) { - Image(systemName: "photo") - .padding(4.0) + if let icon { + switch icon.kind { + case .mediaSource(let mediaSource): + LoadableImage(mediaSource: mediaSource, + size: .init(width: imageContainerSize, + height: imageContainerSize), + imageProvider: context.imageProvider) { + Image(systemName: "photo") + .padding(4.0) + } + .aspectRatio(contentMode: .fill) + case .systemIcon(let systemIconName): + Image(systemName: systemIconName) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(8.0) + case .icon(let iconName): + Image(iconName) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(8.0) } - .aspectRatio(contentMode: .fill) - } - - if let systemIconName = icon?.systemIconName { - Image(systemName: systemIconName) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(8.0) } } } @@ -211,6 +227,9 @@ struct TimelineReplyView_Previews: PreviewProvider { duration: 0, source: nil, thumbnailSource: imageSource)))) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .location(.init(body: "", geoURI: nil)))) } .environmentObject(viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 301b23eae..4fde35c45 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -122,7 +122,7 @@ struct TimelineItemBubbledStylerView: View { @ViewBuilder var styledContent: some View { - if isMediaType { + if shouldFillBubble { contentWithTimestamp .bubbleStyle(inset: false, cornerRadius: cornerRadius, @@ -138,7 +138,7 @@ struct TimelineItemBubbledStylerView: View { @ViewBuilder var contentWithTimestamp: some View { - if isTextItem || isMediaType { + if isTextItem || shouldFillBubble { ZStack(alignment: .bottomTrailing) { contentWithReply interactiveLocalizedSendInfo @@ -165,7 +165,7 @@ struct TimelineItemBubbledStylerView: View { @ViewBuilder var backgroundedLocalizedSendInfo: some View { - if isMediaType { + if shouldFillBubble { localizedSendInfo .padding(.horizontal, 4) .padding(.vertical, 2) @@ -194,7 +194,7 @@ struct TimelineItemBubbledStylerView: View { } .font(.compound.bodyXS) .foregroundColor(timelineItem.properties.deliveryStatus == .sendingFailed ? .compound.textCriticalPrimary : .compound.textSecondary) - .padding(.bottom, isMediaType ? 0 : -4) + .padding(.bottom, shouldFillBubble ? 0 : -4) } @ViewBuilder @@ -230,8 +230,17 @@ struct TimelineItemBubbledStylerView: View { return timelineGroupStyle == .single || timelineGroupStyle == .first ? 8 : 0 } - private var isMediaType: Bool { - timelineItem is ImageRoomTimelineItem || timelineItem is VideoRoomTimelineItem || timelineItem is StickerRoomTimelineItem + private var shouldFillBubble: Bool { + switch timelineItem { + case is ImageRoomTimelineItem, + is VideoRoomTimelineItem, + is StickerRoomTimelineItem: + return true + case let locationTimelineItem as LocationRoomTimelineItem: + return locationTimelineItem.content.geoURI != nil + default: + return false + } } private var alignment: HorizontalAlignment { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift new file mode 100644 index 000000000..6791dd31a --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift @@ -0,0 +1,78 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct LocationRoomTimelineView: View { + let timelineItem: LocationRoomTimelineItem + @Environment(\.timelineStyle) var timelineStyle + + var body: some View { + TimelineStyler(timelineItem: timelineItem) { + if let geoURI = timelineItem.content.geoURI { + let mapSize: CGSize = .init(width: 292, height: 188) + MapLibreStaticMapView(geoURI: geoURI, size: mapSize) { + LocationPinView() + } + .frame(width: mapSize.width, height: mapSize.height) + } else { + FormattedBodyText(text: timelineItem.body) + } + } + } +} + +private extension MapLibreStaticMapView { + init(geoURI: GeoURI, size: CGSize, @ViewBuilder pinAnnotationView: () -> PinAnnotation) { + self.init(coordinates: .init(latitude: geoURI.latitude, longitude: geoURI.longitude), + zoomLevel: 15, + imageSize: size, + attributionPlacement: .bottomLeft, + mapTilerStatic: MapTilerStaticMap(key: ServiceLocator.shared.settings.mapTilerApiKey, + lightURL: ServiceLocator.shared.settings.lightTileMapStyleURL, + darkURL: ServiceLocator.shared.settings.darkTileMapStyleURL), + pinAnnotationView: pinAnnotationView) + } +} + +struct LocationRoomTimelineView_Previews: PreviewProvider { + static let viewModel = RoomScreenViewModel.mock + + static var previews: some View { + body + .environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) + } + + @ViewBuilder + static var body: some View { + LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + sender: .init(id: "Bob"), + content: .init(body: "Fallback geo uri description", geoURI: nil))) + + LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + sender: .init(id: "Bob"), + content: .init(body: "Fallback geo uri description", geoURI: .init(latitude: 41.902782, longitude: 12.496366)))) + } +} diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 8365ccafb..bd23483a4 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -29,6 +29,7 @@ struct DeveloperOptionsScreenViewStateBindings { var userSuggestionsEnabled: Bool var readReceiptsEnabled: Bool var isEncryptionSyncEnabled: Bool + var locationEventsEnabled: Bool } enum DeveloperOptionsScreenViewAction { @@ -36,5 +37,6 @@ enum DeveloperOptionsScreenViewAction { case changedUserSuggestionsEnabled case changedReadReceiptsEnabled case changedIsEncryptionSyncEnabled + case changedLocationEventsEnabled case clearCache } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index ad8d109eb..7fcc287f7 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -28,7 +28,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents, userSuggestionsEnabled: appSettings.userSuggestionsEnabled, readReceiptsEnabled: appSettings.readReceiptsEnabled, - isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled) + isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled, + locationEventsEnabled: appSettings.locationEventsEnabled) let state = DeveloperOptionsScreenViewState(bindings: bindings) super.init(initialViewState: state) @@ -48,6 +49,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings.readReceiptsEnabled = state.bindings.readReceiptsEnabled case .changedIsEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled = state.bindings.isEncryptionSyncEnabled + case .changedLocationEventsEnabled: + appSettings.locationEventsEnabled = state.bindings.locationEventsEnabled case .clearCache: callback?(.clearCache) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index b90a3a94e..9a8da637f 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -52,6 +52,13 @@ struct DeveloperOptionsScreen: View { .onChange(of: context.isEncryptionSyncEnabled) { _ in context.send(viewAction: .changedIsEncryptionSyncEnabled) } + + Toggle(isOn: $context.locationEventsEnabled) { + Text("Location events in timeline") + } + .onChange(of: context.locationEventsEnabled) { _ in + context.send(viewAction: .changedLocationEventsEnabled) + } } Section { diff --git a/ElementX/Sources/Services/Timeline/GeoURI.swift b/ElementX/Sources/Services/Timeline/GeoURI.swift new file mode 100644 index 000000000..c5e02a805 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/GeoURI.swift @@ -0,0 +1,73 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A structure that parses a geo URI (i.e. geo:53.99803101552848,-8.25347900390625;u=10) and constructs their constituent parts. +struct GeoURI: Hashable { + // MARK: - Properties + + let latitude: Double + let longitude: Double + let uncertainty: Double? + + // MARK: - Setup + + init?(string: String) { + guard let parsedURI = Self.parseGeoURI(from: string) else { + MXLog.warning("\(Self.self) failed to parse the string: \(string)") + return nil + } + self = parsedURI + } + + init(latitude: Double, longitude: Double, uncertainty: Double? = nil) { + self.latitude = latitude + self.longitude = longitude + self.uncertainty = uncertainty + } + + var string: String { + if let uncertainty { + return "geo:\(latitude),\(longitude);u=\(uncertainty)" + } else { + return "geo:\(latitude),\(longitude)" + } + } + + // MARK: - Private + + // Parse a geo URI string like "geo:53.99803101552848,-8.25347900390625;u=10" + private static func parseGeoURI(from string: String) -> GeoURI? { + guard + let matchOutput = try? RegexGeoURI.standard.wholeMatch(in: string)?.output, + let latitude = Double(matchOutput.latitude), + let longitude = Double(matchOutput.longitude) + else { + return nil + } + + let uncertainty = matchOutput.uncertainty.flatMap(Double.init) + return .init(latitude: latitude, longitude: longitude, uncertainty: uncertainty) + } +} + +// swiftlint:disable:next large_tuple +private typealias RegexGeoURI = Regex<(Substring, latitude: Substring, longitude: Substring, uncertainty: Substring?)> + +private extension RegexGeoURI { + static let standard: Self = /geo:(?-?\d+(?:\.\d+)?),(?-?\d+(?:\.\d+)?)(?:;u=(?\d+(?:\.\d+)?))?/ +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift index 2d91d74b4..dea98ddde 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift @@ -24,6 +24,7 @@ enum EventBasedMessageTimelineItemContentType: Hashable { case notice(NoticeRoomTimelineItemContent) case text(TextRoomTimelineItemContent) case video(VideoRoomTimelineItemContent) + case location(LocationRoomTimelineItemContent) } protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift new file mode 100644 index 000000000..f54b86f1a --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift @@ -0,0 +1,39 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Hashable { + let id: String + + let timestamp: String + let isOutgoing: Bool + let isEditable: Bool + + let sender: TimelineItemSender + + let content: LocationRoomTimelineItemContent + + var body: String { + content.body + } + + var replyDetails: TimelineItemReplyDetails? + + var properties = RoomTimelineItemProperties() + + var contentType: EventBasedMessageTimelineItemContentType { + .location(content) + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift new file mode 100644 index 000000000..8c1e317e9 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift @@ -0,0 +1,20 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +struct LocationRoomTimelineItemContent: Hashable { + let body: String + let geoURI: GeoURI? +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 48b629f5b..e9be1b576 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -36,7 +36,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { self.stateEventStringBuilder = stateEventStringBuilder } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol? { let isOutgoing = eventItemProxy.isOwn @@ -74,8 +74,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return buildEmoteTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing) case .audio(let content): return buildAudioTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing) - case .location: - return nil + case .location(let content): + guard ServiceLocator.shared.settings.locationEventsEnabled else { + return nil + } + return buildLocationTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing) case .none: return nil } @@ -298,6 +301,24 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), transactionID: eventItemProxy.transactionID)) } + + private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy, + _ messageTimelineItem: Message, + _ messageContent: LocationContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { + LocationRoomTimelineItem(id: eventItemProxy.id, + timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), + isOutgoing: isOutgoing, + isEditable: eventItemProxy.isEditable, + sender: eventItemProxy.sender, + content: buildLocationTimelineItemContent(messageContent), + replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), + properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + reactions: aggregateReactions(eventItemProxy.reactions), + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), + transactionID: eventItemProxy.transactionID)) + } private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] { reactions.map { reaction in @@ -334,7 +355,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) } - + private func buildImageTimelineItemContent(_ messageContent: ImageMessageContent) -> ImageRoomTimelineItemContent { let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } let width = messageContent.info?.width.map(CGFloat.init) @@ -354,7 +375,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { blurhash: messageContent.info?.blurhash, contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) } - + private func buildVideoTimelineItemContent(_ messageContent: VideoMessageContent) -> VideoRoomTimelineItemContent { let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } let width = messageContent.info?.width.map(CGFloat.init) @@ -375,7 +396,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { blurhash: messageContent.info?.blurhash, contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) } - + + private func buildLocationTimelineItemContent(_ locationContent: LocationContent) -> LocationRoomTimelineItemContent { + LocationRoomTimelineItemContent(body: locationContent.body, geoURI: .init(string: locationContent.geoUri)) + } + private func buildFileTimelineItemContent(_ messageContent: FileMessageContent) -> FileRoomTimelineItemContent { let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } @@ -490,8 +515,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyContent = .text(buildTextTimelineItemContent(content)) case .video(let content): replyContent = .video(buildVideoTimelineItemContent(content)) - case .location: - return nil + case .location(let content): + replyContent = .location(buildLocationTimelineItemContent(content)) case .none: return nil } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift index 781186458..cbb3812ad 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -35,6 +35,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { case timelineStart(TimelineStartRoomTimelineItem, TimelineGroupStyle) case state(StateRoomTimelineItem, TimelineGroupStyle) case group(CollapsibleTimelineItem, TimelineGroupStyle) + case location(LocationRoomTimelineItem, TimelineGroupStyle) // swiftlint:disable:next cyclomatic_complexity init(timelineItem: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) { @@ -73,6 +74,8 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { self = .state(item, groupStyle) case let item as CollapsibleTimelineItem: self = .group(item, groupStyle) + case let item as LocationRoomTimelineItem: + self = .location(item, groupStyle) default: fatalError("Unknown timeline item") } @@ -96,7 +99,8 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { .unsupported(let item as RoomTimelineItemProtocol, _), .timelineStart(let item as RoomTimelineItemProtocol, _), .state(let item as RoomTimelineItemProtocol, _), - .group(let item as RoomTimelineItemProtocol, _): + .group(let item as RoomTimelineItemProtocol, _), + .location(let item as RoomTimelineItemProtocol, _): return item.id } } @@ -114,9 +118,14 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { .encrypted(let item as EventBasedTimelineItemProtocol, _), .sticker(let item as EventBasedTimelineItemProtocol, _), .unsupported(let item as EventBasedTimelineItemProtocol, _), - .state(let item as EventBasedTimelineItemProtocol, _): + .state(let item as EventBasedTimelineItemProtocol, _), + .location(let item as EventBasedTimelineItemProtocol, _): return item.properties.deliveryStatus == .sending || item.properties.deliveryStatus == .sendingFailed - default: + case .separator, + .readMarker, + .paginationIndicator, + .timelineStart, + .group: return false } } @@ -124,7 +133,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { /// Whether or not it is possible to send a reaction to this timeline item. var isReactable: Bool { switch self { - case .text, .image, .video, .audio, .file, .emote, .notice, .sticker: + case .text, .image, .video, .audio, .file, .emote, .notice, .sticker, .location: return true case .redacted, .encrypted, .unsupported, .state: // Event based items that aren't reactable return false @@ -178,6 +187,8 @@ extension RoomTimelineViewProvider: View { StateRoomTimelineView(timelineItem: item) case .group(let item, _): CollapsibleRoomTimelineView(timelineItem: item) + case .location(let item, _): + LocationRoomTimelineView(timelineItem: item) } } @@ -199,7 +210,8 @@ extension RoomTimelineViewProvider: View { .unsupported(_, let groupStyle), .timelineStart(_, let groupStyle), .state(_, let groupStyle), - .group(_, let groupStyle): + .group(_, let groupStyle), + .location(_, let groupStyle): return groupStyle } } diff --git a/UnitTests/Sources/GeoURITests.swift b/UnitTests/Sources/GeoURITests.swift new file mode 100644 index 000000000..2657def86 --- /dev/null +++ b/UnitTests/Sources/GeoURITests.swift @@ -0,0 +1,85 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import ElementX +import XCTest + +final class GeoURITests: XCTestCase { + func testValidPositiveCoordinates() throws { + let string = "geo:53.99803101552848,8.25347900390625;u=10.123" + let uri = try XCTUnwrap(GeoURI(string: string)) + XCTAssertEqual(uri.latitude, 53.99803101552848) + XCTAssertEqual(uri.longitude, 8.25347900390625) + XCTAssertEqual(uri.uncertainty, 10.123) + XCTAssertEqual(uri.string, string) + } + + func testValidNegativeCoordinates() throws { + let string = "geo:-53.99803101552848,-8.25347900390625;u=10.0" + let uri = try XCTUnwrap(GeoURI(string: string)) + XCTAssertEqual(uri.latitude, -53.99803101552848) + XCTAssertEqual(uri.longitude, -8.25347900390625) + XCTAssertEqual(uri.uncertainty, 10) + XCTAssertEqual(uri.string, string) + } + + func testValidMixedCoordinates() throws { + let string = "geo:53.99803101552848,-8.25347900390625;u=10.0" + let uri = try XCTUnwrap(GeoURI(string: string)) + XCTAssertEqual(uri.latitude, 53.99803101552848) + XCTAssertEqual(uri.longitude, -8.25347900390625) + XCTAssertEqual(uri.uncertainty, 10) + XCTAssertEqual(uri.string, string) + } + + func testValidCoordinatesNoUncertainty() throws { + let string = "geo:53.99803101552848,-8.25347900390625" + let uri = try XCTUnwrap(GeoURI(string: string)) + XCTAssertEqual(uri.latitude, 53.99803101552848) + XCTAssertEqual(uri.longitude, -8.25347900390625) + XCTAssertNil(uri.uncertainty) + XCTAssertEqual(uri.string, string) + } + + func testValidIntegerCoordinates() throws { + let string = "geo:53,-8;u=35" + let uri = try XCTUnwrap(GeoURI(string: string)) + XCTAssertEqual(uri.latitude, 53) + XCTAssertEqual(uri.longitude, -8) + XCTAssertEqual(uri.uncertainty, 35) + XCTAssertEqual(uri.string, "geo:53.0,-8.0;u=35.0") + } + + func testInvalidURI1() { + let string = "geo:53.99803101552848,-8.25347900390625;" // final ; without a u=number + XCTAssertNil(GeoURI(string: string)) + } + + func testInvalidURI2() { + let string = "geo:53.99803101552848, -8.25347900390625;" // spaces in the middle + XCTAssertNil(GeoURI(string: string)) + } + + func testInvalidURI3() { + let string = "geo:+53.99803101552848,-8.25347900390625" // '+' before a number + XCTAssertNil(GeoURI(string: string)) + } + + func testInvalidURI4() { + let string = "geo:53.99803101552848,-8.25347900390625;u=-20" // u is negative + XCTAssertNil(GeoURI(string: string)) + } +} diff --git a/UnitTests/Sources/InvitesScreenViewModelTests.swift b/UnitTests/Sources/InvitesScreenViewModelTests.swift index 76b28f935..24a18ec93 100644 --- a/UnitTests/Sources/InvitesScreenViewModelTests.swift +++ b/UnitTests/Sources/InvitesScreenViewModelTests.swift @@ -34,14 +34,14 @@ class InvitesScreenViewModelTests: XCTestCase { func testEmptyState() async throws { setupViewModel() - _ = await context.$viewState.values.first(where: { $0.invites != nil }) + _ = await context.$viewState.values.first() let invites = try XCTUnwrap(context.viewState.invites) XCTAssertTrue(invites.isEmpty) } func testListState() async throws { setupViewModel(roomSummaries: .mockInvites) - _ = await context.$viewState.values.first(where: { $0.invites != nil }) + _ = await context.$viewState.values.first(where: { !$0.invites.isEmpty }) let invites = try XCTUnwrap(context.viewState.invites) XCTAssertEqual(invites.count, 2) }