mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Render location events in the timeline (#1136)
* Start location integration in the timeline * Add warning * Add LocationRoomTimelineView * Add GeoURI * Refine LocationRoomTimelineView * Refine LocationRoomTimelineView * Add LocationRoomTimelineView previews * Cleanup code * Mock reply * Add FF for location events * Update project file * Fix MapLibreStaticMapView placeholder * Add MapTilerAttribution * Add LocationPinView * Fix layout * Start reply rendering * Refactor ReplyView.Icon * Add localisations * Fix reactions * Amend GeoURI regex * Add log * Fix tests * Attribution placement refactor * Cleanup * Replace NSRegularExpression with a regex * Cleanup * Update unit tests * isMediaType -> shouldFillBubble
This commit is contained in:
parent
40fab35dd7
commit
07a6235fac
@ -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 = "<group>"; };
|
||||
18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = "<group>"; };
|
||||
18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||
190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = "<group>"; };
|
||||
1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = "<group>"; };
|
||||
1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = "<group>"; };
|
||||
1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; };
|
||||
1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = "<group>"; };
|
||||
1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = "<group>"; };
|
||||
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyMock.swift; sourceTree = "<group>"; };
|
||||
1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = "<group>"; };
|
||||
1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = "<group>"; };
|
||||
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = "<group>"; };
|
||||
1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@ -1038,6 +1047,7 @@
|
||||
7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
||||
748AE77AC3B0A01223033B87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = "<group>"; };
|
||||
@ -1256,6 +1266,7 @@
|
||||
CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = "<group>"; };
|
||||
CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = "<group>"; };
|
||||
CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
||||
CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
|
||||
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
|
||||
@ -1282,6 +1293,7 @@
|
||||
D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = "<group>"; };
|
||||
D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
DA10BE264552E23946FF0499 /* LocationPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPinView.swift; sourceTree = "<group>"; };
|
||||
DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; };
|
||||
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; };
|
||||
DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsKeys.swift; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
22
ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/Contents.json
vendored
Normal file
22
ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/Contents.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
170
ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin-dark.pdf
vendored
Normal file
170
ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin-dark.pdf
vendored
Normal file
@ -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
|
170
ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin.pdf
vendored
Normal file
170
ElementX/Resources/Assets.xcassets/Images/location-pin.imageset/location-pin.pdf
vendored
Normal file
@ -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
|
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -21,27 +21,34 @@ struct MapLibreStaticMapView<PinAnnotation: View>: 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<PinAnnotation: View>: 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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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?
|
||||
}
|
||||
|
38
ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift
Normal file
38
ElementX/Sources/Other/SwiftUI/Views/LocationPinView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
@ViewBuilder
|
||||
var styledContent: some View {
|
||||
if isMediaType {
|
||||
if shouldFillBubble {
|
||||
contentWithTimestamp
|
||||
.bubbleStyle(inset: false,
|
||||
cornerRadius: cornerRadius,
|
||||
@ -138,7 +138,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
@ViewBuilder
|
||||
var contentWithTimestamp: some View {
|
||||
if isTextItem || isMediaType {
|
||||
if isTextItem || shouldFillBubble {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
contentWithReply
|
||||
interactiveLocalizedSendInfo
|
||||
@ -165,7 +165,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
@ViewBuilder
|
||||
var backgroundedLocalizedSendInfo: some View {
|
||||
if isMediaType {
|
||||
if shouldFillBubble {
|
||||
localizedSendInfo
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 2)
|
||||
@ -194,7 +194,7 @@ struct TimelineItemBubbledStylerView<Content: View>: 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<Content: View>: 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 {
|
||||
|
@ -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))))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
73
ElementX/Sources/Services/Timeline/GeoURI.swift
Normal file
73
ElementX/Sources/Services/Timeline/GeoURI.swift
Normal file
@ -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:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
|
||||
}
|
@ -24,6 +24,7 @@ enum EventBasedMessageTimelineItemContentType: Hashable {
|
||||
case notice(NoticeRoomTimelineItemContent)
|
||||
case text(TextRoomTimelineItemContent)
|
||||
case video(VideoRoomTimelineItemContent)
|
||||
case location(LocationRoomTimelineItemContent)
|
||||
}
|
||||
|
||||
protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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?
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
85
UnitTests/Sources/GeoURITests.swift
Normal file
85
UnitTests/Sources/GeoURITests.swift
Normal file
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user