diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index e995517e3..9ac4488fb 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -18,11 +18,13 @@ 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422724361B6555364C43281E /* RoomHeaderView.swift */; }; 05776B005C57E92582F0CF08 /* BuildSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F87116470221880017CF522 /* BuildSettings.swift */; }; 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; + 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */; }; 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 06E93B2E3B32740B40F47CC5 /* ElementNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; + 0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; }; 0E8C480700870BB34A2A360F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; @@ -35,6 +37,7 @@ 132D241B09F9044711FD70A5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; + 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; }; 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; }; 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; @@ -52,6 +55,7 @@ 224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */; }; 226027BE23AF64FA61C7A4C0 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */; }; + 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CF12478983A5EB390FB26 /* MessageComposer.swift */; }; 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; @@ -63,7 +67,6 @@ 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90733775209F4D4D366A268F /* RootRouterType.swift */; }; 2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */; }; - 2E68C57E7D644E94778743D5 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B66E05B6009B0EB1BDBFA6E /* TemplateScreenUITests.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 */; }; @@ -78,7 +81,6 @@ 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; - 3772354754450F2B54107E17 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4EDB32B97910AAAFE632B2 /* TemplateViewModelProtocol.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */; }; 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; }; @@ -90,6 +92,7 @@ 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */; }; 41DFDD212D1BE57CA50D783B /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; }; + 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; }; 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; }; @@ -116,13 +119,14 @@ 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 /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BA045DC4CA12D030ACF558 /* TemplateScreen.swift */; }; 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; 5E1FCC43B738941D5A5F1794 /* SplashScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */; }; + 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; }; 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; }; 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; }; + 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; 6832733838C57A7D3FE8FEB5 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 04C28663564E008DB32B5972 /* Introspect */; }; @@ -140,8 +144,10 @@ 72F6E890820FF606A7E276C8 /* SplashScreenPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */; }; + 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 758BF44CA565AB0AB84F2185 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 75D98001C5AC38B6A5CA897C /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FD9D66B75292F2CC11AA4D2 /* UITestScreenIdentifier.swift */; }; + 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; }; @@ -149,6 +155,7 @@ 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */; }; 79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C37FB986891D90BEAA93EAE /* UserSessionStore.swift */; }; 7A54700193DC1F264368746A /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E077F76026C85ED96FEBB810 /* UserIndicatorPresenter.swift */; }; + 7AE1FFB132F2B84EB8A2AEBC /* TemplateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */; }; @@ -160,6 +167,7 @@ 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; }; 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; }; 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; + 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; }; 86C2E93920FD15AD17E193A9 /* BugReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; 87756CA950ED55870A1AAE8F /* ServerSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */; }; @@ -173,6 +181,7 @@ 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93BA4A81B6D893271101F9F0 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; + 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; @@ -190,6 +199,7 @@ 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */; }; 9D2E03DB175A6AB14589076D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; + 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; }; A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; @@ -201,10 +211,10 @@ A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; + A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; - B0EDAF55877DE19B67837C22 /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C29670CEC77346F31EE94C /* TemplateModels.swift */; }; B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */; }; @@ -215,14 +225,15 @@ B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */; }; + BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.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 */; }; C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */; }; - C1156BBE4F977AEEE1E80C48 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2869CFFF6CD2A642AB4B743 /* TemplateCoordinator.swift */; }; C2CF93B067FD935E4F82FE44 /* SplashScreenPageIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850064FF8D7DB9C875E7AA1A /* SplashScreenPageIndicator.swift */; }; + C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; @@ -237,8 +248,10 @@ 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 */; }; + D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */; }; D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; + D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; @@ -249,13 +262,12 @@ DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; - E75CE800B3E64D0F7F8E228D /* TemplateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateViewModelTests.swift */; }; + E5895C74615CBE8462FB840F /* SessionVerificationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */; }; E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.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 */; }; EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; }; - ED4F663C783E9A8C0E80B983 /* TemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47543EB19F3DCF308751F53C /* TemplateViewModel.swift */; }; 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 */; }; @@ -334,6 +346,7 @@ 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.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 = ""; }; + 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; 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 = ""; }; @@ -352,6 +365,7 @@ 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; 2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; + 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelProtocol.swift; sourceTree = ""; }; 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskProtocol.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 = ""; }; @@ -359,6 +373,7 @@ 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 = ""; }; + 3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelTests.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactory.swift; sourceTree = ""; }; @@ -390,9 +405,9 @@ 4488F5F92A64A137665C96CD /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = pa.lproj/Localizable.strings; sourceTree = ""; }; 44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 453E722A43D092C06FB8E3FA /* tzm */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tzm; path = tzm.lproj/Localizable.strings; sourceTree = ""; }; + 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = ""; }; 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; - 47543EB19F3DCF308751F53C /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.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 = ""; }; @@ -404,7 +419,6 @@ 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 /* TemplateScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenUITests.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; }; @@ -454,6 +468,7 @@ 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProvider.swift; sourceTree = ""; }; 6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = ""; }; 6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModelProtocol.swift; sourceTree = ""; }; + 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationUITests.swift; sourceTree = ""; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskTests.swift; sourceTree = ""; }; 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; @@ -496,6 +511,7 @@ 8888D13645C04AC9818F5778 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; 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 = ""; }; + 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; 8BF686BA36D0C2FA3C63DFDF /* ImageRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomMessage.swift; sourceTree = ""; }; 8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = ""; }; 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailProviderManager.swift; sourceTree = ""; }; @@ -517,6 +533,7 @@ 9772C1D2223108EB3131AEE4 /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = ""; }; 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationIconImage.swift; sourceTree = ""; }; 97F893DBB5F88D746C6DCDE5 /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Localizable.strings; sourceTree = ""; }; + 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenUITests.swift; sourceTree = ""; }; 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; 997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = ""; }; 99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -528,7 +545,7 @@ A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; - A1C29670CEC77346F31EE94C /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = ""; }; + A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = ""; }; A2B6433F516F1E6DFA0E2D89 /* vls */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vls; path = vls.lproj/Localizable.strings; sourceTree = ""; }; A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionModels.swift; sourceTree = ""; }; @@ -558,8 +575,10 @@ B1183B55FF4B01022DA721CB /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = ""; }; B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorDismissal.swift; sourceTree = ""; }; + B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelProtocol.swift; sourceTree = ""; }; B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; + B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModel.swift; sourceTree = ""; }; B516212D9FE785DDD5E490D1 /* BugReportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportModels.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = ""; }; @@ -578,11 +597,12 @@ BEE6BF9BA63FF42F8AF6EEEA /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = ""; }; BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = ""; }; C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; + C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachine.swift; sourceTree = ""; }; C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; - C08E9043618AE5B0BF7B07E1 /* TemplateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelTests.swift; sourceTree = ""; }; C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProtocol.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; + C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; @@ -597,6 +617,7 @@ CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = ""; }; CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsRootView.swift; sourceTree = ""; }; + CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationCoordinator.swift; sourceTree = ""; }; CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CED34C87277BA3CCC6B6EC7A /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; CF3EDF23226895776553F04A /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; @@ -604,6 +625,7 @@ CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementNavigationController.swift; sourceTree = ""; }; CF847B3C1873B8E81CEE7FAC /* SplashScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModel.swift; sourceTree = ""; }; D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; + D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = ""; }; D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = ""; }; D31DC8105C6233E5FFD9B84C /* element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios"; path = .; sourceTree = SOURCE_ROOT; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; @@ -612,18 +634,20 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; + DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.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 = ""; }; DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; + DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.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 = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; - E2869CFFF6CD2A642AB4B743 /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = ""; }; E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; + E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationModels.swift; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = ""; }; E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; @@ -634,6 +658,7 @@ EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; + EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.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 = ""; }; @@ -651,12 +676,11 @@ F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntitityRegex.swift; sourceTree = ""; }; - F9BA045DC4CA12D030ACF558 /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = ""; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = ""; }; + FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomMessage.swift; sourceTree = ""; }; - FF4EDB32B97910AAAFE632B2 /* TemplateViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelProtocol.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -735,6 +759,7 @@ 79E560F5113ED25D172E550C /* Media */, 40E6246F03D1FE377BC5D963 /* Room */, 82D5AD3EAE3A5C1068A44A88 /* Session */, + 5329E48968EB951235E83DAE /* SessionVerification */, FCDF06BDB123505F0334B4F9 /* Timeline */, 90C85A862720155C0CF63B02 /* UserSessionStore */, ); @@ -753,6 +778,14 @@ path = SupportingFiles; sourceTree = ""; }; + 0BA8C419737BDA72B553B129 /* View */ = { + isa = PBXGroup; + children = ( + 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 0ED3F5C21537519389C07644 /* BugReport */ = { isa = PBXGroup; children = ( @@ -915,7 +948,7 @@ 4AC3BA2B379A928301E21004 /* View */ = { isa = PBXGroup; children = ( - F9BA045DC4CA12D030ACF558 /* TemplateScreen.swift */, + 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */, ); path = View; sourceTree = ""; @@ -945,6 +978,16 @@ path = Scripts; sourceTree = ""; }; + 5329E48968EB951235E83DAE /* SessionVerification */ = { + isa = PBXGroup; + children = ( + D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */, + C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */, + 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */, + ); + path = SessionVerification; + sourceTree = ""; + }; 58F951CB7BD7F96C37BE5CAD /* View */ = { isa = PBXGroup; children = ( @@ -1020,7 +1063,7 @@ 73AB116809AE89292624CD8E /* Unit */ = { isa = PBXGroup; children = ( - C08E9043618AE5B0BF7B07E1 /* TemplateViewModelTests.swift */, + 3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */, ); path = Unit; sourceTree = ""; @@ -1042,6 +1085,8 @@ 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, + A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */, + DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */, 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */, AF552BB969DC98A4BB8CF8D5 /* UserIndicators */, @@ -1073,10 +1118,10 @@ 789DD6B31BA8BB4B3A40EF7C /* ElementX */ = { isa = PBXGroup; children = ( - E2869CFFF6CD2A642AB4B743 /* TemplateCoordinator.swift */, - A1C29670CEC77346F31EE94C /* TemplateModels.swift */, - 47543EB19F3DCF308751F53C /* TemplateViewModel.swift */, - FF4EDB32B97910AAAFE632B2 /* TemplateViewModelProtocol.swift */, + DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */, + FAB10E673916D2B8D21FD197 /* TemplateModels.swift */, + EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */, + 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */, 4AC3BA2B379A928301E21004 /* View */, ); path = ElementX; @@ -1191,6 +1236,7 @@ 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, + 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */, E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */, 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */, ); @@ -1296,7 +1342,7 @@ AD5FCF9340D670C526AD17E4 /* UI */ = { isa = PBXGroup; children = ( - 4B66E05B6009B0EB1BDBFA6E /* TemplateScreenUITests.swift */, + 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */, ); path = UI; sourceTree = ""; @@ -1389,6 +1435,19 @@ path = Layout; sourceTree = ""; }; + D958761758AA1110476DE6A3 /* SessionVerification */ = { + isa = PBXGroup; + children = ( + CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */, + E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */, + C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */, + B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */, + B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */, + 0BA8C419737BDA72B553B129 /* View */, + ); + path = SessionVerification; + sourceTree = ""; + }; E59565F441830B19DBAE567C /* Screens */ = { isa = PBXGroup; children = ( @@ -1396,6 +1455,7 @@ 4009BE2E791C16AC6EE39A7E /* BugReport */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, + D958761758AA1110476DE6A3 /* SessionVerification */, 70B74A432C241E56A7ACE610 /* Settings */, 02175C9269C4632DB6D12C25 /* Splash */, B1A847595434E3DD177F5143 /* SplashScreen */, @@ -1604,7 +1664,7 @@ }; }; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; - compatibilityVersion = "Xcode 11.0"; + compatibilityVersion = "Xcode 10.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1808,9 +1868,11 @@ 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, + 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */, + 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */, 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */, - E75CE800B3E64D0F7F8E228D /* TemplateViewModelTests.swift in Sources */, + 7AE1FFB132F2B84EB8A2AEBC /* TemplateViewModelTests.swift in Sources */, 226027BE23AF64FA61C7A4C0 /* TimelineStyle.swift in Sources */, 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */, 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */, @@ -1907,6 +1969,7 @@ 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */, E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */, 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */, + D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */, 4ED453A61AF45EBE18D8BC69 /* NavigationModule.swift in Sources */, 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */, 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */, @@ -1953,6 +2016,14 @@ 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */, BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */, 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */, + 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */, + 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */, + E5895C74615CBE8462FB840F /* SessionVerificationCoordinator.swift in Sources */, + 0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */, + BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */, + 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */, + A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */, + D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */, 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */, 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */, 7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */, @@ -1970,11 +2041,11 @@ 2F94054F50E312AF30BE07F3 /* String.swift in Sources */, A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */, - 5CABC57F620FBB39F4EC127C /* TemplateScreen.swift in Sources */, - C1156BBE4F977AEEE1E80C48 /* TemplateCoordinator.swift in Sources */, - B0EDAF55877DE19B67837C22 /* TemplateModels.swift in Sources */, - ED4F663C783E9A8C0E80B983 /* TemplateViewModel.swift in Sources */, - 3772354754450F2B54107E17 /* TemplateViewModelProtocol.swift in Sources */, + 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */, + 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */, + 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */, + 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */, + 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */, D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */, 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */, 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */, @@ -2026,11 +2097,12 @@ 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, + 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */, 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */, A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */, DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */, B3357B00F1AA930E54F76609 /* Strings.swift in Sources */, - 2E68C57E7D644E94778743D5 /* TemplateScreenUITests.swift in Sources */, + C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */, 0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */, 75D98001C5AC38B6A5CA897C /* UITestScreenIdentifier.swift in Sources */, ); diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index a62de203c..d1129849c 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -7,11 +7,12 @@ // import UIKit +import Combine class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private let window: UIWindow - private var stateMachine: AppCoordinatorStateMachine + private let stateMachine: AppCoordinatorStateMachine private let mainNavigationController: UINavigationController private let splashViewController: UIViewController @@ -120,7 +121,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { case (.signingIn, .succeededSigningIn, .homeScreen): self.hideLoadingIndicator() self.presentHomeScreen() - + case (.initial, .startWithExistingSession, .restoringSession): self.showLoadingIndicator() self.restoreUserSession() @@ -142,10 +143,17 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { self.tearDownUserSession() case (.signingOut, .failedSigningOut, _): self.showLogoutErrorToast() + case (.homeScreen, .showSettingsScreen, .settingsScreen): self.presentSettingsScreen() case (.settingsScreen, .dismissedSettingsScreen, .homeScreen): self.tearDownDismissedSettingsScreen() + + case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen): + self.presentSessionVerification() + case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen): + self.tearDownDismissedSessionVerificationScreen() + default: fatalError("Unknown transition: \(context)") } @@ -205,6 +213,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier)) case .presentSettings: self.stateMachine.processEvent(.showSettingsScreen) + case .verifySession: + self.stateMachine.processEvent(.showSessionVerificationScreen) } } @@ -215,28 +225,9 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { showCrashPopup() } } - - private func presentSettingsScreen() { - let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter, - bugReportService: bugReportService) - let coordinator = SettingsCoordinator(parameters: parameters) - coordinator.callback = { [weak self] action in - guard let self = self else { return } - switch action { - case .logout: - self.stateMachine.processEvent(.attemptSignOut) - } - } - - add(childCoordinator: coordinator) - coordinator.start() - navigationRouter.push(coordinator) { [weak self] in - guard let self = self else { return } - - self.stateMachine.processEvent(.dismissedSettingsScreen) - } - } + // MARK: Rooms + private func presentRoomWithIdentifier(_ roomIdentifier: String) { guard let roomProxy = userSession.clientProxy.rooms.first(where: { $0.id == roomIdentifier }) else { MXLog.error("Invalid room identifier: \(roomIdentifier)") @@ -276,7 +267,30 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { remove(childCoordinator: coordinator) } + + // MARK: Settings + + private func presentSettingsScreen() { + let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter, + bugReportService: bugReportService) + let coordinator = SettingsCoordinator(parameters: parameters) + coordinator.callback = { [weak self] action in + guard let self = self else { return } + switch action { + case .logout: + self.stateMachine.processEvent(.attemptSignOut) + } + } + add(childCoordinator: coordinator) + coordinator.start() + navigationRouter.push(coordinator) { [weak self] in + guard let self = self else { return } + + self.stateMachine.processEvent(.dismissedSettingsScreen) + } + } + private func tearDownDismissedSettingsScreen() { guard let coordinator = childCoordinators.last as? SettingsCoordinator else { fatalError("Invalid coordinator hierarchy: \(childCoordinators)") @@ -285,22 +299,6 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { remove(childCoordinator: coordinator) } - private func showLoadingIndicator() { - loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) - } - - private func hideLoadingIndicator() { - loadingIndicator = nil - } - - private func showLoginErrorToast() { - statusIndicator = indicatorPresenter.present(.error(label: "Failed logging in")) - } - - private func showLogoutErrorToast() { - statusIndicator = indicatorPresenter.present(.error(label: "Failed logging out")) - } - private func showCrashPopup() { let alert = UIAlertController(title: nil, message: ElementL10n.sendBugReportAppCrashed, @@ -360,4 +358,54 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { navigationRouter.dismissModule() remove(childCoordinator: bugReportCoordinator) } + + // MARK: Session verification + + private func presentSessionVerification() { + Task { + guard let sessionVerificationController = userSession.sessionVerificationController else { + fatalError("The sessionVerificationController should aways be valid at this point") + } + + let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) + + let coordinator = SessionVerificationCoordinator(parameters: parameters) + + coordinator.callback = { [weak self] in + self?.navigationRouter.dismissModule() + self?.stateMachine.processEvent(.dismissedSessionVerificationScreen) + } + + add(childCoordinator: coordinator) + navigationRouter.present(coordinator) + + coordinator.start() + } + } + + private func tearDownDismissedSessionVerificationScreen() { + guard let coordinator = childCoordinators.last as? SessionVerificationCoordinator else { + fatalError("Invalid coordinator hierarchy: \(childCoordinators)") + } + + remove(childCoordinator: coordinator) + } + + // MARK: Toasts and loading indicators + + private func showLoadingIndicator() { + loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) + } + + private func hideLoadingIndicator() { + loadingIndicator = nil + } + + private func showLoginErrorToast() { + statusIndicator = indicatorPresenter.present(.error(label: "Failed logging in")) + } + + private func showLogoutErrorToast() { + statusIndicator = indicatorPresenter.present(.error(label: "Failed logging out")) + } } diff --git a/ElementX/Sources/AppCoordinatorStateMachine.swift b/ElementX/Sources/AppCoordinatorStateMachine.swift index f4ebeec2e..81024c955 100644 --- a/ElementX/Sources/AppCoordinatorStateMachine.swift +++ b/ElementX/Sources/AppCoordinatorStateMachine.swift @@ -22,11 +22,17 @@ class AppCoordinatorStateMachine { case restoringSession /// Showing the home screen case homeScreen - /// Showing the settings screen - case settingsScreen + /// Showing a particular room's timeline /// - Parameter roomId: that room's identifier case roomScreen(roomId: String) + + /// Showing the session verification flows + case sessionVerificationScreen + + /// Showing the settings screen + case settingsScreen + /// Processing a sign out request case signingOut } @@ -61,10 +67,16 @@ class AppCoordinatorStateMachine { case showRoomScreen(roomId: String) /// The room screen has been dismissed case dismissedRoomScreen - /// The settings screen has been dismissed - case dismissedSettingsScreen + + /// Request the start of the session verification flow + case showSessionVerificationScreen + /// Session verification has finished + case dismissedSessionVerificationScreen + /// Request settings screen presentation case showSettingsScreen + /// The settings screen has been dismissed + case dismissedSettingsScreen } private let stateMachine: StateMachine @@ -84,9 +96,13 @@ class AppCoordinatorStateMachine { machine.addRoutes(event: .succeededSigningOut, transitions: [ .signingOut => .signedOut ]) machine.addRoutes(event: .failedSigningOut, transitions: [ .signingOut => .settingsScreen ]) + machine.addRoutes(event: .showSettingsScreen, transitions: [ .homeScreen => .settingsScreen ]) machine.addRoutes(event: .dismissedSettingsScreen, transitions: [ .settingsScreen => .homeScreen ]) + machine.addRoutes(event: .showSessionVerificationScreen, transitions: [ .homeScreen => .sessionVerificationScreen ]) + machine.addRoutes(event: .dismissedSessionVerificationScreen, transitions: [ .sessionVerificationScreen => .homeScreen ]) + // Transitions with associated values need to be handled through `addRouteMapping` machine.addRouteMapping { event, fromState, _ in switch (event, fromState) { diff --git a/ElementX/Sources/Other/ImageAnonymizer.swift b/ElementX/Sources/Other/ImageAnonymizer.swift index 3d26011d4..775c31e0b 100644 --- a/ElementX/Sources/Other/ImageAnonymizer.swift +++ b/ElementX/Sources/Other/ImageAnonymizer.swift @@ -61,6 +61,12 @@ struct ImageAnonymizer { } // revision3 doesn't work! faceRequest.revision = VNDetectFaceRectanglesRequestRevision2 + + #if targetEnvironment(simulator) + // Avoid `Could not create inference context` errors on Apple Silicon + // https://www.caseyliss.com/2022/6/20/feedback-is-broken-stop-trying-to-make-radar-happen + faceRequest.usesCPUOnly = true + #endif // perform requests try handler.perform([ diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 1c736d2d2..e594e8674 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -26,6 +26,7 @@ struct HomeScreenCoordinatorParameters { enum HomeScreenCoordinatorAction { case presentRoom(roomIdentifier: String) case presentSettings + case verifySession } final class HomeScreenCoordinator: Coordinator, Presentable { @@ -66,19 +67,31 @@ final class HomeScreenCoordinator: Coordinator, Presentable { self.callback?(.presentRoom(roomIdentifier: roomIdentifier)) case .tapUserAvatar: self.callback?(.presentSettings) + case .verifySession: + self.callback?(.verifySession) } } parameters.userSession.clientProxy .callbacks .receive(on: DispatchQueue.main) - .sink { [weak self] action in - switch action { - case .updatedRoomsList: + .sink { [weak self] callback in + if case .updatedRoomsList = callback { self?.updateRoomsList() } }.store(in: &cancellables) + parameters.userSession.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + switch callback { + case .sessionVerificationNeeded: + self?.viewModel.showSessionVerificationBanner() + case .didVerifySession: + self?.viewModel.hideSessionVerificationBanner() + } + }.store(in: &cancellables) + updateRoomsList() Task { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index e1797cc9f..745ea10c1 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -20,18 +20,22 @@ import UIKit enum HomeScreenViewModelAction { case selectRoom(roomIdentifier: String) case tapUserAvatar + case verifySession } enum HomeScreenViewAction { case loadRoomData(roomIdentifier: String) case selectRoom(roomIdentifier: String) case tapUserAvatar + case verifySession } struct HomeScreenViewState: BindableState { var userDisplayName: String? var userAvatar: UIImage? + var showSessionVerificationBanner: Bool = false + var rooms: [HomeScreenRoom] = [] var isLoadingRooms: Bool = false diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 6bc65ac17..f208b4699 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -27,7 +27,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol private var roomSummaries: [RoomSummaryProtocol]? { didSet { - self.state.isLoadingRooms = (roomSummaries?.count ?? 0 == 0) + state.isLoadingRooms = (roomSummaries?.count ?? 0 == 0) } } @@ -51,6 +51,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol callback?(.selectRoom(roomIdentifier: roomIdentifier)) case .tapUserAvatar: callback?(.tapUserAvatar) + case .verifySession: + callback?(.verifySession) } } @@ -84,11 +86,19 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } func updateWithUserAvatar(_ avatar: UIImage) { - self.state.userAvatar = avatar + state.userAvatar = avatar } func updateWithUserDisplayName(_ displayName: String) { - self.state.userDisplayName = displayName + state.userDisplayName = displayName + } + + func showSessionVerificationBanner() { + state.showSessionVerificationBanner = true + } + + func hideSessionVerificationBanner() { + state.showSessionVerificationBanner = false } // MARK: - Private @@ -105,7 +115,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol private func buildOrUpdateRoomFromSummary(_ roomSummary: RoomSummaryProtocol) -> HomeScreenRoom { let lastMessage = lastMessageFromEventBrief(roomSummary.lastMessage) - guard var room = self.state.rooms.first(where: { $0.id == roomSummary.id }) else { + guard var room = state.rooms.first(where: { $0.id == roomSummary.id }) else { return HomeScreenRoom(id: roomSummary.id, displayName: roomSummary.displayName, topic: roomSummary.topic, diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift index 0d5bc342e..0cbc97a18 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift @@ -26,4 +26,7 @@ protocol HomeScreenViewModelProtocol { func updateWithUserAvatar(_ avatar: UIImage) func updateWithUserDisplayName(_ displayName: String) func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) + + func showSessionVerificationBanner() + func hideSessionVerificationBanner() } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 68514b67a..208c208e9 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -23,13 +23,27 @@ struct HomeScreen: View { // MARK: Views var body: some View { - VStack(spacing: 16.0) { + VStack(spacing: 0.0) { if context.viewState.isLoadingRooms { VStack { Text(ElementL10n.loading) ProgressView() } } else { + + if context.viewState.showSessionVerificationBanner { + HStack { + Text(ElementL10n.verificationVerifyDevice) + Spacer() + Button(ElementL10n.startVerification) { + context.send(viewAction: .verifySession) + } + } + .padding() + .background(Color.element.quaternaryContent) + .padding(.top, 1) + } + List { Section(ElementL10n.rooms) { ForEach(context.viewState.visibleRooms) { room in @@ -52,6 +66,8 @@ struct HomeScreen: View { Spacer() } .background(Color.element.background) + .transition(.slide) + .animation(.default, value: context.viewState.showSessionVerificationBanner) .ignoresSafeArea(.all, edges: .bottom) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -100,6 +116,8 @@ struct HomeScreen: View { struct RoomCell: View { + @ScaledMetric private var avatarSize = 32.0 + let room: HomeScreenRoom let context: HomeScreenViewModel.Context @@ -112,12 +130,12 @@ struct RoomCell: View { Image(uiImage: avatar) .resizable() .scaledToFill() - .frame(width: 40, height: 40) + .frame(width: avatarSize, height: avatarSize) .clipShape(Circle()) } else { PlaceholderAvatarImage(text: room.displayName ?? room.id) .clipShape(Circle()) - .frame(width: 40, height: 40) + .frame(width: avatarSize, height: avatarSize) } VStack(alignment: .leading, spacing: 2.0) { @@ -182,6 +200,8 @@ struct HomeScreen_Previews: PreviewProvider { viewModel.updateWithUserAvatar(avatarImage) } + viewModel.showSessionVerificationBanner() + return NavigationView { HomeScreen(context: viewModel.context) } diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationCoordinator.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationCoordinator.swift new file mode 100644 index 000000000..752e81ab9 --- /dev/null +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationCoordinator.swift @@ -0,0 +1,67 @@ +// +// 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 SessionVerificationCoordinatorParameters { + let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol +} + +final class SessionVerificationCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: SessionVerificationCoordinatorParameters + private let sessionVerificationHostingController: UIViewController + private var sessionVerificationViewModel: SessionVerificationViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var callback: (() -> Void)? + + // MARK: - Setup + + init(parameters: SessionVerificationCoordinatorParameters) { + self.parameters = parameters + + let viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy) + let view = SessionVerificationScreen(context: viewModel.context) + sessionVerificationViewModel = viewModel + sessionVerificationHostingController = UIHostingController(rootView: view) + } + + // MARK: - Public + + func start() { + MXLog.debug("[SessionVerificationCoordinator] did start.") + sessionVerificationViewModel.callback = { [weak self] action in + guard let self = self else { return } + + switch action { + case .finished: + self.callback?() + } + } + } + + func toPresentable() -> UIViewController { + sessionVerificationHostingController + } +} diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift new file mode 100644 index 000000000..5ad69e5d3 --- /dev/null +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationModels.swift @@ -0,0 +1,46 @@ +// +// 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 SessionVerificationViewModelAction { + case finished +} + +// MARK: View + +struct SessionVerificationViewState: BindableState { + var verificationState: SessionVerificationStateMachine.State = .initial + + var shouldDisableDismissButton: Bool { + verificationState != .verified + } + + var shouldDisableCancelButton: Bool { + verificationState == .verified + } +} + +enum SessionVerificationViewAction { + case start + case restart + case accept + case decline + case dismiss + case cancel +} diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationStateMachine.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationStateMachine.swift new file mode 100644 index 000000000..005bb6189 --- /dev/null +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationStateMachine.swift @@ -0,0 +1,135 @@ +// +// SessionVerificationStateMachine.swift +// ElementX +// +// Created by Stefan Ceriu on 15/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import SwiftState + +class SessionVerificationStateMachine { + /// States the SessionVerificationViewModel can find itself in + enum State: StateType { + /// The initial state, before verification started + case initial + /// Waiting for verification acceptance + case requestingVerification + /// Verification accepted and emojis received + case showingChallenge(emojis: [SessionVerificationEmoji]) + /// Emojis match locally + case acceptingChallenge(emojis: [SessionVerificationEmoji]) + /// Emojis do not match locally + case decliningChallenge(emojis: [SessionVerificationEmoji]) + /// Verification successful + case verified + /// User requested verification cancellation + case cancelling + /// The verification has been cancelled, remotely or locally + case cancelled + } + + /// Events that can be triggered on the SessionVerification state machine + enum Event: EventType { + /// Request verification + case requestVerification + /// Has received emojis + case didReceiveChallenge(emojis: [SessionVerificationEmoji]) + /// Emojis match + case acceptChallenge + /// Emojis do not match + case declineChallenge + /// Remote accepted challenge + case didAcceptChallenge + /// Request cancellation + case cancel + /// Verification cancelled + case didCancel + /// Request failed + case didFail + /// Restart the verification flow + case restart + } + + private let stateMachine: StateMachine + + var state: State { + stateMachine.state + } + + // swiftlint:disable cyclomatic_complexity + init() { + stateMachine = StateMachine(state: .initial) { machine in + machine.addRoutes(event: .requestVerification, transitions: [ .initial => .requestingVerification ]) + machine.addRoutes(event: .didFail, transitions: [ .requestingVerification => .initial ]) + + machine.addRoutes(event: .cancel, transitions: [ .requestingVerification => .cancelling ]) + machine.addRoutes(event: .didCancel, transitions: [ .requestingVerification => .cancelled ]) + + // Cancellation request from the other party should either take us from `.cancelling` + // to `.cancelled` or keep us in `.cancelled` if already there. There is more `.didCancel` + // handling in `addRouteMapping` for states containing associated values + machine.addRoutes(event: .didCancel, transitions: [ .cancelling => .cancelled ]) + machine.addRoutes(event: .didCancel, transitions: [ .cancelled => .cancelled ]) + machine.addRoutes(event: .didFail, transitions: [ .cancelled => .cancelled ]) + + machine.addRoutes(event: .restart, transitions: [ .cancelled => .initial ]) + + // Transitions with associated values need to be handled through `addRouteMapping` + machine.addRouteMapping { event, fromState, _ in + switch (event, fromState) { + case (.didReceiveChallenge(let emojis), .requestingVerification): + return .showingChallenge(emojis: emojis) + + case (.acceptChallenge, .showingChallenge(let emojis)): + return .acceptingChallenge(emojis: emojis) + case (.didFail, .acceptingChallenge(let emojis)): + return .showingChallenge(emojis: emojis) + + case (.didAcceptChallenge, .acceptingChallenge): + return .verified + + case (.declineChallenge, .showingChallenge(let emojis)): + return .decliningChallenge(emojis: emojis) + case (.didFail, .decliningChallenge(let emojis)): + return .showingChallenge(emojis: emojis) + + case (.cancel, .showingChallenge): + return .cancelling + case (.cancel, .acceptingChallenge): + return .cancelling + case (.cancel, .decliningChallenge): + return .cancelling + + case (.didCancel, .showingChallenge): + return .cancelled + case (.didCancel, .acceptingChallenge): + return .cancelled + case (.didCancel, .decliningChallenge): + return .cancelled + + default: + return nil + } + } + } + } + // swiftlint:enable cyclomatic_complexity + + /// Attempt to move the state machine to another state through an event + /// It will either invoke the `transitionHandler` or the `errorHandler` depending on its current state + func processEvent(_ event: Event) { + stateMachine.tryEvent(event) + } + + /// Registers a callback for processing state machine transitions + func addTransitionHandler(_ handler: @escaping StateMachine.Handler) { + stateMachine.addAnyHandler(.any => .any, handler: handler) + } + + /// Registers a callback for processing state machine errors + func addErrorHandler(_ handler: @escaping StateMachine.Handler) { + stateMachine.addErrorHandler(handler: handler) + } +} diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift new file mode 100644 index 000000000..e6ae2f917 --- /dev/null +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift @@ -0,0 +1,164 @@ +// +// 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 SessionVerificationViewModelType = StateStoreViewModel + +class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVerificationViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol + + private var stateMachine: SessionVerificationStateMachine + + // MARK: Public + + var callback: ((SessionVerificationViewModelAction) -> Void)? + + // MARK: - Setup + + init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol, + initialState: SessionVerificationViewState = SessionVerificationViewState()) { + + self.sessionVerificationControllerProxy = sessionVerificationControllerProxy + + stateMachine = SessionVerificationStateMachine() + + super.init(initialViewState: initialState) + + setupStateMachine() + + sessionVerificationControllerProxy.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self = self else { return } + + switch callback { + case .receivedVerificationData(let emojis): + self.stateMachine.processEvent(.didReceiveChallenge(emojis: emojis)) + case .finished: + self.stateMachine.processEvent(.didAcceptChallenge) + case .cancelled: + self.stateMachine.processEvent(.didCancel) + case .failed: + self.stateMachine.processEvent(.didFail) + } + }.store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: SessionVerificationViewAction) async { + switch viewAction { + case .start: + stateMachine.processEvent(.requestVerification) + case .restart: + stateMachine.processEvent(.restart) + case .dismiss: + callback?(.finished) + case .cancel: + guard stateMachine.state == .initial || + stateMachine.state == .verified || + stateMachine.state == .cancelled else { + stateMachine.processEvent(.cancel) + return + } + + callback?(.finished) + case .accept: + stateMachine.processEvent(.acceptChallenge) + case .decline: + stateMachine.processEvent(.declineChallenge) + } + } + + // MARK: - Private + + private func setupStateMachine() { + stateMachine.addTransitionHandler { [weak self] context in + guard let self = self else { return } + + self.state.verificationState = context.toState + + switch (context.fromState, context.event, context.toState) { + case (.initial, .requestVerification, .requestingVerification): + self.requestVerification() + case (.showingChallenge, .acceptChallenge, .acceptingChallenge): + self.acceptChallenge() + case (.showingChallenge, .declineChallenge, .decliningChallenge): + self.declineChallenge() + case (_, .cancel, .cancelling): + self.cancelVerification() + default: + break + } + } + + stateMachine.addErrorHandler { context in + fatalError("Failed transition with context: \(context)") + } + } + + private func requestVerification() { + Task { + switch await sessionVerificationControllerProxy.requestVerification() { + case.success: + // Need to wait for the callback from the remote + break + case .failure: + stateMachine.processEvent(.didFail) + } + } + } + + private func cancelVerification() { + Task { + switch await sessionVerificationControllerProxy.cancelVerification() { + case.success: + stateMachine.processEvent(.didCancel) + case .failure: + stateMachine.processEvent(.didFail) + } + } + } + + private func acceptChallenge() { + Task { + switch await sessionVerificationControllerProxy.approveVerification() { + case.success: + // Need to wait for the callback from the remote + break + case .failure: + stateMachine.processEvent(.didFail) + } + } + } + + private func declineChallenge() { + Task { + switch await sessionVerificationControllerProxy.declineVerification() { + case.success: + stateMachine.processEvent(.didCancel) + case .failure: + stateMachine.processEvent(.didFail) + } + } + } +} diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModelProtocol.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModelProtocol.swift new file mode 100644 index 000000000..0877d2ab2 --- /dev/null +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// 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 SessionVerificationViewModelProtocol { + var callback: ((SessionVerificationViewModelAction) -> Void)? { get set } + var context: SessionVerificationViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift new file mode 100644 index 000000000..2ff529325 --- /dev/null +++ b/ElementX/Sources/Screens/SessionVerification/View/SessionVerificationScreen.swift @@ -0,0 +1,204 @@ +// +// 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 SessionVerificationScreen: View { + + @ObservedObject var context: SessionVerificationViewModel.Context + + // MARK: Views + + var body: some View { + NavigationView { + VStack(spacing: 32.0) { + Text(heading) + .font(.body) + .multilineTextAlignment(.center) + .foregroundColor(.element.primaryContent) + .accessibilityIdentifier("titleLabel") + + switch context.viewState.verificationState { + case .initial: + StateIcon(systemName: "lock.shield") + Button(ElementL10n.startVerification) { + context.send(viewAction: .start) + } + .buttonStyle(.elementAction(.regular)) + .accessibilityIdentifier("startButton") + + case .cancelled: + StateIcon(systemName: "xmark.shield") + .accessibilityIdentifier("sessionVerificationFailedIcon") + + Button(ElementL10n.globalRetry) { + context.send(viewAction: .restart) + } + .buttonStyle(.elementAction(.regular)) + .accessibilityIdentifier("restartButton") + + case .requestingVerification: + ProgressView() + .accessibilityIdentifier("requestingVerificationProgressView") + case .cancelling: + ProgressView() + .accessibilityIdentifier("cancellingVerificationProgressView") + case .acceptingChallenge: + ProgressView() + .accessibilityIdentifier("acceptingChallengeProgressView") + case .decliningChallenge: + ProgressView() + .accessibilityIdentifier("decliningChallengeProgressView") + + case .showingChallenge(let emojis): + HStack(spacing: 8.0) { + ForEach(emojis.prefix(4), id: \.self) { emoji in + EmojiView(emoji: emoji) + } + } + HStack(spacing: 8.0) { + ForEach(emojis.suffix(from: 4), id: \.self) { emoji in + EmojiView(emoji: emoji) + } + } + + actionButtons + case .verified: + StateIcon(systemName: "checkmark.shield") + .accessibilityIdentifier("sessionVerificationSucceededIcon") + } + + Spacer() + } + .padding() + .padding(.top, 64) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(ElementL10n.verificationVerifyDevice) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(ElementL10n.done) { + context.send(viewAction: .dismiss) + } + .disabled(context.viewState.shouldDisableDismissButton) + .accessibilityIdentifier("dismissButton") + } + ToolbarItem(placement: .cancellationAction) { + Button(ElementL10n.actionCancel) { + context.send(viewAction: .cancel) + } + .disabled(context.viewState.shouldDisableCancelButton) + .accessibilityIdentifier("cancelButton") + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } + + // MARK: - Private + + private var heading: String { + switch context.viewState.verificationState { + case .initial: + return ElementL10n.verificationOpenOtherToVerify + case .requestingVerification: + return ElementL10n.verificationRequestWaiting + case .acceptingChallenge: + return ElementL10n.verificationRequestWaiting + case .decliningChallenge: + return ElementL10n.verificationRequestWaiting + case .cancelling: + return ElementL10n.verificationRequestWaiting + case .showingChallenge: + return ElementL10n.verificationEmojiNotice + case .verified: + return ElementL10n.verificationConclusionOkSelfNotice + case .cancelled: + return ElementL10n.verificationCancelled + } + } + + private var actionButtons: some View { + HStack(spacing: 16.0) { + Button(ElementL10n.verificationSasDoNotMatch) { + context.send(viewAction: .decline) + } + .buttonStyle(.elementAction(.regular, color: .red)) + .accessibilityLabel("challengeDeclineButton") + + Button(ElementL10n.verificationSasMatch) { + context.send(viewAction: .accept) + } + .buttonStyle(.elementAction(.regular)) + .accessibilityLabel("challengeAcceptButton") + } + .padding(32.0) + } + + struct EmojiView: View { + let emoji: SessionVerificationEmoji + + var body: some View { + VStack(spacing: 16.0) { + Text(emoji.symbol) + .font(.largeTitle) + Text(emoji.description) + .font(.body) + } + .padding(8.0) + } + } + + struct StateIcon: View { + let systemName: String + + var body: some View { + Image(systemName: systemName) + .resizable() + .scaledToFit() + .foregroundColor(.element.accent) + .frame(width: 100, height: 100) + } + } +} + +// MARK: - Previews + +struct SessionVerification_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + body.preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + Group { + sessionVerificationScreen(state: .initial) + sessionVerificationScreen(state: .requestingVerification) + sessionVerificationScreen(state: .cancelled) + + sessionVerificationScreen(state: .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + sessionVerificationScreen(state: .verified) + } + } + + static func sessionVerificationScreen(state: SessionVerificationStateMachine.State) -> some View { + let viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: MockSessionVerificationControllerProxy(), + initialState: SessionVerificationViewState(verificationState: state)) + + return SessionVerificationScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 40e7c67aa..b3019fd65 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -27,6 +27,7 @@ class ClientProxy: ClientProxyProtocol { private let client: Client private let backgroundTaskService: BackgroundTaskServiceProtocol + private var sessionVerificationControllerProxy: SessionVerificationControllerProxy? private(set) var rooms: [RoomProxy] = [] { didSet { @@ -96,11 +97,25 @@ class ClientProxy: ClientProxyProtocol { return Data(bytes: bytes, count: bytes.count) } + func sessionVerificationControllerProxy() async -> Result { + await Task.detached { + do { + let sessionVerificationController = try self.client.getSessionVerificationController() + return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController)) + } catch { + return .failure(.failedRetrievingSessionVerificationController) + } + } + .value + } + // MARK: Private fileprivate func didReceiveSyncUpdate() { Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update") + callbacks.send(.receivedSyncUpdate) + Task.detached { await self.updateRooms() } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index c2700c8ff..9a2221873 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -12,11 +12,13 @@ import Combine enum ClientProxyCallback { case updatedRoomsList + case receivedSyncUpdate } enum ClientProxyError: Error { case failedRetrievingAvatarURL case failedRetrievingDisplayName + case failedRetrievingSessionVerificationController } protocol ClientProxyProtocol { @@ -33,4 +35,6 @@ protocol ClientProxyProtocol { func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data + + func sessionVerificationControllerProxy() async -> Result } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 4c856d374..b9b1d1ae7 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -53,6 +53,10 @@ class RoomProxy: RoomProxyProtocol { backwardStream = room.startLiveEventListener() } + deinit { + room.setDelegate(delegate: nil) + } + var id: String { room.id() } diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index 6efee3326..3c6577686 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -7,13 +7,64 @@ // import Foundation +import Combine class UserSession: UserSessionProtocol { + private var cancellables = Set() + private var checkForSessionVerificationControllerCancellable: AnyCancellable? + let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol + let callbacks = PassthroughSubject() + private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol? init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol) { self.clientProxy = clientProxy self.mediaProvider = mediaProvider + + setupSessionVerificationWatchdog() + } + + // MARK: - Private + + private func setupSessionVerificationWatchdog() { + checkForSessionVerificationControllerCancellable = clientProxy.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + if case .receivedSyncUpdate = callback { + self?.attemptSessionVerification() + } + } + } + + private func attemptSessionVerification() { + Task { + switch await clientProxy.sessionVerificationControllerProxy() { + case .success(let sessionVerificationController): + tearDownSessionVerificationControllerWatchdog() + + if !sessionVerificationController.isVerified { + callbacks.send(.sessionVerificationNeeded) + } + + self.sessionVerificationController = sessionVerificationController + + sessionVerificationController.callbacks.sink { callback in + switch callback { + case .finished: + self.callbacks.send(.didVerifySession) + default: + break + } + }.store(in: &cancellables) + + case .failure(let error): + MXLog.error("Failed getting session verification controller with error: \(error). Will retry on the next sync update.") + } + } + } + + private func tearDownSessionVerificationControllerWatchdog() { + checkForSessionVerificationControllerCancellable = nil } } diff --git a/ElementX/Sources/Services/Session/UserSessionProtocol.swift b/ElementX/Sources/Services/Session/UserSessionProtocol.swift index 927cc0082..dd66ad4f6 100644 --- a/ElementX/Sources/Services/Session/UserSessionProtocol.swift +++ b/ElementX/Sources/Services/Session/UserSessionProtocol.swift @@ -7,8 +7,18 @@ // import Foundation +import Combine + +enum UserSessionCallback { + case sessionVerificationNeeded + case didVerifySession +} protocol UserSessionProtocol { var clientProxy: ClientProxyProtocol { get } var mediaProvider: MediaProviderProtocol { get } + + var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get } + + var callbacks: PassthroughSubject { get } } diff --git a/ElementX/Sources/Services/SessionVerification/MockSessionVerificationControllerProxy.swift b/ElementX/Sources/Services/SessionVerification/MockSessionVerificationControllerProxy.swift new file mode 100644 index 000000000..e28d42f96 --- /dev/null +++ b/ElementX/Sources/Services/SessionVerification/MockSessionVerificationControllerProxy.swift @@ -0,0 +1,62 @@ +// +// MockSessionVerificationControllerProxy.swift +// ElementX +// +// Created by Stefan Ceriu on 07/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import MatrixRustSDK +import Combine + +struct MockSessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol { + var callbacks = PassthroughSubject() + + var isVerified: Bool = false + + func requestVerification() async -> Result { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) { + callbacks.send(.receivedVerificationData(Self.emojis)) + } + + return .success(()) + } + + func approveVerification() async -> Result { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) { + callbacks.send(.finished) + } + + return .success(()) + } + + func declineVerification() async -> Result { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) { + callbacks.send(.cancelled) + } + + return .success(()) + } + + func cancelVerification() async -> Result { + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2.0) { + callbacks.send(.cancelled) + } + + return .success(()) + } + + // MARK: - Private + + static var emojis: [SessionVerificationEmoji] { + [SessionVerificationEmoji(symbol: "🦋", description: "Butterfly"), + SessionVerificationEmoji(symbol: "🐘", description: "Elephant"), + SessionVerificationEmoji(symbol: "🦋", description: "Butterfly"), + SessionVerificationEmoji(symbol: "🎂", description: "Cake"), + SessionVerificationEmoji(symbol: "🎂", description: "Cake"), + SessionVerificationEmoji(symbol: "🏁", description: "Flag"), + SessionVerificationEmoji(symbol: "🌏", description: "Globe") + ] + } +} diff --git a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift new file mode 100644 index 000000000..3157f7b6e --- /dev/null +++ b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift @@ -0,0 +1,124 @@ +// +// SessionVerificationControllerProxy.swift +// ElementX +// +// Created by Stefan Ceriu on 06/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import MatrixRustSDK +import Combine + +private class WeakSessionVerificationControllerProxy: SessionVerificationControllerDelegate { + private weak var proxy: SessionVerificationControllerProxy? + + init(proxy: SessionVerificationControllerProxy) { + self.proxy = proxy + } + + // MARK: - SessionVerificationControllerDelegate + + func didReceiveVerificationData(data: [MatrixRustSDK.SessionVerificationEmoji]) { + proxy?.didReceiveData(data) + } + + func didFail() { + proxy?.didFail() + } + + func didCancel() { + proxy?.didCancel() + } + + func didFinish() { + proxy?.didFinish() + } +} + +class SessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol { + private let sessionVerificationController: SessionVerificationController + + init(sessionVerificationController: SessionVerificationController) { + self.sessionVerificationController = sessionVerificationController + sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self)) + } + + deinit { + sessionVerificationController.setDelegate(delegate: nil) + } + + let callbacks = PassthroughSubject() + + var isVerified: Bool { + sessionVerificationController.isVerified() + } + + func requestVerification() async -> Result { + await Task.detached { + do { + try self.sessionVerificationController.requestVerification() + return .success(()) + } catch { + return .failure(.failedRequestingVerification) + } + } + .value + } + + func approveVerification() async -> Result { + await Task.detached { + do { + try self.sessionVerificationController.approveVerification() + return .success(()) + } catch { + return .failure(.failedApprovingVerification) + } + } + .value + } + + func declineVerification() async -> Result { + await Task.detached { + do { + try self.sessionVerificationController.declineVerification() + return .success(()) + } catch { + return .failure(.failedDecliningVerification) + } + } + .value + } + + func cancelVerification() async -> Result { + await Task.detached { + do { + try self.sessionVerificationController.cancelVerification() + return .success(()) + } catch { + return .failure(.failedCancellingVerification) + } + } + .value + } + + // MARK: - Private + + fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) { + callbacks.send(.receivedVerificationData(data.map { emoji in + SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description()) + })) + } + + fileprivate func didFail() { + callbacks.send(.failed) + } + + fileprivate func didFinish() { + callbacks.send(.finished) + } + + fileprivate func didCancel() { + callbacks.send(.cancelled) + } +} diff --git a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift new file mode 100644 index 000000000..c3f07459d --- /dev/null +++ b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift @@ -0,0 +1,43 @@ +// +// SessionVerificationControllerProxyProtocol.swift +// ElementX +// +// Created by Stefan Ceriu on 07/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation +import Combine + +enum SessionVerificationControllerProxyError: Error { + case failedRequestingVerification + case failedApprovingVerification + case failedDecliningVerification + case failedCancellingVerification +} + +enum SessionVerificationControllerProxyCallback { + case receivedVerificationData([SessionVerificationEmoji]) + case finished + case cancelled + case failed +} + +struct SessionVerificationEmoji: Hashable { + let symbol: String + let description: String +} + +protocol SessionVerificationControllerProxyProtocol { + var callbacks: PassthroughSubject { get } + + var isVerified: Bool { get } + + func requestVerification() async -> Result + + func approveVerification() async -> Result + + func declineVerification() async -> Result + + func cancelVerification() async -> Result +} diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index 378fbc7de..757cb4636 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -18,7 +18,9 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { init(roomProxy: RoomProxyProtocol) { self.roomProxy = roomProxy - self.roomProxy.callbacks.sink { [weak self] callback in + self.roomProxy.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in guard let self = self else { return } switch callback { diff --git a/ElementX/Sources/UITestScreenIdentifier.swift b/ElementX/Sources/UITestScreenIdentifier.swift index ea7f0a2a1..1ec203e33 100644 --- a/ElementX/Sources/UITestScreenIdentifier.swift +++ b/ElementX/Sources/UITestScreenIdentifier.swift @@ -20,6 +20,7 @@ enum UITestScreenIdentifier: String { case splash case roomPlainNoAvatar case roomEncryptedWithAvatar + case sessionVerification } extension UITestScreenIdentifier: CustomStringConvertible { diff --git a/ElementX/Sources/UITestsAppCoordinator.swift b/ElementX/Sources/UITestsAppCoordinator.swift index 99adcfd7b..b74b37bad 100644 --- a/ElementX/Sources/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITestsAppCoordinator.swift @@ -22,13 +22,13 @@ class UITestsAppCoordinator: Coordinator { window.tintColor = .element.accent let screens = mockScreens() - screens.forEach { $0.coordinator.start() } - let rootView = UITestsRootView(mockScreens: screens) { id in guard let screen = screens.first(where: { $0.id == id }) else { fatalError() } + screen.coordinator.start() + self.mainNavigationController.pushViewController(screen.coordinator.toPresentable(), animated: true) } @@ -45,9 +45,9 @@ class UITestsAppCoordinator: Coordinator { } @MainActor -struct MockScreen: Identifiable { +class MockScreen: Identifiable { let id: UITestScreenIdentifier - var coordinator: Coordinator & Presentable { + lazy var coordinator: Coordinator & Presentable = { switch id { case .login: let router = NavigationRouter(navigationController: ElementNavigationController()) @@ -78,17 +78,24 @@ struct MockScreen: Identifiable { case .splash: return SplashScreenCoordinator() case .roomPlainNoAvatar: - let params = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(), - roomName: "Some room name", - roomAvatar: nil, - roomEncryptionBadge: nil) - return RoomScreenCoordinator(parameters: params) + let parameters = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(), + roomName: "Some room name", + roomAvatar: nil, + roomEncryptionBadge: nil) + return RoomScreenCoordinator(parameters: parameters) case .roomEncryptedWithAvatar: - let params = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(), - roomName: "Some room name", - roomAvatar: Asset.Images.appLogo.image, - roomEncryptionBadge: Asset.Images.encryptionTrusted.image) - return RoomScreenCoordinator(parameters: params) + let parameters = RoomScreenCoordinatorParameters(timelineController: MockRoomTimelineController(), + roomName: "Some room name", + roomAvatar: Asset.Images.appLogo.image, + roomEncryptionBadge: Asset.Images.encryptionTrusted.image) + return RoomScreenCoordinator(parameters: parameters) + case .sessionVerification: + let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: MockSessionVerificationControllerProxy()) + return SessionVerificationCoordinator(parameters: parameters) } + }() + + init(id: UITestScreenIdentifier) { + self.id = id } } diff --git a/UITests/Sources/SessionVerificationUITests.swift b/UITests/Sources/SessionVerificationUITests.swift new file mode 100644 index 000000000..280b3eabb --- /dev/null +++ b/UITests/Sources/SessionVerificationUITests.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 XCTest +import ElementX + +class SessionVerificationUITests: XCTestCase { + + func testChallengeMatches() { + let app = Application.launch() + app.goToScreenWithIdentifier(.sessionVerification) + + XCTAssert(app.navigationBars["Verify this session"].exists) + + XCTAssert(app.buttons["startButton"].exists) + XCTAssert(app.buttons["dismissButton"].exists) + XCTAssert(app.staticTexts["titleLabel"].exists) + + app.buttons["startButton"].tap() + + XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists) + XCTAssert(app.buttons["cancelButton"].exists) + + XCTAssert(app.buttons["challengeAcceptButton"].waitForExistence(timeout: 5.0)) + XCTAssert(app.buttons["challengeDeclineButton"].waitForExistence(timeout: 5.0)) + XCTAssert(app.buttons["cancelButton"].waitForExistence(timeout: 5.0)) + + app.buttons["challengeAcceptButton"].tap() + + XCTAssert(app.activityIndicators["acceptingChallengeProgressView"].exists) + XCTAssert(app.buttons["cancelButton"].exists) + + XCTAssert(app.images["sessionVerificationSucceededIcon"].waitForExistence(timeout: 5.0)) + + XCTAssert(app.buttons["dismissButton"].exists) + app.buttons["dismissButton"].tap() + } + + func testChallengeDoesNotMatch() { + let app = Application.launch() + app.goToScreenWithIdentifier(.sessionVerification) + + XCTAssert(app.navigationBars["Verify this session"].exists) + + XCTAssert(app.buttons["startButton"].exists) + XCTAssert(app.buttons["dismissButton"].exists) + XCTAssert(app.staticTexts["titleLabel"].exists) + + app.buttons["startButton"].tap() + + XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists) + XCTAssert(app.buttons["cancelButton"].exists) + + XCTAssert(app.buttons["challengeAcceptButton"].waitForExistence(timeout: 5.0)) + XCTAssert(app.buttons["challengeDeclineButton"].waitForExistence(timeout: 5.0)) + XCTAssert(app.buttons["cancelButton"].waitForExistence(timeout: 5.0)) + + app.buttons["challengeDeclineButton"].tap() + + XCTAssert(app.images["sessionVerificationFailedIcon"].exists) + XCTAssert(app.buttons["restartButton"].exists) + + XCTAssert(app.buttons["dismissButton"].exists) + app.buttons["dismissButton"].tap() + } + + func testSessionVerificationCancelation() { + let app = Application.launch() + app.goToScreenWithIdentifier(.sessionVerification) + + XCTAssert(app.navigationBars["Verify this session"].exists) + + XCTAssert(app.buttons["startButton"].exists) + XCTAssert(app.buttons["dismissButton"].exists) + XCTAssert(app.staticTexts["titleLabel"].exists) + + app.buttons["startButton"].tap() + + XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists) + XCTAssert(app.buttons["cancelButton"].exists) + + app.buttons["cancelButton"].tap() + + XCTAssert(app.images["sessionVerificationFailedIcon"].exists) + XCTAssert(app.buttons["restartButton"].exists) + + XCTAssert(app.buttons["dismissButton"].exists) + app.buttons["dismissButton"].tap() + } +} diff --git a/UnitTests/Sources/SessionVerificationStateMachineTests.swift b/UnitTests/Sources/SessionVerificationStateMachineTests.swift new file mode 100644 index 000000000..e1a14450a --- /dev/null +++ b/UnitTests/Sources/SessionVerificationStateMachineTests.swift @@ -0,0 +1,94 @@ +// +// SessionVerificationStateMachineTests.swift +// UnitTests +// +// Created by Stefan Ceriu on 28/06/2022. +// Copyright © 2022 Element. All rights reserved. +// + +import XCTest + +@testable import ElementX + +@MainActor +class SessionVerificationStateMachineTests: XCTestCase { + private var stateMachine: SessionVerificationStateMachine! + + @MainActor + override func setUpWithError() throws { + stateMachine = SessionVerificationStateMachine() + } + + func testAcceptChallenge() { + XCTAssertEqual(stateMachine.state, .initial) + + stateMachine.processEvent(.requestVerification) + XCTAssertEqual(stateMachine.state, .requestingVerification) + + stateMachine.processEvent(.didReceiveChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + + stateMachine.processEvent(.acceptChallenge) + XCTAssertEqual(stateMachine.state, .acceptingChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + + stateMachine.processEvent(.didAcceptChallenge) + XCTAssertEqual(stateMachine.state, .verified) + } + + func testDeclineChallenge() { + XCTAssertEqual(stateMachine.state, .initial) + + stateMachine.processEvent(.requestVerification) + XCTAssertEqual(stateMachine.state, .requestingVerification) + + stateMachine.processEvent(.didReceiveChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + + stateMachine.processEvent(.declineChallenge) + XCTAssertEqual(stateMachine.state, .decliningChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + + stateMachine.processEvent(.didCancel) + XCTAssertEqual(stateMachine.state, .cancelled) + + stateMachine.processEvent(.restart) + XCTAssertEqual(stateMachine.state, .initial) + } + + func testCancellation() { + XCTAssertEqual(stateMachine.state, .initial) + + stateMachine.processEvent(.requestVerification) + XCTAssertEqual(stateMachine.state, .requestingVerification) + + stateMachine.processEvent(.cancel) + XCTAssertEqual(stateMachine.state, .cancelling) + + stateMachine.processEvent(.didCancel) + XCTAssertEqual(stateMachine.state, .cancelled) + + // This duplication is intentional + stateMachine.processEvent(.didCancel) + XCTAssertEqual(stateMachine.state, .cancelled) + + stateMachine.processEvent(.restart) + XCTAssertEqual(stateMachine.state, .initial) + + stateMachine.processEvent(.requestVerification) + XCTAssertEqual(stateMachine.state, .requestingVerification) + + stateMachine.processEvent(.didReceiveChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + + stateMachine.processEvent(.cancel) + XCTAssertEqual(stateMachine.state, .cancelling) + + stateMachine.processEvent(.didCancel) + XCTAssertEqual(stateMachine.state, .cancelled) + + stateMachine.processEvent(.restart) + XCTAssertEqual(stateMachine.state, .initial) + + stateMachine.processEvent(.restart) + XCTAssertEqual(stateMachine.state, .initial) + } +} diff --git a/UnitTests/Sources/SessionVerificationViewModelTests.swift b/UnitTests/Sources/SessionVerificationViewModelTests.swift new file mode 100644 index 000000000..339a74a46 --- /dev/null +++ b/UnitTests/Sources/SessionVerificationViewModelTests.swift @@ -0,0 +1,154 @@ +// +// 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 +import Combine + +@testable import ElementX + +@MainActor +class SessionVerificationViewModelTests: XCTestCase { + + var viewModel: SessionVerificationViewModelProtocol! + var context: SessionVerificationViewModelType.Context! + var sessionVerificationController: SessionVerificationControllerProxyProtocol! + + @MainActor + override func setUpWithError() throws { + sessionVerificationController = MockSessionVerificationControllerProxy() + viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: sessionVerificationController) + context = viewModel.context + } + + func testRequestVerification() async { + XCTAssertEqual(context.viewState.verificationState, .initial) + + context.send(viewAction: .start) + + await Task.yield() + + XCTAssertEqual(context.viewState.verificationState, .requestingVerification) + } + + func testVerificationCancellation() async { + XCTAssertEqual(context.viewState.verificationState, .initial) + + context.send(viewAction: .start) + + context.send(viewAction: .cancel) + + await Task.yield() + + XCTAssertEqual(context.viewState.verificationState, .cancelling) + + await Task.yield() + + XCTAssertEqual(context.viewState.verificationState, .cancelled) + + context.send(viewAction: .restart) + + await Task.yield() + + XCTAssertEqual(context.viewState.verificationState, .initial) + } + + func testReceiveChallenge() { + setupChallengeReceived() + } + + func testAcceptChallenge() { + + setupChallengeReceived() + + let waitForAcceptance = XCTestExpectation(description: "Wait for acceptance") + + let cancellable = sessionVerificationController.callbacks + .debounce(for: .seconds(2.0), scheduler: DispatchQueue.main) + .sink { callback in + switch callback { + case .finished: + waitForAcceptance.fulfill() + default: + XCTFail("Unexpected session verification controller callback") + } + } + + defer { + cancellable.cancel() + } + + context.send(viewAction: .accept) + + wait(for: [waitForAcceptance], timeout: 10.0) + + XCTAssertEqual(context.viewState.verificationState, .verified) + } + + func testDeclineChallenge() { + + setupChallengeReceived() + + let expectation = XCTestExpectation(description: "Wait for cancellation") + + let cancellable = sessionVerificationController.callbacks + .debounce(for: .seconds(2.0), scheduler: DispatchQueue.main) + .sink { callback in + switch callback { + case .cancelled: + expectation.fulfill() + default: + XCTFail("Unexpected session verification controller callback") + } + } + + defer { + cancellable.cancel() + } + + context.send(viewAction: .decline) + + wait(for: [expectation], timeout: 10.0) + + XCTAssertEqual(context.viewState.verificationState, .cancelled) + } + + // MARK: - Private + + private func setupChallengeReceived() { + let expectation = XCTestExpectation(description: "Wait for challenge") + + let cancellable = sessionVerificationController.callbacks + .debounce(for: .seconds(2.0), scheduler: DispatchQueue.main) + .sink { callback in + switch callback { + case .receivedVerificationData: + expectation.fulfill() + default: + break + } + } + + defer { + cancellable.cancel() + } + + context.send(viewAction: .start) + + wait(for: [expectation], timeout: 10.0) + + XCTAssertEqual(context.viewState.verificationState, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis)) + } +} diff --git a/project.yml b/project.yml index ff35e56fb..3b7f41498 100644 --- a/project.yml +++ b/project.yml @@ -32,6 +32,7 @@ packages: MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift.git exactVersion: 1.0.11-alpha + # path: ../matrix-rust-components-swift DesignKit: path: ./ DTCoreText: