From f0d971c1ab803f2a0e7bc572104695889c3ba562 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 26 Jan 2024 11:39:26 +0200 Subject: [PATCH] Move the account migration screen to within the room list --- ElementX.xcodeproj/project.pbxproj | 48 +--- .../UserSessionFlowCoordinator.swift | 39 +-- ...erSessionFlowCoordinatorStateMachine.swift | 10 - .../Screens/HomeScreen/HomeScreenModels.swift | 3 + .../HomeScreen/HomeScreenViewModel.swift | 96 ++++--- .../Screens/HomeScreen/View/HomeScreen.swift | 253 ++++++------------ .../HomeScreen/View/HomeScreenContent.swift | 189 +++++++++++++ .../MigrationScreenCoordinator.swift | 32 --- .../MigrationScreenModels.swift | 21 -- .../MigrationScreenViewModel.swift | 26 -- .../MigrationScreenViewModelProtocol.swift | 22 -- .../View/MigrationScreen.swift | 58 ---- .../UITests/UITestsAppCoordinator.swift | 3 - .../UITests/UITestsScreenIdentifier.swift | 1 - UITests/Sources/MigrationScreenUITests.swift | 25 -- .../en-GB-iPad-9th-generation.migration.png | 3 - .../Application/en-GB-iPhone-14.migration.png | 3 - .../pseudo-iPad-9th-generation.migration.png | 3 - .../pseudo-iPhone-14.migration.png | 3 - .../MigrationScreenViewModelTests.swift | 24 -- .../test_homeScreen.Migrating.png | 3 + .../PreviewTests/test_migrationScreen.1.png | 3 - 22 files changed, 349 insertions(+), 519 deletions(-) create mode 100644 ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift delete mode 100644 ElementX/Sources/Screens/MigrationScreen/MigrationScreenCoordinator.swift delete mode 100644 ElementX/Sources/Screens/MigrationScreen/MigrationScreenModels.swift delete mode 100644 ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModel.swift delete mode 100644 ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModelProtocol.swift delete mode 100644 ElementX/Sources/Screens/MigrationScreen/View/MigrationScreen.swift delete mode 100644 UITests/Sources/MigrationScreenUITests.swift delete mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.migration.png delete mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.migration.png delete mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.migration.png delete mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.migration.png delete mode 100644 UnitTests/Sources/MigrationScreenViewModelTests.swift create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_homeScreen.Migrating.png delete mode 100644 UnitTests/__Snapshots__/PreviewTests/test_migrationScreen.1.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 28e74688f..15d8b94b5 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -322,7 +322,6 @@ 5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */; }; 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; }; 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; - 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */; }; 520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */; }; 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; @@ -381,12 +380,12 @@ 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; }; 62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; }; + 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; }; 63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */; }; 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */; }; 6409CE10CFF4DCB68C4C3872 /* ScaledPaddingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26C69EC1157D71CC61ADAE4 /* ScaledPaddingModifier.swift */; }; 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; }; 6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */; }; - 644AA5001BCC58D7732EB772 /* MigrationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */; }; 64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */; }; 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */; }; 64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; }; @@ -586,7 +585,6 @@ 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; }; 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; - 968823C9DBF3062729413EBF /* MigrationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DF0BFE5637EA42F5651FE8 /* MigrationScreenCoordinator.swift */; }; 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; @@ -617,7 +615,6 @@ 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; }; 9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; }; - 9DE98D3EC47742A0F9F9EC3C /* MigrationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5685139D0B72BED3503EFCC /* MigrationScreen.swift */; }; 9DF3F6318A4402305F5EB869 /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */; }; 9E838A62918E47BC72D6640D /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */; }; 9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */; }; @@ -712,7 +709,6 @@ B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; }; B444F9C184A377C1B481F07F /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; B45F20A1C3F1CE19D5B8BA74 /* InvitesScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */; }; - B46EBC7B96CCB64FF8E110DC /* MigrationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD0FAE9EA761DA175D31CC7 /* MigrationScreenModels.swift */; }; B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */; }; @@ -789,7 +785,6 @@ C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */; }; - C5946E4A3D4295F002F0B3DC /* MigrationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B59EC4B0C93254089EAACB /* MigrationScreenViewModelTests.swift */; }; C5A07E2D88BE7D51DCECD166 /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */; }; C67FCC854F3A6FC7A2EC04D0 /* MediaUploadPreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */; }; C6C06DDA8881260303FBA3A0 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; }; @@ -973,7 +968,6 @@ F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; }; F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */; }; - F692D4AF571333C0D785725A /* MigrationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBC5E7C0F8337D2A46EB2DD /* MigrationScreenViewModelProtocol.swift */; }; F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; }; F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; @@ -1131,7 +1125,6 @@ 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = ""; }; 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; - 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; @@ -1323,7 +1316,6 @@ 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelProtocol.swift; sourceTree = ""; }; 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = ""; }; 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenModels.swift; sourceTree = ""; }; - 46B59EC4B0C93254089EAACB /* MigrationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModelTests.swift; sourceTree = ""; }; 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModel.swift; sourceTree = ""; }; 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenUITests.swift; sourceTree = ""; }; @@ -1406,7 +1398,6 @@ 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingLogo.swift; sourceTree = ""; }; 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = ""; }; 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = ""; }; - 5CD0FAE9EA761DA175D31CC7 /* MigrationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenModels.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = ""; }; 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledFrameModifier.swift; sourceTree = ""; }; @@ -1490,7 +1481,6 @@ 74E08B8A66948E9690F38B94 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposter.swift; sourceTree = ""; }; 75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineView.swift; sourceTree = ""; }; - 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenUITests.swift; sourceTree = ""; }; 76310030C831D4610A705603 /* URLComponentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsTests.swift; sourceTree = ""; }; 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; 7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = ""; }; @@ -1636,7 +1626,7 @@ A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; A34A814CBD56230BC74FFCF4 /* MXLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = ""; }; - A3DF0BFE5637EA42F5651FE8 /* MigrationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenCoordinator.swift; sourceTree = ""; }; + A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = ""; }; A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = ""; }; A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; A40F1985065500F0E7F61A27 /* PollFormScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1840,7 +1830,6 @@ D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineItem.swift; sourceTree = ""; }; D53D6BB7E8E5EC031281872C /* OnboardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenViewModelTests.swift; sourceTree = ""; }; D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelProtocol.swift; sourceTree = ""; }; - D5685139D0B72BED3503EFCC /* MigrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreen.swift; sourceTree = ""; }; D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; D5E26C54362206BBDD096D83 /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceTests.swift; sourceTree = ""; }; @@ -1912,7 +1901,6 @@ E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = ""; }; EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenModels.swift; sourceTree = ""; }; - EBBC5E7C0F8337D2A46EB2DD /* MigrationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModelProtocol.swift; sourceTree = ""; }; EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenUITests.swift; sourceTree = ""; }; @@ -2944,6 +2932,7 @@ children = ( D7BEB970F500BFB248443FA1 /* BloomView.swift */, B902EA6CD3296B0E10EE432B /* HomeScreen.swift */, + A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */, C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */, 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */, 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */, @@ -3307,7 +3296,6 @@ AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */, 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */, 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */, - 46B59EC4B0C93254089EAACB /* MigrationScreenViewModelTests.swift */, F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */, 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */, 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */, @@ -3498,14 +3486,6 @@ path = View; sourceTree = ""; }; - 79A3F9F48E7D5B189A63BACB /* View */ = { - isa = PBXGroup; - children = ( - D5685139D0B72BED3503EFCC /* MigrationScreen.swift */, - ); - path = View; - sourceTree = ""; - }; 79E560F5113ED25D172E550C /* Media */ = { isa = PBXGroup; children = ( @@ -3658,18 +3638,6 @@ path = MediaPickerScreen; sourceTree = ""; }; - 888C38D172A591977316DB1E /* MigrationScreen */ = { - isa = PBXGroup; - children = ( - A3DF0BFE5637EA42F5651FE8 /* MigrationScreenCoordinator.swift */, - 5CD0FAE9EA761DA175D31CC7 /* MigrationScreenModels.swift */, - 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */, - EBBC5E7C0F8337D2A46EB2DD /* MigrationScreenViewModelProtocol.swift */, - 79A3F9F48E7D5B189A63BACB /* View */, - ); - path = MigrationScreen; - sourceTree = ""; - }; 8A9C09B6A392465E03B8D1B1 /* IntegrationTests */ = { isa = PBXGroup; children = ( @@ -3804,7 +3772,6 @@ 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */, 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */, - 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */, 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */, B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */, 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */, @@ -4541,7 +4508,6 @@ 87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */, 23605DD08620BE6558242469 /* MediaUploadPreviewScreen */, 3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */, - 888C38D172A591977316DB1E /* MigrationScreen */, 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */, A448A3A8F764174C60CD0CA1 /* Other */, 5970F275D6014548DCED6106 /* ReportContentScreen */, @@ -5291,7 +5257,6 @@ B9A8C34A00D03094C0CF56F3 /* MediaUploadPreviewScreenViewModelTests.swift in Sources */, 23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */, F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */, - C5946E4A3D4295F002F0B3DC /* MigrationScreenViewModelTests.swift in Sources */, 4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */, 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */, DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */, @@ -5555,6 +5520,7 @@ 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */, D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, + 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, 77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */, 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */, @@ -5660,11 +5626,6 @@ F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */, C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */, C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, - 9DE98D3EC47742A0F9F9EC3C /* MigrationScreen.swift in Sources */, - 968823C9DBF3062729413EBF /* MigrationScreenCoordinator.swift in Sources */, - B46EBC7B96CCB64FF8E110DC /* MigrationScreenModels.swift in Sources */, - 644AA5001BCC58D7732EB772 /* MigrationScreenViewModel.swift in Sources */, - F692D4AF571333C0D785725A /* MigrationScreenViewModelProtocol.swift in Sources */, 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */, EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */, B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, @@ -6072,7 +6033,6 @@ 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */, 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */, - 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */, 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */, AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */, 92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index a7025dc32..cfbc8df2f 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -42,7 +42,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private var bugReportFlowCoordinator: BugReportFlowCoordinator? private var cancellables = Set() - private var migrationCancellable: AnyCancellable? private let sidebarNavigationStackCoordinator: NavigationStackCoordinator private let detailNavigationStackCoordinator: NavigationStackCoordinator @@ -137,10 +136,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } func start() { - if appSettings.migratedAccounts[userSession.userID] != true { - // Show the migration screen for a new account. - stateMachine.processEvent(.startWithMigration) - } else if !appSettings.hasShownWelcomeScreen { + if !appSettings.hasShownWelcomeScreen { stateMachine.processEvent(.startWithWelcomeScreen) } else { // Otherwise go straight to the home screen. @@ -220,12 +216,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case (.initial, .start, .roomList): presentHomeScreen() - case (.initial, .startWithMigration, .migration): - presentMigrationScreen() // Full screen cover - presentHomeScreen() // Have the home screen ready to show underneath - case (.migration, .completeMigration, .roomList): - dismissMigrationScreen() - case (.initial, .startWithWelcomeScreen, .welcomeScreen): presentHomeScreen() presentWelcomeScreen() @@ -304,33 +294,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentMigrationScreen() { - // Listen for the first sync to finish. - migrationCancellable = userSession.clientProxy.callbacks - .receive(on: DispatchQueue.main) - .sink { [weak self] callback in - guard let self, stateMachine.state == .migration, case .receivedSyncUpdate = callback else { return } - migrationCancellable = nil - appSettings.migratedAccounts[userSession.userID] = true - stateMachine.processEvent(.completeMigration) - } - - let coordinator = MigrationScreenCoordinator() - navigationSplitCoordinator.setFullScreenCoverCoordinator(coordinator) - } - - private func dismissMigrationScreen() { - navigationSplitCoordinator.setFullScreenCoverCoordinator(nil) - - // Not sure why but the full screen closure dismissal closure doesn't seem to work properly - // And not using the DispatchQueue.main results in the the screen getting presented as full screen too. - if !appSettings.hasShownWelcomeScreen { - DispatchQueue.main.async { - self.stateMachine.processEvent(.presentWelcomeScreen) - } - } - } - private func presentHomeScreen() { let parameters = HomeScreenCoordinatorParameters(userSession: userSession, attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index cbea83b59..7c7dc7cda 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -23,9 +23,6 @@ class UserSessionFlowCoordinatorStateMachine { /// The initial state, used before the coordinator starts case initial - /// Showing the migration screen whilst the proxy performs an initial sync. - case migration - /// Showing the welcome screen. case welcomeScreen @@ -63,11 +60,6 @@ class UserSessionFlowCoordinatorStateMachine { /// **Note:** This is event is only for users who used the app before v1.1.8. /// It can be removed once the older TestFlight builds have expired. case startWithWelcomeScreen - /// Start the user session flows with a migration screen. - case startWithMigration - - /// Request to transition from the migration state to the home screen. - case completeMigration /// Request presentation of the welcome screen. case presentWelcomeScreen @@ -124,9 +116,7 @@ class UserSessionFlowCoordinatorStateMachine { private func configure() { stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomID: nil)]) - stateMachine.addRoutes(event: .startWithMigration, transitions: [.initial => .migration]) stateMachine.addRoutes(event: .startWithWelcomeScreen, transitions: [.initial => .welcomeScreen]) - stateMachine.addRoutes(event: .completeMigration, transitions: [.migration => .roomList(selectedRoomID: nil)]) stateMachine.addRoutes(event: .dismissedWelcomeScreen, transitions: [.welcomeScreen => .roomList(selectedRoomID: nil)]) stateMachine.addRouteMapping { event, fromState, _ in diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index db52a4435..dc4f64b43 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -52,12 +52,15 @@ enum HomeScreenViewAction { } enum HomeScreenRoomListMode: CustomStringConvertible { + case migration case skeletons case empty case rooms var description: String { switch self { + case .migration: + return "Showing account migration" case .skeletons: return "Showing placeholders" case .empty: diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 2077068fe..9f3fca583 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -27,6 +27,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol private let roomSummaryProvider: RoomSummaryProviderProtocol? private let inviteSummaryProvider: RoomSummaryProviderProtocol? + private var migrationCancellable: AnyCancellable? + private var visibleItemRangeObservationToken: AnyCancellable? private let visibleItemRangePublisher = CurrentValueSubject<(range: Range, isScrolling: Bool), Never>((0..<0, false)) @@ -101,7 +103,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } .store(in: &cancellables) - setupRoomSummaryProviderSubscriptions() + setupRoomListSubscriptions() updateRooms() } @@ -166,7 +168,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } } - private func setupRoomSummaryProviderSubscriptions() { + private func setupRoomListSubscriptions() { guard let roomSummaryProvider, let inviteSummaryProvider else { MXLog.error("Room summary provider unavailable") return @@ -174,42 +176,32 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol ServiceLocator.shared.analytics.signpost.beginFirstRooms() + let hasUserBeenMigrated = appSettings.migratedAccounts[userSession.userID] == true + + if !hasUserBeenMigrated { + state.roomListMode = .migration + + MXLog.info("Account not migrated, setting view room list mode to \"\(state.roomListMode)\"") + + migrationCancellable = userSession.clientProxy.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self, case .receivedSyncUpdate = callback else { return } + migrationCancellable = nil + appSettings.migratedAccounts[userSession.userID] = true + + MXLog.info("Received first sync response, updating room list mode") + + updateRoomListMode(with: roomSummaryProvider.statePublisher.value) + } + } + roomSummaryProvider.statePublisher .receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self else { return } - let isLoadingData = !state.isLoaded - let hasNoRooms = state.isLoaded && state.totalNumberOfRooms == 0 - - var roomListMode = self.state.roomListMode - if isLoadingData { - roomListMode = .skeletons - } else if hasNoRooms { - roomListMode = .empty - } else { - roomListMode = .rooms - } - - guard roomListMode != self.state.roomListMode else { - return - } - - if roomListMode == .rooms, self.state.roomListMode == .skeletons { - ServiceLocator.shared.analytics.signpost.endFirstRooms() - } - - self.state.roomListMode = roomListMode - - MXLog.info("Received room summary provider update, setting view room list mode to \"\(self.state.roomListMode)\"") - - // Delay user profile detail loading until after the initial room list loads - if roomListMode == .rooms { - Task { - await self.userSession.clientProxy.loadUserAvatarURL() - await self.userSession.clientProxy.loadUserDisplayName() - } - } + updateRoomListMode(with: state) } .store(in: &cancellables) @@ -240,6 +232,44 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .store(in: &cancellables) } + private func updateRoomListMode(with roomSummaryProviderState: RoomSummaryProviderState) { + guard appSettings.migratedAccounts[userSession.userID] == true else { + // Ignore room summary provider updates while "migrating" + return + } + + let isLoadingData = !roomSummaryProviderState.isLoaded + let hasNoRooms = roomSummaryProviderState.isLoaded && roomSummaryProviderState.totalNumberOfRooms == 0 + + var roomListMode = state.roomListMode + if isLoadingData { + roomListMode = .skeletons + } else if hasNoRooms { + roomListMode = .empty + } else { + roomListMode = .rooms + } + + guard roomListMode != state.roomListMode else { + return + } + + if roomListMode == .rooms, state.roomListMode == .skeletons { + ServiceLocator.shared.analytics.signpost.endFirstRooms() + } + + state.roomListMode = roomListMode + + MXLog.info("Received room summary provider update, setting view room list mode to \"\(state.roomListMode)\"") + // Delay user profile detail loading until after the initial room list loads + if roomListMode == .rooms { + Task { + await self.userSession.clientProxy.loadUserAvatarURL() + await self.userSession.clientProxy.loadUserDisplayName() + } + } + } + private func installListRangeModifiers() { guard visibleItemRangeObservationToken == nil else { return diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 2fc30b652..49732d6cd 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -32,102 +32,49 @@ struct HomeScreen: View { @State private var hairlineView: UIView? var body: some View { - GeometryReader { geometry in - ScrollView { - switch context.viewState.roomListMode { - case .skeletons: - LazyVStack(spacing: 0) { - ForEach(context.viewState.visibleRooms) { room in - HomeScreenRoomCell(room: room, context: context, isSelected: false) - .redacted(reason: .placeholder) - .shimmer() // Putting this directly on the LazyVStack creates an accordion animation on iOS 16. - } + HomeScreenContent(context: context, scrollViewAdapter: scrollViewAdapter) + .alert(item: $context.alertInfo) + .alert(item: $context.leaveRoomAlertItem, + actions: leaveRoomAlertActions, + message: leaveRoomAlertMessage) + .navigationTitle(L10n.screenRoomlistMainSpaceTitle) + .toolbar { toolbar } + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + .track(screen: .home) + .introspect(.viewController, on: .supportedVersions) { controller in + Task { + if bloomView == nil { + makeBloomView(controller: controller) } - .disabled(true) - case .empty: - HomeScreenEmptyStateLayout(minHeight: geometry.size.height) { - topSection - - HomeScreenEmptyStateView(context: context) - .layoutPriority(1) + } + let isTopController = controller.navigationController?.topViewController != controller + let isHidden = isTopController || context.isSearchFieldFocused + if let bloomView { + bloomView.isHidden = isHidden + UIView.transition(with: bloomView, duration: 1.75, options: .curveEaseInOut) { + bloomView.alpha = isTopController ? 0 : 1 } - case .rooms: - LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { - Section { - HomeScreenRoomList(context: context) - } header: { - topSection - } + } + gradientView?.isHidden = isHidden + navigationBarContainer?.clipsToBounds = !isHidden + hairlineView?.isHidden = isHidden || !scrollViewAdapter.isAtTopEdge.value + if !isHidden { + updateBloomCenter() + } + } + .onReceive(scrollViewAdapter.isAtTopEdge.removeDuplicates()) { value in + hairlineView?.isHidden = !value + guard let gradientView else { + return + } + if value { + UIView.transition(with: gradientView, duration: 0.3, options: .curveEaseIn) { + gradientView.alpha = 0 } - .searchable(text: $context.searchQuery) - .compoundSearchField() - .disableAutocorrection(true) + } else { + gradientView.alpha = 1 } } - .introspect(.scrollView, on: .supportedVersions) { scrollView in - guard scrollView != scrollViewAdapter.scrollView else { return } - scrollViewAdapter.scrollView = scrollView - } - .onReceive(scrollViewAdapter.didScroll) { _ in - updateVisibleRange() - } - .onReceive(scrollViewAdapter.isScrolling) { _ in - updateVisibleRange() - } - .onChange(of: context.searchQuery) { _ in - updateVisibleRange() - } - .onChange(of: context.viewState.visibleRooms) { _ in - updateVisibleRange() - } - .scrollDismissesKeyboard(.immediately) - .scrollDisabled(context.viewState.roomListMode == .skeletons) - .scrollBounceBehavior(context.viewState.roomListMode == .empty ? .basedOnSize : .automatic) - .animation(.elementDefault, value: context.viewState.roomListMode) - .animation(.none, value: context.viewState.visibleRooms) - } - .alert(item: $context.alertInfo) - .alert(item: $context.leaveRoomAlertItem, - actions: leaveRoomAlertActions, - message: leaveRoomAlertMessage) - .navigationTitle(L10n.screenRoomlistMainSpaceTitle) - .toolbar { toolbar } - .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) - .track(screen: .home) - .introspect(.viewController, on: .supportedVersions) { controller in - Task { - if bloomView == nil { - makeBloomView(controller: controller) - } - } - let isTopController = controller.navigationController?.topViewController != controller - let isHidden = isTopController || context.isSearchFieldFocused - if let bloomView { - bloomView.isHidden = isHidden - UIView.transition(with: bloomView, duration: 1.75, options: .curveEaseInOut) { - bloomView.alpha = isTopController ? 0 : 1 - } - } - gradientView?.isHidden = isHidden - navigationBarContainer?.clipsToBounds = !isHidden - hairlineView?.isHidden = isHidden || !scrollViewAdapter.isAtTopEdge.value - if !isHidden { - updateBloomCenter() - } - } - .onReceive(scrollViewAdapter.isAtTopEdge.removeDuplicates()) { value in - hairlineView?.isHidden = !value - guard let gradientView else { - return - } - if value { - UIView.transition(with: gradientView, duration: 0.3, options: .curveEaseIn) { - gradientView.alpha = 0 - } - } else { - gradientView.alpha = 1 - } - } } // MARK: - Private @@ -189,35 +136,6 @@ struct HomeScreen: View { let center = leftBarButtonView.convert(leftBarButtonView.center, to: navigationBarContainer.coordinateSpace) bloomView.center = center } - - @ViewBuilder - /// The session verification banner and invites button if either are needed. - private var topSection: some View { - VStack(spacing: 0) { - if context.viewState.shouldShowFilters { - filters - } - - if context.viewState.showSessionVerificationBanner { - HomeScreenSessionVerificationBanner(context: context) - } else if context.viewState.showRecoveryKeyConfirmationBanner { - HomeScreenRecoveryKeyConfirmationBanner(context: context) - } - - if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused { - HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) { - context.send(viewAction: .selectInvites) - } - .accessibilityIdentifier(A11yIdentifiers.homeScreen.invites) - .frame(maxWidth: .infinity, alignment: .trailing) - } - } - .background(Color.compound.bgCanvasDefault) - } - - private var filters: some View { - RoomListFiltersView(state: context.viewState.filtersState) - } @ToolbarContentBuilder private var toolbar: some ToolbarContent { @@ -225,7 +143,7 @@ struct HomeScreen: View { HomeScreenUserMenuButton(context: context) } - ToolbarItemGroup(placement: .primaryAction) { + ToolbarItem(placement: .primaryAction) { newRoomButton } } @@ -234,41 +152,20 @@ struct HomeScreen: View { BloomView(context: context) } + @ViewBuilder private var newRoomButton: some View { - Button { - context.send(viewAction: .startChat) - } label: { - CompoundIcon(\.edit) + switch context.viewState.roomListMode { + case .empty, .rooms: + Button { + context.send(viewAction: .startChat) + } label: { + CompoundIcon(\.edit) + } + .accessibilityLabel(L10n.actionStartChat) + .accessibilityIdentifier(A11yIdentifiers.homeScreen.startChat) + default: + EmptyView() } - .accessibilityLabel(L10n.actionStartChat) - .accessibilityIdentifier(A11yIdentifiers.homeScreen.startChat) - } - - /// Often times the scroll view's content size isn't correct yet when this method is called e.g. when cancelling a search - /// Dispatch it with a delay to allow the UI to update and the computations to be correct - /// Once we move to iOS 17 we should remove all of this and use scroll anchors instead - private func updateVisibleRange() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { delayedUpdateVisibleRange() } - } - - private func delayedUpdateVisibleRange() { - guard let scrollView = scrollViewAdapter.scrollView, - context.viewState.visibleRooms.count > 0 else { - return - } - - guard scrollView.contentSize.height > scrollView.bounds.height else { - return - } - - let adjustedContentSize = max(scrollView.contentSize.height - scrollView.contentInset.top - scrollView.contentInset.bottom, scrollView.bounds.height) - let cellHeight = adjustedContentSize / Double(context.viewState.visibleRooms.count) - - let firstIndex = Int(max(0.0, scrollView.contentOffset.y + scrollView.contentInset.top) / cellHeight) - let lastIndex = Int(max(0.0, scrollView.contentOffset.y + scrollView.bounds.height) / cellHeight) - - // This will be deduped and throttled on the view model layer - context.send(viewAction: .updateVisibleItemRange(range: firstIndex.. HomeScreenViewModel { - let clientProxy = MockClientProxy(userID: "@alice:example.com", - roomSummaryProvider: MockRoomSummaryProvider(state: state)) + static func viewModel(_ mode: HomeScreenRoomListMode) -> HomeScreenViewModel { + let userID = mode == .migration ? "@unmigrated_alice:example.com" : "@alice:example.com" + + let appSettings = AppSettings() // This uses shared storage under the hood + appSettings.migratedAccounts[userID] = mode != .migration + + let roomSummaryProviderState: MockRoomSummaryProviderState = switch mode { + case .migration: + .loading + case .skeletons: + .loading + case .empty: + .loaded([]) + case .rooms: + .loaded(.mockRooms) + } + + let clientProxy = MockClientProxy(userID: userID, + roomSummaryProvider: MockRoomSummaryProvider(state: roomSummaryProviderState)) let userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), @@ -320,7 +239,7 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview { return HomeScreenViewModel(userSession: userSession, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), - appSettings: ServiceLocator.shared.settings, + appSettings: appSettings, userIndicatorController: ServiceLocator.shared.userIndicatorController) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift new file mode 100644 index 000000000..487586d3f --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -0,0 +1,189 @@ +// +// Copyright 2024 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 HomeScreenContent: View { + @Environment(\.verticalSizeClass) private var verticalSizeClass + + @ObservedObject var context: HomeScreenViewModel.Context + let scrollViewAdapter: ScrollViewAdapter + + var body: some View { + switch context.viewState.roomListMode { + case .migration: + migrationView + default: + roomList + } + } + + private var roomList: some View { + GeometryReader { geometry in + ScrollView { + switch context.viewState.roomListMode { + case .skeletons: + LazyVStack(spacing: 0) { + ForEach(context.viewState.visibleRooms) { room in + HomeScreenRoomCell(room: room, context: context, isSelected: false) + .redacted(reason: .placeholder) + .shimmer() // Putting this directly on the LazyVStack creates an accordion animation on iOS 16. + } + } + .disabled(true) + case .empty: + HomeScreenEmptyStateLayout(minHeight: geometry.size.height) { + topSection + + HomeScreenEmptyStateView(context: context) + .layoutPriority(1) + } + case .rooms: + LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { + Section { + HomeScreenRoomList(context: context) + } header: { + topSection + } + } + .searchable(text: $context.searchQuery) + .compoundSearchField() + .disableAutocorrection(true) + case .migration: + EmptyView() + } + } + .introspect(.scrollView, on: .supportedVersions) { scrollView in + guard scrollView != scrollViewAdapter.scrollView else { return } + scrollViewAdapter.scrollView = scrollView + } + .onReceive(scrollViewAdapter.didScroll) { _ in + updateVisibleRange() + } + .onReceive(scrollViewAdapter.isScrolling) { _ in + updateVisibleRange() + } + .onChange(of: context.searchQuery) { _ in + updateVisibleRange() + } + .onChange(of: context.viewState.visibleRooms) { _ in + updateVisibleRange() + } + .scrollDismissesKeyboard(.immediately) + .scrollDisabled(context.viewState.roomListMode == .skeletons) + .scrollBounceBehavior(context.viewState.roomListMode == .empty ? .basedOnSize : .automatic) + .animation(.elementDefault, value: context.viewState.roomListMode) + .animation(.none, value: context.viewState.visibleRooms) + } + } + + @ViewBuilder + /// The session verification banner and invites button if either are needed. + private var topSection: some View { + VStack(spacing: 0) { + if context.viewState.shouldShowFilters { + filters + } + + if context.viewState.showSessionVerificationBanner { + HomeScreenSessionVerificationBanner(context: context) + } else if context.viewState.showRecoveryKeyConfirmationBanner { + HomeScreenRecoveryKeyConfirmationBanner(context: context) + } + + if context.viewState.hasPendingInvitations, !context.isSearchFieldFocused { + HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) { + context.send(viewAction: .selectInvites) + } + .accessibilityIdentifier(A11yIdentifiers.homeScreen.invites) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + .background(Color.compound.bgCanvasDefault) + } + + private var filters: some View { + RoomListFiltersView(state: context.viewState.filtersState) + } + + @ViewBuilder + private var migrationView: some View { + if UIDevice.current.isPhone { + if verticalSizeClass == .compact { + migrationViewContent + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + WaitingDialog { + migrationViewContent + } bottomContent: { + EmptyView() + } + } + } else { + migrationViewContent + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + private var migrationViewContent: some View { + VStack(spacing: 16) { + ProgressView() + .tint(.compound.iconPrimary) + .padding(.bottom, 4) + + Text(L10n.screenMigrationTitle.tinting(".", color: Asset.Colors.brandColor.swiftUIColor)) + .minimumScaleFactor(0.01) + .font(.compound.headingXLBold) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textPrimary) + + Text(L10n.screenMigrationMessage) + .minimumScaleFactor(0.01) + .font(.compound.bodyLG) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textPrimary) + .accessibilityIdentifier(A11yIdentifiers.migrationScreen.message) + } + .padding(.horizontal) + } + + /// Often times the scroll view's content size isn't correct yet when this method is called e.g. when cancelling a search + /// Dispatch it with a delay to allow the UI to update and the computations to be correct + /// Once we move to iOS 17 we should remove all of this and use scroll anchors instead + private func updateVisibleRange() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { delayedUpdateVisibleRange() } + } + + private func delayedUpdateVisibleRange() { + guard let scrollView = scrollViewAdapter.scrollView, + context.viewState.visibleRooms.count > 0 else { + return + } + + guard scrollView.contentSize.height > scrollView.bounds.height else { + return + } + + let adjustedContentSize = max(scrollView.contentSize.height - scrollView.contentInset.top - scrollView.contentInset.bottom, scrollView.bounds.height) + let cellHeight = adjustedContentSize / Double(context.viewState.visibleRooms.count) + + let firstIndex = Int(max(0.0, scrollView.contentOffset.y + scrollView.contentInset.top) / cellHeight) + let lastIndex = Int(max(0.0, scrollView.contentOffset.y + scrollView.bounds.height) / cellHeight) + + // This will be deduped and throttled on the view model layer + context.send(viewAction: .updateVisibleItemRange(range: firstIndex.. AnyView { - AnyView(MigrationScreen(context: viewModel.context)) - } -} diff --git a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenModels.swift b/ElementX/Sources/Screens/MigrationScreen/MigrationScreenModels.swift deleted file mode 100644 index 529abff3f..000000000 --- a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenModels.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct MigrationScreenViewState: BindableState { } - -enum MigrationScreenViewAction { } diff --git a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModel.swift b/ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModel.swift deleted file mode 100644 index 386fe3ad5..000000000 --- a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModel.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Combine -import SwiftUI - -typealias MigrationScreenViewModelType = StateStoreViewModel - -class MigrationScreenViewModel: MigrationScreenViewModelType, MigrationScreenViewModelProtocol { - init() { - super.init(initialViewState: MigrationScreenViewState()) - } -} diff --git a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModelProtocol.swift b/ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModelProtocol.swift deleted file mode 100644 index 447f17c0d..000000000 --- a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenViewModelProtocol.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Combine - -@MainActor -protocol MigrationScreenViewModelProtocol { - var context: MigrationScreenViewModelType.Context { get } -} diff --git a/ElementX/Sources/Screens/MigrationScreen/View/MigrationScreen.swift b/ElementX/Sources/Screens/MigrationScreen/View/MigrationScreen.swift deleted file mode 100644 index 6c249e747..000000000 --- a/ElementX/Sources/Screens/MigrationScreen/View/MigrationScreen.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct MigrationScreen: View { - @ObservedObject var context: MigrationScreenViewModel.Context - - var body: some View { - WaitingDialog { - content - } bottomContent: { - EmptyView() - } - .navigationBarBackButtonHidden() - } - - var content: some View { - VStack(spacing: 16) { - ProgressView() - .tint(.compound.iconPrimary) - .padding(.bottom, 4) - - Text(L10n.screenMigrationTitle.tinting(".", color: Asset.Colors.brandColor.swiftUIColor)) - .font(.compound.headingXLBold) - .multilineTextAlignment(.center) - .foregroundColor(.compound.textPrimary) - - Text(L10n.screenMigrationMessage) - .font(.compound.bodyLG) - .multilineTextAlignment(.center) - .foregroundColor(.compound.textPrimary) - .accessibilityIdentifier(A11yIdentifiers.migrationScreen.message) - } - } -} - -// MARK: - Previews - -struct MigrationScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = MigrationScreenViewModel() - static var previews: some View { - MigrationScreen(context: viewModel.context) - } -} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index edc707c7c..f8b486127 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -131,9 +131,6 @@ class MockScreen: Identifiable { analytics: ServiceLocator.shared.analytics)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator - case .migration: - let coordinator = MigrationScreenCoordinator() - return coordinator case .authenticationFlow: let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(), diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index aa5febcd1..9700eee43 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -27,7 +27,6 @@ enum UITestsScreenIdentifier: String { case waitlist case analyticsPrompt case analyticsSettingsScreen - case migration case templateScreen case appLockFlow case appLockFlowAlternateWindow diff --git a/UITests/Sources/MigrationScreenUITests.swift b/UITests/Sources/MigrationScreenUITests.swift deleted file mode 100644 index b7c365e1e..000000000 --- a/UITests/Sources/MigrationScreenUITests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -@MainActor -class MigrationScreenUITests: XCTestCase { - func testRegularScreen() async throws { - let app = Application.launch(.migration) - try await app.assertScreenshot(.migration) - } -} diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.migration.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.migration.png deleted file mode 100644 index 9dcbd556e..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.migration.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be0caf4575522004856575896db9e521b132b64ed60e7973be9af121acbbc6eb -size 705140 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.migration.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.migration.png deleted file mode 100644 index b4aa3117c..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.migration.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a16d4694488603b5f3c7c2fbb9753376e546534dc9ce8496eed2710ff6cc52a5 -size 444000 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.migration.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.migration.png deleted file mode 100644 index e0f15d49a..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.migration.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:816e426325a232a5d61d062a2e66b949276ade47fd997553f5ed9d97a9113e4a -size 718647 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.migration.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.migration.png deleted file mode 100644 index bf08b15f0..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.migration.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0b1aaa41fa4331da2eb5b2c424644613ade03e2278589a826b89529ebf9f192 -size 471933 diff --git a/UnitTests/Sources/MigrationScreenViewModelTests.swift b/UnitTests/Sources/MigrationScreenViewModelTests.swift deleted file mode 100644 index 816738d44..000000000 --- a/UnitTests/Sources/MigrationScreenViewModelTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -@testable import ElementX - -@MainActor -class MigrationScreenViewModelTests: XCTestCase { - // Nothing to test, the view model has no mutable state. -} diff --git a/UnitTests/__Snapshots__/PreviewTests/test_homeScreen.Migrating.png b/UnitTests/__Snapshots__/PreviewTests/test_homeScreen.Migrating.png new file mode 100644 index 000000000..0ae28d466 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_homeScreen.Migrating.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03df974befe5829a81b36bdecf2ed379c107c2897dfd9910b567db75c0d82cb1 +size 545917 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_migrationScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_migrationScreen.1.png deleted file mode 100644 index e282ee004..000000000 --- a/UnitTests/__Snapshots__/PreviewTests/test_migrationScreen.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f649975887f30bff1bb3e4749546a41d4f52798e9a9ed00adeb10373d5973bdb -size 972754