Permalink polish (#2771)

* Fix unbalanced padding.

* Animate the scroll to an event when it is already loaded.

* Perform the timeline clearance immediately before replacing the items.
Hides the empty timeline that was briefly shown.

* Make switching timeline more resilient.
This commit is contained in:
Doug
2024-05-02 17:51:38 +01:00
committed by GitHub
parent ec19b5a0c4
commit d067e844da
23 changed files with 152 additions and 98 deletions
@@ -96,7 +96,7 @@ struct RoomScreen: View {
.id(context.viewState.roomID)
.environmentObject(context)
.environment(\.timelineStyle, context.viewState.timelineStyle)
.environment(\.focussedEventID, context.viewState.timelineViewState.focussedEventID)
.environment(\.focussedEventID, context.viewState.timelineViewState.focussedEvent?.eventID)
.overlay(alignment: .bottomTrailing) {
scrollToBottomButton
}
@@ -28,6 +28,7 @@ private struct HighlightedTimelineItemModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding(.top, isHighlighted ? 1 : 0)
.background {
if isHighlighted {
VStack(spacing: 0) {
@@ -47,7 +48,9 @@ private struct HighlightedTimelineItemModifier: ViewModifier {
}
}
// swiftlint:disable line_length
// MARK: - Previews
// swiftlint:disable line_length blanket_disable_command
struct HighlightedTimelineItemModifier_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
ScrollView {
@@ -67,6 +70,7 @@ struct HighlightedTimelineItemModifier_Previews: PreviewProvider, TestablePrevie
.highlightedTimelineItem(true)
}
}
.previewDisplayName("Layout")
}
struct Bubble: View {
@@ -85,4 +89,25 @@ struct HighlightedTimelineItemModifier_Previews: PreviewProvider, TestablePrevie
}
}
// swiftlint:enable line_length
/// A preview that allows quick testing of the highlight appearance across various timeline scenarios.
struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
static let focussedEventID = "RoomTimelineItemFixtures.default.5"
static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Preview room")),
focussedEventID: focussedEventID,
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
static var previews: some View {
NavigationStack {
RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar.mock())
}
.previewDisplayName("Timeline")
}
}
@@ -66,7 +66,7 @@ class TimelineTableViewController: UIViewController {
}
applySnapshot()
if timelineItemsDictionary.isEmpty {
paginatePublisher.send()
}
@@ -104,15 +104,14 @@ class TimelineTableViewController: UIViewController {
}
}
/// The ID of the focussed event if navigating to an event permalink within the room.
var focussedEventID: String?
/// Whether the timeline should scroll to `focussedEventID` when that item is added to the data source.
/// This is necessary as the focussed event can be set before the timeline builder has built its item.
var focussedEventNeedsDisplay = false {
/// Whether the table view is about to load items from a new timeline or not.
var isSwitchingTimelines = false
/// The focussed event if navigating to an event permalink within the room.
var focussedEvent: TimelineViewState.FocussedEvent? {
didSet {
guard focussedEventNeedsDisplay, let focussedEventID else { return }
scrollToItem(eventID: focussedEventID, animated: false)
guard let focussedEvent, focussedEvent.appearance != .hasAppeared else { return }
scrollToItem(eventID: focussedEvent.eventID, animated: focussedEvent.appearance == .animated)
}
}
@@ -300,7 +299,7 @@ class TimelineTableViewController: UIViewController {
let newestItemIdentifier = snapshot.mainItemIdentifiers.first
let currentNewestItemIdentifier = currentSnapshot.mainItemIdentifiers.first
let newestItemIDChanged = snapshot.numberOfMainItems > 0 && currentSnapshot.numberOfMainItems > 0 && newestItemIdentifier != currentNewestItemIdentifier
let animated = isLive && newestItemIDChanged
let animated = isLive && !isSwitchingTimelines && newestItemIDChanged
let layout: Layout? = if !isLive, newestItemIDChanged {
snapshotLayout()
@@ -310,10 +309,16 @@ class TimelineTableViewController: UIViewController {
dataSource.apply(snapshot, animatingDifferences: animated)
if let focussedEventID, focussedEventNeedsDisplay {
scrollToItem(eventID: focussedEventID, animated: false)
if let focussedEvent, focussedEvent.appearance != .hasAppeared {
scrollToItem(eventID: focussedEvent.eventID, animated: focussedEvent.appearance == .animated)
} else if let layout {
restoreLayout(layout)
} else if isSwitchingTimelines {
scrollToNewestItem(animated: false)
}
if isSwitchingTimelines {
coordinator.send(viewAction: .hasSwitchedTimeline)
}
}
@@ -335,7 +340,7 @@ class TimelineTableViewController: UIViewController {
/// Scrolls to the item with the corresponding event ID if loaded in the timeline.
private func scrollToItem(eventID: String, animated: Bool) {
if let kvPair = timelineItemsDictionary.first(where: { $0.value.identifier.eventID == focussedEventID }),
if let kvPair = timelineItemsDictionary.first(where: { $0.value.identifier.eventID == eventID }),
let indexPath = dataSource?.indexPath(for: kvPair.key) {
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
coordinator.send(viewAction: .scrolledToFocussedItem)
@@ -50,6 +50,10 @@ struct TimelineView: UIViewControllerRepresentable {
/// Updates the specified table view's properties from the current view state.
func update(tableViewController: TimelineTableViewController, timelineStyle: TimelineStyle) {
if tableViewController.isSwitchingTimelines != context.viewState.timelineViewState.isSwitchingTimelines {
// Must come before timelineItemsDictionary in order to disable animations.
tableViewController.isSwitchingTimelines = context.viewState.timelineViewState.isSwitchingTimelines
}
if tableViewController.timelineStyle != timelineStyle {
tableViewController.timelineStyle = timelineStyle
}
@@ -62,11 +66,8 @@ struct TimelineView: UIViewControllerRepresentable {
if tableViewController.isLive != context.viewState.timelineViewState.isLive {
tableViewController.isLive = context.viewState.timelineViewState.isLive
}
if tableViewController.focussedEventID != context.viewState.timelineViewState.focussedEventID {
tableViewController.focussedEventID = context.viewState.timelineViewState.focussedEventID
}
if tableViewController.focussedEventNeedsDisplay != context.viewState.timelineViewState.focussedEventNeedsDisplay {
tableViewController.focussedEventNeedsDisplay = context.viewState.timelineViewState.focussedEventNeedsDisplay
if tableViewController.focussedEvent != context.viewState.timelineViewState.focussedEvent {
tableViewController.focussedEvent = context.viewState.timelineViewState.focussedEvent
}
if tableViewController.typingMembers.members != context.viewState.typingMembers {