From d74158ced1a0c9c773b7485651139cf18331c2c2 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Tue, 28 Jun 2022 12:23:35 +0100 Subject: [PATCH] #40: Add the login screen from EI. - Remove SSO and replace fallback with OIDC. --- ElementX.xcodeproj/project.pbxproj | 132 ++++++------ .../en.lproj/Untranslated.strings | 8 + ElementX/Sources/BuildSettings.swift | 3 + .../Generated/Strings+Untranslated.swift | 8 + ElementX/Sources/Other/Logging/MXLog.swift | 15 ++ .../SwiftUI/ErrorHandling/AlertInfo.swift | 89 +++++++++ .../FramePreferenceKey.swift | 0 .../ViewFrameReader.swift | 0 .../LabelledActivityIndicatorView.swift | 7 +- .../UserIndicators/RoundedToastView.swift | 21 +- .../AuthenticationCoordinator.swift | 13 +- .../LoginScreen/LoginCoordinator.swift | 188 ++++++++++++++++++ .../LoginScreen/LoginHomeserver.swift | 89 +++++++++ .../LoginScreen/LoginMode.swift | 56 ++++++ .../LoginScreen/LoginModels.swift | 103 ++++++++++ .../LoginScreen/LoginViewModel.swift | 78 ++++++++ .../LoginScreen/LoginViewModelProtocol.swift | 36 ++++ .../LoginScreen/View/LoginScreen.swift | 180 +++++++++++++++++ .../View/LoginServerInfoSection.swift | 64 ++++++ .../LoginScreen/LoginScreenCoordinator.swift | 72 ------- .../LoginScreen/LoginScreenModels.swift | 45 ----- .../LoginScreen/LoginScreenViewModel.swift | 46 ----- .../LoginScreenViewModelProtocol.swift | 23 --- .../LoginScreen/View/LoginScreen.swift | 73 ------- .../SplashScreenCoordinator.swift | 2 - .../SplashScreen/SplashScreenModels.swift | 3 - .../SplashScreen/SplashScreenViewModel.swift | 12 +- .../SplashScreen/View/SplashScreen.swift | 3 +- ElementX/Sources/UITestScreenIdentifier.swift | 2 + ElementX/Sources/UITestsAppCoordinator.swift | 12 +- ElementX/SupportingFiles/target.yml | 20 +- UITests/Sources/LoginScreenUITests.swift | 124 +++++++++++- .../Sources/LoginScreenViewModelTests.swift | 30 --- UnitTests/Sources/LoginViewModelTests.swift | 149 ++++++++++++++ changelog.d/40.change | 2 +- 35 files changed, 1315 insertions(+), 393 deletions(-) create mode 100644 ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift rename ElementX/Sources/Other/SwiftUI/{Views/ViewFrameReader => Layout}/FramePreferenceKey.swift (100%) rename ElementX/Sources/Other/SwiftUI/{Views/ViewFrameReader => Layout}/ViewFrameReader.swift (100%) create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/LoginModels.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift create mode 100644 ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift delete mode 100644 ElementX/Sources/Screens/LoginScreen/LoginScreenCoordinator.swift delete mode 100644 ElementX/Sources/Screens/LoginScreen/LoginScreenModels.swift delete mode 100644 ElementX/Sources/Screens/LoginScreen/LoginScreenViewModel.swift delete mode 100644 ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift delete mode 100644 ElementX/Sources/Screens/LoginScreen/View/LoginScreen.swift delete mode 100644 UnitTests/Sources/LoginScreenViewModelTests.swift create mode 100644 UnitTests/Sources/LoginViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index bcf2c79fdf..127c8405e2 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 */ @@ -61,11 +61,10 @@ 2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */; }; 2E68C57E7D644E94778743D5 /* TemplateSimpleScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B66E05B6009B0EB1BDBFA6E /* TemplateSimpleScreenUITests.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; + 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */; }; 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */; }; - 306CC09DF101E7E9CDE79AA5 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C8F70ADAFB63907B862E5D /* LoginScreenCoordinator.swift */; }; - 33912D1B9264D897033E0681 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0779B2CC9A687CBB82A5B920 /* LoginScreenViewModelProtocol.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; @@ -75,6 +74,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 */; }; + 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 */; }; 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; }; @@ -89,6 +89,7 @@ 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; }; 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */; }; 499A26EB06C97E48C27A2DB9 /* BuildSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F87116470221880017CF522 /* BuildSettings.swift */; }; + 49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D58333B377888012740101 /* LoginViewModel.swift */; }; 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; 4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; }; 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */; }; @@ -102,11 +103,11 @@ 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; }; 524C9C31EF8D58C2249F8A10 /* sample_screenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */; }; 53504DF61DBC81ACC9B4D275 /* SplashScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */; }; + 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; - 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; 5CABC57F620FBB39F4EC127C /* TemplateSimpleScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BA045DC4CA12D030ACF558 /* TemplateSimpleScreen.swift */; }; 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; @@ -124,9 +125,9 @@ 6C72F66DA26A0956E9A9077A /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEB3259B2208E5AE5BB3F65 /* Settings.swift */; }; 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; 6F2AB43A1EFAD8A97AF41A15 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; + 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; }; 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; - 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; }; 75D98001C5AC38B6A5CA897C /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FD9D66B75292F2CC11AA4D2 /* UITestScreenIdentifier.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; @@ -138,15 +139,16 @@ 7B3D3AFD511D496DED18910B /* TemplateSimpleScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C485C186CEC78443DA96BDC8 /* TemplateSimpleScreenViewModelTests.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; - 7C9121245B11CA48307CB462 /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8FD25EB4DF66625B74E4505 /* LoginScreenViewModel.swift */; }; 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */; }; 7DE5EB4CB2401C672257283C /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */; }; 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47564C584F614B7287F3EB /* RootRouter.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; }; 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; }; - 84520E7A7A72FDECAB89789E /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB3E99D445CFCB3AA3F34FB /* FramePreferenceKey.swift */; }; + 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; }; + 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 */; }; 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 */; }; @@ -155,6 +157,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 */; }; + 91CC102A286A0D9400B6E687 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91CC1029286A0D9400B6E687 /* LoginScreenUITests.swift */; }; 93BA4A81B6D893271101F9F0 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; @@ -174,13 +177,14 @@ A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; A4E885358D7DD5A072A06824 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; + A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; }; A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */; }; A636D4900E0D98ED91536482 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3EDF23226895776553F04A /* AppCoordinator.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; + A8177B197C2E3DB7ACB63088 /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88981CE56026FE761433BA56 /* LoginViewModelTests.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; - A941EAD7F407F2ED6DA54A31 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA97D630B74B0616C1468CBD /* LoginScreen.swift */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; B0EDAF55877DE19B67837C22 /* TemplateSimpleScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C29670CEC77346F31EE94C /* TemplateSimpleScreenModels.swift */; }; @@ -192,6 +196,7 @@ B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.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 */; }; BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */; }; BF35062D06888FA80BD139FF /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB7F9D6FC121204D59E18DF /* Presentable.swift */; }; @@ -209,6 +214,7 @@ CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */; }; + CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */; }; D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; @@ -222,7 +228,6 @@ DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */; }; - E9CEAF2C38E4E00459B811D9 /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */; }; EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; }; EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; @@ -231,7 +236,6 @@ EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EF99A92701E401C4CD5ADC50 /* SplashScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */; }; - F01DB7DD607015557CD48B33 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242A3BC7FAE2256930FB8527 /* ViewFrameReader.swift */; }; F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */; }; F2DD8661B5C0BA2BB526FA6C /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD51F9FDC91C231906D76C8 /* KeychainControllerProtocol.swift */; }; F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */; }; @@ -270,7 +274,6 @@ 04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = ""; }; 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueueTests.swift; sourceTree = ""; }; 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = ""; }; - 0779B2CC9A687CBB82A5B920 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; 08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = ""; }; 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; @@ -303,17 +306,15 @@ 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; - 1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = ""; }; 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; 1C429043E986008B97736636 /* ab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ab; path = ab.lproj/Localizable.strings; sourceTree = ""; }; - 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; + 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelProtocol.swift; sourceTree = ""; }; 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; 2112A6CFEA46E672D90EBF54 /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kab; path = kab.lproj/Localizable.strings; sourceTree = ""; }; 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = ""; }; 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = ""; }; 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; - 242A3BC7FAE2256930FB8527 /* ViewFrameReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFrameReader.swift; sourceTree = ""; }; 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -325,6 +326,8 @@ 2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; 2BEB3259B2208E5AE5BB3F65 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 2CF9FE7E0CF9F40D1509E63A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = ""; }; + 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertInfo.swift; sourceTree = ""; }; + 31B01468022EC826CB2FD2C0 /* LoginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModels.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenUITests.swift; sourceTree = ""; }; 32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; @@ -362,6 +365,7 @@ 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47543EB19F3DCF308751F53C /* TemplateSimpleScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenViewModel.swift; sourceTree = ""; }; 475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = ""; }; + 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashViewController.xib; sourceTree = ""; }; 48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -370,10 +374,12 @@ 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; 4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = ""; }; + 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4B66E05B6009B0EB1BDBFA6E /* TemplateSimpleScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenUITests.swift; sourceTree = ""; }; 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewState.swift; sourceTree = ""; }; 4C8D988E82A8DFA13BE46F7C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pl; path = pl.lproj/Localizable.stringsdict; sourceTree = ""; }; 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; 4DF56C3239EA3C16951E1E66 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTableViewAdapter.swift; sourceTree = ""; }; @@ -389,7 +395,6 @@ 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = ""; }; - 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = ""; }; 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = ""; }; 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = ""; }; @@ -407,7 +412,6 @@ 616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = ""; }; 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = ""; }; 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorRequest.swift; sourceTree = ""; }; - 61C8F70ADAFB63907B862E5D /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = ""; }; 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; @@ -440,6 +444,7 @@ 7BDF6A69C2BB99535193E554 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = ""; }; + 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginServerInfoSection.swift; sourceTree = ""; }; 7DA80FADE73CDF01E96F5B8E /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/Localizable.strings; sourceTree = ""; }; 7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = ""; }; @@ -460,6 +465,7 @@ 878B7C1885486FB4BE41631D /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = iw; path = iw.lproj/Localizable.stringsdict; sourceTree = ""; }; 885D8C42DD17625B5261BEFF /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; 8888D13645C04AC9818F5778 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 88981CE56026FE761433BA56 /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomMessage.swift; sourceTree = ""; }; @@ -469,7 +475,9 @@ 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; + 91CC1029286A0D9400B6E687 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactoryProtocol.swift; sourceTree = ""; }; + 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = ""; }; 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; 93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = ""; }; 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; @@ -530,7 +538,6 @@ B83CB897B183BF3C33715F55 /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-IN"; path = "bn-IN.lproj/Localizable.stringsdict"; sourceTree = ""; }; B8A56EA2A5AE726F445CB2E3 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eo; path = eo.lproj/Localizable.stringsdict; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; - BA97D630B74B0616C1468CBD /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; BC9B05D6B293A039EB963CA7 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; BE03C54FC7AAE0FC03EC8976 /* SplashScreenPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPage.swift; sourceTree = ""; }; BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; @@ -572,6 +579,7 @@ D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = ""; }; + DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = ""; }; E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenter.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; @@ -585,20 +593,20 @@ 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 = ""; }; - E8FD25EB4DF66625B74E4505 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; 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 = ""; }; - EDB3E99D445CFCB3AA3F34FB /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.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 = ""; }; + EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFrameReader.swift; sourceTree = ""; }; EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = ""; }; F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = ""; }; F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; + F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; @@ -662,6 +670,7 @@ 052CC920F473C10B509F9FC1 /* SwiftUI */ = { isa = PBXGroup; children = ( + 595B8797ED6A7489ABDCE384 /* ErrorHandling */, CE2FBFD64A89F5DBE4EB30DB /* Layout */, 10578D9852BA78D309A1CBDF /* ViewModel */, 328DD5DA1281F758B72006C7 /* Views */, @@ -744,20 +753,10 @@ path = Resources; sourceTree = ""; }; - 304D3532D4FFC1F0ABC0626E /* ViewFrameReader */ = { - isa = PBXGroup; - children = ( - EDB3E99D445CFCB3AA3F34FB /* FramePreferenceKey.swift */, - 242A3BC7FAE2256930FB8527 /* ViewFrameReader.swift */, - ); - path = ViewFrameReader; - sourceTree = ""; - }; 328DD5DA1281F758B72006C7 /* Views */ = { isa = PBXGroup; children = ( 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, - 304D3532D4FFC1F0ABC0626E /* ViewFrameReader */, ); path = Views; sourceTree = ""; @@ -783,14 +782,6 @@ path = Members; sourceTree = ""; }; - 36E57D24D3A207ABA19B6515 /* View */ = { - isa = PBXGroup; - children = ( - BA97D630B74B0616C1468CBD /* LoginScreen.swift */, - ); - path = View; - sourceTree = ""; - }; 4009BE2E791C16AC6EE39A7E /* BugReport */ = { isa = PBXGroup; children = ( @@ -910,16 +901,21 @@ path = View; sourceTree = ""; }; - 5958CAF6E56422496E0063AF /* LoginScreen */ = { + 595B8797ED6A7489ABDCE384 /* ErrorHandling */ = { isa = PBXGroup; children = ( - 61C8F70ADAFB63907B862E5D /* LoginScreenCoordinator.swift */, - 1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */, - E8FD25EB4DF66625B74E4505 /* LoginScreenViewModel.swift */, - 0779B2CC9A687CBB82A5B920 /* LoginScreenViewModelProtocol.swift */, - 36E57D24D3A207ABA19B6515 /* View */, + 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */, ); - path = LoginScreen; + path = ErrorHandling; + sourceTree = ""; + }; + 605F8221E52991786397FCC9 /* View */ = { + isa = PBXGroup; + children = ( + 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */, + 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */, + ); + path = View; sourceTree = ""; }; 679E9837ECA8D6776079D16E /* RoomScreen */ = { @@ -981,6 +977,7 @@ isa = PBXGroup; children = ( AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */, + 88981CE56026FE761433BA56 /* LoginViewModelTests.swift */, EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */, DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */, @@ -989,7 +986,6 @@ FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, - 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, @@ -1111,6 +1107,20 @@ path = UserSessionStore; sourceTree = ""; }; + 90F48FEF84016ED42A94BA24 /* LoginScreen */ = { + isa = PBXGroup; + children = ( + DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */, + 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */, + 4B41FABA2B0AEF4389986495 /* LoginMode.swift */, + 31B01468022EC826CB2FD2C0 /* LoginModels.swift */, + F2D58333B377888012740101 /* LoginViewModel.swift */, + 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */, + 605F8221E52991786397FCC9 /* View */, + ); + path = LoginScreen; + sourceTree = ""; + }; 9413F680ECDFB2B0DDB0DEF2 /* Packages */ = { isa = PBXGroup; children = ( @@ -1125,9 +1135,9 @@ 7D0CBC76C80E04345E11F2DB /* Application.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, - 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */, + 91CC1029286A0D9400B6E687 /* LoginScreenUITests.swift */, 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */, ); path = Sources; @@ -1310,7 +1320,9 @@ CE2FBFD64A89F5DBE4EB30DB /* Layout */ = { isa = PBXGroup; children = ( + 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */, 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */, + EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */, ); path = Layout; sourceTree = ""; @@ -1321,7 +1333,6 @@ E74CD7681375AD2EAA34D66B /* Authentication */, 4009BE2E791C16AC6EE39A7E /* BugReport */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, - 5958CAF6E56422496E0063AF /* LoginScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, 70B74A432C241E56A7ACE610 /* Settings */, 02175C9269C4632DB6D12C25 /* Splash */, @@ -1361,6 +1372,7 @@ children = ( D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */, 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */, + 90F48FEF84016ED42A94BA24 /* LoginScreen */, ); path = Authentication; sourceTree = ""; @@ -1471,11 +1483,11 @@ isa = PBXNativeTarget; buildConfigurationList = B15427F8699AD5A5FC75C17E /* Build configuration list for PBXNativeTarget "ElementX" */; buildPhases = ( + A7130911BCB2DF3D249A1836 /* 🛠 SwiftGen */, 9797D588420FCBBC228A63C9 /* Sources */, 215E1D91B98672C856F559D0 /* Resources */, EE878EAA342710DB973E0A87 /* Frameworks */, 98CA896D84BFD53B2554E891 /* ⚠️ SwiftLint */, - A7130911BCB2DF3D249A1836 /* 🛠 SwiftGen */, ); buildRules = ( ); @@ -1516,7 +1528,7 @@ }; }; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; - compatibilityVersion = "Xcode 10.0"; + compatibilityVersion = "Xcode 11.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1699,6 +1711,7 @@ buildActionMask = 2147483647; files = ( 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */, + A8177B197C2E3DB7ACB63088 /* LoginViewModelTests.swift in Sources */, 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */, @@ -1708,7 +1721,6 @@ EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, - 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, @@ -1728,6 +1740,7 @@ D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */, 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */, FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */, + A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */, A636D4900E0D98ED91536482 /* AppCoordinator.swift in Sources */, B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */, 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */, @@ -1764,7 +1777,7 @@ 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */, F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */, A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */, - 84520E7A7A72FDECAB89789E /* FramePreferenceKey.swift in Sources */, + 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */, 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, @@ -1780,11 +1793,14 @@ F2DD8661B5C0BA2BB526FA6C /* KeychainControllerProtocol.swift in Sources */, 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */, D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */, - A941EAD7F407F2ED6DA54A31 /* LoginScreen.swift in Sources */, - 306CC09DF101E7E9CDE79AA5 /* LoginScreenCoordinator.swift in Sources */, - E9CEAF2C38E4E00459B811D9 /* LoginScreenModels.swift in Sources */, - 7C9121245B11CA48307CB462 /* LoginScreenViewModel.swift in Sources */, - 33912D1B9264D897033E0681 /* LoginScreenViewModelProtocol.swift in Sources */, + 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */, + 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */, + CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */, + 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */, + 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */, + BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */, + 49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */, + 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */, B94368839BDB69172E28E245 /* MXLog.swift in Sources */, BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */, F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */, @@ -1894,7 +1910,7 @@ 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, 79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */, EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */, - F01DB7DD607015557CD48B33 /* ViewFrameReader.swift in Sources */, + 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */, 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */, 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */, 50391038BC50C8ED9A4D88A0 /* WeakDictionaryReference.swift in Sources */, @@ -1906,11 +1922,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 91CC102A286A0D9400B6E687 /* LoginScreenUITests.swift in Sources */, 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 499A26EB06C97E48C27A2DB9 /* BuildSettings.swift in Sources */, 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, - 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */, A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 34e608b1bb..dbab7baab6 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -5,3 +5,11 @@ "settings_timeline_style" = "Timeline Style"; "room_timeline_style_plain_long_description" = "Plain Timeline"; "room_timeline_style_bubbled_long_description" = "Bubbled Timeline"; + +// MARK: - Authentication + +"authentication_login_title" = "Welcome back!"; +"authentication_login_forgot_password" = "Forgot password"; + +"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"; diff --git a/ElementX/Sources/BuildSettings.swift b/ElementX/Sources/BuildSettings.swift index 23bb029d70..c4a88a961f 100644 --- a/ElementX/Sources/BuildSettings.swift +++ b/ElementX/Sources/BuildSettings.swift @@ -9,6 +9,9 @@ import Foundation final class BuildSettings { + + // MARK: - Servers + static let defaultHomeserverURLString = "https://matrix.org" // MARK: - Bug report static let bugReportServiceBaseUrlString = "https://riot.im/bugreports" diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 4e320fc3d5..dd5b63c302 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,14 @@ 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 { + /// Forgot password + public static let authenticationLoginForgotPassword = ElementL10n.tr("Untranslated", "authentication_login_forgot_password") + /// Welcome back! + public static let authenticationLoginTitle = ElementL10n.tr("Untranslated", "authentication_login_title") + /// Join millions for free on the largest public server + public static let authenticationServerInfoMatrixDescription = ElementL10n.tr("Untranslated", "authentication_server_info_matrix_description") + /// Choose your server to store your data + public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title") /// Bubbled Timeline public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description") /// Plain Timeline diff --git a/ElementX/Sources/Other/Logging/MXLog.swift b/ElementX/Sources/Other/Logging/MXLog.swift index d55380d008..9f296fde8b 100644 --- a/ElementX/Sources/Other/Logging/MXLog.swift +++ b/ElementX/Sources/Other/Logging/MXLog.swift @@ -133,6 +133,21 @@ private var logger: SwiftyBeaver.Type = { logger.error(message, file, function, line: line) } + public static func failure(_ message: @autoclosure () -> Any, _ + file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + logger.error(message(), file, function, line: line, context: context) + assertionFailure("\(message())") + } + + @available(swift, obsoleted: 5.4) + @objc public static func logFailure(_ message: String, file: String, function: String, line: Int) { + logger.error(message, file, function, line: line) + assertionFailure(message) + } + // MARK: - Private fileprivate static func configureLogger(_ logger: SwiftyBeaver.Type, withConfiguration configuration: MXLogConfiguration) { diff --git a/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift b/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift new file mode 100644 index 0000000000..58213cd729 --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/ErrorHandling/AlertInfo.swift @@ -0,0 +1,89 @@ +// +// 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 + +/// A type that describes an alert to be shown to the user. +/// +/// The alert info can be added to the view state bindings and used as an alert's `item`: +/// ``` +/// MyView +/// .alert(item: $viewModel.alertInfo) { $0.alert } +/// ``` +struct AlertInfo: Identifiable { + /// An identifier that can be used to distinguish one error from another. + let id: T + /// The alert's title. + let title: String + /// The alert's message (optional). + var message: String? + /// The alert's primary button title and action. Defaults to an Ok button with no action. + var primaryButton: (title: String, action: (() -> Void)?) = (ElementL10n.ok, nil) + /// The alert's secondary button title and action. + var secondaryButton: (title: String, action: (() -> Void)?)? +} + +extension AlertInfo { + /// Initialises the type with the title from an `Error`'s localised description along with the default Ok button. + /// + /// Currently this initialiser creates an alert for every error, however in the future it may be updated to filter + /// out some specific errors such as cancellation and networking issues that create too much noise or are + /// indicated to the user using other mechanisms. + init(error: Error) where T == String { + self.init(id: error.localizedDescription, + title: error.localizedDescription) + } + + /// Initialises the type with a generic title and message for an unknown error along with the default Ok button. + /// - Parameters: + /// - id: An ID that identifies the error. + /// - error: The Error that occurred. + init(id: T) { + self.id = id + title = ElementL10n.dialogTitleError + message = ElementL10n.unknownError + } +} + +extension AlertInfo { + private var messageText: Text? { + guard let message = message else { return nil } + return Text(message) + } + + /// Returns a SwiftUI `Alert` created from this alert info, using default button + /// styles for both primary and (if set) secondary buttons. + var alert: Alert { + if let secondaryButton = secondaryButton { + return Alert(title: Text(title), + message: messageText, + primaryButton: alertButton(for: primaryButton), + secondaryButton: alertButton(for: secondaryButton)) + } else { + return Alert(title: Text(title), + message: messageText, + dismissButton: alertButton(for: primaryButton)) + } + } + + private func alertButton(for buttonParameters: (title: String, action: (() -> Void)?)) -> Alert.Button { + guard let action = buttonParameters.action else { + return .default(Text(buttonParameters.title)) + } + + return .default(Text(buttonParameters.title), action: action) + } +} diff --git a/ElementX/Sources/Other/SwiftUI/Views/ViewFrameReader/FramePreferenceKey.swift b/ElementX/Sources/Other/SwiftUI/Layout/FramePreferenceKey.swift similarity index 100% rename from ElementX/Sources/Other/SwiftUI/Views/ViewFrameReader/FramePreferenceKey.swift rename to ElementX/Sources/Other/SwiftUI/Layout/FramePreferenceKey.swift diff --git a/ElementX/Sources/Other/SwiftUI/Views/ViewFrameReader/ViewFrameReader.swift b/ElementX/Sources/Other/SwiftUI/Layout/ViewFrameReader.swift similarity index 100% rename from ElementX/Sources/Other/SwiftUI/Views/ViewFrameReader/ViewFrameReader.swift rename to ElementX/Sources/Other/SwiftUI/Layout/ViewFrameReader.swift diff --git a/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift b/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift index 1aec8b32f4..848e5328f6 100644 --- a/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift +++ b/ElementX/Sources/Other/UserIndicators/LabelledActivityIndicatorView.swift @@ -22,16 +22,14 @@ final class LabelledActivityIndicatorView: UIView { static let padding = UIEdgeInsets(top: 20, left: 40, bottom: 15, right: 40) static let activityIndicatorScale = CGFloat(1.5) static let cornerRadius: CGFloat = 12.0 - static let stackBackgroundOpacity: CGFloat = 0.9 static let stackSpacing: CGFloat = 15 static let backgroundOpacity: CGFloat = 0.5 } private let stackBackgroundView: UIView = { - let view = UIView() + let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) view.layer.cornerRadius = Constants.cornerRadius - view.alpha = Constants.stackBackgroundOpacity - view.backgroundColor = .gray.withAlphaComponent(0.75) + view.clipsToBounds = true return view }() @@ -67,6 +65,7 @@ final class LabelledActivityIndicatorView: UIView { private func setup(text: String) { setupStackView() label.text = text + label.textColor = .element.primaryContent } private func setupStackView() { diff --git a/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift index 3a0306e190..53d892a3b1 100644 --- a/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift +++ b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift @@ -72,12 +72,27 @@ class RoundedToastView: UIView { private func setup(viewState: ToastViewState) { - backgroundColor = .gray.withAlphaComponent(0.75) + backgroundColor = .clear + clipsToBounds = true + setupBackgroundMaterial() setupStackView() stackView.addArrangedSubview(toastView(for: viewState.style)) stackView.addArrangedSubview(label) label.text = viewState.label + label.textColor = .element.primaryContent + } + + private func setupBackgroundMaterial() { + let material = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) + addSubview(material) + material.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + material.topAnchor.constraint(equalTo: topAnchor), + material.bottomAnchor.constraint(equalTo: bottomAnchor), + material.leadingAnchor.constraint(equalTo: leadingAnchor), + material.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) } private func setupStackView() { @@ -101,10 +116,10 @@ class RoundedToastView: UIView { case .loading: return activityIndicator case .success: - imageView.image = UIImage(systemName: "checkmark.circle") + imageView.image = UIImage(systemName: "checkmark") return imageView case .error: - imageView.image = UIImage(systemName: "x.circle") + imageView.image = UIImage(systemName: "xmark") return imageView } } diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index 2fc2d782c9..42f8ff10c2 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -55,8 +55,6 @@ class AuthenticationCoordinator: Coordinator { switch action { case .login: self.showLoginScreen() - case .register: - fatalError("Not implemented") } } @@ -69,8 +67,9 @@ class AuthenticationCoordinator: Coordinator { } private func showLoginScreen() { - let parameters = LoginScreenCoordinatorParameters() - let coordinator = LoginScreenCoordinator(parameters: parameters) + let homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString) + let parameters = LoginCoordinatorParameters(navigationRouter: navigationRouter, homeserver: homeserver) + let coordinator = LoginCoordinator(parameters: parameters) coordinator.callback = { [weak self, weak coordinator] action in guard let self = self, let coordinator = coordinator else { @@ -78,9 +77,9 @@ class AuthenticationCoordinator: Coordinator { } switch action { - case .login(let result): + case .login(let username, let password): Task { - switch await self.login(username: result.username, password: result.password) { + switch await self.login(username: username, password: password) { case .success(let userSession): self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession) self.remove(childCoordinator: coordinator) @@ -90,6 +89,8 @@ class AuthenticationCoordinator: Coordinator { MXLog.error("Failed logging in user with error: \(error)") } } + case .continueWithOIDC: + break } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift new file mode 100644 index 0000000000..4c452c33a6 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginCoordinator.swift @@ -0,0 +1,188 @@ +// +// 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 +import MatrixRustSDK + +struct LoginCoordinatorParameters { + let navigationRouter: NavigationRouterType + /// The homeserver to be shown initially. + let homeserver: LoginHomeserver +} + +enum LoginCoordinatorAction: CustomStringConvertible { + /// Login with the associated username and password. + case login(username: String, password: String) + /// Continue using OIDC. + case continueWithOIDC + + /// A string representation of the action, ignoring any associated values that could leak PII. + var description: String { + switch self { + case .login: + return "login" + case .continueWithOIDC: + return "continueWithOIDC" + } + } +} + +final class LoginCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LoginCoordinatorParameters + private let loginHostingController: UIViewController + private var loginViewModel: LoginViewModelProtocol + + private var currentTask: Task? { + willSet { + currentTask?.cancel() + } + } + + private var navigationRouter: NavigationRouterType { parameters.navigationRouter } + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var activityIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var callback: (@MainActor (LoginCoordinatorAction) -> Void)? + + // MARK: - Setup + + init(parameters: LoginCoordinatorParameters) { + self.parameters = parameters + + let viewModel = LoginViewModel(homeserver: parameters.homeserver) + loginViewModel = viewModel + + let view = LoginScreen(viewModel: viewModel.context) + loginHostingController = UIHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: loginHostingController) + } + + // MARK: - Public + func start() { + MXLog.debug("[LoginCoordinator] did start.") + + loginViewModel.callback = { [weak self] action in + guard let self = self else { return } + MXLog.debug("[LoginCoordinator] LoginViewModel did callback with result: \(action).") + + switch action { + case .selectServer: + self.presentServerSelectionScreen() + case .parseUsername(let username): + self.parseUsername(username) + case .forgotPassword: + self.showForgotPasswordScreen() + case .login(let username, let password): + self.login(username: username, password: password) + case .continueWithOIDC: + self.callback?(.continueWithOIDC) + } + } + } + + func toPresentable() -> UIViewController { + loginHostingController + } + + // MARK: - Private + + /// Show a blocking activity indicator whilst saving. + private func startLoading(isInteractionBlocking: Bool) { + activityIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: isInteractionBlocking)) + + if !isInteractionBlocking { + loginViewModel.update(isLoading: true) + } + } + + /// Show a non-blocking indicator that an operation was successful. + private func indicateSuccess() { + activityIndicator = indicatorPresenter.present(.success(label: ElementL10n.dialogTitleSuccess)) + } + + /// Show a non-blocking indicator that an operation failed. + private func indicateFailure() { + activityIndicator = indicatorPresenter.present(.error(label: ElementL10n.dialogTitleError)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loginViewModel.update(isLoading: false) + activityIndicator = nil + } + + /// Processes an error to either update the flow or display it to the user. + private func handleError(_ error: Error) { + loginViewModel.displayError(.alert(error.localizedDescription)) + } + + /// Requests the authentication coordinator to log in using the specified credentials. + private func login(username: String, password: String) { + var username = loginViewModel.context.username + + if !isMXID(username: username) { + let homeserver = loginViewModel.context.viewState.homeserver + username = "@\(username):\(homeserver.address)" + } + + callback?(.login(username: username, password: password)) + } + + /// Parses the specified username and looks up the homeserver when a Matrix ID is entered. + private func parseUsername(_ username: String) { + guard isMXID(username: username) else { return } + + let domain = String(username.split(separator: ":")[1]) + + let homeserver = LoginHomeserver(address: domain) + updateViewModel(homeserver: homeserver) + indicateSuccess() + } + + /// Checks whether the specified username is a Matrix ID or not. + private func isMXID(username: String) -> Bool { + let range = NSRange(location: 0, length: username.count) + + let detector = try? NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) + return detector?.numberOfMatches(in: username, range: range) ?? 0 > 0 + } + + /// Updates the view model with a different homeserver. + private func updateViewModel(homeserver: LoginHomeserver) { + loginViewModel.update(homeserver: homeserver) + indicateSuccess() + } + + /// 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")) + } + + /// Shows the forgot password screen. + private func showForgotPasswordScreen() { + loginViewModel.displayError(.alert("Not implemented.")) + } +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift new file mode 100644 index 0000000000..cdc8389185 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -0,0 +1,89 @@ +// +// 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 + +/// Information about a homeserver that is ready for display in the authentication flow. +struct LoginHomeserver: Equatable { + /// The homeserver string to be shown to the user. + let address: String + /// Whether or not the homeserver is matrix.org. + let isMatrixDotOrg: Bool + /// The types login supported by the homeserver. + let loginMode: LoginMode +} + +extension LoginHomeserver { + /// Temporary initialiser for use until the FFI has homeserver discovery etc. + init(address: String) { + let address = Self.sanitized(address).components(separatedBy: "://").last ?? address + + self.address = address + isMatrixDotOrg = address == "matrix.org" + loginMode = .password + } + + /// Sanitizes a user entered homeserver address with the following rules + /// - Trim any whitespace. + /// - Lowercase the address. + /// - Ensure the address contains a scheme, otherwise make it `https`. + /// - Remove any trailing slashes. + static func sanitized(_ address: String) -> String { + var address = address.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + + if !address.contains("://") { + address = "https://\(address)" + } + + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + return address + } +} + +// MARK: - Mocks + +extension LoginHomeserver { + /// A mock homeserver that is configured just like matrix.org. + static var mockMatrixDotOrg: LoginHomeserver { + LoginHomeserver(address: "matrix.org", + isMatrixDotOrg: true, + loginMode: .password) + } + + /// A mock homeserver that supports login and registration via a password but has no SSO providers. + static var mockBasicServer: LoginHomeserver { + LoginHomeserver(address: "example.com", + isMatrixDotOrg: false, + loginMode: .password) + } + + /// A mock homeserver that supports only supports authentication via a single SSO provider. + static var mockOIDC: LoginHomeserver { + LoginHomeserver(address: "company.com", + isMatrixDotOrg: false, + // swiftlint:disable:next force_unwrapping + loginMode: .oidc(URL(string: "https://auth.company.com")!)) + } + + /// A mock homeserver that only with no supported login flows. + static var mockUnsupported: LoginHomeserver { + LoginHomeserver(address: "server.net", + isMatrixDotOrg: false, + loginMode: .unsupported) + } + +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift new file mode 100644 index 0000000000..abee84de61 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift @@ -0,0 +1,56 @@ +// +// 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 + +/// The supported forms of login that a homeserver allows. +enum LoginMode: Equatable { + /// The login mode hasn't been determined yet. + case unknown + /// The homeserver supports login via OpenID Connect at the associated URL. + case oidc(URL) + /// The homeserver supports login with a password. + case password + /// The homeserver only allows login with unsupported mechanisms. Use fallback instead. + case unsupported + + var supportsOIDCFlow: Bool { + switch self { + case .oidc: + return true + default: + return false + } + } + + var supportsPasswordFlow: Bool { + switch self { + case .password: + return true + default: + return false + } + } + + var isUnsupported: Bool { + switch self { + case .unsupported: + return true + default: + return false + } + } +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginModels.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginModels.swift new file mode 100644 index 0000000000..294a9d7605 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginModels.swift @@ -0,0 +1,103 @@ +// +// 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 LoginViewModelAction: CustomStringConvertible { + /// The user would like to select another server. + case selectServer + /// Parse the username and update the homeserver if included. + case parseUsername(String) + /// The user would like to reset their password. + case forgotPassword + /// Login using the supplied credentials. + case login(username: String, password: String) + /// Continue using OIDC. + case continueWithOIDC + + /// A string representation of the action, ignoring any associated values that could leak PII. + var description: String { + switch self { + case .selectServer: + return "selectServer" + case .parseUsername: + return "parseUsername" + case .forgotPassword: + return "forgotPassword" + case .login: + return "login" + case .continueWithOIDC: + return "continueWithOIDC" + } + } +} + +// MARK: View + +struct LoginViewState: BindableState { + /// Data about the selected homeserver. + var homeserver: LoginHomeserver + /// Whether a new homeserver is currently being loaded. + var isLoading: Bool = false + /// View state that can be bound to from SwiftUI. + var bindings: LoginBindings + + /// The types of login supported by the homeserver. + var loginMode: LoginMode { homeserver.loginMode } + + /// `true` if the username and password are ready to be submitted. + var hasValidCredentials: Bool { + !bindings.username.isEmpty && !bindings.password.isEmpty + } + + /// `true` when valid credentials have been entered and a homeserver has been loaded. + var canSubmit: Bool { + hasValidCredentials && !isLoading + } +} + +struct LoginBindings { + /// The username input by the user. + var username = "" + /// The password input by the user. + var password = "" + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum LoginViewAction { + /// The user would like to select another server. + case selectServer + /// Parse the username to detect if a homeserver is included. + case parseUsername + /// The user would like to reset their password. + case forgotPassword + /// Continue using the input username and password. + case next + /// Continue using OIDC. + case continueWithOIDC +} + +enum LoginErrorType: Hashable { + /// A specific error message shown in an alert. + case alert(String) + /// Looking up the homeserver from the username failed. + case invalidHomeserver + /// The response from the homeserver was unexpected. + case unknown +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift new file mode 100644 index 0000000000..97f65298ac --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.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 SwiftUI + +typealias LoginViewModelType = StateStoreViewModel + +class LoginViewModel: LoginViewModelType, LoginViewModelProtocol { + + // MARK: - Properties + + // MARK: Public + + var callback: (@MainActor (LoginViewModelAction) -> Void)? + + // MARK: - Setup + + init(homeserver: LoginHomeserver) { + let bindings = LoginBindings() + let viewState = LoginViewState(homeserver: homeserver, bindings: bindings) + + super.init(initialViewState: viewState) + } + + // MARK: - Public + + override func process(viewAction: LoginViewAction) async { + switch viewAction { + case .selectServer: + callback?(.selectServer) + case .parseUsername: + callback?(.parseUsername(state.bindings.username)) + case .forgotPassword: + callback?(.forgotPassword) + case .next: + callback?(.login(username: state.bindings.username, password: state.bindings.password)) + case .continueWithOIDC: + callback?(.continueWithOIDC) + } + } + + func update(isLoading: Bool) { + guard state.isLoading != isLoading else { return } + state.isLoading = isLoading + } + + func update(homeserver: LoginHomeserver) { + state.homeserver = homeserver + } + + func displayError(_ type: LoginErrorType) { + switch type { + case .alert(let message): + state.bindings.alertInfo = AlertInfo(id: type, + title: ElementL10n.dialogTitleError, + message: message) + case .invalidHomeserver: + state.bindings.alertInfo = AlertInfo(id: type, + title: ElementL10n.dialogTitleError, + message: ElementL10n.loginSigninMatrixIdErrorInvalidMatrixId) + case .unknown: + state.bindings.alertInfo = AlertInfo(id: type) + } + } +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModelProtocol.swift new file mode 100644 index 0000000000..ad2766a926 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModelProtocol.swift @@ -0,0 +1,36 @@ +// +// 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 LoginViewModelProtocol { + + var callback: (@MainActor (LoginViewModelAction) -> Void)? { get set } + var context: LoginViewModelType.Context { get } + + /// Update the view to reflect that a new homeserver is being loaded. + /// - Parameter isLoading: Whether or not the homeserver is being loaded. + func update(isLoading: Bool) + + /// Update the view with new homeserver information. + /// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`. + func update(homeserver: LoginHomeserver) + + /// Display an error to the user. + /// - Parameter type: The type of error to be displayed. + func displayError(_ type: LoginErrorType) +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift new file mode 100644 index 0000000000..4b31d42636 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -0,0 +1,180 @@ +// +// 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 LoginScreen: View { + + // MARK: - Properties + + // MARK: Private + + /// The focus state of the username text field. + @FocusState private var isUsernameFocused: Bool + /// The focus state of the password text field. + @FocusState private var isPasswordFocused: Bool + + // MARK: Public + + @ObservedObject var viewModel: LoginViewModel.Context + + var body: some View { + ScrollView { + VStack(spacing: 0) { + header + .padding(.top, UIConstants.topPaddingToNavigationBar) + .padding(.bottom, 36) + + serverInfo + .padding(.leading, 12) + + Rectangle() + .fill(Color.element.quinaryContent) + .frame(height: 1) + .padding(.vertical, 21) + + switch viewModel.viewState.loginMode { + case .password: + loginForm + case .oidc: + oidcButton + default: + loginUnavailableText + } + + } + .readableFrame() + .padding(.horizontal, 16) + .padding(.bottom, 16) + } + .background(Color.element.background.ignoresSafeArea()) + .alert(item: $viewModel.alertInfo) { $0.alert } + } + + /// The header containing a Welcome Back title. + var header: some View { + Text(ElementL10n.authenticationLoginTitle) + .font(.element.title2B) + .multilineTextAlignment(.center) + .foregroundColor(.element.primaryContent) + } + + /// 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) + } + } + + /// 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) + .focused($isUsernameFocused) + .textFieldStyle(.elementInput()) + .disableAutocorrection(true) + .textContentType(.username) + .autocapitalization(.none) + .submitLabel(.next) + .onChange(of: isUsernameFocused, perform: usernameFocusChanged) + .onSubmit { isPasswordFocused = true } + .accessibilityIdentifier("usernameTextField") + + Spacer().frame(height: 20) + + SecureField(ElementL10n.loginSignupPasswordHint, text: $viewModel.password) + .focused($isPasswordFocused) + .textFieldStyle(.elementInput()) + .textContentType(.password) + .submitLabel(.done) + .onSubmit(submit) + .accessibilityIdentifier("passwordTextField") + + Button { viewModel.send(viewAction: .forgotPassword) } label: { + Text(ElementL10n.authenticationLoginForgotPassword) + .font(.element.body) + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.bottom, 8) + + Button(action: submit) { + Text(ElementL10n.loginSignupSubmit) + } + .buttonStyle(.elementAction(.xLarge)) + .disabled(!viewModel.viewState.canSubmit) + .accessibilityIdentifier("nextButton") + } + } + + /// The OIDC button that can be used for login. + var oidcButton: some View { + Button { viewModel.send(viewAction: .continueWithOIDC) } label: { + Text(ElementL10n.loginContinue) + } + .buttonStyle(.elementAction(.xLarge)) + .accessibilityIdentifier("oidcButton") + } + + /// Text shown if neither password or OIDC login is supported. + var loginUnavailableText: some View { + Text(ElementL10n.autodiscoverWellKnownError) + .font(.body) + .multilineTextAlignment(.center) + .foregroundColor(.element.primaryContent) + .frame(maxWidth: .infinity) + .accessibilityIdentifier("unsupportedServerText") + } + + /// Parses the username for a homeserver. + private func usernameFocusChanged(isFocussed: Bool) { + guard !isFocussed, !viewModel.username.isEmpty else { return } + viewModel.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) + } +} + +// MARK: - Previews + +struct Login_Previews: PreviewProvider { + static let credentialsViewModel: LoginViewModel = { + let viewModel = LoginViewModel(homeserver: .mockMatrixDotOrg) + viewModel.context.username = "alice" + viewModel.context.password = "password" + return viewModel + }() + + static var previews: some View { + screen(for: LoginViewModel(homeserver: .mockMatrixDotOrg)) + screen(for: credentialsViewModel) + screen(for: LoginViewModel(homeserver: .mockBasicServer)) + screen(for: LoginViewModel(homeserver: .mockOIDC)) + } + + static func screen(for viewModel: LoginViewModel) -> some View { + NavigationView { + LoginScreen(viewModel: viewModel.context) + .navigationBarTitleDisplayMode(.inline) + .tint(.element.accent) + } + .navigationViewStyle(.stack) + } +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift new file mode 100644 index 0000000000..5a3cfe529d --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginServerInfoSection.swift @@ -0,0 +1,64 @@ +// +// 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 + +/// A view that shows information about the chosen homeserver, +/// along with an edit button to pick a different one. +struct LoginServerInfoSection: View { + + // MARK: - Public + + /// The address shown for the server. + let address: String + /// Whether or not to show the matrix.org description. + let showMatrixDotOrgInfo: Bool + /// The action performed when tapping the edit button. + let editAction: () -> Void + + // MARK: - Views + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(ElementL10n.authenticationServerInfoTitle) + .font(.element.subheadline) + .foregroundColor(.element.secondaryContent) + + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(address) + .font(.element.body) + .foregroundColor(.element.primaryContent) + + if showMatrixDotOrgInfo { + Text(ElementL10n.authenticationServerInfoMatrixDescription) + .font(.element.caption1) + .foregroundColor(.element.tertiaryContent) + .accessibilityIdentifier("serverDescriptionText") + } + } + + Spacer() + + Button(action: editAction) { + Text(ElementL10n.edit) + .padding(.vertical, 2) + } + .buttonStyle(.elementGhost()) + } + } + } +} diff --git a/ElementX/Sources/Screens/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/LoginScreen/LoginScreenCoordinator.swift deleted file mode 100644 index 082d695efe..0000000000 --- a/ElementX/Sources/Screens/LoginScreen/LoginScreenCoordinator.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// 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 LoginScreenCoordinatorParameters { - -} - -enum LoginScreenCoordinatorAction { - case login((username: String, password: String)) -} - -final class LoginScreenCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - - private let parameters: LoginScreenCoordinatorParameters - private let loginScreenHostingController: UIViewController - private var loginScreenViewModel: LoginScreenViewModelProtocol - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] - var callback: ((LoginScreenCoordinatorAction) -> Void)? - - // MARK: - Setup - - init(parameters: LoginScreenCoordinatorParameters) { - self.parameters = parameters - - loginScreenViewModel = LoginScreenViewModel() - let view = LoginScreen(context: loginScreenViewModel.context) - - loginScreenHostingController = UIHostingController(rootView: view) - loginScreenHostingController.isModalInPresentation = true - - loginScreenViewModel.callback = { [weak self] action in - MXLog.debug("[LoginScreenCoordinator] LoginScreenViewModel did complete.") - guard let self = self else { return } - switch action { - case .login(let credentials): - self.callback?(.login(credentials)) - } - } - } - - // MARK: - Public - func start() { - - } - - func toPresentable() -> UIViewController { - return self.loginScreenHostingController - } -} diff --git a/ElementX/Sources/Screens/LoginScreen/LoginScreenModels.swift b/ElementX/Sources/Screens/LoginScreen/LoginScreenModels.swift deleted file mode 100644 index 3e848f8761..0000000000 --- a/ElementX/Sources/Screens/LoginScreen/LoginScreenModels.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 - -enum LoginScreenViewModelAction { - case login((username: String, password: String)) -} - -enum LoginScreenViewAction { - case login -} - -struct LoginScreenViewState: BindableState { - var bindings: LoginScreenViewStateBindings - var hasCredentials: Bool { !bindings.username.isEmpty && !bindings.password.isEmpty } -} - -struct LoginScreenViewStateBindings { - var username: String - var password: String -} - -struct LoginScreenErrorAlertInfo: Identifiable { - enum AlertType { - case genericFailure - } - - let id: AlertType - let title: String - let subtitle: String -} diff --git a/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModel.swift b/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModel.swift deleted file mode 100644 index 90378bd06b..0000000000 --- a/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModel.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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 LoginScreenViewModelType = StateStoreViewModel - -class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol { - - // MARK: - Properties - - // MARK: Private - - // MARK: Public - - var callback: ((LoginScreenViewModelAction) -> Void)? - - // MARK: - Setup - - init() { - super.init(initialViewState: LoginScreenViewState(bindings: LoginScreenViewStateBindings(username: "", - password: ""))) - } - - // MARK: - Public - - override func process(viewAction: LoginScreenViewAction) async { - switch viewAction { - case .login: - callback?(.login((username: context.username, password: context.password))) - } - } -} diff --git a/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift b/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift deleted file mode 100644 index 4b2158437a..0000000000 --- a/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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 LoginScreenViewModelProtocol { - var callback: ((LoginScreenViewModelAction) -> Void)? { get set } - var context: LoginScreenViewModelType.Context { get } -} diff --git a/ElementX/Sources/Screens/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/LoginScreen/View/LoginScreen.swift deleted file mode 100644 index 3ce116dc3f..0000000000 --- a/ElementX/Sources/Screens/LoginScreen/View/LoginScreen.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// 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 -import DesignKit - -struct LoginScreen: View { - - @ObservedObject var context: LoginScreenViewModel.Context - - enum Field { case username, password } - @FocusState private var focussedField: Field? - - var body: some View { - VStack { - TextField("Username", text: $context.username) - .textFieldStyle(.elementInput()) - .disableAutocorrection(true) - .textContentType(.username) - .autocapitalization(.none) - .focused($focussedField, equals: .username) - .submitLabel(.next) - .onSubmit { focussedField = .password } - - SecureField("Password", text: $context.password) - .textFieldStyle(.elementInput()) - .textContentType(.password) - .focused($focussedField, equals: .password) - .submitLabel(.go) - .onSubmit(submit) - - Button("Login", action: submit) - .buttonStyle(.elementAction(.xLarge)) - .disabled(!context.viewState.hasCredentials) - } - .padding(.horizontal, 8.0) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.element.background.ignoresSafeArea()) - .navigationTitle("Login") - .navigationBarTitleDisplayMode(.inline) - } - - func submit() { - guard context.viewState.hasCredentials else { return } - context.send(viewAction: .login) - focussedField = nil - } -} - -// MARK: - Previews - -struct LoginScreen_Previews: PreviewProvider { - static var previews: some View { - let viewModel = LoginScreenViewModel() - NavigationView { - LoginScreen(context: viewModel.context) - } - .navigationViewStyle(.stack) - } -} diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift b/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift index 269cb73a2b..3db6a7a755 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SplashScreen/SplashScreenCoordinator.swift @@ -54,8 +54,6 @@ final class SplashScreenCoordinator: Coordinator, Presentable { switch action { case .login: self.callback?(.login) - case .register: - self.callback?(.register) } } } diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift b/ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift index 61a82d4184..fc8c7c0689 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift +++ b/ElementX/Sources/Screens/SplashScreen/SplashScreenModels.swift @@ -19,7 +19,6 @@ import SwiftUI // MARK: - Coordinator enum SplashScreenCoordinatorAction { - case register case login } @@ -33,7 +32,6 @@ struct SplashScreenPageContent { // MARK: View model enum SplashScreenViewModelAction { - case register case login } @@ -91,6 +89,5 @@ struct SplashScreenBindings { } enum SplashScreenViewAction { - case register case login } diff --git a/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift b/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift index f6be2a9655..ae4a54b243 100644 --- a/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift +++ b/ElementX/Sources/Screens/SplashScreen/SplashScreenViewModel.swift @@ -39,18 +39,8 @@ class SplashScreenViewModel: SplashScreenViewModelType, SplashScreenViewModelPro override func process(viewAction: SplashScreenViewAction) async { switch viewAction { - case .register: - register() case .login: - login() + callback?(.login) } } - - private func register() { - callback?(.register) - } - - private func login() { - callback?(.login) - } } diff --git a/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift b/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift index 3ec8ccc97a..f8144bdc25 100644 --- a/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift +++ b/ElementX/Sources/Screens/SplashScreen/View/SplashScreen.swift @@ -15,6 +15,7 @@ // import SwiftUI +import DesignKit /// The splash screen shown at the beginning of the onboarding flow. struct SplashScreen: View { @@ -219,6 +220,6 @@ struct SplashScreen_Previews: PreviewProvider { static var previews: some View { SplashScreen(viewModel: viewModel.context) - .accentColor(.element.accent) + .tint(.element.accent) } } diff --git a/ElementX/Sources/UITestScreenIdentifier.swift b/ElementX/Sources/UITestScreenIdentifier.swift index a7357855aa..ea7f0a2a1b 100644 --- a/ElementX/Sources/UITestScreenIdentifier.swift +++ b/ElementX/Sources/UITestScreenIdentifier.swift @@ -10,6 +10,8 @@ import Foundation enum UITestScreenIdentifier: String { case login + case loginOIDC + case loginUnsupported case simpleRegular case simpleUpgrade case settings diff --git a/ElementX/Sources/UITestsAppCoordinator.swift b/ElementX/Sources/UITestsAppCoordinator.swift index 69065bb7f7..5da22d882f 100644 --- a/ElementX/Sources/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITestsAppCoordinator.swift @@ -50,7 +50,17 @@ struct MockScreen: Identifiable { var coordinator: Coordinator & Presentable { switch id { case .login: - return LoginScreenCoordinator(parameters: .init()) + let router = NavigationRouter(navigationController: ElementNavigationController()) + return LoginCoordinator(parameters: .init(navigationRouter: router, + homeserver: .mockMatrixDotOrg)) + case .loginOIDC: + let router = NavigationRouter(navigationController: ElementNavigationController()) + return LoginCoordinator(parameters: .init(navigationRouter: router, + homeserver: .mockOIDC)) + case .loginUnsupported: + let router = NavigationRouter(navigationController: ElementNavigationController()) + return LoginCoordinator(parameters: .init(navigationRouter: router, + homeserver: .mockUnsupported)) case .simpleRegular: return TemplateSimpleScreenCoordinator(parameters: .init(promptType: .regular)) case .simpleUpgrade: diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 44e3c7dfaf..60b50752fa 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -58,26 +58,28 @@ targets: CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h - postBuildScripts: - - name: ⚠️ SwiftLint + preBuildScripts: + - name: 🛠 SwiftGen runOnlyWhenInstalling: false shell: /bin/sh script: | export PATH="$PATH:/opt/homebrew/bin" - if which swiftlint >/dev/null; then - swiftlint + if which swiftgen >/dev/null; then + swiftgen config run --config Tools/SwiftGen/swiftgen-config.yml else - echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" + echo "warning: SwiftGen not installed, download from https://github.com/SwiftGen/SwiftGen" fi - - name: 🛠 SwiftGen + + postBuildScripts: + - name: ⚠️ SwiftLint runOnlyWhenInstalling: false shell: /bin/sh script: | export PATH="$PATH:/opt/homebrew/bin" - if which swiftgen >/dev/null; then - swiftgen config run --config Tools/SwiftGen/swiftgen-config.yml + if which swiftlint >/dev/null; then + swiftlint else - echo "warning: SwiftGen not installed, download from https://github.com/SwiftGen/SwiftGen" + echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" fi dependencies: diff --git a/UITests/Sources/LoginScreenUITests.swift b/UITests/Sources/LoginScreenUITests.swift index e30e59a1f3..df454d01e5 100644 --- a/UITests/Sources/LoginScreenUITests.swift +++ b/UITests/Sources/LoginScreenUITests.swift @@ -15,14 +15,128 @@ // import XCTest +import ElementX +@MainActor class LoginScreenUITests: XCTestCase { - func testInitialStateComponents() { - let app = Application.launch() + var app: XCUIApplication! + + @MainActor + override func setUp() async throws { + app = nil + } + + func testMatrixDotOrg() { + app = Application.launch() app.goToScreenWithIdentifier(.login) - XCTAssert(app.buttons["Login"].exists) - XCTAssert(app.textFields["Username"].exists) - XCTAssert(app.secureTextFields["Password"].exists) + let state = "matrix.org" + validateServerDescriptionIsVisible(for: state) + validateLoginFormIsVisible(for: state) + validateOIDCButtonIsHidden(for: state) + validateNextButtonIsDisabled(for: state) + validateUnsupportedServerTextIsHidden(for: state) + + app.textFields.element.tap() + app.typeText("@test:server.com") + + app.secureTextFields.element.tap() + app.typeText("12345678") + + validateNextButtonIsEnabled(for: "matrix.org with credentials entered") + } + + func testOIDC() { + app = Application.launch() + app.goToScreenWithIdentifier(.loginOIDC) + + let state = "an OIDC only server" + validateServerDescriptionIsHidden(for: state) + validateLoginFormIsHidden(for: state) + validateOIDCButtonIsShown(for: state) + validateUnsupportedServerTextIsHidden(for: state) + } + + func testUnsupported() { + app = Application.launch() + app.goToScreenWithIdentifier(.loginUnsupported) + + let state = "an unsupported server" + validateServerDescriptionIsHidden(for: state) + validateLoginFormIsHidden(for: state) + validateOIDCButtonIsHidden(for: state) + validateUnsupportedServerTextIsShown(for: state) + } + + /// Checks that the server description label is shown. + func validateServerDescriptionIsVisible(for state: String) { + let descriptionLabel = app.staticTexts["serverDescriptionText"] + XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).") + } + + /// Checks that the server description label is hidden. + func validateServerDescriptionIsHidden(for state: String) { + let descriptionLabel = app.staticTexts["serverDescriptionText"] + XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).") + } + + /// Checks that the username and password text fields are shown along with the next button. + func validateLoginFormIsVisible(for state: String) { + let usernameTextField = app.textFields.element + let passwordTextField = app.secureTextFields.element + let nextButton = app.buttons["nextButton"] + + XCTAssertTrue(usernameTextField.exists, "Username input should be shown for \(state).") + XCTAssertTrue(passwordTextField.exists, "Password input should be shown for \(state).") + XCTAssertTrue(nextButton.exists, "The next button should be shown for \(state).") + } + + /// Checks that the username and password text fields are hidden along with the next button. + func validateLoginFormIsHidden(for state: String) { + let usernameTextField = app.textFields.element + let passwordTextField = app.secureTextFields.element + let nextButton = app.buttons["nextButton"] + + XCTAssertFalse(usernameTextField.exists, "Username input should not be shown for \(state).") + XCTAssertFalse(passwordTextField.exists, "Password input should not be shown for \(state).") + XCTAssertFalse(nextButton.exists, "The next button should not be shown for \(state).") + } + + /// Checks that the next button is shown but is disabled. + func validateNextButtonIsDisabled(for state: String) { + let nextButton = app.buttons["nextButton"] + XCTAssertTrue(nextButton.exists, "The next button should be shown.") + XCTAssertFalse(nextButton.isEnabled, "The next button should be disabled for \(state).") + } + + /// Checks that the next button is shown and is enabled. + func validateNextButtonIsEnabled(for state: String) { + let nextButton = app.buttons["nextButton"] + XCTAssertTrue(nextButton.exists, "The next button should be shown.") + XCTAssertTrue(nextButton.isEnabled, "The next button should be enabled for \(state).") + } + + /// Checks that the OIDC button is shown on the screen. + func validateOIDCButtonIsShown(for state: String) { + let oidcButton = app.buttons["oidcButton"] + XCTAssertTrue(oidcButton.exists, "The OIDC button should be shown for \(state).") + } + + /// Checks that the OIDC button is not shown on the screen. + func validateOIDCButtonIsHidden(for state: String) { + let oidcButton = app.buttons["oidcButton"] + XCTAssertFalse(oidcButton.exists, "The OIDC button should be hidden for \(state).") + } + + /// Checks that the unsupported homeserver text is shown on the screen. + func validateUnsupportedServerTextIsShown(for state: String) { + let unsupportedText = app.staticTexts["unsupportedServerText"] + XCTAssertTrue(unsupportedText.exists, "The unsupported homeserver text should be shown for \(state).") + } + + /// Checks that the unsupported homeserver text is not shown on the screen. + func validateUnsupportedServerTextIsHidden(for state: String) { + let unsupportedText = app.staticTexts["unsupportedServerText"] + XCTAssertFalse(unsupportedText.exists, "The unsupported homeserver text should be hidden for \(state).") } } diff --git a/UnitTests/Sources/LoginScreenViewModelTests.swift b/UnitTests/Sources/LoginScreenViewModelTests.swift deleted file mode 100644 index 8e571e5708..0000000000 --- a/UnitTests/Sources/LoginScreenViewModelTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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 XCTest - -@testable import ElementX - -class LoginScreenViewModelTests: XCTestCase { - - override func setUpWithError() throws { - - } - - func testInitialState() { - - } -} diff --git a/UnitTests/Sources/LoginViewModelTests.swift b/UnitTests/Sources/LoginViewModelTests.swift new file mode 100644 index 0000000000..903d7edc73 --- /dev/null +++ b/UnitTests/Sources/LoginViewModelTests.swift @@ -0,0 +1,149 @@ +// +// 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 XCTest + +@testable import ElementX + +@MainActor +class LoginViewModelTests: XCTestCase { + let defaultHomeserver = LoginHomeserver.mockMatrixDotOrg + var viewModel: LoginViewModelProtocol! + var context: LoginViewModelType.Context! + + @MainActor override func setUp() async throws { + viewModel = LoginViewModel(homeserver: defaultHomeserver) + context = viewModel.context + } + + func testMatrixDotOrg() { + // Given the initial view model configured for matrix.org. + let homeserver = defaultHomeserver + + // Then the view state should contain a homeserver that matches matrix.org and show the login form. + XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should match the original.") + XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") + } + + func testBasicServer() { + // Given a basic server example.com that only supports password registration. + let homeserver = LoginHomeserver.mockBasicServer + + // When updating the view model with the server. + viewModel.update(homeserver: homeserver) + + // Then the view state should be updated with the homeserver and show the login form. + XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should should match the new homeserver.") + XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") + } + + func testUsernameWithEmptyPassword() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When entering a username without a password. + context.username = "bob" + context.password = "" + + // Then the credentials should be considered invalid. + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + } + + func testEmptyUsernameWithPassword() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When entering a password without a username. + context.username = "" + context.password = "12345678" + + // Then the credentials should be considered invalid. + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + } + + func testValidCredentials() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When entering a username and an 8-character password. + context.username = "bob" + context.password = "12345678" + + // Then the credentials should be considered valid. + XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.") + XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") + } + + func testLoadingServer() { + // Given a form with valid credentials. + context.username = "bob" + context.password = "12345678" + XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.") + XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.") + XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") + + // When updating the view model whilst loading a homeserver. + viewModel.update(isLoading: true) + + // Then the view state should reflect that the homeserver is loading. + XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When updating the view model after loading a homeserver. + viewModel.update(isLoading: false) + + // Then the view state should reflect that the homeserver is now loaded. + XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.") + XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") + } + + func testOIDCServer() { + // Given a basic server example.com that supports OIDC registration. + let homeserver = LoginHomeserver.mockOIDC + + // When updating the view model with the server. + viewModel.update(homeserver: homeserver) + + // Then the view state should be updated with the homeserver and show the OIDC button. + XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.") + } + + func testLogsForPassword() { + // Given the coordinator and view model results that contain passwords. + let password = "supersecretpassword" + let viewModelAction: LoginViewModelAction = .login(username: "Alice", password: password) + let coordinatorAction: LoginCoordinatorAction = .login(username: "Alice", password: password) + + // When creating a string representation of those results (e.g. for logging). + let viewModelActionString = "\(viewModelAction)" + let coordinatorActionString = "\(coordinatorAction)" + + // Then the password should not be included in that string. + XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.") + XCTAssertFalse("\(coordinatorActionString)".contains(password), "The password must not be included in any strings.") + } +} diff --git a/changelog.d/40.change b/changelog.d/40.change index 4cb73763ac..ead2bd5214 100644 --- a/changelog.d/40.change +++ b/changelog.d/40.change @@ -1 +1 @@ -Add a UserSessionStore and the splash screen from Element iOS. +Add a the splash screen and login screen from Element iOS along with a UserSessionStore.