From 867cce7b99aadc09dd18267d82dbfc1076d14152 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 23 Sep 2022 13:21:54 +0300 Subject: [PATCH] Implemented room list loading skeletons and placeholder rooms (#206) --- ElementX.xcodeproj/project.pbxproj | 40 ++++---- ElementX/Sources/AppCoordinator.swift | 3 +- .../Screens/HomeScreen/HomeScreenModels.swift | 37 ++++++-- .../HomeScreen/HomeScreenViewModel.swift | 95 ++++++++++++------- .../Screens/HomeScreen/View/HomeScreen.swift | 55 +++++------ .../HomeScreen/View/HomeScreenRoomCell.swift | 17 +++- .../RoomScreen/RoomScreenViewModel.swift | 3 +- .../SessionVerificationViewModel.swift | 3 +- .../Settings/View/SettingsScreen.swift | 4 +- .../Services/Client/MockClientProxy.swift | 2 +- .../RoomSummary/MockRoomSummaryProvider.swift | 77 ++++++++++----- ...Summary.swift => RoomSummaryDetails.swift} | 2 +- .../RoomSummary/RoomSummaryProvider.swift | 81 ++++++++-------- .../RoomSummaryProviderProtocol.swift | 38 ++++++-- .../Services/Session/UserSession.swift | 3 +- .../Timeline/RoomTimelineController.swift | 3 +- 16 files changed, 283 insertions(+), 180 deletions(-) rename ElementX/Sources/Services/Room/RoomSummary/{RoomSummary.swift => RoomSummaryDetails.swift} (96%) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index ab0c1440b..b826c6f93 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 */ @@ -138,7 +138,6 @@ 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9010EE0CC913D095887EF36E /* OIDCService.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; - 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 5B2C4C17888FC095ED6880B2 /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */; }; 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; @@ -255,6 +254,7 @@ A663FE6704CB500EBE782AE1 /* AnalyticsPromptCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; + A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; }; AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; }; @@ -267,6 +267,7 @@ B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; }; B09514A0A3EB3C19A4FD0B71 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBDE671A613B3EB70794C4 /* SoftLogoutScreen.swift */; }; B245583C63F8F90357B87FAE /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 04C28663564E008DB32B5972 /* Introspect */; }; + B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; @@ -276,6 +277,7 @@ B6F92EBE04D4AABF30B9E73A /* AnalyticsPromptModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8BA82CF99D843FEF680E91 /* AnalyticsPromptModels.swift */; }; B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; + BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */; }; BB4C6F362F75933DDDE30F3E /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */; }; @@ -310,6 +312,7 @@ D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; }; D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; }; + D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; @@ -333,8 +336,6 @@ EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; }; EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; }; EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; }; - ECA5A34628DC837E0024C8BE /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA5A34528DC837E0024C8BE /* AvatarSize.swift */; }; - ECA5A34828DC959F0024C8BE /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA5A34728DC959F0024C8BE /* ImageCache.swift */; }; EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; @@ -348,7 +349,6 @@ F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; - F764BE976EAB76D63E7C1678 /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8023C7413A426FBF0A52B684 /* RoundedCornerShape.swift */; }; F99FB21EFC6D99D247FE7CBE /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D82E84F90358CC1118E6034B /* Introspect */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF05352F28D4E7336228E9F4 /* ActivityIndicatorView.swift */; }; @@ -421,6 +421,7 @@ 124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = ""; }; 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = ""; }; 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModel.swift; sourceTree = ""; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; @@ -451,7 +452,6 @@ 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = ""; }; 289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = ""; }; 28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = ""; }; - 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 = ""; }; 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutCoordinator.swift; sourceTree = ""; }; @@ -523,6 +523,7 @@ 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; 4DF56C3239EA3C16951E1E66 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; + 4E2245243369B99216C7D84E /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTableViewAdapter.swift; sourceTree = ""; }; 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxyProtocol.swift; sourceTree = ""; }; 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; @@ -572,6 +573,7 @@ 68232D336E2B546AD95B78B5 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = ""; }; 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; + 695AD3FAEAFEE32A6C73E8CB /* element-x-ios copy */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios copy"; path = .; sourceTree = SOURCE_ROOT; }; 6A1AAC8EB2992918D01874AC /* rue */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = rue; path = rue.lproj/Localizable.strings; sourceTree = ""; }; 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProperties+Element.swift"; sourceTree = ""; }; 6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = ""; }; @@ -606,12 +608,12 @@ 7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = ""; }; 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = ""; }; 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = ""; }; - 8023C7413A426FBF0A52B684 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = ""; }; 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = ""; }; 81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8210612D17A39369480FC183 /* MediaSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSource.swift; sourceTree = ""; }; + 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; 84E92FF38EBC12EC2452C79C /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = ""; }; 850064FF8D7DB9C875E7AA1A /* SplashScreenPageIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageIndicator.swift; sourceTree = ""; }; 858F8D0B0D51CC41BAA18E24 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; @@ -631,7 +633,7 @@ 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = ""; }; 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 90733775209F4D4D366A268F /* RootRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; @@ -763,7 +765,6 @@ 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; }; D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; @@ -784,6 +785,7 @@ 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 = ""; }; + E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; @@ -797,8 +799,6 @@ E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; - ECA5A34528DC837E0024C8BE /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; - ECA5A34728DC959F0024C8BE /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; 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 = ""; }; @@ -1013,7 +1013,7 @@ isa = PBXGroup; children = ( 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, - 8023C7413A426FBF0A52B684 /* RoundedCornerShape.swift */, + 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, ); path = Views; sourceTree = ""; @@ -1097,11 +1097,11 @@ B6E89E530A8E92EC44301CA1 /* Bundle.swift */, A9FAFE1C2149E6AC8156ED2B /* Collection.swift */, E26747B3154A5DBC3A7E24A5 /* Image.swift */, + 4E2245243369B99216C7D84E /* ImageCache.swift */, 40B21E611DADDEF00307E7AC /* String.swift */, A40C19719687984FD9478FBE /* Task.swift */, 287FC98AF2664EAD79C0D902 /* UIDevice.swift */, 227AC5D71A4CE43512062243 /* URL.swift */, - ECA5A34728DC959F0024C8BE /* ImageCache.swift */, ); path = Extensions; sourceTree = ""; @@ -1284,7 +1284,7 @@ isa = PBXGroup; children = ( 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */, - 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */, + 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */, CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */, 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */, ); @@ -1474,7 +1474,7 @@ 9413F680ECDFB2B0DDB0DEF2 /* Packages */ = { isa = PBXGroup; children = ( - D31DC8105C6233E5FFD9B84C /* element-x-ios */, + 695AD3FAEAFEE32A6C73E8CB /* element-x-ios copy */, ); name = Packages; sourceTree = SOURCE_ROOT; @@ -1678,6 +1678,7 @@ C0937E3B06A8F0E2DB7C8241 /* Other */ = { isa = PBXGroup; children = ( + E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */, 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */, E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, 95CC95CD75B688E946438165 /* Coordinator.swift */, @@ -1687,7 +1688,6 @@ 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, BB3073CCD77D906B330BC1D6 /* Tests.swift */, - ECA5A34528DC837E0024C8BE /* AvatarSize.swift */, 44BBB96FAA2F0D53C507396B /* Extensions */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, 06501F0E978B2D5C92771DC7 /* Logging */, @@ -2332,6 +2332,7 @@ B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */, 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */, 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */, + D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, @@ -2373,6 +2374,7 @@ 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, 03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */, 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */, + BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */, DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */, @@ -2425,7 +2427,6 @@ DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, - ECA5A34828DC959F0024C8BE /* ImageCache.swift in Sources */, 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */, 00EA14F62DCEF62CDE4808D6 /* RedactedRoomTimelineItem.swift in Sources */, 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */, @@ -2440,7 +2441,7 @@ 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */, 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */, 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, - 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */, + A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */, 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */, AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */, 78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */, @@ -2456,7 +2457,7 @@ CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */, 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */, 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */, - F764BE976EAB76D63E7C1678 /* RoundedCornerShape.swift in Sources */, + B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */, 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */, CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */, 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */, @@ -2534,7 +2535,6 @@ 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */, C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */, 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */, - ECA5A34628DC837E0024C8BE /* AvatarSize.swift in Sources */, 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */, 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */, 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 844012e8c..820670c06 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -334,7 +334,8 @@ class AppCoordinator: Coordinator { default: break } - }.store(in: &cancellables) + } + .store(in: &cancellables) } private func deobserveUserSessionChanges() { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index dd4dc9617..00d45459e 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -38,6 +38,11 @@ enum HomeScreenViewAction { case skipSessionVerification } +enum HomeScreenRoomListMode { + case skeletons + case rooms +} + struct HomeScreenViewState: BindableState { var userID: String var userDisplayName: String? @@ -47,11 +52,13 @@ struct HomeScreenViewState: BindableState { var rooms: [HomeScreenRoom] = [] - var isLoadingRooms: Bool { - rooms.isEmpty - } + var roomListMode: HomeScreenRoomListMode = .rooms var visibleRooms: [HomeScreenRoom] { + if roomListMode == .skeletons { + return placeholderRooms + } + if bindings.searchQuery.isEmpty { return rooms } @@ -60,6 +67,12 @@ struct HomeScreenViewState: BindableState { } var bindings = HomeScreenViewStateBindings() + + var placeholderRooms: [HomeScreenRoom] { + (1...10).map { index in + HomeScreenRoom.placeholder(id: "\(index)") + } + } } struct HomeScreenViewStateBindings { @@ -78,12 +91,16 @@ struct HomeScreenRoom: Identifiable, Equatable { var lastMessage: AttributedString? var avatar: UIImage? -} - -extension MutableCollection where Element == HomeScreenRoom { - mutating func updateEach(_ update: (inout Element) -> Void) { - for index in indices { - update(&self[index]) - } + + var isPlaceholder = false + + static func placeholder(id: String) -> HomeScreenRoom { + HomeScreenRoom(id: id, + name: "Placeholder room name", + hasUnreads: false, + timestamp: "Now", + lastMessage: AttributedString("Last message"), + avatar: UIImage(systemName: "photo"), + isPlaceholder: true) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 0bb5c8370..63634759e 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -28,6 +28,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // MARK: - Setup + // swiftlint:disable:next function_body_length init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) { self.userSession = userSession roomSummaryProvider = userSession.clientProxy.roomSummaryProvider @@ -46,18 +47,36 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol default: break } - }.store(in: &cancellables) + } + .store(in: &cancellables) - roomSummaryProvider.callbacks + Publishers.CombineLatest3(roomSummaryProvider.statePublisher, + roomSummaryProvider.countPublisher, + roomSummaryProvider.roomListPublisher) .receive(on: DispatchQueue.main) - .sink { [weak self] callback in - switch callback { - case .updatedRoomSummaries: - Task { - await self?.updateRooms() + .sink { [weak self] state, totalCount, rooms in + if state != .live { + if totalCount == 0 || rooms.count != totalCount { + self?.state.roomListMode = .skeletons + } else { + self?.state.roomListMode = .rooms } + } else if totalCount == 0 { + #warning("Empty state but it never happens because SS never goes into live for empty accounts") + } else { + self?.state.roomListMode = .rooms } - }.store(in: &cancellables) + } + .store(in: &cancellables) + + roomSummaryProvider.roomListPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + Task { + await self?.updateRooms() + } + } + .store(in: &cancellables) Task { if case let .success(userAvatarURLString) = await userSession.clientProxy.loadUserAvatarURLString() { @@ -65,15 +84,17 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.userAvatar = avatar } } - - await updateRooms() } - + Task { if case let .success(userDisplayName) = await userSession.clientProxy.loadUserDisplayName() { state.userDisplayName = userDisplayName } } + + Task { + await updateRooms() + } } // MARK: - Public @@ -96,23 +117,22 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // MARK: - Private private func loadDataForRoomIdentifier(_ identifier: String) { - guard let summary = roomSummaryProvider.roomSummaries.first(where: { $0.id == identifier }), - var room = state.rooms.first(where: { $0.id == identifier }) else { + guard let summary = roomSummaryProvider.roomListPublisher.value.first(where: { $0.asFilled?.id == identifier })?.asFilled, + let homeRoomIndex = state.rooms.firstIndex(where: { $0.id == identifier }) else { return } - if room.avatar != nil { + var details = state.rooms[homeRoomIndex] + + guard details.avatar == nil, + let avatarURLString = summary.avatarURLString else { return } - if let avatarURLString = summary.avatarURLString { - Task { - if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) { - if let index = state.rooms.firstIndex(of: room) { - room.avatar = image - state.rooms[index] = room - } - } + Task { + if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) { + details.avatar = image + state.rooms[homeRoomIndex] = details } } } @@ -121,20 +141,25 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.rooms = await Task.detached { var rooms = [HomeScreenRoom]() - for summary in self.roomSummaryProvider.roomSummaries { - let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home)) - - var timestamp: String? - if let lastMessageTimestamp = summary.lastMessageTimestamp { - timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened) + for summary in self.roomSummaryProvider.roomListPublisher.value { + switch summary { + case .empty(let id): + rooms.append(HomeScreenRoom.placeholder(id: id)) + case .filled(let summary): + let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home)) + + var timestamp: String? + if let lastMessageTimestamp = summary.lastMessageTimestamp { + timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened) + } + + rooms.append(HomeScreenRoom(id: summary.id, + name: summary.name, + hasUnreads: summary.unreadNotificationCount > 0, + timestamp: timestamp, + lastMessage: summary.lastMessage, + avatar: avatarImage)) } - - rooms.append(HomeScreenRoom(id: summary.id, - name: summary.name, - hasUnreads: summary.unreadNotificationCount > 0, - timestamp: timestamp, - lastMessage: summary.lastMessage, - avatar: avatarImage)) } return rooms diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 3062d586d..94dd965a7 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -23,32 +23,28 @@ struct HomeScreen: View { // MARK: Views var body: some View { - VStack(spacing: 0.0) { - if context.viewState.isLoadingRooms { - VStack { - Text(ElementL10n.loading) - ProgressView() - } - } else { - ScrollView { - if context.viewState.showSessionVerificationBanner { - sessionVerificationBanner - } - - LazyVStack { - ForEach(context.viewState.visibleRooms) { room in - HomeScreenRoomCell(room: room, context: context) - } - } - .searchable(text: $context.searchQuery) - .animation(.default, value: context.viewState.visibleRooms) - .padding(.horizontal) - } + ScrollView { + if context.viewState.showSessionVerificationBanner { + sessionVerificationBanner } - Spacer() + LazyVStack { + ForEach(context.viewState.visibleRooms) { room in + if room.isPlaceholder { + HomeScreenRoomCell(room: room, context: context) + .redacted(reason: .placeholder) + .disabled(true) + } else { + HomeScreenRoomCell(room: room, context: context) + } + } + } + .animation(.default, value: context.viewState.visibleRooms) + .padding(.horizontal) + .searchable(text: $context.searchQuery) } - .transition(.slide) + .disabled(context.viewState.roomListMode == .skeletons) + .animation(.elementDefault, value: context.viewState.roomListMode) .animation(.elementDefault, value: context.viewState.showSessionVerificationBanner) .ignoresSafeArea(.all, edges: .bottom) .navigationTitle(ElementL10n.allChats) @@ -169,14 +165,19 @@ struct HomeScreen: View { struct HomeScreen_Previews: PreviewProvider { static var previews: some View { - body.preferredColorScheme(.light) + body(.loading).preferredColorScheme(.light) .tint(.element.accent) - body.preferredColorScheme(.dark) + body(.loading).preferredColorScheme(.dark) + .tint(.element.accent) + body(.loaded).preferredColorScheme(.light) + .tint(.element.accent) + body(.loaded).preferredColorScheme(.dark) .tint(.element.accent) } - static var body: some View { - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe"), + static func body(_ state: MockRoomSummaryProviderState) -> some View { + let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe", + roomSummaryProvider: MockRoomSummaryProvider(state: state)), mediaProvider: MockMediaProvider()) let viewModel = HomeScreenViewModel(userSession: userSession, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index 09440d84b..64c014fbb 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -108,14 +108,27 @@ struct HomeScreenRoomCell_Previews: PreviewProvider { } static var body: some View { - let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe"), + let summaryProvider = MockRoomSummaryProvider(state: .loaded) + + let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "John Doe", roomSummaryProvider: summaryProvider), mediaProvider: MockMediaProvider()) let viewModel = HomeScreenViewModel(userSession: userSession, attributedStringBuilder: AttributedStringBuilder()) + let rooms: [HomeScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary in + guard let summary = summary.asFilled else { + return nil + } + + return HomeScreenRoom(id: summary.id, + name: summary.name, + hasUnreads: summary.unreadNotificationCount > 0, + timestamp: Date.now.formatted(date: .omitted, time: .shortened)) + } + return VStack { - ForEach(viewModel.context.viewState.rooms) { room in + ForEach(rooms) { room in HomeScreenRoomCell(room: room, context: viewModel.context) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index cc2e12743..2d45945f3 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -58,7 +58,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem) } - }.store(in: &cancellables) + } + .store(in: &cancellables) state.contextMenuBuilder = buildContexMenuForItemId(_:) diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift index e1b5313c0..89d3636c1 100644 --- a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift @@ -63,7 +63,8 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer case .failed: self.stateMachine.processEvent(.didFail) } - }.store(in: &cancellables) + } + .store(in: &cancellables) } // MARK: - Public diff --git a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift index 1ef8508f3..5bbba4d52 100644 --- a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift @@ -242,13 +242,13 @@ struct Settings_Previews: PreviewProvider { body.preferredColorScheme(.light) body.preferredColorScheme(.dark) } - + @ViewBuilder static var body: some View { let userSession = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@userid:example.com"), mediaProvider: MockMediaProvider()) let viewModel = SettingsViewModel(withUserSession: userSession) - + NavigationView { SettingsScreen(context: viewModel.context) .previewInterfaceOrientation(.portrait) diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index be7b2879b..41a8b6d56 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -26,7 +26,7 @@ struct MockClientProxy: ClientProxyProtocol { let homeserver = "" let restoreToken: String? = nil - let roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider() + var roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider() func startSync() { } diff --git a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift index 2bef449b5..a991590ad 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummaryProvider.swift @@ -17,33 +17,58 @@ import Combine import Foundation +enum MockRoomSummaryProviderState { + case loading + case loaded +} + class MockRoomSummaryProvider: RoomSummaryProviderProtocol { - var callbacks = PassthroughSubject() - var stateUpdatePublisher = CurrentValueSubject(.cold) - var countUpdatePublisher = CurrentValueSubject(0) - - var roomSummaries: [RoomSummary] = [ - RoomSummary(id: "1", name: "First room", - isDirect: true, - avatarURLString: nil, - lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."), - lastMessageTimestamp: .now, - unreadNotificationCount: 4), - RoomSummary(id: "2", - name: "Second room", - isDirect: true, - avatarURLString: nil, - lastMessage: nil, - lastMessageTimestamp: nil, - unreadNotificationCount: 1), - RoomSummary(id: "3", - name: "Third room", - isDirect: true, - avatarURLString: nil, - lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"), - lastMessageTimestamp: .now, - unreadNotificationCount: 0) - ] + let roomListPublisher: CurrentValueSubject<[RoomSummary], Never> + let statePublisher: CurrentValueSubject + let countPublisher: CurrentValueSubject func updateRoomsWithIdentifiers(_ identifiers: [String]) { } + + convenience init() { + self.init(state: .loading) + } + + init(state: MockRoomSummaryProviderState) { + switch state { + case .loading: + roomListPublisher = .init([]) + statePublisher = .init(.cold) + countPublisher = .init(0) + case .loaded: + roomListPublisher = .init(Self.rooms) + statePublisher = .init(.live) + countPublisher = .init(UInt(Self.rooms.count)) + } + } + + // MARK: - Private + + static let rooms: [RoomSummary] = [ + .filled(details: RoomSummaryDetails(id: "1", name: "First room", + isDirect: true, + avatarURLString: nil, + lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."), + lastMessageTimestamp: .now, + unreadNotificationCount: 4)), + .filled(details: RoomSummaryDetails(id: "2", + name: "Second room", + isDirect: true, + avatarURLString: "mockImageURLString", + lastMessage: nil, + lastMessageTimestamp: nil, + unreadNotificationCount: 1)), + .filled(details: RoomSummaryDetails(id: "3", + name: "Third room", + isDirect: true, + avatarURLString: nil, + lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"), + lastMessageTimestamp: .now, + unreadNotificationCount: 0)), + .empty(id: "3") + ] } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift similarity index 96% rename from ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift rename to ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift index e7a8411ec..35fc248bc 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryDetails.swift @@ -16,7 +16,7 @@ import Foundation -struct RoomSummary { +struct RoomSummaryDetails { let id: String let name: String let isDirect: Bool diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index e0bf51b65..4d55043f0 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -60,22 +60,24 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private var listUpdateObserverToken: StoppableSpawn? private var stateUpdateObserverToken: StoppableSpawn? + private var countUpdateObserverToken: StoppableSpawn? private var cancellables = Set() - - let callbacks = PassthroughSubject() - let stateUpdatePublisher = CurrentValueSubject(.cold) - let countUpdatePublisher = CurrentValueSubject(0) - private(set) var roomSummaries: [RoomSummary] = [] { + let roomListPublisher = CurrentValueSubject<[RoomSummary], Never>([]) + let statePublisher = CurrentValueSubject(.cold) + let countPublisher = CurrentValueSubject(0) + + private var rooms: [RoomSummary] = [] { didSet { - callbacks.send(.updatedRoomSummaries) + roomListPublisher.send(rooms) } } deinit { listUpdateObserverToken?.cancel() stateUpdateObserverToken?.cancel() + countUpdateObserverToken?.cancel() } init(slidingSyncController: SlidingSyncProtocol, slidingSyncView: SlidingSyncViewProtocol, roomMessageFactory: RoomMessageFactoryProtocol) { @@ -87,39 +89,40 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { weakProvider.stateUpdatePublisher .map(RoomSummaryProviderState.init) - .subscribe(stateUpdatePublisher) + .subscribe(statePublisher) .store(in: &cancellables) weakProvider.countUpdatePublisher - .subscribe(countUpdatePublisher) + .subscribe(countPublisher) .store(in: &cancellables) weakProvider.roomListDiffPublisher .collect(.byTime(DispatchQueue.global(qos: .background), 0.5)) .sink { self.updateRoomsWithDiffs($0) } .store(in: &cancellables) - + listUpdateObserverToken = slidingSyncView.observeRoomList(observer: weakProvider) stateUpdateObserverToken = slidingSyncView.observeState(observer: weakProvider) + countUpdateObserverToken = slidingSyncView.observeRoomsCount(observer: weakProvider) } func updateRoomsWithIdentifiers(_ identifiers: [String]) { Task.detached { - guard self.stateUpdatePublisher.value == .live else { + guard self.statePublisher.value == .live else { return } var changes = [CollectionDifference.Change]() for identifier in identifiers { - guard let oldSummary = self.roomSummaries.first(where: { $0.id == identifier }), - let index = self.roomSummaries.firstIndex(where: { $0.id == identifier }) else { + guard let index = self.rooms.firstIndex(where: { $0.id == identifier }) else { continue } - let newSummary = self.buildRoomSummaryForIdentifier(identifier) + let oldRoom = self.rooms[index] + let newRoom = self.buildRoomSummaryForIdentifier(identifier) - changes.append(.remove(offset: index, element: oldSummary, associatedWith: nil)) - changes.append(.insert(offset: index, element: newSummary, associatedWith: nil)) + changes.append(.remove(offset: index, element: oldRoom, associatedWith: nil)) + changes.append(.insert(offset: index, element: newRoom, associatedWith: nil)) } guard let diff = CollectionDifference(changes) else { @@ -127,31 +130,25 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { return } - guard let newSummaries = self.roomSummaries.applying(diff) else { + guard let newSummaries = self.rooms.applying(diff) else { MXLog.error("Failed applying diff: \(diff)") return } - self.roomSummaries = newSummaries + self.rooms = newSummaries } } // MARK: - Private fileprivate func updateRoomsWithDiffs(_ diffs: [SlidingSyncViewRoomsListDiff]) { - roomSummaries = diffs + rooms = diffs .compactMap(buildDiff) - .reduce(roomSummaries) { $0.applying($1) ?? $0 } + .reduce(rooms) { $0.applying($1) ?? $0 } } private func buildEmptyRoomSummary(forIdentifier identifier: String = UUID().uuidString) -> RoomSummary { - RoomSummary(id: identifier, - name: "", - isDirect: false, - avatarURLString: nil, - lastMessage: nil, - lastMessageTimestamp: nil, - unreadNotificationCount: 0) + .empty(id: identifier) } private func buildRoomSummaryForIdentifier(_ identifier: String) -> RoomSummary { @@ -170,14 +167,14 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { } lastMessageTimestamp = lastMessage.originServerTs } - - return RoomSummary(id: room.roomId(), - name: room.name() ?? room.roomId(), - isDirect: room.isDm() ?? false, - avatarURLString: room.fullRoom()?.avatarUrl(), - lastMessage: attributedLastMessage, - lastMessageTimestamp: lastMessageTimestamp, - unreadNotificationCount: UInt(room.unreadNotifications().notificationCount())) + + return .filled(details: RoomSummaryDetails(id: room.roomId(), + name: room.name() ?? room.roomId(), + isDirect: room.isDm() ?? false, + avatarURLString: room.fullRoom()?.avatarUrl(), + lastMessage: attributedLastMessage, + lastMessageTimestamp: lastMessageTimestamp, + unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()))) } private func buildSummaryForRoomListEntry(_ entry: RoomListEntry) -> RoomSummary { @@ -202,7 +199,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { switch diff { case .push(value: let value): let summary = buildSummaryForRoomListEntry(value) - changes.append(.insert(offset: Int(roomSummaries.count), element: summary, associatedWith: nil)) + changes.append(.insert(offset: Int(rooms.count), element: summary, associatedWith: nil)) case .updateAt(let index, let value): let summary = buildSummaryForRoomListEntry(value) changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil)) @@ -211,14 +208,14 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { let summary = buildSummaryForRoomListEntry(value) changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil)) case .move(let oldIndex, let newIndex): - let summary = roomSummaries[Int(oldIndex)] + let summary = rooms[Int(oldIndex)] changes.append(.remove(offset: Int(oldIndex), element: summary, associatedWith: nil)) changes.append(.insert(offset: Int(newIndex), element: summary, associatedWith: nil)) case .removeAt(let index): - let summary = roomSummaries[Int(index)] + let summary = rooms[Int(index)] changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil)) case .replace(let values): - for (index, summary) in roomSummaries.enumerated() { + for (index, summary) in rooms.enumerated() { changes.append(.remove(offset: index, element: summary, associatedWith: nil)) } @@ -253,12 +250,14 @@ extension SlidingSyncViewRoomsListDiff { extension RoomSummaryProviderState { init(slidingSyncState: SlidingSyncState) { switch slidingSyncState { + case .cold: + self = .cold + case .preload: + self = .preload case .catchingUp: - self = .live + self = .catchingUp case .live: self = .live - default: - self = .cold } } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift index 7f531ae99..715aadba6 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift @@ -17,26 +17,44 @@ import Combine import Foundation -enum RoomSummaryProviderCallback { - case updatedRoomSummaries -} - enum RoomSummaryProviderState { case cold + case preload + case catchingUp case live } +enum RoomSummary: Identifiable { + case empty(id: String) + case filled(details: RoomSummaryDetails) + + var id: String { + switch self { + case .empty(let id): + return id + case .filled(let summary): + return summary.id + } + } + + var asFilled: RoomSummaryDetails? { + guard case let .filled(details) = self else { + return nil + } + + return details + } +} + protocol RoomSummaryProviderProtocol { - var callbacks: PassthroughSubject { get } + /// Publishes the currently available room summaries + var roomListPublisher: CurrentValueSubject<[RoomSummary], Never> { get } /// Publishes the current state the summary provider is finding itself in - var stateUpdatePublisher: CurrentValueSubject { get } + var statePublisher: CurrentValueSubject { get } /// Publishes the total number of rooms - var countUpdatePublisher: CurrentValueSubject { get } - - /// The current list of rooms currently provided by the sliding sync view - var roomSummaries: [RoomSummary] { get } + var countPublisher: CurrentValueSubject { get } /// Invoked by the sliding sync controller whenever certain rooms have updated /// without necessarily changing their position in the list diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index cd104489f..033298408 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -73,7 +73,8 @@ class UserSession: UserSessionProtocol { default: break } - }.store(in: &cancellables) + } + .store(in: &cancellables) case .failure(let error): MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.") diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index c10947583..dbd728dfb 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -60,7 +60,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol { case .updatedMessages: self.updateTimelineItems() } - }.store(in: &cancellables) + } + .store(in: &cancellables) updateTimelineItems()