diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1e8f39027..a3785034c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -43,7 +43,7 @@ 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; }; - 10516CF20E8B5852F4C444FD /* AnalyticsPromptScreenCheckmarkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1253D3E9395A0493DB944B9 /* AnalyticsPromptScreenCheckmarkItem.swift */; }; + 10516CF20E8B5852F4C444FD /* RoundedLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1253D3E9395A0493DB944B9 /* RoundedLabelItem.swift */; }; 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; }; 126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */; }; 12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */; }; @@ -56,6 +56,7 @@ 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; }; 155063E980E763D4910EA3CF /* Analytics+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */; }; 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; + 15705159D584B189BC48CE50 /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC2812E6AB8C477A199E565 /* WelcomeScreen.swift */; }; 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; }; 158A2D528CC78C4E7A8ED608 /* MockRoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */; }; 15D867E638BFD0E5E71DB1EF /* List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFEF3AC64B1358083F76B8B /* List.swift */; }; @@ -85,6 +86,7 @@ 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; + 2185C1F6724C78FFF355D6FA /* WelcomeScreenScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */; }; 2198B4458AFF69102BBCC370 /* RoomTimelineItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7D3C686C214470F4911618 /* RoomTimelineItemViewModel.swift */; }; 21BF2B7CEDFE3CA67C5355AD /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = C733D11B421CFE3A657EF230 /* test_image.png */; }; 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; @@ -149,6 +151,7 @@ 36AD4DD4C798E22584ED3200 /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; }; 36CD6E11B37396E14F032CB6 /* Flow in Frameworks */ = {isa = PBXBuildFile; productRef = 4D7E6BFC89715FC3CF0349D0 /* Flow */; }; 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; }; + 383055C6ABE5BE058CEE1DDB /* WelcomeScreenScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FE5EF0AFFE360C66420AAE /* WelcomeScreenScreenCoordinator.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; 38896D54D6D675534E606195 /* RoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */; }; 3910D3A2EF98587C0E7B9CCB /* EmojiMartEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F7F3CF7E70518BD7D25E04 /* EmojiMartEmoji.swift */; }; @@ -333,6 +336,7 @@ 7E3C34BC10936AD4F77975F4 /* EmojiMartJSONLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39001365B76B89983FDB7AD8 /* EmojiMartJSONLoader.swift */; }; 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */; }; 7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */; }; + 7F02063FB3D1C3E5601471A1 /* WelcomeScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */; }; 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; }; @@ -401,6 +405,7 @@ 9408CE8B8865C0C8DD4C9869 /* NoticeRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */; }; 9462C62798F47E39DCC182D2 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA89A2DD51B6BBE1DA55E263 /* Application.swift */; }; 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; + 94CEF587A3994A36A46D8334 /* WelcomeScreenScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7101698791B321A76F552804 /* WelcomeScreenScreenViewModelProtocol.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 95690DDD9D547D3D842ACBE3 /* AnalyticsSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */; }; 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; }; @@ -536,6 +541,7 @@ BB784A02BADB03C820617A46 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */; }; BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; BD203FC6A7AE7637EA003643 /* RoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */; }; + BD2BF1EC73FFB0C01552ECDA /* WelcomeScreenScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB782CE6176A5D2C082EC5D /* WelcomeScreenScreenModels.swift */; }; BD6D98676111DA8FC2BE4908 /* InvitesScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */; }; BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */; }; BDA68E8D95B2B24B28825B8B /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */; }; @@ -618,6 +624,7 @@ D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; }; DC08ADC41E792086A340A8B3 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */; }; + DC1BB5EE5F4D9B6A1F98A77A /* WelcomeScreenScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC2E8E1B20BB2EA07B0B61E /* WelcomeScreenScreenViewModel.swift */; }; DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */; }; DE0BBA736557B42BC0DA6CBF /* TimelineEventProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B62EE933FC3D5651AF4607 /* TimelineEventProxy.swift */; }; DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; @@ -814,6 +821,7 @@ 0F19DBE940499D3E3DD405D8 /* RoomMemberDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenUITests.swift; sourceTree = ""; }; 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = ""; }; 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenCoordinator.swift; sourceTree = ""; }; + 0FB782CE6176A5D2C082EC5D /* WelcomeScreenScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenModels.swift; sourceTree = ""; }; 1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = ""; }; 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = ""; }; @@ -1009,6 +1017,7 @@ 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConsentState.swift; sourceTree = ""; }; 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItem.swift; sourceTree = ""; }; 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = ""; }; + 57FE5EF0AFFE360C66420AAE /* WelcomeScreenScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenCoordinator.swift; sourceTree = ""; }; 584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelTests.swift; sourceTree = ""; }; 592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreStaticMapView.swift; sourceTree = ""; }; @@ -1075,6 +1084,7 @@ 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = ""; }; + 7101698791B321A76F552804 /* WelcomeScreenScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModelProtocol.swift; sourceTree = ""; }; 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = ""; }; 71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineControllerFactory.swift; sourceTree = ""; }; 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreen.swift; sourceTree = ""; }; @@ -1115,6 +1125,7 @@ 84816E0D2F34E368BF64FA60 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModel.swift; sourceTree = ""; }; 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunnerTests.swift; sourceTree = ""; }; + 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModelTests.swift; sourceTree = ""; }; 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; 86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1125,6 +1136,7 @@ 893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = ""; }; 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = ""; }; 897DF5E9A70CE05A632FC8AF /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; + 8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenUITests.swift; sourceTree = ""; }; 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -1165,6 +1177,7 @@ 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = ""; }; 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; + 9AC2812E6AB8C477A199E565 /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = ""; }; 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreen.swift; sourceTree = ""; }; 9B65A314DF40B6BBF775C2BC /* AnalyticsPromptScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenCoordinator.swift; sourceTree = ""; }; 9B663BE498BB39EADC24025D /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = ""; }; @@ -1352,7 +1365,7 @@ DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineItem.swift; sourceTree = ""; }; E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerAuthorization.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; - E1253D3E9395A0493DB944B9 /* AnalyticsPromptScreenCheckmarkItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenCheckmarkItem.swift; sourceTree = ""; }; + E1253D3E9395A0493DB944B9 /* RoundedLabelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLabelItem.swift; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = ""; }; E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; @@ -1427,6 +1440,7 @@ FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; + FEC2E8E1B20BB2EA07B0B61E /* WelcomeScreenScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModel.swift; sourceTree = ""; }; FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsScreen.swift; sourceTree = ""; }; FFECCE59967018204876D0A5 /* LocationMarkerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMarkerView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1554,7 +1568,7 @@ isa = PBXGroup; children = ( 5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */, - E1253D3E9395A0493DB944B9 /* AnalyticsPromptScreenCheckmarkItem.swift */, + E1253D3E9395A0493DB944B9 /* RoundedLabelItem.swift */, ); path = View; sourceTree = ""; @@ -2427,6 +2441,7 @@ 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */, BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */, C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */, + 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */, 53280D2292E6C9C7821773FD /* UserSession */, 70C5B842301AC281DF374E41 /* Extensions */, 7583EAC171059A86B767209F /* MediaProvider */, @@ -2712,6 +2727,18 @@ path = View; sourceTree = ""; }; + 93F4D089C78719B688D576ED /* WelcomeScreenScreen */ = { + isa = PBXGroup; + children = ( + 57FE5EF0AFFE360C66420AAE /* WelcomeScreenScreenCoordinator.swift */, + 0FB782CE6176A5D2C082EC5D /* WelcomeScreenScreenModels.swift */, + FEC2E8E1B20BB2EA07B0B61E /* WelcomeScreenScreenViewModel.swift */, + 7101698791B321A76F552804 /* WelcomeScreenScreenViewModelProtocol.swift */, + C5B85A56479F1382EA5A9913 /* View */, + ); + path = WelcomeScreenScreen; + sourceTree = ""; + }; 9413F680ECDFB2B0DDB0DEF2 /* Packages */ = { isa = PBXGroup; children = ( @@ -2774,6 +2801,7 @@ DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */, F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */, ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */, + 8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */, ); path = Sources; sourceTree = ""; @@ -3154,6 +3182,14 @@ path = CreateRoom; sourceTree = ""; }; + C5B85A56479F1382EA5A9913 /* View */ = { + isa = PBXGroup; + children = ( + 9AC2812E6AB8C477A199E565 /* WelcomeScreen.swift */, + ); + path = View; + sourceTree = ""; + }; CA15BB3F6C62B35AE2C281A9 /* Provider */ = { isa = PBXGroup; children = ( @@ -3321,6 +3357,7 @@ 3153FCA3F4B0E88B16D99D12 /* SessionVerificationScreen */, 70B74A432C241E56A7ACE610 /* Settings */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */, + 93F4D089C78719B688D576ED /* WelcomeScreenScreen */, ); path = Screens; sourceTree = ""; @@ -4010,6 +4047,7 @@ 81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */, 99F8DA4CCC6772EE5FE68E24 /* ViewModelContext.swift in Sources */, FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */, + 7F02063FB3D1C3E5601471A1 /* WelcomeScreenScreenViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4037,7 +4075,7 @@ 8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */, EDC1031A7CFB3406A9DA3175 /* AnalyticsLocationType.swift in Sources */, 9DF3F6318A4402305F5EB869 /* AnalyticsPromptScreen.swift in Sources */, - 10516CF20E8B5852F4C444FD /* AnalyticsPromptScreenCheckmarkItem.swift in Sources */, + 10516CF20E8B5852F4C444FD /* RoundedLabelItem.swift in Sources */, 5F28C9146694B381BB82E18C /* AnalyticsPromptScreenCoordinator.swift in Sources */, 496CC9D59ACFAB84FD9B3B5F /* AnalyticsPromptScreenModels.swift in Sources */, 0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */, @@ -4507,6 +4545,11 @@ 2F66701B15657A87B4AC3A0A /* WaitlistScreenModels.swift in Sources */, CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */, B717A820BE02C6FE2CB53F6E /* WaitlistScreenViewModelProtocol.swift in Sources */, + 15705159D584B189BC48CE50 /* WelcomeScreen.swift in Sources */, + 383055C6ABE5BE058CEE1DDB /* WelcomeScreenScreenCoordinator.swift in Sources */, + BD2BF1EC73FFB0C01552ECDA /* WelcomeScreenScreenModels.swift in Sources */, + DC1BB5EE5F4D9B6A1F98A77A /* WelcomeScreenScreenViewModel.swift in Sources */, + 94CEF587A3994A36A46D8334 /* WelcomeScreenScreenViewModelProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4549,6 +4592,7 @@ 84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */, B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */, 2DA90E38FF4E696825810C1A /* WaitlistScreenUITests.swift in Sources */, + 2185C1F6724C78FFF355D6FA /* WelcomeScreenScreenUITests.swift in Sources */, 588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 5d0b95441..1c0b67d25 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -279,7 +279,7 @@ "screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; "screen_room_details_encryption_enabled_title" = "Message encryption enabled"; "screen_room_details_invite_people_title" = "Invite people"; -"screen_room_details_notification_title" = "Notification"; +"screen_room_details_notification_title" = "Notifications"; "screen_room_details_room_name_label" = "Room name"; "screen_room_details_share_room_title" = "Share room"; "screen_room_details_updating_room" = "Updating room…"; @@ -336,6 +336,12 @@ "screen_waitlist_message_success" = "Welcome to %1$@!"; "screen_waitlist_title" = "You’re almost there."; "screen_waitlist_title_success" = "You're in."; +"screen_welcome_bullet_1" = "Calls, location sharing, search and more will be added later this year."; +"screen_welcome_bullet_2" = "Message history for encrypted rooms won’t be available in this update."; +"screen_welcome_bullet_3" = "We’d love to hear from you, let us know what you think via the settings page."; +"screen_welcome_button" = "Let's go!"; +"screen_welcome_subtitle" = "Here’s what you need to know:"; +"screen_welcome_title" = "Welcome to %1$@!"; "session_verification_banner_message" = "Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards."; "session_verification_banner_title" = "Verify it’s you"; "settings_rageshake" = "Rageshake"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 6baf3e9b2..9d767e1ed 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -33,6 +33,7 @@ final class AppSettings { case readReceiptsEnabled case locationEventsEnabled case shareLocationEnabled + case hasShownWelcomeScreen } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -89,6 +90,9 @@ final class AppSettings { /// The task identifier used for background app refresh. Also used in main target's the Info.plist let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh" + + @UserPreference(key: UserDefaultsKeys.hasShownWelcomeScreen, defaultValue: false, storageType: .userDefaults(store)) + var hasShownWelcomeScreen: Bool // MARK: - Authentication @@ -119,7 +123,7 @@ final class AppSettings { /// proxy that it is the first sync (or that an upgrade on the backend will involve a slower sync). @UserPreference(key: UserDefaultsKeys.migratedAccounts, defaultValue: [:], storageType: .userDefaults(store)) var migratedAccounts: [String: Bool] - + // MARK: - Notifications var pusherAppId: String { diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index ce811c0bc..242fd2be1 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -37,7 +37,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private let sidebarNavigationStackCoordinator: NavigationStackCoordinator private let detailNavigationStackCoordinator: NavigationStackCoordinator - + private let selectedRoomSubject = CurrentValueSubject(nil) var callback: ((UserSessionFlowCoordinatorAction) -> Void)? @@ -85,6 +85,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { if appSettings.migratedAccounts[userSession.userID] != true { // Show the migration screen for a new account. stateMachine.processEvent(.startWithMigration) + } else if !appSettings.hasShownWelcomeScreen { + stateMachine.processEvent(.startWithWelcomeScreen) } else { // Otherwise go straight to the home screen. stateMachine.processEvent(.start) @@ -106,7 +108,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { return // Not ready to handle a route. case .roomList: break // Nothing to tidy up on the home screen. - case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen: + case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen, .welcomeScreen: navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated) } @@ -135,13 +137,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { let animated = (context.userInfo as? UserSessionFlowCoordinatorStateMachine.EventUserInfo)?.animated ?? true switch (context.fromState, context.event, context.toState) { case (.initial, .start, .roomList): - self.presentHomeScreen() + presentHomeScreen() case (.initial, .startWithMigration, .migration): - self.presentMigrationScreen() // Full screen cover - self.presentHomeScreen() // Have the home screen ready to show underneath + presentMigrationScreen() // Full screen cover + presentHomeScreen() // Have the home screen ready to show underneath case (.migration, .completeMigration, .roomList): - self.dismissMigrationScreen() + dismissMigrationScreen() + + case (.initial, .startWithWelcomeScreen, .welcomeScreen): + presentHomeScreen() + presentWelcomeScreen() + case (.roomList, .presentWelcomeScreen, .welcomeScreen): + presentWelcomeScreen() + case (.welcomeScreen, .dismissedWelcomeScreen, .roomList): + break case(.roomList, .selectRoom, .roomList): break @@ -154,27 +164,27 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { break case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen): - self.presentSessionVerification(animated: animated) + presentSessionVerification(animated: animated) case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList): break case (.roomList, .showSettingsScreen, .settingsScreen): - self.presentSettingsScreen(animated: animated) + presentSettingsScreen(animated: animated) case (.settingsScreen, .dismissedSettingsScreen, .roomList): break case (.roomList, .feedbackScreen, .feedbackScreen): - self.presentFeedbackScreen(animated: animated) + presentFeedbackScreen(animated: animated) case (.feedbackScreen, .dismissedFeedbackScreen, .roomList): break case (.roomList, .showStartChatScreen, .startChatScreen): - self.presentStartChat(animated: animated) + presentStartChat(animated: animated) case (.startChatScreen, .dismissedStartChatScreen, .roomList): break case (.roomList, .showInvitesScreen, .invitesScreen): - self.presentInvitesList(animated: animated) + presentInvitesList(animated: animated) case (.invitesScreen, .showInvitesScreen, .invitesScreen): break case (.invitesScreen, .closedInvitesScreen, .roomList): @@ -216,6 +226,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private func dismissMigrationScreen() { navigationSplitCoordinator.setFullScreenCoverCoordinator(nil) + + // Not sure why but the full screen closure dismissal closure doesn't seem to work properly + // And not using the DispatchQueue.main results in the the screen getting presented as full screen too. + if !appSettings.hasShownWelcomeScreen { + DispatchQueue.main.async { + self.stateMachine.processEvent(.presentWelcomeScreen) + } + } } // swiftlint:disable:next cyclomatic_complexity @@ -257,6 +275,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { sidebarNavigationStackCoordinator.setRootCoordinator(coordinator) } + + private func presentWelcomeScreen() { + let welcomeScreenCoordinator = WelcomeScreenScreenCoordinator() + welcomeScreenCoordinator.actions.sink { [weak self] action in + switch action { + case .dismiss: + self?.navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + navigationSplitCoordinator.setSheetCoordinator(welcomeScreenCoordinator) { [weak self] in + self?.stateMachine.processEvent(.dismissedWelcomeScreen) + } + } // MARK: Settings diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index d3b4efdac..11aec940d 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -25,6 +25,9 @@ class UserSessionFlowCoordinatorStateMachine { /// Showing the migration screen whilst the proxy performs an initial sync. case migration + + /// Showing the welcome screen. + case welcomeScreen /// Showing the home screen. The `selectedRoomId` represents the timeline shown on the detail panel (if any) case roomList(selectedRoomId: String?) @@ -53,11 +56,15 @@ class UserSessionFlowCoordinatorStateMachine { enum Event: EventType { /// Start the user session flows case start - + /// Starts the user session flows with the welcome screen + case startWithWelcomeScreen /// Start the user session flows with a migration screen. case startWithMigration /// Request to transition from the migration state to the home screen. case completeMigration + + case presentWelcomeScreen + case dismissedWelcomeScreen /// Request presentation for a particular room /// - Parameter roomId:the room identifier @@ -106,8 +113,10 @@ class UserSessionFlowCoordinatorStateMachine { private func configure() { stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)]) stateMachine.addRoutes(event: .startWithMigration, transitions: [.initial => .migration]) + stateMachine.addRoutes(event: .startWithWelcomeScreen, transitions: [.initial => .welcomeScreen]) stateMachine.addRoutes(event: .completeMigration, transitions: [.migration => .roomList(selectedRoomId: nil)]) - + stateMachine.addRoutes(event: .dismissedWelcomeScreen, transitions: [.welcomeScreen => .roomList(selectedRoomId: nil)]) + stateMachine.addRouteMapping { event, fromState, _ in switch (event, fromState) { case (.selectRoom(let roomId), .roomList): @@ -146,6 +155,9 @@ class UserSessionFlowCoordinatorStateMachine { case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)): return .roomList(selectedRoomId: selectedRoomId) + + case (.presentWelcomeScreen, .roomList): + return .welcomeScreen default: return nil diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 386ed83ae..3c459dc49 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -704,7 +704,7 @@ public enum L10n { public static var screenRoomDetailsInvitePeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_invite_people_title") } /// Leave room public static var screenRoomDetailsLeaveRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_leave_room_title") } - /// Notification + /// Notifications public static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") } /// People public static var screenRoomDetailsPeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_people_title") } @@ -850,6 +850,20 @@ public enum L10n { public static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") } /// You're in. public static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") } + /// Calls, location sharing, search and more will be added later this year. + public static var screenWelcomeBullet1: String { return L10n.tr("Localizable", "screen_welcome_bullet_1") } + /// Message history for encrypted rooms won’t be available in this update. + public static var screenWelcomeBullet2: String { return L10n.tr("Localizable", "screen_welcome_bullet_2") } + /// We’d love to hear from you, let us know what you think via the settings page. + public static var screenWelcomeBullet3: String { return L10n.tr("Localizable", "screen_welcome_bullet_3") } + /// Let's go! + public static var screenWelcomeButton: String { return L10n.tr("Localizable", "screen_welcome_button") } + /// Here’s what you need to know: + public static var screenWelcomeSubtitle: String { return L10n.tr("Localizable", "screen_welcome_subtitle") } + /// Welcome to %1$@! + public static func screenWelcomeTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_welcome_title", String(describing: p1)) + } /// Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards. public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") } /// Verify it’s you diff --git a/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift b/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift index a3ca9fff4..f94db5d7f 100644 --- a/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift +++ b/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift @@ -58,17 +58,32 @@ struct AnalyticsPromptScreen: View { .foregroundColor(.compound.textSecondary) } } + + @ViewBuilder + private var checkMark: some View { + Image(systemName: "checkmark.circle") + .symbolVariant(.fill) + .symbolRenderingMode(.palette) + .foregroundStyle(Color.compound.iconAccentTertiary, Color.compound.textOnSolidPrimary) + } /// The list of re-assurances about analytics. private var checkmarkList: some View { VStack(alignment: .leading, spacing: 4) { - AnalyticsPromptScreenCheckmarkItem(title: context.viewState.strings.point1, listPosition: .top) - AnalyticsPromptScreenCheckmarkItem(title: context.viewState.strings.point2, listPosition: .middle) - AnalyticsPromptScreenCheckmarkItem(title: context.viewState.strings.point3, listPosition: .bottom) + checkMarkItem(title: context.viewState.strings.point1, position: .top) + checkMarkItem(title: context.viewState.strings.point2, position: .middle) + checkMarkItem(title: context.viewState.strings.point3, position: .bottom) } .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity) } + + @ViewBuilder + private func checkMarkItem(title: String, position: ListPosition) -> some View { + RoundedLabelItem(title: title, listPosition: position) { + checkMark + } + } /// The stack of enable/disable buttons. private var buttons: some View { diff --git a/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreenCheckmarkItem.swift b/ElementX/Sources/Screens/AnalyticsPromptScreen/View/RoundedLabelItem.swift similarity index 58% rename from ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreenCheckmarkItem.swift rename to ElementX/Sources/Screens/AnalyticsPromptScreen/View/RoundedLabelItem.swift index 2116f1a85..a45c8123b 100644 --- a/ElementX/Sources/Screens/AnalyticsPromptScreen/View/AnalyticsPromptScreenCheckmarkItem.swift +++ b/ElementX/Sources/Screens/AnalyticsPromptScreen/View/RoundedLabelItem.swift @@ -16,33 +16,31 @@ import SwiftUI -struct AnalyticsPromptScreenCheckmarkItem: View { - /// Represents the position of a checkmark item in a list. - enum ListPosition { - case top, middle, bottom - - /// The corners that should be rounded for this position. - var roundedCorners: UIRectCorner { - switch self { - case .top: - return [.topLeft, .topRight] - case .middle: - return [] - case .bottom: - return [.bottomLeft, .bottomRight] - } +/// Represents the position of a checkmark item in a list. +enum ListPosition { + case top, middle, bottom + + /// The corners that should be rounded for this position. + var roundedCorners: UIRectCorner { + switch self { + case .top: + return [.topLeft, .topRight] + case .middle: + return [] + case .bottom: + return [.bottomLeft, .bottomRight] } } - +} + +struct RoundedLabelItem: View { let title: String let listPosition: ListPosition + let iconContent: () -> Icon var body: some View { Label { Text(title) } icon: { - Image(systemName: "checkmark.circle") - .symbolVariant(.fill) - .symbolRenderingMode(.palette) - .foregroundStyle(Color.compound.iconAccentTertiary, Color.compound.textOnSolidPrimary) + iconContent() } .labelStyle(CheckmarkLabelStyle()) .padding(.horizontal, 20) @@ -68,14 +66,32 @@ private struct CheckmarkLabelStyle: LabelStyle { struct AnalyticsPromptScreenCheckmarkItem_Previews: PreviewProvider { static let strings = AnalyticsPromptScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL) + + @ViewBuilder + static var testImage1: some View { + Image(systemName: "circle") + } + + @ViewBuilder + static var testImage2: some View { + Image(systemName: "square") + } static var previews: some View { VStack(alignment: .leading, spacing: 4) { - AnalyticsPromptScreenCheckmarkItem(title: strings.point1, listPosition: .top) - AnalyticsPromptScreenCheckmarkItem(title: strings.point2, listPosition: .middle) - AnalyticsPromptScreenCheckmarkItem(title: "This is a short string.", listPosition: .middle) - AnalyticsPromptScreenCheckmarkItem(title: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.", - listPosition: .bottom) + RoundedLabelItem(title: strings.point1, listPosition: .top) { + testImage1 + } + RoundedLabelItem(title: strings.point2, listPosition: .middle) { + testImage2 + } + RoundedLabelItem(title: "This is a short string.", listPosition: .middle) { + testImage1 + } + RoundedLabelItem(title: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.", + listPosition: .bottom) { + testImage2 + } } .padding() } diff --git a/ElementX/Sources/Screens/Authentication/UIConstants.swift b/ElementX/Sources/Screens/Authentication/UIConstants.swift index d2edcda13..2f0d96993 100644 --- a/ElementX/Sources/Screens/Authentication/UIConstants.swift +++ b/ElementX/Sources/Screens/Authentication/UIConstants.swift @@ -29,7 +29,9 @@ struct UIConstants { /// The padding used to the top of the view for breaker screens that don't have a navigation bar. static let onboardingBreakerScreenTopPadding: CGFloat = 80 - + + static let welcomeScreenTopPadding: CGFloat = 122 + /// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`. static func spacerHeight(in geometry: GeometryProxy) -> CGFloat { max(0, (geometry.size.height - maxContentHeight) / 2) diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift new file mode 100644 index 000000000..665992d79 --- /dev/null +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift @@ -0,0 +1,108 @@ +// +// Copyright 2022 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 WelcomeScreen: View { + @ScaledMetric var iconSize = 20 + @ObservedObject var context: WelcomeScreenScreenViewModel.Context + + var body: some View { + FullscreenDialog(topPadding: UIConstants.welcomeScreenTopPadding) { + mainContent + } bottomContent: { + button + } + .background(OnboardingBackgroundImage()) + .environment(\.backgroundStyle, AnyShapeStyle(Color.clear)) + .onAppear { + context.send(viewAction: .appeared) + } + } + + @ViewBuilder + private var mainContent: some View { + VStack(spacing: 42) { + header + list + } + } + + @ViewBuilder + private var header: some View { + VStack(spacing: 32) { + Image(asset: Asset.Images.launchLogo) + .resizable() + .scaledToFit() + .frame(width: 118, height: 118) + .accessibilityHidden(true) + title + } + } + + @ViewBuilder + private var title: some View { + VStack(spacing: 12) { + Text(context.viewState.title) + .font(Font.compound.headingLGBold) + .foregroundColor(Color.compound.textPrimary) + .multilineTextAlignment(.center) + Text(context.viewState.subtitle) + .font(Font.compound.bodyMD) + .foregroundColor(Color.compound.textPrimary) + .multilineTextAlignment(.center) + } + } + + private var list: some View { + VStack(alignment: .leading, spacing: 4) { + RoundedLabelItem(title: context.viewState.bullet1, listPosition: .top) { + Image(systemName: "exclamationmark.transmission") + .foregroundColor(.compound.iconSecondary) + } + RoundedLabelItem(title: context.viewState.bullet2, listPosition: .middle) { + Image(systemName: "lock") + .foregroundColor(.compound.iconSecondary) + } + RoundedLabelItem(title: context.viewState.bullet3, listPosition: .bottom) { + Image(systemName: "plus.bubble") + .foregroundColor(.compound.iconSecondary) + } + } + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + } + + @ViewBuilder + private var button: some View { + Button { + context.send(viewAction: .doneTapped) + } label: { + Text(context.viewState.buttonTitle) + } + .buttonStyle(.elementAction(.xLarge)) + } +} + +// MARK: - Previews + +struct WelcomeScreen_Previews: PreviewProvider { + static let viewModel = WelcomeScreenScreenViewModel() + + static var previews: some View { + WelcomeScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift new file mode 100644 index 000000000..ee79df7fa --- /dev/null +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift @@ -0,0 +1,51 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +enum WelcomeScreenScreenCoordinatorAction { + case dismiss +} + +final class WelcomeScreenScreenCoordinator: CoordinatorProtocol { + private var viewModel: WelcomeScreenScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init() { + viewModel = WelcomeScreenScreenViewModel() + } + + func start() { + viewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss: + actionsSubject.send(.dismiss) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(WelcomeScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenModels.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenModels.swift new file mode 100644 index 000000000..6d3834ea4 --- /dev/null +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenModels.swift @@ -0,0 +1,35 @@ +// +// Copyright 2022 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 + +enum WelcomeScreenScreenViewModelAction { + case dismiss +} + +struct WelcomeScreenScreenViewState: BindableState { + let title = L10n.screenWelcomeTitle(InfoPlistReader.main.bundleDisplayName) + let subtitle = L10n.screenWelcomeSubtitle + let bullet1 = L10n.screenWelcomeBullet1 + let bullet2 = L10n.screenWelcomeBullet2 + let bullet3 = L10n.screenWelcomeBullet3 + let buttonTitle = L10n.screenWelcomeButton +} + +enum WelcomeScreenScreenViewAction { + case doneTapped + case appeared +} diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenViewModel.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenViewModel.swift new file mode 100644 index 000000000..71f104baf --- /dev/null +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenViewModel.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias WelcomeScreenScreenViewModelType = StateStoreViewModel + +class WelcomeScreenScreenViewModel: WelcomeScreenScreenViewModelType, WelcomeScreenScreenViewModelProtocol { + let appSettings: AppSettings + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(appSettings: AppSettings = ServiceLocator.shared.settings) { + self.appSettings = appSettings + super.init(initialViewState: WelcomeScreenScreenViewState()) + } + + // MARK: - Public + + override func process(viewAction: WelcomeScreenScreenViewAction) { + switch viewAction { + case .doneTapped: + actionsSubject.send(.dismiss) + case .appeared: + appSettings.hasShownWelcomeScreen = true + } + } +} diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenViewModelProtocol.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenViewModelProtocol.swift new file mode 100644 index 000000000..c5a5a2c49 --- /dev/null +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol WelcomeScreenScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: WelcomeScreenScreenViewModelType.Context { get } +} diff --git a/UITests/Sources/WelcomeScreenScreenUITests.swift b/UITests/Sources/WelcomeScreenScreenUITests.swift new file mode 100644 index 000000000..a1495906a --- /dev/null +++ b/UITests/Sources/WelcomeScreenScreenUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 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 ElementX +import XCTest + +@MainActor +class WelcomeScreenScreenUITests: XCTestCase { } diff --git a/UnitTests/Sources/WelcomeScreenScreenViewModelTests.swift b/UnitTests/Sources/WelcomeScreenScreenViewModelTests.swift new file mode 100644 index 000000000..5f28781f5 --- /dev/null +++ b/UnitTests/Sources/WelcomeScreenScreenViewModelTests.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 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 XCTest + +@testable import ElementX + +@MainActor +class WelcomeScreenScreenViewModelTests: XCTestCase { } diff --git a/changelog.d/pr-1259.feature b/changelog.d/pr-1259.feature new file mode 100644 index 000000000..bcc483747 --- /dev/null +++ b/changelog.d/pr-1259.feature @@ -0,0 +1 @@ +Added a welcome screen that will appear only once. \ No newline at end of file