mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Use a UITableView component for the timeline. (#349)
* Use a collection view for the timeline. * Switch to a table view.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 51;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -98,7 +98,6 @@
|
|||||||
323F36D880363C473B81A9EA /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; };
|
323F36D880363C473B81A9EA /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; };
|
||||||
3274219F7F26A5C6C2C55630 /* FilePreviewViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */; };
|
3274219F7F26A5C6C2C55630 /* FilePreviewViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */; };
|
||||||
32BA37B01B05261FCF2D4B45 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090CA61A835C151CEDF8F372 /* WeakDictionaryKeyReference.swift */; };
|
32BA37B01B05261FCF2D4B45 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090CA61A835C151CEDF8F372 /* WeakDictionaryKeyReference.swift */; };
|
||||||
33665CE55037D029ED7D867E /* TimelineScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A627CE1429374617334FA5E9 /* TimelineScrollView.swift */; };
|
|
||||||
33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
|
33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
|
||||||
33D630461FC4562CC767EE9F /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; };
|
33D630461FC4562CC767EE9F /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; };
|
||||||
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; };
|
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; };
|
||||||
@@ -114,6 +113,7 @@
|
|||||||
38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; };
|
38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; };
|
||||||
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */; };
|
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */; };
|
||||||
3A64A93A651A3CB8774ADE8E /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; };
|
3A64A93A651A3CB8774ADE8E /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; };
|
||||||
|
3B0DF16FEA8481061C28A3BE /* TimelineTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170A84E8957BF97A26E962D5 /* TimelineTableView.swift */; };
|
||||||
3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; };
|
3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; };
|
||||||
3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
||||||
3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; };
|
3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; };
|
||||||
@@ -139,11 +139,9 @@
|
|||||||
49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; };
|
49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; };
|
||||||
4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; };
|
4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; };
|
||||||
4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; };
|
4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; };
|
||||||
4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */; };
|
|
||||||
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; };
|
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; };
|
||||||
4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; };
|
4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; };
|
||||||
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; };
|
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; };
|
||||||
500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; };
|
|
||||||
50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
|
50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
|
||||||
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; };
|
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; };
|
||||||
51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; };
|
51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; };
|
||||||
@@ -527,6 +525,7 @@
|
|||||||
142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = "<group>"; };
|
142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = "<group>"; };
|
||||||
167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModel.swift; sourceTree = "<group>"; };
|
167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModel.swift; sourceTree = "<group>"; };
|
||||||
16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; };
|
16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; };
|
||||||
|
170A84E8957BF97A26E962D5 /* TimelineTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableView.swift; sourceTree = "<group>"; };
|
||||||
1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = "<group>"; };
|
1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = "<group>"; };
|
||||||
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = "<group>"; };
|
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = "<group>"; };
|
||||||
179423E34EE846E048E49CBF /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = "<group>"; };
|
179423E34EE846E048E49CBF /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = "<group>"; };
|
||||||
@@ -715,7 +714,6 @@
|
|||||||
7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = "<group>"; };
|
7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = "<group>"; };
|
7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = "<group>"; };
|
||||||
7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewScreen.swift; sourceTree = "<group>"; };
|
7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewScreen.swift; sourceTree = "<group>"; };
|
||||||
804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = "<group>"; };
|
|
||||||
8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@@ -725,7 +723,6 @@
|
|||||||
858F8D0B0D51CC41BAA18E24 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
858F8D0B0D51CC41BAA18E24 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
85CB1DDCEE53B946D09DF4F6 /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-BD"; path = "bn-BD.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
85CB1DDCEE53B946D09DF4F6 /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-BD"; path = "bn-BD.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
873718F8BD17B778C5141C45 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = "<group>"; };
|
873718F8BD17B778C5141C45 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
|
||||||
878B7C1885486FB4BE41631D /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = iw; path = iw.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
878B7C1885486FB4BE41631D /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = iw; path = iw.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
885D8C42DD17625B5261BEFF /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
|
885D8C42DD17625B5261BEFF /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
|
||||||
8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = "<group>"; };
|
8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = "<group>"; };
|
||||||
@@ -733,12 +730,12 @@
|
|||||||
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
|
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
|
||||||
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = "<group>"; };
|
8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = "<group>"; };
|
||||||
8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; path = LICENSE; sourceTree = "<group>"; };
|
8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||||
8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = "<group>"; };
|
8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = "<group>"; };
|
||||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
|
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||||
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||||
@@ -788,7 +785,6 @@
|
|||||||
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = "<group>"; };
|
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = "<group>"; };
|
||||||
A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||||
A5B0B1226DA8DB55918B34CD /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
|
A5B0B1226DA8DB55918B34CD /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
|
||||||
A627CE1429374617334FA5E9 /* TimelineScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineScrollView.swift; sourceTree = "<group>"; };
|
|
||||||
A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; 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>"; };
|
||||||
@@ -929,7 +925,7 @@
|
|||||||
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; 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>"; };
|
||||||
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>"; };
|
||||||
EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.swift; sourceTree = "<group>"; };
|
EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.swift; sourceTree = "<group>"; };
|
||||||
EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
@@ -1628,10 +1624,8 @@
|
|||||||
422724361B6555364C43281E /* RoomHeaderView.swift */,
|
422724361B6555364C43281E /* RoomHeaderView.swift */,
|
||||||
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */,
|
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */,
|
||||||
B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */,
|
B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */,
|
||||||
804F9B0FABE093C7284CD09B /* TimelineItemList.swift */,
|
|
||||||
A627CE1429374617334FA5E9 /* TimelineScrollView.swift */,
|
|
||||||
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */,
|
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */,
|
||||||
874A1842477895F199567BD7 /* TimelineView.swift */,
|
170A84E8957BF97A26E962D5 /* TimelineTableView.swift */,
|
||||||
A312471EA62EFB0FD94E60DC /* Style */,
|
A312471EA62EFB0FD94E60DC /* Style */,
|
||||||
CCD48459CA34A1928EC7A26A /* Supplementary */,
|
CCD48459CA34A1928EC7A26A /* Supplementary */,
|
||||||
B7D3886505ECC85A06DA8258 /* Timeline */,
|
B7D3886505ECC85A06DA8258 /* Timeline */,
|
||||||
@@ -1913,14 +1907,6 @@
|
|||||||
path = NSE;
|
path = NSE;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
B3E78735F63FA93FAAAF700A /* MockUserNotificationController.swift~refs */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
F798CDE87F83A94B8BC2E18A /* remotes */,
|
|
||||||
);
|
|
||||||
path = "MockUserNotificationController.swift~refs";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B442FCF47E0A6F28D7D50A4D /* FilePreview */ = {
|
B442FCF47E0A6F28D7D50A4D /* FilePreview */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -2008,13 +1994,6 @@
|
|||||||
path = UITests;
|
path = UITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
C5A8A8B1C16BBFEA4B9D5988 /* origin */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
);
|
|
||||||
path = origin;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
CA555F7C7CA382ACACF0D82B /* Keychain */ = {
|
CA555F7C7CA382ACACF0D82B /* Keychain */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -2072,13 +2051,6 @@
|
|||||||
path = Vendor;
|
path = Vendor;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D14F980E72A97D6169A499E8 /* ImageViewer */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
);
|
|
||||||
path = ImageViewer;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D958761758AA1110476DE6A3 /* SessionVerification */ = {
|
D958761758AA1110476DE6A3 /* SessionVerification */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -2102,7 +2074,6 @@
|
|||||||
CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */,
|
CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */,
|
||||||
649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */,
|
649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */,
|
||||||
F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */,
|
F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */,
|
||||||
B3E78735F63FA93FAAAF700A /* MockUserNotificationController.swift~refs */,
|
|
||||||
);
|
);
|
||||||
path = UserNotifications;
|
path = UserNotifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2143,7 +2114,6 @@
|
|||||||
4009BE2E791C16AC6EE39A7E /* BugReport */,
|
4009BE2E791C16AC6EE39A7E /* BugReport */,
|
||||||
B442FCF47E0A6F28D7D50A4D /* FilePreview */,
|
B442FCF47E0A6F28D7D50A4D /* FilePreview */,
|
||||||
B53CA9BECD3F97805E1432D0 /* HomeScreen */,
|
B53CA9BECD3F97805E1432D0 /* HomeScreen */,
|
||||||
D14F980E72A97D6169A499E8 /* ImageViewer */,
|
|
||||||
3F38EAC92E2281990E65DAF2 /* OnboardingScreen */,
|
3F38EAC92E2281990E65DAF2 /* OnboardingScreen */,
|
||||||
A448A3A8F764174C60CD0CA1 /* Other */,
|
A448A3A8F764174C60CD0CA1 /* Other */,
|
||||||
679E9837ECA8D6776079D16E /* RoomScreen */,
|
679E9837ECA8D6776079D16E /* RoomScreen */,
|
||||||
@@ -2201,14 +2171,6 @@
|
|||||||
path = Background;
|
path = Background;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
F798CDE87F83A94B8BC2E18A /* remotes */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C5A8A8B1C16BBFEA4B9D5988 /* origin */,
|
|
||||||
);
|
|
||||||
path = remotes;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
FCDF06BDB123505F0334B4F9 /* Timeline */ = {
|
FCDF06BDB123505F0334B4F9 /* Timeline */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -2933,15 +2895,13 @@
|
|||||||
5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */,
|
5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */,
|
||||||
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */,
|
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */,
|
||||||
01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */,
|
01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */,
|
||||||
4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */,
|
|
||||||
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */,
|
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */,
|
||||||
440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */,
|
440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */,
|
||||||
9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */,
|
9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */,
|
||||||
33665CE55037D029ED7D867E /* TimelineScrollView.swift in Sources */,
|
|
||||||
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */,
|
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */,
|
||||||
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */,
|
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */,
|
||||||
FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */,
|
FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */,
|
||||||
500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */,
|
3B0DF16FEA8481061C28A3BE /* TimelineTableView.swift in Sources */,
|
||||||
7732B2F635626BE1C1CD92A4 /* UIActivityViewControllerWrapper.swift in Sources */,
|
7732B2F635626BE1C1CD92A4 /* UIActivityViewControllerWrapper.swift in Sources */,
|
||||||
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */,
|
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */,
|
||||||
A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */,
|
A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
"room_timeline_replying_to" = "Replying to %@";
|
"room_timeline_replying_to" = "Replying to %@";
|
||||||
"room_timeline_editing" = "Editing";
|
"room_timeline_editing" = "Editing";
|
||||||
|
"room_timeline_syncing" = "Syncing";
|
||||||
|
|
||||||
"session_verification_banner_title" = "Help keep your messages secure";
|
"session_verification_banner_title" = "Help keep your messages secure";
|
||||||
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
|
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ extension ElementL10n {
|
|||||||
public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description")
|
public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description")
|
||||||
/// Plain Timeline
|
/// Plain Timeline
|
||||||
public static let roomTimelineStylePlainLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_plain_long_description")
|
public static let roomTimelineStylePlainLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_plain_long_description")
|
||||||
|
/// Syncing
|
||||||
|
public static let roomTimelineSyncing = ElementL10n.tr("Untranslated", "room_timeline_syncing")
|
||||||
/// Would you like to submit a bug report?
|
/// Would you like to submit a bug report?
|
||||||
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
||||||
/// You took a screenshot
|
/// You took a screenshot
|
||||||
|
|||||||
@@ -25,30 +25,29 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isScrolling = PassthroughSubject<Bool, Never>()
|
var isScrolling = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
private func update() {
|
private func update(_ scrollView: UIScrollView) {
|
||||||
guard let scrollView else { return }
|
|
||||||
isScrolling.send(scrollView.isDragging || scrollView.isDecelerating)
|
isScrolling.send(scrollView.isDragging || scrollView.isDecelerating)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
update()
|
update(scrollView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
update()
|
update(scrollView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
update()
|
update(scrollView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
update()
|
update(scrollView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||||
update()
|
update(scrollView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Combine
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
enum RoomScreenViewModelAction {
|
enum RoomScreenViewModelAction {
|
||||||
@@ -29,7 +29,7 @@ enum RoomScreenComposerMode: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenViewAction {
|
enum RoomScreenViewAction {
|
||||||
case loadPreviousPage
|
case paginateBackwards
|
||||||
case itemAppeared(id: String)
|
case itemAppeared(id: String)
|
||||||
case itemDisappeared(id: String)
|
case itemDisappeared(id: String)
|
||||||
case itemTapped(id: String)
|
case itemTapped(id: String)
|
||||||
@@ -56,12 +56,22 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var sendButtonDisabled: Bool {
|
var sendButtonDisabled: Bool {
|
||||||
bindings.composerText.count == 0
|
bindings.composerText.count == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scrollToBottomPublisher = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
/// Returns the opacity that the supplied timeline item's cell should be.
|
||||||
|
func opacity(for item: RoomTimelineViewProvider) -> CGFloat {
|
||||||
|
guard case let .reply(selectedItemID, _) = composerMode else { return 1.0 }
|
||||||
|
return selectedItemID == item.id ? 1.0 : 0.5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewStateBindings {
|
struct RoomScreenViewStateBindings {
|
||||||
var composerText: String
|
var composerText: String
|
||||||
var composerFocused: Bool
|
var composerFocused: Bool
|
||||||
|
|
||||||
|
var scrollToBottomButtonVisible = false
|
||||||
|
|
||||||
/// Information describing the currently displayed alert.
|
/// Information describing the currently displayed alert.
|
||||||
var alertInfo: AlertInfo<RoomScreenErrorType>?
|
var alertInfo: AlertInfo<RoomScreenErrorType>?
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, RoomScreenViewAction>
|
typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, RoomScreenViewAction>
|
||||||
@@ -21,6 +22,7 @@ typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, Roo
|
|||||||
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
|
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
static let backPaginationPageSize: UInt = 20
|
static let backPaginationPageSize: UInt = 20
|
||||||
|
static let backPaginationIndicatorID = "RoomBackPagination"
|
||||||
}
|
}
|
||||||
|
|
||||||
private let timelineController: RoomTimelineControllerProtocol
|
private let timelineController: RoomTimelineControllerProtocol
|
||||||
@@ -60,8 +62,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem)
|
self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem)
|
||||||
case .startedBackPaginating:
|
case .startedBackPaginating:
|
||||||
self.state.isBackPaginating = true
|
self.state.isBackPaginating = true
|
||||||
|
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Constants.backPaginationIndicatorID,
|
||||||
|
type: .toast,
|
||||||
|
title: ElementL10n.roomTimelineSyncing,
|
||||||
|
persistent: true))
|
||||||
case .finishedBackPaginating:
|
case .finishedBackPaginating:
|
||||||
self.state.isBackPaginating = false
|
self.state.isBackPaginating = false
|
||||||
|
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Constants.backPaginationIndicatorID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -86,11 +93,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
override func process(viewAction: RoomScreenViewAction) async {
|
override func process(viewAction: RoomScreenViewAction) async {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .loadPreviousPage:
|
case .paginateBackwards:
|
||||||
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
|
await paginateBackwards()
|
||||||
default:
|
|
||||||
#warning("Treat errors")
|
|
||||||
}
|
|
||||||
case .itemAppeared(let id):
|
case .itemAppeared(let id):
|
||||||
await timelineController.processItemAppearance(id)
|
await timelineController.processItemAppearance(id)
|
||||||
case .itemDisappeared(let id):
|
case .itemDisappeared(let id):
|
||||||
@@ -118,6 +122,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func paginateBackwards() async {
|
||||||
|
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
|
||||||
|
default:
|
||||||
|
#warning("Treat errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func itemTapped(with itemId: String) async {
|
private func itemTapped(with itemId: String) async {
|
||||||
state.showLoading = true
|
state.showLoading = true
|
||||||
|
|||||||
@@ -17,51 +17,70 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct RoomScreen: View {
|
struct RoomScreen: View {
|
||||||
|
@ObservedObject private var settings = ElementSettings.shared
|
||||||
@ObservedObject var context: RoomScreenViewModel.Context
|
@ObservedObject var context: RoomScreenViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
timeline
|
||||||
VStack(spacing: 0.0) {
|
.safeAreaInset(edge: .bottom) { messageComposer }
|
||||||
TimelineView()
|
|
||||||
.environmentObject(context)
|
|
||||||
|
|
||||||
MessageComposer(text: $context.composerText,
|
|
||||||
focused: $context.composerFocused,
|
|
||||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
|
||||||
type: context.viewState.composerMode) {
|
|
||||||
sendMessage()
|
|
||||||
} replyCancellationAction: {
|
|
||||||
context.send(viewAction: .cancelReply)
|
|
||||||
} editCancellationAction: {
|
|
||||||
context.send(viewAction: .cancelEdit)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar { toolbar }
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
.overlay { loadingIndicator }
|
||||||
RoomHeaderView(context: context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert(item: $context.alertInfo) { $0.alert }
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
||||||
|
}
|
||||||
if context.viewState.showLoading {
|
|
||||||
ProgressView()
|
var timeline: some View {
|
||||||
.progressViewStyle(.circular)
|
TimelineTableView()
|
||||||
.tint(.element.primaryContent)
|
.environmentObject(context)
|
||||||
.padding(16)
|
.timelineStyle(settings.timelineStyle)
|
||||||
.background(Color.element.quinaryContent)
|
.overlay(alignment: .bottomTrailing) { scrollToBottomButton }
|
||||||
.cornerRadius(8)
|
}
|
||||||
}
|
|
||||||
|
var messageComposer: some View {
|
||||||
|
MessageComposer(text: $context.composerText,
|
||||||
|
focused: $context.composerFocused,
|
||||||
|
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||||
|
type: context.viewState.composerMode) {
|
||||||
|
sendMessage()
|
||||||
|
} replyCancellationAction: {
|
||||||
|
context.send(viewAction: .cancelReply)
|
||||||
|
} editCancellationAction: {
|
||||||
|
context.send(viewAction: .cancelEdit)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollToBottomButton: some View {
|
||||||
|
Button { context.viewState.scrollToBottomPublisher.send(()) } label: {
|
||||||
|
Image(uiImage: Asset.Images.timelineScrollToBottom.image)
|
||||||
|
.shadow(radius: 2.0)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.opacity(context.scrollToBottomButtonVisible ? 1.0 : 0.0)
|
||||||
|
.animation(.elementDefault, value: context.scrollToBottomButtonVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var loadingIndicator: some View {
|
||||||
|
if context.viewState.showLoading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.tint(.element.primaryContent)
|
||||||
|
.padding(16)
|
||||||
|
.background(Color.element.quinaryContent)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toolbar: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
RoomHeaderView(context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendMessage() {
|
private func sendMessage() {
|
||||||
guard !context.viewState.sendButtonDisabled else {
|
guard !context.viewState.sendButtonDisabled else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
context.send(viewAction: .sendMessage)
|
context.send(viewAction: .sendMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2022 New Vector Ltd
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct TimelineItemList: View {
|
|
||||||
@ObservedObject private var settings = ElementSettings.shared
|
|
||||||
|
|
||||||
@State private var timelineItems: [RoomTimelineViewProvider] = []
|
|
||||||
@State private var viewFrame: CGRect = .zero
|
|
||||||
@State private var pinnedItem: PinnedItem?
|
|
||||||
|
|
||||||
@Binding var visibleEdges: [VerticalEdge]
|
|
||||||
/// The last known value of the visible edges. This is stored because `visibleEdges`
|
|
||||||
/// updates at the same time as the `viewFrame` but we need to know the previous
|
|
||||||
/// value when the keyboard appears to determine whether to scroll to the bottom.
|
|
||||||
@State private var cachedVisibleEdges: [VerticalEdge] = []
|
|
||||||
|
|
||||||
@EnvironmentObject var context: RoomScreenViewModel.Context
|
|
||||||
|
|
||||||
let scrollToBottomPublisher: PassthroughSubject<Void, Never>
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollViewReader { scrollView in
|
|
||||||
TimelineScrollView(visibleEdges: $visibleEdges) {
|
|
||||||
// The scroll view already contains a VStack so simply provide the content to fill it.
|
|
||||||
|
|
||||||
ProgressView()
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.opacity(context.viewState.isBackPaginating ? 1.0 : 0.0)
|
|
||||||
.animation(.elementDefault, value: context.viewState.isBackPaginating)
|
|
||||||
|
|
||||||
ForEach(isRunningPreviews ? context.viewState.items : timelineItems) { item in
|
|
||||||
item
|
|
||||||
.contextMenu {
|
|
||||||
context.viewState.contextMenuBuilder?(item.id)
|
|
||||||
.id(item.id)
|
|
||||||
}
|
|
||||||
.opacity(opacityForItem(item))
|
|
||||||
.padding(settings.timelineStyle.rowInsets)
|
|
||||||
.onAppear {
|
|
||||||
context.send(viewAction: .itemAppeared(id: item.id))
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
context.send(viewAction: .itemDisappeared(id: item.id))
|
|
||||||
}
|
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
|
||||||
context.send(viewAction: .linkClicked(url: url))
|
|
||||||
return .systemAction
|
|
||||||
})
|
|
||||||
.onTapGesture {
|
|
||||||
context.send(viewAction: .itemTapped(id: item.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: visibleEdges) { edges in
|
|
||||||
cachedVisibleEdges = edges
|
|
||||||
// Paginate when the top becomes visible
|
|
||||||
guard edges.contains(.top) else { return }
|
|
||||||
requestBackPagination()
|
|
||||||
}
|
|
||||||
.onChange(of: context.viewState.isBackPaginating) { isBackPaginating in
|
|
||||||
guard !isBackPaginating else { return }
|
|
||||||
|
|
||||||
// Repeat the pagination if the top edge is still visible.
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
||||||
guard visibleEdges.contains(.top) else { return }
|
|
||||||
requestBackPagination()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: pinnedItem) { item in
|
|
||||||
guard let item else { return }
|
|
||||||
|
|
||||||
if item.animated {
|
|
||||||
withAnimation(Animation.elementDefault) {
|
|
||||||
scrollView.scrollTo(item.id, anchor: item.anchor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scrollView.scrollTo(item.id, anchor: item.anchor)
|
|
||||||
}
|
|
||||||
|
|
||||||
pinnedItem = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scrollDismissesKeyboard(.immediately)
|
|
||||||
.background(ViewFrameReader(frame: $viewFrame))
|
|
||||||
.timelineStyle(settings.timelineStyle)
|
|
||||||
.onAppear {
|
|
||||||
timelineItems = context.viewState.items
|
|
||||||
}
|
|
||||||
.onReceive(scrollToBottomPublisher) {
|
|
||||||
scrollToBottom(animated: true)
|
|
||||||
}
|
|
||||||
.onChange(of: context.viewState.items) { items in
|
|
||||||
guard
|
|
||||||
!context.viewState.items.isEmpty,
|
|
||||||
context.viewState.items.count != timelineItems.count
|
|
||||||
else {
|
|
||||||
// Update the items, but don't worry about scrolling if the count is unchanged.
|
|
||||||
timelineItems = items
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin to the bottom if empty
|
|
||||||
if timelineItems.isEmpty {
|
|
||||||
if let lastItem = context.viewState.items.last {
|
|
||||||
let pinnedItem = PinnedItem(id: lastItem.id, anchor: .bottom, animated: false)
|
|
||||||
timelineItems = context.viewState.items
|
|
||||||
self.pinnedItem = pinnedItem
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin to the new bottom if visible
|
|
||||||
if visibleEdges.contains(.bottom), let newLastItem = context.viewState.items.last {
|
|
||||||
let pinnedItem = PinnedItem(id: newLastItem.id, anchor: .bottom, animated: false)
|
|
||||||
timelineItems = context.viewState.items
|
|
||||||
self.pinnedItem = pinnedItem
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin to the old topmost visible
|
|
||||||
if visibleEdges.contains(.top), let currentFirstItem = timelineItems.first {
|
|
||||||
let pinnedItem = PinnedItem(id: currentFirstItem.id, anchor: .top, animated: false)
|
|
||||||
timelineItems = context.viewState.items
|
|
||||||
self.pinnedItem = pinnedItem
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise just update the items
|
|
||||||
timelineItems = context.viewState.items
|
|
||||||
}
|
|
||||||
.onChange(of: viewFrame) { _ in
|
|
||||||
// Use the cached version as visibleEdges will already have changed
|
|
||||||
// (but its onChange handler is yet to be called - possible race condition?)
|
|
||||||
guard cachedVisibleEdges.contains(.bottom) else { return }
|
|
||||||
|
|
||||||
// Pin the timeline to the bottom if was there on the frame change
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
scrollToBottom(animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
|
|
||||||
private func scrollToBottom(animated: Bool = false) {
|
|
||||||
if let lastItem = timelineItems.last {
|
|
||||||
pinnedItem = PinnedItem(id: lastItem.id, anchor: .bottom, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func requestBackPagination() {
|
|
||||||
guard !context.viewState.isBackPaginating else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
context.send(viewAction: .loadPreviousPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func opacityForItem(_ item: RoomTimelineViewProvider) -> Double {
|
|
||||||
guard case let .reply(selectedItemId, _) = context.viewState.composerMode else {
|
|
||||||
return 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedItemId == item.id ? 1.0 : 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isRunningPreviews: Bool {
|
|
||||||
#if DEBUG
|
|
||||||
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
|
||||||
#else
|
|
||||||
return false
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct PinnedItem: Equatable {
|
|
||||||
let id: String
|
|
||||||
let anchor: UnitPoint
|
|
||||||
let animated: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TimelineItemList_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
|
||||||
roomName: nil)
|
|
||||||
|
|
||||||
TimelineItemList(visibleEdges: .constant([]), scrollToBottomPublisher: PassthroughSubject())
|
|
||||||
.environmentObject(viewModel.context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2022 New Vector Ltd
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct VisibleEdgesKey: PreferenceKey {
|
|
||||||
static var defaultValue: [VerticalEdge] = []
|
|
||||||
|
|
||||||
static func reduce(value: inout [VerticalEdge], nextValue: () -> [VerticalEdge]) {
|
|
||||||
value = nextValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A SwiftUI scroll view with the following customisations for a room timeline
|
|
||||||
/// - The content is laid out starting at the bottom.
|
|
||||||
/// - Top and bottom edge visibility detection for triggering other behaviours.
|
|
||||||
struct TimelineScrollView<Content: View>: View {
|
|
||||||
@Binding var visibleEdges: [VerticalEdge]
|
|
||||||
|
|
||||||
@ViewBuilder var content: () -> Content
|
|
||||||
|
|
||||||
/// A small threshold added to the edge detection to allow a bit of leniency.
|
|
||||||
private let edgeDetectionThreshold: CGFloat = 15
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { scrollViewGeometry in
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Spacer()
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
.frame(minHeight: scrollViewGeometry.size.height)
|
|
||||||
.background {
|
|
||||||
GeometryReader { contentGeometry in
|
|
||||||
Color.clear
|
|
||||||
.preference(key: VisibleEdgesKey.self,
|
|
||||||
value: visibleEdges(of: contentGeometry, in: scrollViewGeometry))
|
|
||||||
}
|
|
||||||
.onPreferenceChange(VisibleEdgesKey.self) {
|
|
||||||
visibleEdges = $0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func visibleEdges(of contentGeometry: GeometryProxy, in scrollViewGeometry: GeometryProxy) -> [VerticalEdge] {
|
|
||||||
let frame = contentGeometry.frame(in: .global)
|
|
||||||
let isTopVisible = scrollViewGeometry.frame(in: .global).contains(CGPoint(x: frame.midX, y: frame.minY + edgeDetectionThreshold))
|
|
||||||
let isBottomVisible = scrollViewGeometry.frame(in: .global).contains(CGPoint(x: frame.midX, y: frame.maxY - edgeDetectionThreshold))
|
|
||||||
|
|
||||||
switch (isTopVisible, isBottomVisible) {
|
|
||||||
case (false, false):
|
|
||||||
return []
|
|
||||||
case (true, false):
|
|
||||||
return [.top]
|
|
||||||
case (false, true):
|
|
||||||
return [.bottom]
|
|
||||||
case (true, true):
|
|
||||||
return [.top, .bottom]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2022 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
class TimelineItemCell: UITableViewCell {
|
||||||
|
static let reuseIdentifier = "TimelineItemCell"
|
||||||
|
|
||||||
|
var item: RoomTimelineViewProvider?
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
item = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A table view wrapper that displays the timeline of a room.
|
||||||
|
struct TimelineTableView: UIViewRepresentable {
|
||||||
|
@EnvironmentObject private var viewModelContext: RoomScreenViewModel.Context
|
||||||
|
@Environment(\.timelineStyle) private var timelineStyle
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITableView {
|
||||||
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
tableView.register(TimelineItemCell.self, forCellReuseIdentifier: TimelineItemCell.reuseIdentifier)
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.allowsSelection = false
|
||||||
|
tableView.keyboardDismissMode = .onDrag
|
||||||
|
context.coordinator.tableView = tableView
|
||||||
|
viewModelContext.send(viewAction: .paginateBackwards)
|
||||||
|
return tableView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITableView, context: Context) {
|
||||||
|
context.coordinator.update()
|
||||||
|
|
||||||
|
if context.coordinator.timelineStyle != timelineStyle {
|
||||||
|
context.coordinator.timelineStyle = timelineStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(viewModelContext: viewModelContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Coordinator
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class Coordinator: NSObject {
|
||||||
|
let viewModelContext: RoomScreenViewModel.Context
|
||||||
|
|
||||||
|
var tableView: UITableView? {
|
||||||
|
didSet {
|
||||||
|
registerFrameObserver()
|
||||||
|
configureDataSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var timelineStyle: TimelineStyle = .bubbles
|
||||||
|
var timelineItems: [RoomTimelineViewProvider] = [] {
|
||||||
|
didSet {
|
||||||
|
guard !scrollAdapter.isScrolling.value else {
|
||||||
|
// Delay updating until scrolling has stopped as programatic
|
||||||
|
// changes to the scroll position kills any inertia.
|
||||||
|
hasPendingUpdates = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
applySnapshot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mode of the message composer. This is used to render selected
|
||||||
|
/// items in the timeline when replying, editing etc.
|
||||||
|
var composerMode: RoomScreenComposerMode = .default {
|
||||||
|
didSet {
|
||||||
|
// Reload the visible items in order to update their opacity.
|
||||||
|
// Applying a snapshot won't work in this instance as the items don't change.
|
||||||
|
reloadVisibleItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether or not the timeline is waiting for more messages to be added to the top.
|
||||||
|
var isBackPaginating = false {
|
||||||
|
didSet {
|
||||||
|
// Paginate again if the threshold hasn't been satisfied.
|
||||||
|
paginateBackwardsPublisher.send(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The table's diffable data source.
|
||||||
|
private var dataSource: UITableViewDiffableDataSource<TimelineSection, RoomTimelineViewProvider>?
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
/// The scroll view adapter used to detect whether scrolling is in progress.
|
||||||
|
private let scrollAdapter = ScrollViewAdapter()
|
||||||
|
/// A publisher used to throttle back pagination requests.
|
||||||
|
///
|
||||||
|
/// Our view actions get wrapped in a `Task` so it is possible that a second call in
|
||||||
|
/// quick succession can execute before ``isBackPaginating`` becomes `true`.
|
||||||
|
private let paginateBackwardsPublisher = PassthroughSubject<Void, Never>()
|
||||||
|
/// Whether or not the ``timelineItems`` value should be applied when scrolling stops.
|
||||||
|
private var hasPendingUpdates = false
|
||||||
|
/// The observation token used to handle frame changes.
|
||||||
|
private var frameObserverToken: NSKeyValueObservation?
|
||||||
|
/// Yucky hack to fix some layouts where the scroll view doesn't make it to the bottom on keyboard appearance.
|
||||||
|
var keyboardWillShowLayout: LayoutDescriptor?
|
||||||
|
|
||||||
|
init(viewModelContext: RoomScreenViewModel.Context) {
|
||||||
|
self.viewModelContext = viewModelContext
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
viewModelContext.viewState.scrollToBottomPublisher
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.scrollToBottom(animated: true)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
scrollAdapter.isScrolling
|
||||||
|
.sink { [weak self] isScrolling in
|
||||||
|
guard !isScrolling, let self, self.hasPendingUpdates else { return }
|
||||||
|
// When scrolling has stopped, apply any pending updates.
|
||||||
|
self.applySnapshot()
|
||||||
|
self.hasPendingUpdates = false
|
||||||
|
self.paginateBackwardsPublisher.send(())
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
paginateBackwardsPublisher
|
||||||
|
.collect(.byTime(DispatchQueue.main, 0.1))
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.paginateBackwardsIfNeeded()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.keyboardWillShowLayout = self.layout()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self, let layout = self.keyboardWillShowLayout, layout.isBottomVisible else { return }
|
||||||
|
self.scrollToBottom(animated: false) // Force the bottom to be visible as some timelines misbehave.
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configures a diffable data source for the timeline's table view.
|
||||||
|
private func configureDataSource() {
|
||||||
|
guard let tableView else { return }
|
||||||
|
|
||||||
|
dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, timelineItem in
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: TimelineItemCell.reuseIdentifier, for: indexPath)
|
||||||
|
guard let self, let cell = cell as? TimelineItemCell else { return cell }
|
||||||
|
|
||||||
|
// A local reference to avoid capturing self in the cell configuration.
|
||||||
|
let viewModelContext = self.viewModelContext
|
||||||
|
|
||||||
|
cell.item = timelineItem
|
||||||
|
cell.contentConfiguration = UIHostingConfiguration {
|
||||||
|
timelineItem
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.opacity(viewModelContext.viewState.opacity(for: timelineItem))
|
||||||
|
.contextMenu {
|
||||||
|
viewModelContext.viewState.contextMenuBuilder?(timelineItem.id)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModelContext.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
viewModelContext.send(viewAction: .itemDisappeared(id: timelineItem.id))
|
||||||
|
}
|
||||||
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
|
viewModelContext.send(viewAction: .linkClicked(url: url))
|
||||||
|
return .systemAction
|
||||||
|
})
|
||||||
|
.onTapGesture {
|
||||||
|
viewModelContext.send(viewAction: .itemTapped(id: timelineItem.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.margins(.all, self.timelineStyle.rowInsets)
|
||||||
|
.minSize(height: 1)
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an observer on the frame of the table view in order to keep the
|
||||||
|
/// last item visible when the keyboard is shown or the window resizes.
|
||||||
|
private func registerFrameObserver() {
|
||||||
|
// Remove the existing observer if necessary
|
||||||
|
frameObserverToken?.invalidate()
|
||||||
|
|
||||||
|
frameObserverToken = tableView?.observe(\.frame, options: .new) { [weak self] _, _ in
|
||||||
|
self?.handleFrameChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the table's layout if necessary after the frame changed.
|
||||||
|
private nonisolated func handleFrameChange() {
|
||||||
|
Task { @MainActor in
|
||||||
|
guard self.composerMode == .default else { return }
|
||||||
|
|
||||||
|
// The table view is yet to update its layout so layout() returns a
|
||||||
|
// description of the timeline before the frame change occurs.
|
||||||
|
let previousLayout = self.layout()
|
||||||
|
if previousLayout.isBottomVisible {
|
||||||
|
self.scrollToBottom(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the table view's internal state from the view model's context.
|
||||||
|
func update() {
|
||||||
|
if timelineItems != viewModelContext.viewState.items {
|
||||||
|
timelineItems = viewModelContext.viewState.items
|
||||||
|
}
|
||||||
|
if isBackPaginating != viewModelContext.viewState.isBackPaginating {
|
||||||
|
isBackPaginating = viewModelContext.viewState.isBackPaginating
|
||||||
|
}
|
||||||
|
if composerMode != viewModelContext.viewState.composerMode {
|
||||||
|
composerMode = viewModelContext.viewState.composerMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the table view with the latest items from the ``timelineItems`` array. After
|
||||||
|
/// updating the data, the table will be scrolled to the bottom if it was visible otherwise
|
||||||
|
/// the scroll position will be updated to maintain the position of the last visible item.
|
||||||
|
private func applySnapshot() {
|
||||||
|
guard let dataSource else { return }
|
||||||
|
|
||||||
|
let previousLayout = layout()
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, RoomTimelineViewProvider>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
snapshot.appendItems(timelineItems)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
updateTopPadding()
|
||||||
|
|
||||||
|
guard snapshot.numberOfItems != previousLayout.numberOfItems else { return }
|
||||||
|
|
||||||
|
if previousLayout.isBottomVisible {
|
||||||
|
scrollToBottom(animated: false)
|
||||||
|
} else if let pinnedItem = previousLayout.pinnedItem {
|
||||||
|
restoreScrollPosition(using: pinnedItem, and: snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reloads all of the visible timeline items.
|
||||||
|
///
|
||||||
|
/// This only needs to be called when some state internal to this table view changes that
|
||||||
|
/// will affect the appearance of those items. Any updates to the items themselves should
|
||||||
|
/// use ``applySnapshot()`` which handles everything in the diffable data source.
|
||||||
|
private func reloadVisibleItems() {
|
||||||
|
guard let tableView, let visibleIndexPaths = tableView.indexPathsForVisibleRows, let dataSource else { return }
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.reloadItems(visibleIndexPaths.compactMap { dataSource.itemIdentifier(for: $0) })
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a description of the current layout in order to update the
|
||||||
|
/// scroll position after adding/updating items to the timeline.
|
||||||
|
private func layout() -> LayoutDescriptor {
|
||||||
|
guard let tableView, let dataSource else { return LayoutDescriptor() }
|
||||||
|
|
||||||
|
let snapshot = dataSource.snapshot()
|
||||||
|
var layout = LayoutDescriptor(numberOfItems: snapshot.numberOfItems)
|
||||||
|
|
||||||
|
guard !snapshot.itemIdentifiers.isEmpty else {
|
||||||
|
layout.isBottomVisible = true
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let bottomItemIndexPath = tableView.indexPathsForVisibleRows?.last,
|
||||||
|
let bottomItem = dataSource.itemIdentifier(for: bottomItemIndexPath)
|
||||||
|
else { return layout }
|
||||||
|
|
||||||
|
let bottomCellFrame = tableView.cellFrame(for: bottomItem)
|
||||||
|
layout.pinnedItem = PinnedItem(id: bottomItem.id, position: .bottom, frame: bottomCellFrame)
|
||||||
|
layout.isBottomVisible = bottomItem == snapshot.itemIdentifiers.last
|
||||||
|
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the additional padding added to the top of the table (via a header)
|
||||||
|
/// in order to fill the timeline from the bottom of the view upwards.
|
||||||
|
private func updateTopPadding() {
|
||||||
|
guard let tableView else { return }
|
||||||
|
|
||||||
|
let contentHeight = tableView.contentSize.height - (tableView.tableHeaderView?.frame.height ?? 0)
|
||||||
|
let height = tableView.visibleSize.height - contentHeight
|
||||||
|
|
||||||
|
if height > 0 {
|
||||||
|
let frame = CGRect(origin: .zero, size: CGSize(width: tableView.contentSize.width, height: height))
|
||||||
|
tableView.tableHeaderView = UIView(frame: frame) // Updating an existing view's height doesn't move the cells.
|
||||||
|
} else {
|
||||||
|
tableView.tableHeaderView = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether or not the bottom of the scroll view is visible (with some small tolerance added).
|
||||||
|
private func isAtBottom(of scrollView: UIScrollView) -> Bool {
|
||||||
|
scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.visibleSize.height - 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrolls to the bottom of the timeline.
|
||||||
|
private func scrollToBottom(animated: Bool) {
|
||||||
|
guard let lastItem = timelineItems.last,
|
||||||
|
let lastIndexPath = dataSource?.indexPath(for: lastItem)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
tableView?.scrollToRow(at: lastIndexPath, at: .bottom, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the position of the timeline using the supplied item and snapshot.
|
||||||
|
private func restoreScrollPosition(using pinnedItem: PinnedItem, and snapshot: NSDiffableDataSourceSnapshot<TimelineSection, RoomTimelineViewProvider>) {
|
||||||
|
guard let tableView,
|
||||||
|
let item = snapshot.itemIdentifiers.first(where: { $0.id == pinnedItem.id }),
|
||||||
|
let indexPath = dataSource?.indexPath(for: item)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
// Scroll the item into view.
|
||||||
|
tableView.scrollToRow(at: indexPath, at: pinnedItem.position, animated: false)
|
||||||
|
|
||||||
|
guard let oldFrame = pinnedItem.frame, let newFrame = tableView.cellFrame(for: item) else { return }
|
||||||
|
|
||||||
|
// Remove any unwanted offset that was added by scrollToRow.
|
||||||
|
let deltaY = newFrame.maxY - oldFrame.maxY
|
||||||
|
if deltaY != 0 {
|
||||||
|
tableView.contentOffset.y += deltaY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether or a backwards pagination is needed and requests one if so.
|
||||||
|
///
|
||||||
|
/// Prefer not to call this directly, instead using ``paginateBackwardsPublisher`` to throttle requests.
|
||||||
|
private func paginateBackwardsIfNeeded() {
|
||||||
|
guard let tableView,
|
||||||
|
!isBackPaginating,
|
||||||
|
!hasPendingUpdates,
|
||||||
|
tableView.contentOffset.y < tableView.visibleSize.height * 2.0
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
viewModelContext.send(viewAction: .paginateBackwards)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
|
extension TimelineTableView.Coordinator: UITableViewDelegate {
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
let isAtBottom = isAtBottom(of: scrollView)
|
||||||
|
|
||||||
|
if !viewModelContext.scrollToBottomButtonVisible, isAtBottom {
|
||||||
|
DispatchQueue.main.async { self.viewModelContext.scrollToBottomButtonVisible = true }
|
||||||
|
} else if viewModelContext.scrollToBottomButtonVisible, !isAtBottom {
|
||||||
|
DispatchQueue.main.async { self.viewModelContext.scrollToBottomButtonVisible = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
paginateBackwardsPublisher.send(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ScrollViewAdapter
|
||||||
|
|
||||||
|
// Required delegate methods are forwarded to the adapter so others can be implemented.
|
||||||
|
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
scrollAdapter.scrollViewWillBeginDragging(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
scrollAdapter.scrollViewDidEndDragging(scrollView, willDecelerate: decelerate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
scrollAdapter.scrollViewDidEndScrollingAnimation(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
scrollAdapter.scrollViewDidEndDecelerating(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||||
|
scrollAdapter.scrollViewDidScrollToTop(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout Types
|
||||||
|
|
||||||
|
extension TimelineTableView.Coordinator {
|
||||||
|
/// The sections of the table view used in the diffable data source.
|
||||||
|
enum TimelineSection { case main }
|
||||||
|
|
||||||
|
/// A description of the timeline's layout.
|
||||||
|
struct LayoutDescriptor {
|
||||||
|
var numberOfItems = 0
|
||||||
|
var pinnedItem: PinnedItem?
|
||||||
|
var isBottomVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An item that should have its position pinned after updates.
|
||||||
|
struct PinnedItem {
|
||||||
|
let id: String
|
||||||
|
let position: UITableView.ScrollPosition
|
||||||
|
let frame: CGRect?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cell Layout
|
||||||
|
|
||||||
|
private extension UITableView {
|
||||||
|
/// Returns the frame of the cell for a particular timeline item.
|
||||||
|
func cellFrame(for item: RoomTimelineViewProvider) -> CGRect? {
|
||||||
|
guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item == item }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return convert(timelineCell.frame, to: superview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct TimelineTableView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
|
timelineViewFactory: RoomTimelineViewFactory(),
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
roomName: "Preview room")
|
||||||
|
|
||||||
|
NavigationView {
|
||||||
|
RoomScreen(context: viewModel.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2022 New Vector Ltd
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
import Introspect
|
|
||||||
|
|
||||||
struct TimelineView: View {
|
|
||||||
@State private var visibleEdges: [VerticalEdge] = []
|
|
||||||
@State private var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
|
|
||||||
@State private var scrollToBottomButtonVisible = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack(alignment: .bottomTrailing) {
|
|
||||||
TimelineItemList(visibleEdges: $visibleEdges, scrollToBottomPublisher: scrollToBottomPublisher)
|
|
||||||
scrollToBottomButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var scrollToBottomButton: some View {
|
|
||||||
Button { scrollToBottomPublisher.send(()) } label: {
|
|
||||||
Image(uiImage: Asset.Images.timelineScrollToBottom.image)
|
|
||||||
.shadow(radius: 2.0)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.onChange(of: visibleEdges) { edges in
|
|
||||||
scrollToBottomButtonVisible = !edges.contains(.bottom)
|
|
||||||
}
|
|
||||||
.opacity(scrollToBottomButtonVisible ? 1.0 : 0.0)
|
|
||||||
.animation(.elementDefault, value: scrollToBottomButtonVisible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TimelineView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
|
||||||
roomName: nil)
|
|
||||||
|
|
||||||
TimelineView()
|
|
||||||
.environmentObject(viewModel.context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct MediaSourceProxy: Equatable {
|
struct MediaSourceProxy: Hashable {
|
||||||
let underlyingSource: MediaSource
|
let underlyingSource: MediaSource
|
||||||
|
|
||||||
init(source: MediaSource) {
|
init(source: MediaSource) {
|
||||||
@@ -32,9 +32,16 @@ struct MediaSourceProxy: Equatable {
|
|||||||
underlyingSource.url()
|
underlyingSource.url()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Equatable
|
}
|
||||||
|
|
||||||
|
// MARK: - Hashable
|
||||||
|
|
||||||
|
extension MediaSource: Hashable {
|
||||||
|
public static func == (lhs: MediaSource, rhs: MediaSource) -> Bool {
|
||||||
|
lhs.url() == rhs.url()
|
||||||
|
}
|
||||||
|
|
||||||
static func == (lhs: MediaSourceProxy, rhs: MediaSourceProxy) -> Bool {
|
public func hash(into hasher: inout Hasher) {
|
||||||
lhs.url == rhs.url
|
hasher.combine(url())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Represents all reactions of the same type for a single event.
|
/// Represents all reactions of the same type for a single event.
|
||||||
struct AggregatedReaction: Equatable, Hashable {
|
struct AggregatedReaction: Hashable {
|
||||||
/// The reaction that was sent.
|
/// The reaction that was sent.
|
||||||
let key: String
|
let key: String
|
||||||
/// The number of times this reactions was sent.
|
/// The number of times this reactions was sent.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ protocol MessageContentProtocol: RoomMessageEventContentProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The delivery status for the item.
|
/// The delivery status for the item.
|
||||||
enum MessageTimelineItemDeliveryStatus: Equatable {
|
enum MessageTimelineItemDeliveryStatus: Hashable {
|
||||||
case unknown
|
case unknown
|
||||||
case sending
|
case sending
|
||||||
case sent(elapsedTime: TimeInterval)
|
case sent(elapsedTime: TimeInterval)
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
enum TimelineItemInGroupState {
|
enum TimelineItemInGroupState: Hashable {
|
||||||
case single
|
case single
|
||||||
case beginning
|
case beginning
|
||||||
case middle
|
case middle
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
enum EncryptionType: Equatable {
|
enum EncryptionType: Hashable {
|
||||||
case megolmV1AesSha2(sessionId: String)
|
case megolmV1AesSha2(sessionId: String)
|
||||||
case olmV1Curve25519AesSha2(senderKey: String)
|
case olmV1Curve25519AesSha2(senderKey: String)
|
||||||
case unknown
|
case unknown
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Properties of a matrix event that are common between all timeline items.
|
/// Properties of a matrix event that are common between all timeline items.
|
||||||
struct RoomTimelineItemProperties: Equatable {
|
struct RoomTimelineItemProperties: Hashable {
|
||||||
/// Whether the item has been edited.
|
/// Whether the item has been edited.
|
||||||
var isEdited = false
|
var isEdited = false
|
||||||
/// The aggregated reactions that have been sent for this item.
|
/// The aggregated reactions that have been sent for this item.
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Equatable {
|
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let text: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum RoomTimelineViewProvider: Identifiable, Equatable {
|
enum RoomTimelineViewProvider: Identifiable, Hashable {
|
||||||
case text(TextRoomTimelineItem)
|
case text(TextRoomTimelineItem)
|
||||||
case separator(SeparatorRoomTimelineItem)
|
case separator(SeparatorRoomTimelineItem)
|
||||||
case image(ImageRoomTimelineItem)
|
case image(ImageRoomTimelineItem)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Re-write the timeline view to be backed by a UITableView to fix scroll glitches.
|
||||||
Reference in New Issue
Block a user