Refactor the NSDiffableDataSourceLayer (#1235)

* first part, using references

* better animations

* delete view provider and replaced wth two files

* ordered dictionary usage

* bubbled styler view previews

* plain style is back

* fix

* read marker previews restored

* updated tests

* code improvements

* better naming
This commit is contained in:
Mauro
2023-07-04 16:08:43 +02:00
committed by GitHub
parent eba3e5d5e6
commit b294278170
15 changed files with 376 additions and 316 deletions
+15 -11
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 51; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -464,6 +464,7 @@
A680F54935A6ADEA4ED6C38F /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */; }; A680F54935A6ADEA4ED6C38F /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */; };
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; }; A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; };
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
A72F51AB2A54364E0038A02F /* RoomTimelineItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A72F51AA2A54364E0038A02F /* RoomTimelineItemViewModel.swift */; };
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.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 */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
@@ -587,7 +588,7 @@
CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; };
CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */; }; CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */; };
CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */; }; CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */; };
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineItemView.swift */; };
D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; };
D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; };
D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; }; D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; };
@@ -824,7 +825,7 @@
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; }; 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; };
12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = "<group>"; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = "<group>"; };
12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = "<group>"; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = "<group>"; }; 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = "<group>"; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = "<group>"; };
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
@@ -959,7 +960,7 @@
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; }; 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = "<group>"; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = "<group>"; };
47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = "<group>"; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = "<group>"; };
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; };
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; };
@@ -1126,7 +1127,7 @@
8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = "<group>"; }; 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = "<group>"; };
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = "<group>"; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = "<group>"; };
8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; };
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
@@ -1189,6 +1190,7 @@
A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = "<group>"; }; A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = "<group>"; };
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
A72F51AA2A54364E0038A02F /* RoomTimelineItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewModel.swift; sourceTree = "<group>"; };
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -1205,7 +1207,7 @@
ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; }; ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = "<group>"; }; AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = "<group>"; };
AC3F82523D6F48B926D6AF68 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; }; AC3F82523D6F48B926D6AF68 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = "<group>"; }; ACB6C5E4950B6C9842F35A38 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = "<group>"; };
ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenUITests.swift; sourceTree = "<group>"; }; ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenUITests.swift; sourceTree = "<group>"; };
AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenUITests.swift; sourceTree = "<group>"; }; ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenUITests.swift; sourceTree = "<group>"; };
@@ -1232,7 +1234,7 @@
B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = "<group>"; }; B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = "<group>"; };
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; }; B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = "<group>"; };
B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; };
B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = "<group>"; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = "<group>"; };
B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
@@ -1309,7 +1311,7 @@
CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; }; CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; }; CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = "<group>"; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = "<group>"; };
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = "<group>"; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = "<group>"; };
D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
@@ -1379,7 +1381,7 @@
ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = "<group>"; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = "<group>"; };
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; }; ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = "<group>"; };
ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = "<group>"; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = "<group>"; };
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
@@ -2778,8 +2780,9 @@
8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */, 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */,
105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */, 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */,
7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */, 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */,
A72F51AA2A54364E0038A02F /* RoomTimelineItemViewModel.swift */,
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */, ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */,
ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */, ACB6C5E4950B6C9842F35A38 /* RoomTimelineItemView.swift */,
75D1D02F7F3AC1122FCFB4F3 /* Items */, 75D1D02F7F3AC1122FCFB4F3 /* Items */,
); );
path = TimelineItems; path = TimelineItems;
@@ -4261,6 +4264,7 @@
0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */, 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */,
9408CE8B8865C0C8DD4C9869 /* NoticeRoomTimelineItemContent.swift in Sources */, 9408CE8B8865C0C8DD4C9869 /* NoticeRoomTimelineItemContent.swift in Sources */,
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */,
A72F51AB2A54364E0038A02F /* RoomTimelineItemViewModel.swift in Sources */,
3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */, 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */,
CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */, CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */,
652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */, 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */,
@@ -4347,7 +4351,7 @@
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */, 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */,
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */, 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */,
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */, 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */, CF82143AA4A4F7BD11D22946 /* RoomTimelineItemView.swift in Sources */,
B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */, B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */,
50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */, 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */,
D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */, D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */,
@@ -18,6 +18,8 @@ import Combine
import SwiftUI import SwiftUI
import UIKit import UIKit
import OrderedCollections
enum RoomScreenViewModelAction { enum RoomScreenViewModelAction {
case displayRoomDetails case displayRoomDetails
case displayEmojiPicker(itemID: String) case displayEmojiPicker(itemID: String)
@@ -84,7 +86,7 @@ struct RoomScreenViewState: BindableState {
var roomId: String var roomId: String
var roomTitle = "" var roomTitle = ""
var roomAvatarURL: URL? var roomAvatarURL: URL?
var items: [RoomTimelineViewProvider] = [] var itemsDictionary = OrderedDictionary<String, RoomTimelineItemViewModel>()
var members: [String: RoomMemberState] = [:] var members: [String: RoomMemberState] = [:]
var canBackPaginate = true var canBackPaginate = true
var isBackPaginating = false var isBackPaginating = false
@@ -102,6 +104,14 @@ struct RoomScreenViewState: BindableState {
var sendButtonDisabled: Bool { var sendButtonDisabled: Bool {
bindings.composerText.count == 0 bindings.composerText.count == 0
} }
var itemIDs: [String] {
itemsDictionary.keys.elements
}
var itemViewModels: [RoomTimelineItemViewModel] {
itemsDictionary.values.elements
}
let scrollToBottomPublisher = PassthroughSubject<Void, Never>() let scrollToBottomPublisher = PassthroughSubject<Void, Never>()
} }
@@ -16,6 +16,7 @@
import Algorithms import Algorithms
import Combine import Combine
import OrderedCollections
import SwiftUI import SwiftUI
typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, RoomScreenViewAction> typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, RoomScreenViewAction>
@@ -116,7 +117,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .tappedOnUser(userID: let userID): case .tappedOnUser(userID: let userID):
Task { await handleTappedUser(userID: userID) } Task { await handleTappedUser(userID: userID) }
case .displayEmojiPicker(let itemID): case .displayEmojiPicker(let itemID):
guard let item = state.items.first(where: { $0.id == itemID }), item.isReactable else { return } guard let item = state.itemsDictionary[itemID], item.isReactable else { return }
callback?(.displayEmojiPicker(itemID: itemID)) callback?(.displayEmojiPicker(itemID: itemID))
case .reactionSummary(let itemId, let key): case .reactionSummary(let itemId, let key):
showReactionSummary(for: itemId, selectedKey: key) showReactionSummary(for: itemId, selectedKey: key)
@@ -232,8 +233,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
private func buildTimelineViews() { private func buildTimelineViews() {
var timelineViews = [RoomTimelineViewProvider]() var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewModel>()
let itemsGroupedByTimelineDisplayStyle = timelineController.timelineItems.chunked { current, next in let itemsGroupedByTimelineDisplayStyle = timelineController.timelineItems.chunked { current, next in
canGroupItem(timelineItem: current, with: next) canGroupItem(timelineItem: current, with: next)
} }
@@ -246,24 +247,38 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
if itemGroup.count == 1 { if itemGroup.count == 1 {
if let firstItem = itemGroup.first { if let firstItem = itemGroup.first {
timelineViews.append(RoomTimelineViewProvider(timelineItem: firstItem, groupStyle: .single)) timelineItemsDictionary.updateValue(updateViewModel(item: firstItem, groupStyle: .single),
forKey: firstItem.id)
} }
} else { } else {
for (index, item) in itemGroup.enumerated() { for (index, item) in itemGroup.enumerated() {
if index == 0 { if index == 0 {
timelineViews.append(RoomTimelineViewProvider(timelineItem: item, groupStyle: .first)) timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .first),
forKey: item.id)
} else if index == itemGroup.count - 1 { } else if index == itemGroup.count - 1 {
timelineViews.append(RoomTimelineViewProvider(timelineItem: item, groupStyle: .last)) timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .last),
forKey: item.id)
} else { } else {
timelineViews.append(RoomTimelineViewProvider(timelineItem: item, groupStyle: .middle)) timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .middle),
forKey: item.id)
} }
} }
} }
} }
state.items = timelineViews state.itemsDictionary = timelineItemsDictionary
} }
private func updateViewModel(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewModel {
if let timelineItemViewModel = state.itemsDictionary[item.id] {
timelineItemViewModel.groupStyle = groupStyle
timelineItemViewModel.type = .init(item: item)
return timelineItemViewModel
} else {
return RoomTimelineItemViewModel(item: item, groupStyle: groupStyle)
}
}
private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool { private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool {
if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem { if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem {
return false return false
@@ -307,7 +307,7 @@ private extension View {
struct TimelineItemBubbledStylerView_Previews: PreviewProvider { struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock static let viewModel = RoomScreenViewModel.mock
static var previews: some View { static var previews: some View {
mockTimeline mockTimeline
.previewDisplayName("Mock Timeline") .previewDisplayName("Mock Timeline")
@@ -320,40 +320,40 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
replies replies
.previewDisplayName("Replies") .previewDisplayName("Replies")
} }
static var mockTimeline: some View { static var mockTimeline: some View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.state.items) { item in ForEach(viewModel.state.itemViewModels) { itemViewModel in
item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells RoomTimelineItemView(viewModel: itemViewModel)
.padding(TimelineStyle.bubbles.rowInsets)
// Insets added in the table view cells
} }
} }
} }
.environment(\.timelineStyle, .bubbles) .environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
} }
static var replies: some View { static var replies: some View {
VStack { VStack {
RoomTimelineViewProvider.text(TextRoomTimelineItem(id: "", RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "",
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
sender: .init(id: "whoever"), sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."), content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), contentType: .text(.init(body: "Short")))), groupStyle: .single))
.single)
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "",
RoomTimelineViewProvider.text(TextRoomTimelineItem(id: "", timestamp: "10:42",
timestamp: "10:42", isOutgoing: true,
isOutgoing: true, isEditable: false,
isEditable: false, sender: .init(id: "whoever"),
sender: .init(id: "whoever"), content: .init(body: "Short message"),
content: .init(body: "Short message"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), contentType: .text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))), groupStyle: .single))
contentType: .text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))),
.single)
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
} }
@@ -135,12 +135,12 @@ struct TimelineItemPlainStylerView<Content: View>: View {
struct TimelineItemPlainStylerView_Previews: PreviewProvider { struct TimelineItemPlainStylerView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock static let viewModel = RoomScreenViewModel.mock
static var previews: some View { static var previews: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
let item = MockRoomTimelineController().timelineItems[index] let item = MockRoomTimelineController().timelineItems[index]
RoomTimelineViewProvider(timelineItem: item, groupStyle: .single) RoomTimelineItemView(viewModel: .init(item: item, groupStyle: .single))
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells .padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
} }
} }
@@ -53,7 +53,7 @@ struct TimelineItemStyler_Previews: PreviewProvider {
}() }()
static let sendingLast: TextRoomTimelineItem = { static let sendingLast: TextRoomTimelineItem = {
let id = viewModel.state.items.last?.id ?? UUID().uuidString let id = viewModel.state.itemIDs.last ?? UUID().uuidString
var result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) var result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
result.properties.deliveryStatus = .sending result.properties.deliveryStatus = .sending
return result return result
@@ -66,7 +66,7 @@ struct TimelineItemStyler_Previews: PreviewProvider {
}() }()
static let sentLast: TextRoomTimelineItem = { static let sentLast: TextRoomTimelineItem = {
let id = viewModel.state.items.last?.id ?? UUID().uuidString let id = viewModel.state.itemIDs.last ?? UUID().uuidString
let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
return result return result
}() }()
@@ -23,14 +23,10 @@ struct TimelineItemStatusView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context @EnvironmentObject private var context: RoomScreenViewModel.Context
private var isLastOutgoingMessage: Bool { private var isLastOutgoingMessage: Bool {
context.viewState.items.last?.id == timelineItem.id && context.viewState.itemIDs.last == timelineItem.id &&
timelineItem.isOutgoing timelineItem.isOutgoing
} }
private var isLast: Bool {
context.viewState.items.last?.id == timelineItem.id
}
var body: some View { var body: some View {
if !timelineItem.properties.orderedReadReceipts.isEmpty, readReceiptsEnabled { if !timelineItem.properties.orderedReadReceipts.isEmpty, readReceiptsEnabled {
readReceipts readReceipts
@@ -18,20 +18,20 @@ import SwiftUI
struct CollapsibleRoomTimelineView: View { struct CollapsibleRoomTimelineView: View {
private let timelineItem: CollapsibleTimelineItem private let timelineItem: CollapsibleTimelineItem
private let timelineViews: [RoomTimelineViewProvider] private let timelineViewModels: [RoomTimelineItemViewModel]
@State private var isExpanded = false @State private var isExpanded = false
init(timelineItem: CollapsibleTimelineItem) { init(timelineItem: CollapsibleTimelineItem) {
self.timelineItem = timelineItem self.timelineItem = timelineItem
timelineViews = timelineItem.items.map { RoomTimelineViewProvider(timelineItem: $0, groupStyle: .single) } timelineViewModels = timelineItem.items.map { .init(item: $0, groupStyle: .single) }
} }
var body: some View { var body: some View {
DisclosureGroup(L10n.roomTimelineStateChanges(timelineItem.items.count), isExpanded: $isExpanded) { DisclosureGroup(L10n.roomTimelineStateChanges(timelineItem.items.count), isExpanded: $isExpanded) {
Group { Group {
ForEach(timelineViews) { timelineView in ForEach(timelineViewModels) { viewModel in
timelineView.body RoomTimelineItemView(viewModel: viewModel)
} }
}.transition(.opacity.animation(.elementDefault)) }.transition(.opacity.animation(.elementDefault))
} }
@@ -37,27 +37,27 @@ struct ReadMarkerRoomTimelineView: View {
struct ReadMarkerRoomTimelineView_Previews: PreviewProvider { struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock static let viewModel = RoomScreenViewModel.mock
static let item = ReadMarkerRoomTimelineItem() static let item = ReadMarkerRoomTimelineItem()
static var previews: some View { static var previews: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
RoomTimelineViewProvider.separator(.init(id: "Separator", text: "Today"), .single) RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single))
RoomTimelineViewProvider.text(.init(id: "", RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "",
timestamp: "", timestamp: "",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
sender: .init(id: "1", displayName: "Bob"), sender: .init(id: "1", displayName: "Bob"),
content: .init(body: "This is another message")), .single) content: .init(body: "This is another message"))), groupStyle: .single))
ReadMarkerRoomTimelineView(timelineItem: item) ReadMarkerRoomTimelineView(timelineItem: item)
RoomTimelineViewProvider.separator(.init(id: "Separator", text: "Today"), .single) RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single))
RoomTimelineViewProvider.text(.init(id: "", RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "",
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "", displayName: "Alice"), sender: .init(id: "", displayName: "Alice"),
content: .init(body: "This is a message")), .single) content: .init(body: "This is a message"))), groupStyle: .single))
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
@@ -17,12 +17,14 @@
import Combine import Combine
import SwiftUI import SwiftUI
import OrderedCollections
/// A table view cell that displays a timeline item in a room. The cell is intended /// A table view cell that displays a timeline item in a room. The cell is intended
/// to be configured to display a SwiftUI view and not use any UIKit. /// to be configured to display a SwiftUI view and not use any UIKit.
class TimelineItemCell: UITableViewCell { class TimelineItemCell: UITableViewCell {
static let reuseIdentifier = "TimelineItemCell" static let reuseIdentifier = "TimelineItemCell"
var item: RoomTimelineViewProvider? var item: RoomTimelineItemViewModel?
override func prepareForReuse() { override func prepareForReuse() {
item = nil item = nil
@@ -38,7 +40,7 @@ class TimelineTableViewController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .plain) private let tableView = UITableView(frame: .zero, style: .plain)
var timelineStyle: TimelineStyle var timelineStyle: TimelineStyle
var timelineItems: [RoomTimelineViewProvider] = [] { var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewModel>() {
didSet { didSet {
guard !scrollAdapter.isScrolling.value else { guard !scrollAdapter.isScrolling.value else {
// Delay updating until scrolling has stopped as programatic // Delay updating until scrolling has stopped as programatic
@@ -46,15 +48,15 @@ class TimelineTableViewController: UIViewController {
hasPendingUpdates = true hasPendingUpdates = true
return return
} }
applySnapshot() applySnapshot()
if timelineItems.isEmpty { if timelineItemsDictionary.isEmpty {
paginateBackwardsPublisher.send() paginateBackwardsPublisher.send()
} }
} }
} }
/// The mode of the message composer. This is used to render selected /// The mode of the message composer. This is used to render selected
/// items in the timeline when replying, editing etc. /// items in the timeline when replying, editing etc.
var composerMode: RoomScreenComposerMode = .default var composerMode: RoomScreenComposerMode = .default
@@ -73,9 +75,13 @@ class TimelineTableViewController: UIViewController {
var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)? var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)?
@Binding private var scrollToBottomButtonVisible: Bool @Binding private var scrollToBottomButtonVisible: Bool
private var timelineItemsIDs: [String] {
timelineItemsDictionary.keys.elements
}
/// The table's diffable data source. /// The table's diffable data source.
private var dataSource: UITableViewDiffableDataSource<TimelineSection, RoomTimelineViewProvider>? private var dataSource: UITableViewDiffableDataSource<TimelineSection, String>?
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
/// The scroll view adapter used to detect whether scrolling is in progress. /// The scroll view adapter used to detect whether scrolling is in progress.
@@ -183,24 +189,28 @@ class TimelineTableViewController: UIViewController {
/// Configures a diffable data source for the timeline's table view. /// Configures a diffable data source for the timeline's table view.
private func configureDataSource() { private func configureDataSource() {
dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, timelineItem in dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, id in
let cell = tableView.dequeueReusableCell(withIdentifier: TimelineItemCell.reuseIdentifier, for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: TimelineItemCell.reuseIdentifier, for: indexPath)
guard let self, let cell = cell as? TimelineItemCell else { return cell } guard let self, let cell = cell as? TimelineItemCell else { return cell }
// A local reference to avoid capturing self in the cell configuration. // A local reference to avoid capturing self in the cell configuration.
let coordinator = self.coordinator let coordinator = self.coordinator
cell.item = timelineItem let viewModel = timelineItemsDictionary[id]
cell.item = viewModel
guard let viewModel else {
return cell
}
cell.contentConfiguration = UIHostingConfiguration { cell.contentConfiguration = UIHostingConfiguration {
timelineItem RoomTimelineItemView(viewModel: viewModel)
.id(timelineItem.id) .id(id)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu .environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu
.onAppear { .onAppear {
coordinator.send(viewAction: .itemAppeared(id: timelineItem.id)) coordinator.send(viewAction: .itemAppeared(id: id))
} }
.onDisappear { .onDisappear {
coordinator.send(viewAction: .itemDisappeared(id: timelineItem.id)) coordinator.send(viewAction: .itemDisappeared(id: id))
} }
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
coordinator.send(viewAction: .linkClicked(url: url)) coordinator.send(viewAction: .linkClicked(url: url))
@@ -213,6 +223,8 @@ class TimelineTableViewController: UIViewController {
return cell return cell
} }
dataSource?.defaultRowAnimation = .fade
tableView.delegate = self tableView.delegate = self
} }
@@ -226,13 +238,13 @@ class TimelineTableViewController: UIViewController {
let previousLayout = layout() let previousLayout = layout()
self.previousLayout = previousLayout self.previousLayout = previousLayout
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, RoomTimelineViewProvider>() var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, String>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems(timelineItems) snapshot.appendItems(timelineItemsIDs)
dataSource.apply(snapshot, animatingDifferences: false) dataSource.apply(snapshot, animatingDifferences: false)
updateTopPadding() updateTopPadding()
if previousLayout.isBottomVisible { if previousLayout.isBottomVisible {
scrollToBottom(animated: false) scrollToBottom(animated: false)
} else if let pinnedItem = previousLayout.pinnedItem { } else if let pinnedItem = previousLayout.pinnedItem {
@@ -254,12 +266,12 @@ class TimelineTableViewController: UIViewController {
} }
guard let bottomItemIndexPath = tableView.indexPathsForVisibleRows?.last, guard let bottomItemIndexPath = tableView.indexPathsForVisibleRows?.last,
let bottomItem = dataSource.itemIdentifier(for: bottomItemIndexPath) let bottomID = dataSource.itemIdentifier(for: bottomItemIndexPath)
else { return layout } else { return layout }
let bottomCellFrame = tableView.cellFrame(for: bottomItem) let bottomCellFrame = tableView.cellFrame(for: bottomID)
layout.pinnedItem = PinnedItem(id: bottomItem.id, position: .bottom, frame: bottomCellFrame) layout.pinnedItem = PinnedItem(id: bottomID, position: .bottom, frame: bottomCellFrame)
layout.isBottomVisible = bottomItem == snapshot.itemIdentifiers.last layout.isBottomVisible = bottomID == snapshot.itemIdentifiers.last
return layout return layout
} }
@@ -288,16 +300,16 @@ class TimelineTableViewController: UIViewController {
/// Scrolls to the bottom of the timeline. /// Scrolls to the bottom of the timeline.
private func scrollToBottom(animated: Bool) { private func scrollToBottom(animated: Bool) {
guard let lastItem = timelineItems.last, guard let lastItemID = timelineItemsIDs.last,
let lastIndexPath = dataSource?.indexPath(for: lastItem) let lastIndexPath = dataSource?.indexPath(for: lastItemID)
else { return } else { return }
tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: animated) tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: animated)
} }
/// Restores the position of the timeline using the supplied item and snapshot. /// Restores the position of the timeline using the supplied item and snapshot.
private func restoreScrollPosition(using pinnedItem: PinnedItem, and snapshot: NSDiffableDataSourceSnapshot<TimelineSection, RoomTimelineViewProvider>) { private func restoreScrollPosition(using pinnedItem: PinnedItem, and snapshot: NSDiffableDataSourceSnapshot<TimelineSection, String>) {
guard let item = snapshot.itemIdentifiers.first(where: { $0.id == pinnedItem.id }), guard let item = snapshot.itemIdentifiers.first(where: { $0 == pinnedItem.id }),
let indexPath = dataSource?.indexPath(for: item) let indexPath = dataSource?.indexPath(for: item)
else { return } else { return }
@@ -398,8 +410,8 @@ extension TimelineTableViewController {
private extension UITableView { private extension UITableView {
/// Returns the frame of the cell for a particular timeline item. /// Returns the frame of the cell for a particular timeline item.
func cellFrame(for item: RoomTimelineViewProvider) -> CGRect? { func cellFrame(for id: String) -> CGRect? {
guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item == item }) else { guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item?.id == id }) else {
return nil return nil
} }
@@ -46,7 +46,7 @@ struct TimelineView: UIViewControllerRepresentable {
init(viewModelContext: RoomScreenViewModel.Context) { init(viewModelContext: RoomScreenViewModel.Context) {
context = viewModelContext context = viewModelContext
if viewModelContext.viewState.items.isEmpty { if viewModelContext.viewState.itemViewModels.isEmpty {
viewModelContext.send(viewAction: .paginateBackwards) viewModelContext.send(viewAction: .paginateBackwards)
} }
} }
@@ -56,8 +56,8 @@ struct TimelineView: UIViewControllerRepresentable {
if tableViewController.timelineStyle != timelineStyle { if tableViewController.timelineStyle != timelineStyle {
tableViewController.timelineStyle = timelineStyle tableViewController.timelineStyle = timelineStyle
} }
if tableViewController.timelineItems != context.viewState.items { if tableViewController.timelineItemsDictionary != context.viewState.itemsDictionary {
tableViewController.timelineItems = context.viewState.items tableViewController.timelineItemsDictionary = context.viewState.itemsDictionary
} }
if tableViewController.canBackPaginate != context.viewState.canBackPaginate { if tableViewController.canBackPaginate != context.viewState.canBackPaginate {
tableViewController.canBackPaginate = context.viewState.canBackPaginate tableViewController.canBackPaginate = context.viewState.canBackPaginate
@@ -0,0 +1,70 @@
//
// 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 RoomTimelineItemView: View {
@ObservedObject var viewModel: RoomTimelineItemViewModel
var body: some View {
timelineView
.environment(\.timelineGroupStyle, viewModel.groupStyle)
}
@ViewBuilder private var timelineView: some View {
switch viewModel.type {
case .text(let item):
TextRoomTimelineView(timelineItem: item)
case .separator(let item):
SeparatorRoomTimelineView(timelineItem: item)
case .image(let item):
ImageRoomTimelineView(timelineItem: item)
case .video(let item):
VideoRoomTimelineView(timelineItem: item)
case .audio(let item):
AudioRoomTimelineView(timelineItem: item)
case .file(let item):
FileRoomTimelineView(timelineItem: item)
case .emote(let item):
EmoteRoomTimelineView(timelineItem: item)
case .notice(let item):
NoticeRoomTimelineView(timelineItem: item)
case .redacted(let item):
RedactedRoomTimelineView(timelineItem: item)
case .encrypted(let item):
EncryptedRoomTimelineView(timelineItem: item)
case .readMarker(let item):
ReadMarkerRoomTimelineView(timelineItem: item)
case .paginationIndicator(let item):
PaginationIndicatorRoomTimelineView(timelineItem: item)
case .sticker(let item):
StickerRoomTimelineView(timelineItem: item)
case .unsupported(let item):
UnsupportedRoomTimelineView(timelineItem: item)
case .timelineStart(let item):
TimelineStartRoomTimelineView(timelineItem: item)
case .state(let item):
StateRoomTimelineView(timelineItem: item)
case .group(let item):
CollapsibleRoomTimelineView(timelineItem: item)
case .location(let item):
LocationRoomTimelineView(timelineItem: item)
}
}
var timelineGroupStyle: TimelineGroupStyle {
viewModel.groupStyle
}
}
@@ -0,0 +1,146 @@
//
// Copyright 2023 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
final class RoomTimelineItemViewModel: Identifiable, Equatable, ObservableObject {
static func == (lhs: RoomTimelineItemViewModel, rhs: RoomTimelineItemViewModel) -> Bool {
lhs.type == rhs.type && lhs.groupStyle == rhs.groupStyle
}
@Published var type: RoomTimelineItemType
@Published var groupStyle: TimelineGroupStyle
var id: String {
type.id
}
convenience init(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) {
self.init(type: .init(item: item), groupStyle: groupStyle)
}
init(type: RoomTimelineItemType, groupStyle: TimelineGroupStyle) {
self.type = type
self.groupStyle = groupStyle
}
var isReactable: Bool {
type.isReactable
}
}
enum RoomTimelineItemType: Equatable {
case text(TextRoomTimelineItem)
case separator(SeparatorRoomTimelineItem)
case image(ImageRoomTimelineItem)
case video(VideoRoomTimelineItem)
case audio(AudioRoomTimelineItem)
case file(FileRoomTimelineItem)
case emote(EmoteRoomTimelineItem)
case notice(NoticeRoomTimelineItem)
case redacted(RedactedRoomTimelineItem)
case encrypted(EncryptedRoomTimelineItem)
case readMarker(ReadMarkerRoomTimelineItem)
case paginationIndicator(PaginationIndicatorRoomTimelineItem)
case sticker(StickerRoomTimelineItem)
case unsupported(UnsupportedRoomTimelineItem)
case timelineStart(TimelineStartRoomTimelineItem)
case state(StateRoomTimelineItem)
case group(CollapsibleTimelineItem)
case location(LocationRoomTimelineItem)
// swiftlint:disable:next cyclomatic_complexity
init(item: RoomTimelineItemProtocol) {
switch item {
case let item as TextRoomTimelineItem:
self = .text(item)
case let item as ImageRoomTimelineItem:
self = .image(item)
case let item as VideoRoomTimelineItem:
self = .video(item)
case let item as AudioRoomTimelineItem:
self = .audio(item)
case let item as FileRoomTimelineItem:
self = .file(item)
case let item as SeparatorRoomTimelineItem:
self = .separator(item)
case let item as NoticeRoomTimelineItem:
self = .notice(item)
case let item as EmoteRoomTimelineItem:
self = .emote(item)
case let item as RedactedRoomTimelineItem:
self = .redacted(item)
case let item as EncryptedRoomTimelineItem:
self = .encrypted(item)
case let item as ReadMarkerRoomTimelineItem:
self = .readMarker(item)
case let item as PaginationIndicatorRoomTimelineItem:
self = .paginationIndicator(item)
case let item as StickerRoomTimelineItem:
self = .sticker(item)
case let item as UnsupportedRoomTimelineItem:
self = .unsupported(item)
case let item as TimelineStartRoomTimelineItem:
self = .timelineStart(item)
case let item as StateRoomTimelineItem:
self = .state(item)
case let item as CollapsibleTimelineItem:
self = .group(item)
case let item as LocationRoomTimelineItem:
self = .location(item)
default:
fatalError("Unknown timeline item")
}
}
var id: String {
switch self {
case .text(let item as RoomTimelineItemProtocol),
.separator(let item as RoomTimelineItemProtocol),
.image(let item as RoomTimelineItemProtocol),
.video(let item as RoomTimelineItemProtocol),
.audio(let item as RoomTimelineItemProtocol),
.file(let item as RoomTimelineItemProtocol),
.emote(let item as RoomTimelineItemProtocol),
.notice(let item as RoomTimelineItemProtocol),
.redacted(let item as RoomTimelineItemProtocol),
.encrypted(let item as RoomTimelineItemProtocol),
.readMarker(let item as RoomTimelineItemProtocol),
.paginationIndicator(let item as RoomTimelineItemProtocol),
.sticker(let item as RoomTimelineItemProtocol),
.unsupported(let item as RoomTimelineItemProtocol),
.timelineStart(let item as RoomTimelineItemProtocol),
.state(let item as RoomTimelineItemProtocol),
.group(let item as RoomTimelineItemProtocol),
.location(let item as RoomTimelineItemProtocol):
return item.id
}
}
/// Whether or not it is possible to send a reaction to this timeline item.
var isReactable: Bool {
switch self {
case .text, .image, .video, .audio, .file, .emote, .notice, .sticker, .location:
return true
case .redacted, .encrypted, .unsupported, .state: // Event based items that aren't reactable
return false
case .timelineStart, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
return false
case .group:
return false
}
}
}
@@ -1,193 +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
import SwiftUI
enum RoomTimelineViewProvider: Identifiable, Hashable {
case text(TextRoomTimelineItem, TimelineGroupStyle)
case separator(SeparatorRoomTimelineItem, TimelineGroupStyle)
case image(ImageRoomTimelineItem, TimelineGroupStyle)
case video(VideoRoomTimelineItem, TimelineGroupStyle)
case audio(AudioRoomTimelineItem, TimelineGroupStyle)
case file(FileRoomTimelineItem, TimelineGroupStyle)
case emote(EmoteRoomTimelineItem, TimelineGroupStyle)
case notice(NoticeRoomTimelineItem, TimelineGroupStyle)
case redacted(RedactedRoomTimelineItem, TimelineGroupStyle)
case encrypted(EncryptedRoomTimelineItem, TimelineGroupStyle)
case readMarker(ReadMarkerRoomTimelineItem, TimelineGroupStyle)
case paginationIndicator(PaginationIndicatorRoomTimelineItem, TimelineGroupStyle)
case sticker(StickerRoomTimelineItem, TimelineGroupStyle)
case unsupported(UnsupportedRoomTimelineItem, TimelineGroupStyle)
case timelineStart(TimelineStartRoomTimelineItem, TimelineGroupStyle)
case state(StateRoomTimelineItem, TimelineGroupStyle)
case group(CollapsibleTimelineItem, TimelineGroupStyle)
case location(LocationRoomTimelineItem, TimelineGroupStyle)
// swiftlint:disable:next cyclomatic_complexity
init(timelineItem: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) {
switch timelineItem {
case let item as TextRoomTimelineItem:
self = .text(item, groupStyle)
case let item as ImageRoomTimelineItem:
self = .image(item, groupStyle)
case let item as VideoRoomTimelineItem:
self = .video(item, groupStyle)
case let item as AudioRoomTimelineItem:
self = .audio(item, groupStyle)
case let item as FileRoomTimelineItem:
self = .file(item, groupStyle)
case let item as SeparatorRoomTimelineItem:
self = .separator(item, groupStyle)
case let item as NoticeRoomTimelineItem:
self = .notice(item, groupStyle)
case let item as EmoteRoomTimelineItem:
self = .emote(item, groupStyle)
case let item as RedactedRoomTimelineItem:
self = .redacted(item, groupStyle)
case let item as EncryptedRoomTimelineItem:
self = .encrypted(item, groupStyle)
case let item as ReadMarkerRoomTimelineItem:
self = .readMarker(item, groupStyle)
case let item as PaginationIndicatorRoomTimelineItem:
self = .paginationIndicator(item, groupStyle)
case let item as StickerRoomTimelineItem:
self = .sticker(item, groupStyle)
case let item as UnsupportedRoomTimelineItem:
self = .unsupported(item, groupStyle)
case let item as TimelineStartRoomTimelineItem:
self = .timelineStart(item, groupStyle)
case let item as StateRoomTimelineItem:
self = .state(item, groupStyle)
case let item as CollapsibleTimelineItem:
self = .group(item, groupStyle)
case let item as LocationRoomTimelineItem:
self = .location(item, groupStyle)
default:
fatalError("Unknown timeline item")
}
}
var id: String {
switch self {
case .text(let item as RoomTimelineItemProtocol, _),
.separator(let item as RoomTimelineItemProtocol, _),
.image(let item as RoomTimelineItemProtocol, _),
.video(let item as RoomTimelineItemProtocol, _),
.audio(let item as RoomTimelineItemProtocol, _),
.file(let item as RoomTimelineItemProtocol, _),
.emote(let item as RoomTimelineItemProtocol, _),
.notice(let item as RoomTimelineItemProtocol, _),
.redacted(let item as RoomTimelineItemProtocol, _),
.encrypted(let item as RoomTimelineItemProtocol, _),
.readMarker(let item as RoomTimelineItemProtocol, _),
.paginationIndicator(let item as RoomTimelineItemProtocol, _),
.sticker(let item as RoomTimelineItemProtocol, _),
.unsupported(let item as RoomTimelineItemProtocol, _),
.timelineStart(let item as RoomTimelineItemProtocol, _),
.state(let item as RoomTimelineItemProtocol, _),
.group(let item as RoomTimelineItemProtocol, _),
.location(let item as RoomTimelineItemProtocol, _):
return item.id
}
}
/// Whether or not it is possible to send a reaction to this timeline item.
var isReactable: Bool {
switch self {
case .text, .image, .video, .audio, .file, .emote, .notice, .sticker, .location:
return true
case .redacted, .encrypted, .unsupported, .state: // Event based items that aren't reactable
return false
case .timelineStart, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
return false
case .group:
return false
}
}
}
extension RoomTimelineViewProvider: View {
var body: some View {
timelineView
.environment(\.timelineGroupStyle, timelineGroupStyle)
}
@ViewBuilder private var timelineView: some View {
switch self {
case .text(let item, _):
TextRoomTimelineView(timelineItem: item)
case .separator(let item, _):
SeparatorRoomTimelineView(timelineItem: item)
case .image(let item, _):
ImageRoomTimelineView(timelineItem: item)
case .video(let item, _):
VideoRoomTimelineView(timelineItem: item)
case .audio(let item, _):
AudioRoomTimelineView(timelineItem: item)
case .file(let item, _):
FileRoomTimelineView(timelineItem: item)
case .emote(let item, _):
EmoteRoomTimelineView(timelineItem: item)
case .notice(let item, _):
NoticeRoomTimelineView(timelineItem: item)
case .redacted(let item, _):
RedactedRoomTimelineView(timelineItem: item)
case .encrypted(let item, _):
EncryptedRoomTimelineView(timelineItem: item)
case .readMarker(let item, _):
ReadMarkerRoomTimelineView(timelineItem: item)
case .paginationIndicator(let item, _):
PaginationIndicatorRoomTimelineView(timelineItem: item)
case .sticker(let item, _):
StickerRoomTimelineView(timelineItem: item)
case .unsupported(let item, _):
UnsupportedRoomTimelineView(timelineItem: item)
case .timelineStart(let item, _):
TimelineStartRoomTimelineView(timelineItem: item)
case .state(let item, _):
StateRoomTimelineView(timelineItem: item)
case .group(let item, _):
CollapsibleRoomTimelineView(timelineItem: item)
case .location(let item, _):
LocationRoomTimelineView(timelineItem: item)
}
}
var timelineGroupStyle: TimelineGroupStyle {
switch self {
case .text(_, let groupStyle),
.separator(_, let groupStyle),
.image(_, let groupStyle),
.video(_, let groupStyle),
.audio(_, let groupStyle),
.file(_, let groupStyle),
.emote(_, let groupStyle),
.notice(_, let groupStyle),
.redacted(_, let groupStyle),
.encrypted(_, let groupStyle),
.readMarker(_, let groupStyle),
.paginationIndicator(_, let groupStyle),
.sticker(_, let groupStyle),
.unsupported(_, let groupStyle),
.timelineStart(_, let groupStyle),
.state(_, let groupStyle),
.group(_, let groupStyle),
.location(_, let groupStyle):
return groupStyle
}
}
}
@@ -51,9 +51,9 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Then the messages should be grouped together. // Then the messages should be grouped together.
XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .first, "Nothing should prevent the first message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.items[1].timelineGroupStyle, .middle, "Nothing should prevent the middle message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.")
XCTAssertEqual(viewModel.state.items[2].timelineGroupStyle, .last, "Nothing should prevent the last message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
} }
func testMessageGroupingMultipleSenders() { func testMessageGroupingMultipleSenders() {
@@ -84,12 +84,12 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Then the messages should be grouped by sender. // Then the messages should be grouped by sender.
XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .single, "A message should not be grouped when the sender changes.") XCTAssertEqual(viewModel.state.itemViewModels[0].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.items[1].timelineGroupStyle, .single, "A message should not be grouped when the sender changes.") XCTAssertEqual(viewModel.state.itemViewModels[1].groupStyle, .single, "A message should not be grouped when the sender changes.")
XCTAssertEqual(viewModel.state.items[2].timelineGroupStyle, .first, "A group should start with a new sender if there are more messages from that sender.") XCTAssertEqual(viewModel.state.itemViewModels[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.items[3].timelineGroupStyle, .last, "A group should be ended when the sender changes in the next message.") XCTAssertEqual(viewModel.state.itemViewModels[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
XCTAssertEqual(viewModel.state.items[4].timelineGroupStyle, .first, "A group should start with a new sender if there are more messages from that sender.") XCTAssertEqual(viewModel.state.itemViewModels[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
XCTAssertEqual(viewModel.state.items[5].timelineGroupStyle, .last, "A group should be ended when the sender changes in the next message.") XCTAssertEqual(viewModel.state.itemViewModels[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
} }
func testMessageGroupingWithLeadingReactions() { func testMessageGroupingWithLeadingReactions() {
@@ -115,9 +115,9 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Then the first message should not be grouped but the other two should. // Then the first message should not be grouped but the other two should.
XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .single, "When the first message has reactions it should not be grouped.") XCTAssertEqual(viewModel.state.itemViewModels[0].groupStyle, .single, "When the first message has reactions it should not be grouped.")
XCTAssertEqual(viewModel.state.items[1].timelineGroupStyle, .first, "A new group should be made when the preceding message has reactions.") XCTAssertEqual(viewModel.state.itemViewModels[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.")
XCTAssertEqual(viewModel.state.items[2].timelineGroupStyle, .last, "Nothing should prevent the last message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
} }
func testMessageGroupingWithInnerReactions() { func testMessageGroupingWithInnerReactions() {
@@ -143,9 +143,9 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Then the first and second messages should be grouped and the last one should not. // Then the first and second messages should be grouped and the last one should not.
XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .first, "Nothing should prevent the first message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.items[1].timelineGroupStyle, .last, "When the message has reactions, the group should end here.") XCTAssertEqual(viewModel.state.itemViewModels[1].groupStyle, .last, "When the message has reactions, the group should end here.")
XCTAssertEqual(viewModel.state.items[2].timelineGroupStyle, .single, "The last message should not be grouped when the preceding message has reactions.") XCTAssertEqual(viewModel.state.itemViewModels[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.")
} }
func testMessageGroupingWithTrailingReactions() { func testMessageGroupingWithTrailingReactions() {
@@ -171,9 +171,9 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Then the messages should be grouped together. // Then the messages should be grouped together.
XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .first, "Nothing should prevent the first message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
XCTAssertEqual(viewModel.state.items[1].timelineGroupStyle, .middle, "Nothing should prevent the second message from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.")
XCTAssertEqual(viewModel.state.items[2].timelineGroupStyle, .last, "Reactions on the last message should not prevent it from being grouped.") XCTAssertEqual(viewModel.state.itemViewModels[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.")
} }
func testGoToUserDetailsSuccessNoDelay() async { func testGoToUserDetailsSuccessNoDelay() async {