diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 28089a196..0e2332ee4 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -43,6 +43,7 @@ 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */; }; 191161FE9E0DA89704301F37 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; + 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */; }; 1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; @@ -79,6 +80,7 @@ 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; 3772354754450F2B54107E17 /* TemplateSimpleScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4EDB32B97910AAAFE632B2 /* TemplateSimpleScreenViewModelProtocol.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; + 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */; }; 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; }; 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; }; 3C549A0BF39F8A854D45D9FD /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; @@ -129,6 +131,7 @@ 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; }; + 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; }; 6C72F66DA26A0956E9A9077A /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEB3259B2208E5AE5BB3F65 /* Settings.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; @@ -160,6 +163,7 @@ 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; 86C2E93920FD15AD17E193A9 /* BugReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; + 87756CA950ED55870A1AAE8F /* ServerSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */; }; 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; @@ -168,6 +172,7 @@ 8D9F646387DF656EF91EE4CB /* RoomMessageFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93BA4A81B6D893271101F9F0 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */; }; @@ -180,6 +185,7 @@ 9AC5F8142413862A9E3A2D98 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; 9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; + 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */; }; 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */; }; @@ -198,6 +204,7 @@ A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; + B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; B0EDAF55877DE19B67837C22 /* TemplateSimpleScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C29670CEC77346F31EE94C /* TemplateSimpleScreenModels.swift */; }; B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; @@ -208,6 +215,7 @@ B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; + BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */; }; BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EF188681D6B6068CFAEAFC3F /* MXLogger.m */; }; BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */; }; BE3237142FA6E1A13C0E7D11 /* RoomSummaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */; }; @@ -305,6 +313,7 @@ 0DD16CE9A66C9040B066AD60 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = ""; }; 0E7062F88E9D5F79C8A80524 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = ""; }; 0EE9EAF0309A2A1D67D8FAF5 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = ""; }; + 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelProtocol.swift; sourceTree = ""; }; 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; 1027BB9A852F445B7623897F /* ElementSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementSettings.swift; sourceTree = ""; }; 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = ""; }; @@ -317,6 +326,7 @@ 124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = ""; }; 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModel.swift; sourceTree = ""; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; @@ -506,6 +516,7 @@ 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactoryProtocol.swift; sourceTree = ""; }; 9772C1D2223108EB3131AEE4 /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = ""; }; + 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationIconImage.swift; sourceTree = ""; }; 97F893DBB5F88D746C6DCDE5 /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Localizable.strings; sourceTree = ""; }; 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; 997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = ""; }; @@ -513,12 +524,15 @@ 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; + 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionCoordinator.swift; sourceTree = ""; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; + A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; A1C29670CEC77346F31EE94C /* TemplateSimpleScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenModels.swift; sourceTree = ""; }; A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = ""; }; A2B6433F516F1E6DFA0E2D89 /* vls */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vls; path = vls.lproj/Localizable.strings; sourceTree = ""; }; + A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = ""; }; A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = ""; }; @@ -612,12 +626,14 @@ E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = ""; }; + E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; + EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactoryProtocol.swift; sourceTree = ""; }; EF188681D6B6068CFAEAFC3F /* MXLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogger.m; sourceTree = ""; }; @@ -805,6 +821,19 @@ path = Members; sourceTree = ""; }; + 3510020809E49EFA146296AD /* ServerSelection */ = { + isa = PBXGroup; + children = ( + A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */, + 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */, + A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */, + 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */, + 0F52BF30D12BA3BD3D3DBB8F /* ServerSelectionViewModelProtocol.swift */, + 9D54059E4E42176B3ABB729F /* View */, + ); + path = ServerSelection; + sourceTree = ""; + }; 4009BE2E791C16AC6EE39A7E /* BugReport */ = { isa = PBXGroup; children = ( @@ -1012,6 +1041,7 @@ A05707BF550D770168A406DB /* LoginViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, + EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */, AF552BB969DC98A4BB8CF8D5 /* UserIndicators */, @@ -1212,6 +1242,14 @@ path = WeakDictionary; sourceTree = ""; }; + 9D54059E4E42176B3ABB729F /* View */ = { + isa = PBXGroup; + children = ( + E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */, + ); + path = View; + sourceTree = ""; + }; A0C06C0F6A8621B22BFAEB56 /* Localizations */ = { isa = PBXGroup; children = ( @@ -1395,8 +1433,10 @@ isa = PBXGroup; children = ( D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */, + 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */, 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */, 90F48FEF84016ED42A94BA24 /* LoginScreen */, + 3510020809E49EFA146296AD /* ServerSelection */, ); path = Authentication; sourceTree = ""; @@ -1564,7 +1604,7 @@ }; }; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; - compatibilityVersion = "Xcode 10.0"; + compatibilityVersion = "Xcode 11.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1767,6 +1807,7 @@ 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, + 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */, 7B3D3AFD511D496DED18910B /* TemplateSimpleScreenViewModelTests.swift in Sources */, @@ -1793,6 +1834,7 @@ 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */, A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */, EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */, + B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, @@ -1864,6 +1906,7 @@ 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */, 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */, E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */, + 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */, 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */, 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */, 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */, @@ -1905,6 +1948,11 @@ CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */, 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */, 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */, + 87756CA950ED55870A1AAE8F /* ServerSelectionCoordinator.swift in Sources */, + 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */, + 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */, + BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */, + 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */, 6C72F66DA26A0956E9A9077A /* Settings.swift in Sources */, 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */, 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */, diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Server Selection Icon.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Server Selection Icon.imageset/Contents.json new file mode 100644 index 000000000..fec295dec --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/Authentication/Server Selection Icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_server_selection_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/ElementX/Resources/Assets.xcassets/Images/Authentication/Server Selection Icon.imageset/authentication_server_selection_icon.svg b/ElementX/Resources/Assets.xcassets/Images/Authentication/Server Selection Icon.imageset/authentication_server_selection_icon.svg new file mode 100644 index 000000000..17b23458e --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/Authentication/Server Selection Icon.imageset/authentication_server_selection_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 1.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 1.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 1.imageset/OnboardingSplashScreenPage1.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 2.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 2.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 2.imageset/OnboardingSplashScreenPage2.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 3.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 3.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 3.imageset/OnboardingSplashScreenPage3.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 4.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 4.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4-Dark.pdf diff --git a/ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf b/ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf similarity index 100% rename from ElementX/Resources/Assets.xcassets/Images/Splash Screen/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf rename to ElementX/Resources/Assets.xcassets/Images/Authentication/Splash Screen Page 4.imageset/OnboardingSplashScreenPage4.pdf diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index dbab7baab..cd02c044c 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -1,4 +1,9 @@ +/* Used for testing */ "untranslated" = "Untranslated"; + +"action_confirm" = "Confirm"; +"action_next" = "Next"; + "screenshot_detected_title" = "You took a screenshot"; "screenshot_detected_message" = "Would you like to submit a bug report?"; @@ -13,3 +18,9 @@ "authentication_server_info_title" = "Choose your server to store your data"; "authentication_server_info_matrix_description" = "Join millions for free on the largest public server"; + +"server_selection_title" = "Choose your server"; +"server_selection_message" = "What is the address of your server? A server is like a home for all your data."; +"server_selection_server_url" = "Server URL"; +"server_selection_server_footer" = "You can only connect to a server that has already been set up"; +"server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct."; diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index fc5791599..7438510d7 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -20,13 +20,14 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name internal enum Asset { internal enum Images { - internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal") - internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted") - internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning") + internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon") internal static let splashScreenPage1 = ImageAsset(name: "Images/Splash Screen Page 1") internal static let splashScreenPage2 = ImageAsset(name: "Images/Splash Screen Page 2") internal static let splashScreenPage3 = ImageAsset(name: "Images/Splash Screen Page 3") internal static let splashScreenPage4 = ImageAsset(name: "Images/Splash Screen Page 4") + internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal") + internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted") + internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning") internal static let appLogo = ImageAsset(name: "Images/app-logo") internal static let closeCircle = ImageAsset(name: "Images/close_circle") internal static let timelineComposerSendMessage = ImageAsset(name: "Images/timelineComposerSendMessage") diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index dd5b63c30..e15e875ab 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,10 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces extension ElementL10n { + /// Confirm + public static let actionConfirm = ElementL10n.tr("Untranslated", "action_confirm") + /// Next + public static let actionNext = ElementL10n.tr("Untranslated", "action_next") /// Forgot password public static let authenticationLoginForgotPassword = ElementL10n.tr("Untranslated", "authentication_login_forgot_password") /// Welcome back! @@ -26,6 +30,16 @@ extension ElementL10n { public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message") /// You took a screenshot public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title") + /// Cannot find a server at this URL, please check it is correct. + public static let serverSelectionGenericError = ElementL10n.tr("Untranslated", "server_selection_generic_error") + /// What is the address of your server? A server is like a home for all your data. + public static let serverSelectionMessage = ElementL10n.tr("Untranslated", "server_selection_message") + /// You can only connect to a server that has already been set up + public static let serverSelectionServerFooter = ElementL10n.tr("Untranslated", "server_selection_server_footer") + /// Server URL + public static let serverSelectionServerUrl = ElementL10n.tr("Untranslated", "server_selection_server_url") + /// Choose your server + public static let serverSelectionTitle = ElementL10n.tr("Untranslated", "server_selection_title") /// Timeline Style public static let settingsTimelineStyle = ElementL10n.tr("Untranslated", "settings_timeline_style") /// Untranslated diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift b/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift new file mode 100644 index 000000000..a45247157 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/AuthenticationIconImage.swift @@ -0,0 +1,41 @@ +// +// 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 + +/// An image that is styled for use as the screen icon in the onboarding flow. +struct AuthenticationIconImage: View { + + let image: ImageAsset + + var body: some View { + Image(image.name) + .resizable() + .renderingMode(.template) + .foregroundColor(.element.accent) + .frame(width: 90, height: 90) + .background(.white, in: Circle().inset(by: 2)) + .accessibilityHidden(true) + } +} + +// MARK: - Previews + +struct AuthenticationIconImage_Previews: PreviewProvider { + static var previews: some View { + AuthenticationIconImage(image: Asset.Images.serverSelectionIcon) + } +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift index 4c452c33a..af31c6005 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -74,7 +74,7 @@ final class LoginCoordinator: Coordinator, Presentable { let viewModel = LoginViewModel(homeserver: parameters.homeserver) loginViewModel = viewModel - let view = LoginScreen(viewModel: viewModel.context) + let view = LoginScreen(context: viewModel.context) loginHostingController = UIHostingController(rootView: view) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController) @@ -178,7 +178,34 @@ final class LoginCoordinator: Coordinator, Presentable { /// Presents the server selection screen as a modal. private func presentServerSelectionScreen() { - loginViewModel.displayError(.alert("Not implemented. Enter a full Matrix ID such as @user:server.com")) + MXLog.debug("[LoginCoordinator] presentServerSelectionScreen") + let parameters = ServerSelectionCoordinatorParameters(homeserver: loginViewModel.context.viewState.homeserver, + hasModalPresentation: true) + let coordinator = ServerSelectionCoordinator(parameters: parameters) + coordinator.callback = { [weak self, weak coordinator] result in + guard let self = self, let coordinator = coordinator else { return } + self.serverSelectionCoordinator(coordinator, didCompleteWith: result) + } + + coordinator.start() + add(childCoordinator: coordinator) + + let modalRouter = NavigationRouter(navigationController: ElementNavigationController()) + modalRouter.setRootModule(coordinator) + + navigationRouter.present(modalRouter, animated: true) + } + + /// Handles the result from the server selection modal, dismissing it after updating the view. + private func serverSelectionCoordinator(_ coordinator: ServerSelectionCoordinator, + didCompleteWith result: ServerSelectionCoordinatorResult) { + navigationRouter.dismissModule(animated: true) { [weak self] in + if case let .selected(homeserver) = result { + self?.updateViewModel(homeserver: homeserver) + } + + self?.remove(childCoordinator: coordinator) + } } /// Shows the forgot password screen. diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift index 4b31d4263..d665e6a1c 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -29,7 +29,7 @@ struct LoginScreen: View { // MARK: Public - @ObservedObject var viewModel: LoginViewModel.Context + @ObservedObject var context: LoginViewModel.Context var body: some View { ScrollView { @@ -46,7 +46,7 @@ struct LoginScreen: View { .frame(height: 1) .padding(.vertical, 21) - switch viewModel.viewState.loginMode { + switch context.viewState.loginMode { case .password: loginForm case .oidc: @@ -61,7 +61,7 @@ struct LoginScreen: View { .padding(.bottom, 16) } .background(Color.element.background.ignoresSafeArea()) - .alert(item: $viewModel.alertInfo) { $0.alert } + .alert(item: $context.alertInfo) { $0.alert } } /// The header containing a Welcome Back title. @@ -74,16 +74,16 @@ struct LoginScreen: View { /// The sever information section that includes a button to select a different server. var serverInfo: some View { - LoginServerInfoSection(address: viewModel.viewState.homeserver.address, - showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) { - viewModel.send(viewAction: .selectServer) + LoginServerInfoSection(address: context.viewState.homeserver.address, + showMatrixDotOrgInfo: context.viewState.homeserver.isMatrixDotOrg) { + context.send(viewAction: .selectServer) } } /// The form with text fields for username and password, along with a submit button. var loginForm: some View { VStack(spacing: 14) { - TextField(ElementL10n.loginSigninUsernameHint, text: $viewModel.username) + TextField(ElementL10n.loginSigninUsernameHint, text: $context.username) .focused($isUsernameFocused) .textFieldStyle(.elementInput()) .disableAutocorrection(true) @@ -96,7 +96,7 @@ struct LoginScreen: View { Spacer().frame(height: 20) - SecureField(ElementL10n.loginSignupPasswordHint, text: $viewModel.password) + SecureField(ElementL10n.loginSignupPasswordHint, text: $context.password) .focused($isPasswordFocused) .textFieldStyle(.elementInput()) .textContentType(.password) @@ -104,7 +104,7 @@ struct LoginScreen: View { .onSubmit(submit) .accessibilityIdentifier("passwordTextField") - Button { viewModel.send(viewAction: .forgotPassword) } label: { + Button { context.send(viewAction: .forgotPassword) } label: { Text(ElementL10n.authenticationLoginForgotPassword) .font(.element.body) } @@ -115,14 +115,14 @@ struct LoginScreen: View { Text(ElementL10n.loginSignupSubmit) } .buttonStyle(.elementAction(.xLarge)) - .disabled(!viewModel.viewState.canSubmit) + .disabled(!context.viewState.canSubmit) .accessibilityIdentifier("nextButton") } } /// The OIDC button that can be used for login. var oidcButton: some View { - Button { viewModel.send(viewAction: .continueWithOIDC) } label: { + Button { context.send(viewAction: .continueWithOIDC) } label: { Text(ElementL10n.loginContinue) } .buttonStyle(.elementAction(.xLarge)) @@ -141,14 +141,14 @@ struct LoginScreen: View { /// Parses the username for a homeserver. private func usernameFocusChanged(isFocussed: Bool) { - guard !isFocussed, !viewModel.username.isEmpty else { return } - viewModel.send(viewAction: .parseUsername) + guard !isFocussed, !context.username.isEmpty else { return } + context.send(viewAction: .parseUsername) } /// Sends the `next` view action so long as valid credentials have been input. private func submit() { - guard viewModel.viewState.canSubmit else { return } - viewModel.send(viewAction: .next) + guard context.viewState.canSubmit else { return } + context.send(viewAction: .next) } } @@ -171,7 +171,7 @@ struct Login_Previews: PreviewProvider { static func screen(for viewModel: LoginViewModel) -> some View { NavigationView { - LoginScreen(viewModel: viewModel.context) + LoginScreen(context: viewModel.context) .navigationBarTitleDisplayMode(.inline) .tint(.element.accent) } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift new file mode 100644 index 000000000..d00971c79 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/MockServerSelectionScreenState.swift @@ -0,0 +1,44 @@ +// +// Copyright 2021 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 + +enum MockServerSelectionScreenState: CaseIterable { + case matrix + case emptyAddress + case invalidAddress + case nonModal + + /// Generate the view struct for the screen state. + @MainActor var viewModel: ServerSelectionViewModel { + switch self { + case .matrix: + return ServerSelectionViewModel(homeserverAddress: "https://matrix.org", + hasModalPresentation: true) + case .emptyAddress: + return ServerSelectionViewModel(homeserverAddress: "", + hasModalPresentation: true) + case .invalidAddress: + let viewModel = ServerSelectionViewModel(homeserverAddress: "thisisbad", + hasModalPresentation: true) + viewModel.displayError(.footerMessage(ElementL10n.unknownError)) + return viewModel + case .nonModal: + return ServerSelectionViewModel(homeserverAddress: "https://matrix.org", + hasModalPresentation: false) + } + } +} diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift new file mode 100644 index 000000000..16083b175 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionCoordinator.swift @@ -0,0 +1,110 @@ +// +// Copyright 2021 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 ServerSelectionCoordinatorParameters { + /// The homeserver to be shown initially. + let homeserver: LoginHomeserver + /// Whether the screen is presented modally or within a navigation stack. + let hasModalPresentation: Bool +} + +enum ServerSelectionCoordinatorResult { + case selected(LoginHomeserver) + case dismiss +} + +final class ServerSelectionCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: ServerSelectionCoordinatorParameters + private let serverSelectionHostingController: UIViewController + private var serverSelectionViewModel: ServerSelectionViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var callback: (@MainActor (ServerSelectionCoordinatorResult) -> Void)? + + // MARK: - Setup + + init(parameters: ServerSelectionCoordinatorParameters) { + self.parameters = parameters + + let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.homeserver.address, + hasModalPresentation: parameters.hasModalPresentation) + let view = ServerSelectionScreen(context: viewModel.context) + serverSelectionViewModel = viewModel + serverSelectionHostingController = UIHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: serverSelectionHostingController) + } + + // MARK: - Public + + func start() { + MXLog.debug("[ServerSelectionCoordinator] did start.") + + serverSelectionViewModel.callback = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[ServerSelectionCoordinator] ServerSelectionViewModel did complete with result: \(result).") + + switch result { + case .confirm(let homeserverAddress): + self.useHomeserver(homeserverAddress) + case .dismiss: + self.callback?(.dismiss) + } + } + } + + func toPresentable() -> UIViewController { + serverSelectionHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } + + /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. + private func useHomeserver(_ homeserverAddress: String) { + startLoading() + + let homeserverAddress = LoginHomeserver.sanitized(homeserverAddress) + + stopLoading() + callback?(.selected(LoginHomeserver(address: homeserverAddress))) + } +} diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift new file mode 100644 index 000000000..e5e5792ef --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionModels.swift @@ -0,0 +1,78 @@ +// +// Copyright 2021 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 + +// MARK: View model + +enum ServerSelectionViewModelResult { + /// The user would like to use the homeserver at the given address. + case confirm(homeserverAddress: String) + /// Dismiss the view without using the entered address. + case dismiss +} + +// MARK: View + +struct ServerSelectionViewState: BindableState { + /// View state that can be bound to from SwiftUI. + var bindings: ServerSelectionBindings + /// An error message to be shown in the text field footer. + var footerErrorMessage: String? + /// Whether the screen is presented modally or within a navigation stack. + var hasModalPresentation: Bool + + /// The message to show in the text field footer. + var footerMessage: String { + footerErrorMessage ?? ElementL10n.serverSelectionServerFooter + } + + /// The title shown on the confirm button. + var buttonTitle: String { + hasModalPresentation ? ElementL10n.actionConfirm : ElementL10n.actionNext + } + + /// The text field is showing an error. + var isShowingFooterError: Bool { + footerErrorMessage != nil + } + + /// Whether it is possible to continue when tapping the confirmation button. + var hasValidationError: Bool { + bindings.homeserverAddress.isEmpty || isShowingFooterError + } +} + +struct ServerSelectionBindings { + /// The homeserver address input by the user. + var homeserverAddress: String + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum ServerSelectionViewAction { + /// The user would like to use the homeserver at the input address. + case confirm + /// Dismiss the view without using the entered address. + case dismiss + /// Clear any errors shown in the text field footer. + case clearFooterError +} + +enum ServerSelectionErrorType: Hashable { + /// An error message to be shown in the text field footer. + case footerMessage(String) +} diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift new file mode 100644 index 000000000..6bae4d54f --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift @@ -0,0 +1,68 @@ +// +// Copyright 2021 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 + +typealias ServerSelectionViewModelType = StateStoreViewModel + +class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)? + + // MARK: - Setup + + init(homeserverAddress: String, hasModalPresentation: Bool) { + let bindings = ServerSelectionBindings(homeserverAddress: homeserverAddress) + super.init(initialViewState: ServerSelectionViewState(bindings: bindings, + hasModalPresentation: hasModalPresentation)) + } + + // MARK: - Public + + override func process(viewAction: ServerSelectionViewAction) async { + switch viewAction { + case .confirm: + callback?(.confirm(homeserverAddress: state.bindings.homeserverAddress)) + case .dismiss: + callback?(.dismiss) + case .clearFooterError: + clearFooterError() + } + } + + func displayError(_ type: ServerSelectionErrorType) { + switch type { + case .footerMessage(let message): + withAnimation { + state.footerErrorMessage = message + } + } + } + + // MARK: - Private + + /// Clear any errors shown in the text field footer. + private func clearFooterError() { + guard state.footerErrorMessage != nil else { return } + withAnimation { state.footerErrorMessage = nil } + } +} diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift new file mode 100644 index 000000000..e6094708b --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModelProtocol.swift @@ -0,0 +1,27 @@ +// +// Copyright 2021 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 + +@MainActor +protocol ServerSelectionViewModelProtocol { + + var callback: (@MainActor (ServerSelectionViewModelResult) -> Void)? { get set } + var context: ServerSelectionViewModelType.Context { get } + + /// Displays an error to the user. + func displayError(_ type: ServerSelectionErrorType) +} diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift new file mode 100644 index 000000000..ffa06b6ad --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/View/ServerSelectionScreen.swift @@ -0,0 +1,123 @@ +// +// Copyright 2021 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 ServerSelectionScreen: View { + + // MARK: - Properties + + // MARK: Private + + @FocusState var isTextFieldFocused: Bool + + // MARK: Public + + @ObservedObject var context: ServerSelectionViewModel.Context + + // MARK: Views + + var body: some View { + ScrollView { + VStack(spacing: 0) { + header + .padding(.top, UIConstants.topPaddingToNavigationBar) + .padding(.bottom, 36) + + serverForm + } + .readableFrame() + .padding(.horizontal, 16) + } + .background(Color.element.background, ignoresSafeAreaEdges: .all) + .toolbar { toolbar } + .alert(item: $context.alertInfo) { $0.alert } + } + + /// The title, message and icon at the top of the screen. + var header: some View { + VStack(spacing: 8) { + AuthenticationIconImage(image: Asset.Images.serverSelectionIcon) + .padding(.bottom, 8) + + Text(ElementL10n.serverSelectionTitle) + .font(.element.title2B) + .multilineTextAlignment(.center) + .foregroundColor(.element.primaryContent) + + Text(ElementL10n.serverSelectionMessage) + .font(.element.body) + .multilineTextAlignment(.center) + .foregroundColor(.element.secondaryContent) + } + } + + /// The text field and confirm button where the user enters a server URL. + var serverForm: some View { + VStack(alignment: .leading, spacing: 12) { + TextField(ElementL10n.serverSelectionServerUrl, text: $context.homeserverAddress) + .focused($isTextFieldFocused) + .textFieldStyle(.elementInput(footerText: context.viewState.footerMessage, + isError: context.viewState.isShowingFooterError)) + .keyboardType(.URL) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: context.homeserverAddress) { _ in context.send(viewAction: .clearFooterError) } + .submitLabel(.done) + .onSubmit(submit) + .accessibilityIdentifier("addressTextField") + + Button(action: submit) { + Text(context.viewState.buttonTitle) + } + .buttonStyle(.elementAction(.xLarge)) + .disabled(context.viewState.hasValidationError) + .accessibilityIdentifier("confirmButton") + } + } + + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + if context.viewState.hasModalPresentation { + Button { context.send(viewAction: .dismiss) } label: { + Text(ElementL10n.actionCancel) + } + .accessibilityIdentifier("dismissButton") + } + } + } + + /// Sends the `confirm` view action so long as the text field input is valid. + func submit() { + guard !context.viewState.hasValidationError else { return } + context.send(viewAction: .confirm) + } +} + +// MARK: - Previews + +struct ServerSelection_Previews: PreviewProvider { + static var previews: some View { + ForEach(MockServerSelectionScreenState.allCases, id: \.self) { state in + NavigationView { + ServerSelectionScreen(context: state.viewModel.context) + .tint(.element.accent) + } + .navigationViewStyle(.stack) + } + } +} diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift b/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift index 3db6a7a75..d683f7be5 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift @@ -38,7 +38,7 @@ final class SplashScreenCoordinator: Coordinator, Presentable { init() { let viewModel = SplashScreenViewModel() - let view = SplashScreen(viewModel: viewModel.context) + let view = SplashScreen(context: viewModel.context) splashScreenViewModel = viewModel splashScreenHostingController = UIHostingController(rootView: view) diff --git a/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift b/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift index fdd9fcb32..728567285 100644 --- a/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift +++ b/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift @@ -28,7 +28,7 @@ struct SplashScreen: View { @Environment(\.layoutDirection) private var layoutDirection private var isLeftToRight: Bool { layoutDirection == .leftToRight } - private var pageCount: Int { viewModel.viewState.content.count } + private var pageCount: Int { context.viewState.content.count } /// A timer to automatically animate the pages. @State private var pageTimer: Timer? @@ -37,7 +37,7 @@ struct SplashScreen: View { // MARK: Public - @ObservedObject var viewModel: SplashScreenViewModel.Context + @ObservedObject var context: SplashScreenViewModel.Context var body: some View { GeometryReader { geometry in @@ -49,12 +49,12 @@ struct SplashScreen: View { HStack(alignment: .top, spacing: 0) { // Add a hidden page at the start of the carousel duplicating the content of the last page - SplashScreenPage(content: viewModel.viewState.content[pageCount - 1]) + SplashScreenPage(content: context.viewState.content[pageCount - 1]) .frame(width: geometry.size.width) .accessibilityIdentifier("hiddenPage") ForEach(0..