diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index d25eb8e60..f4d0e75df 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -36,6 +36,7 @@ final class AppSettings { case shareLocationEnabled case hasShownWelcomeScreen case notificationSettingsEnabled + case timelineDiffableAnimationsEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -226,4 +227,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.notificationSettingsEnabled, defaultValue: false, storageType: .userDefaults(store)) var notificationSettingsEnabled + + @UserPreference(key: UserDefaultsKeys.timelineDiffableAnimationsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var timelineDiffableAnimationsEnabled } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift index 9988c7206..dd5c40c96 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift @@ -97,6 +97,8 @@ class TimelineTableViewController: UIViewController { private var previousLayout: LayoutDescriptor? /// Whether or not the view has been shown on screen yet. private var hasAppearedOnce = false + /// Whether the scroll and the animations should happen + private var shouldAnimate = false init(coordinator: TimelineView.Coordinator, timelineStyle: TimelineStyle, @@ -168,6 +170,11 @@ class TimelineTableViewController: UIViewController { guard !hasAppearedOnce else { return } scrollToBottom(animated: false) hasAppearedOnce = true + // This is to prevent the SS proxy issue that forces a full refresh of all the timeline items + // the first time the timeline is opened + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.shouldAnimate = ServiceLocator.shared.settings.timelineDiffableAnimationsEnabled + } } override func didMove(toParent parent: UIViewController?) { @@ -231,8 +238,7 @@ class TimelineTableViewController: UIViewController { return cell } -// dataSource?.defaultRowAnimation = .automatic - + dataSource?.defaultRowAnimation = .fade tableView.delegate = self } @@ -249,13 +255,14 @@ class TimelineTableViewController: UIViewController { snapshot.appendSections([.main]) snapshot.appendItems(timelineItemsIDs) - MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: dataSource.snapshot().itemIdentifiers))") - dataSource.apply(snapshot, animatingDifferences: false) - - // Probably redundant now we observe content size changes… - // Leaving in place for the release and will reassess after. - updateTopPadding() - + let currentSnapshot = dataSource.snapshot() + MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: currentSnapshot.itemIdentifiers))") + + // We only animate if the last item has changed + // We don't care to animate backpagination since we want to keep the scrolling position when that happens + let animated = shouldAnimate && snapshot.itemIdentifiers.last != currentSnapshot.itemIdentifiers.last + dataSource.apply(snapshot, animatingDifferences: animated) + if previousLayout.isBottomVisible { scrollToBottom(animated: false) } else if let pinnedItem = previousLayout.pinnedItem { diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index f9f615104..68d8aa4c2 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -32,6 +32,7 @@ struct DeveloperOptionsScreenViewStateBindings { var locationEventsEnabled: Bool var shareLocationEnabled: Bool var notificationSettingsEnabled: Bool + var timelineDiffableAnimationsEnabled: Bool } enum DeveloperOptionsScreenViewAction { @@ -42,5 +43,6 @@ enum DeveloperOptionsScreenViewAction { case changedLocationEventsEnabled case changedShareLocationEnabled case changedNotificationSettingsEnabled + case changedTimelineDiffableAnimationsEnabled case clearCache } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 33796519f..82e366009 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -32,7 +32,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled, locationEventsEnabled: appSettings.locationEventsEnabled, shareLocationEnabled: appSettings.shareLocationEnabled, - notificationSettingsEnabled: appSettings.notificationSettingsEnabled) + notificationSettingsEnabled: appSettings.notificationSettingsEnabled, + timelineDiffableAnimationsEnabled: appSettings.timelineDiffableAnimationsEnabled) let state = DeveloperOptionsScreenViewState(bindings: bindings) super.init(initialViewState: state) @@ -58,6 +59,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings.shareLocationEnabled = state.bindings.shareLocationEnabled case .changedNotificationSettingsEnabled: appSettings.notificationSettingsEnabled = state.bindings.notificationSettingsEnabled + case .changedTimelineDiffableAnimationsEnabled: + appSettings.timelineDiffableAnimationsEnabled = state.bindings.timelineDiffableAnimationsEnabled case .clearCache: callback?(.clearCache) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 82935f274..b62fd321d 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -79,6 +79,13 @@ struct DeveloperOptionsScreen: View { .onChange(of: context.userSuggestionsEnabled) { _ in context.send(viewAction: .changedUserSuggestionsEnabled) } + + Toggle(isOn: $context.timelineDiffableAnimationsEnabled) { + Text("Enable diffable animations in the timeline") + } + .onChange(of: context.timelineDiffableAnimationsEnabled) { _ in + context.send(viewAction: .changedTimelineDiffableAnimationsEnabled) + } } Section { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 98fbade41..7cbb078f2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -355,8 +355,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)) let dateString = date.formatted(date: .complete, time: .omitted) - // Separators without stable identifiers cause UI glitches - let identifier = "\(chunkIndex)-\(dateString)" + let identifier = date.formatted(.iso8601.dateSeparator(.dash)) return SeparatorRoomTimelineItem(id: .init(timelineID: identifier), text: dateString) case .readMarker: return ReadMarkerRoomTimelineItem()