From ffd6c825ef66d6a4653cc35a5d2f7c6b3b11519c Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 22 May 2025 15:28:43 +0300 Subject: [PATCH] Introduce a new SwiftUI TimelineView layer (wrapping the representable) to hold all the timeline specific bindings that are currently duplicated throughout the screens a timeline is used. --- .../View/PinnedEventsTimelineScreen.swift | 25 +--------- .../Screens/RoomScreen/View/RoomScreen.swift | 47 ++----------------- .../View/ThreadTimelineScreen.swift | 30 +----------- .../TimelineTableViewController.swift | 4 +- .../Screens/Timeline/View/TimelineView.swift | 45 +++++++++++++++++- 5 files changed, 52 insertions(+), 99 deletions(-) diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 3a8935952..430404bb4 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -28,26 +28,6 @@ struct PinnedEventsTimelineScreen: View { .background(.compound.bgCanvasDefault) .interactiveDismissDisabled() .timelineMediaPreview(viewModel: $context.mediaPreviewViewModel) - .sheet(item: $timelineContext.manageMemberViewModel) { - ManageRoomMemberSheetView(context: $0.context) - } - .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } - .sheet(item: $timelineContext.actionMenuInfo) { info in - let actions = TimelineItemMenuActionProvider(timelineItem: info.item, - canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf, - canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers, - canCurrentUserPin: timelineContext.viewState.canCurrentUserPin, - pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, - isDM: timelineContext.viewState.isDirectOneToOneRoom, - isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - timelineKind: timelineContext.viewState.timelineKind, - emojiProvider: timelineContext.viewState.emojiProvider) - .makeActions() - if let actions { - TimelineItemMenu(item: info.item, actions: actions) - .environmentObject(timelineContext) - } - } } @ViewBuilder @@ -68,10 +48,7 @@ struct PinnedEventsTimelineScreen: View { .padding(.top, 48) .padding(.horizontal, 16) } else { - TimelineView() - .id(timelineContext.viewState.roomID) - .environmentObject(timelineContext) - .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) + TimelineView(timelineContext: timelineContext) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index ce7e431a1..e7f3a526a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -26,7 +26,10 @@ struct RoomScreen: View { } var body: some View { - timeline + TimelineView(timelineContext: timelineContext) + .overlay(alignment: .bottomTrailing) { + scrollToBottomButton + } .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .overlay(alignment: .top) { pinnedItemsBanner @@ -65,38 +68,6 @@ struct RoomScreen: View { .toolbar { toolbar } .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .overlay { loadingIndicator } - .alert(item: $timelineContext.alertInfo) - .sheet(item: $timelineContext.manageMemberViewModel) { - ManageRoomMemberSheetView(context: $0.context) - } - .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } - .sheet(item: $timelineContext.actionMenuInfo) { info in - let actions = TimelineItemMenuActionProvider(timelineItem: info.item, - canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf, - canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers, - canCurrentUserPin: timelineContext.viewState.canCurrentUserPin, - pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, - isDM: timelineContext.viewState.isDirectOneToOneRoom, - isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - timelineKind: timelineContext.viewState.timelineKind, - emojiProvider: timelineContext.viewState.emojiProvider) - .makeActions() - if let actions { - TimelineItemMenu(item: info.item, actions: actions) - .environmentObject(timelineContext) - } - } - .sheet(item: $timelineContext.reactionSummaryInfo) { - ReactionsSummaryView(reactions: $0.reactions, - members: timelineContext.viewState.members, - mediaProvider: timelineContext.mediaProvider, - selectedReactionKey: $0.selectedKey) - .edgesIgnoringSafeArea([.bottom]) - } - .sheet(item: $timelineContext.readReceiptsSummaryInfo) { - ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) - .environmentObject(timelineContext) - } .timelineMediaPreview(viewModel: $roomContext.mediaPreviewViewModel) .track(screen: .Room) .onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in @@ -110,16 +81,6 @@ struct RoomScreen: View { } .sentryTrace("\(Self.self)") } - - private var timeline: some View { - TimelineView() - .id(timelineContext.viewState.roomID) - .environmentObject(timelineContext) - .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) - .overlay(alignment: .bottomTrailing) { - scrollToBottomButton - } - } @ViewBuilder private var pinnedItemsBanner: some View { diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift index 1479c8f4c..c00ca36c4 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift @@ -13,39 +13,11 @@ struct ThreadTimelineScreen: View { @ObservedObject var timelineContext: TimelineViewModel.Context var body: some View { - content + TimelineView(timelineContext: timelineContext) .navigationTitle("Thread") .navigationBarTitleDisplayMode(.inline) .background(.compound.bgCanvasDefault) .interactiveDismissDisabled() .timelineMediaPreview(viewModel: $context.mediaPreviewViewModel) - .sheet(item: $timelineContext.manageMemberViewModel) { - ManageRoomMemberSheetView(context: $0.context) - } - .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } - .sheet(item: $timelineContext.actionMenuInfo) { info in - let actions = TimelineItemMenuActionProvider(timelineItem: info.item, - canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf, - canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers, - canCurrentUserPin: timelineContext.viewState.canCurrentUserPin, - pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, - isDM: timelineContext.viewState.isDirectOneToOneRoom, - isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - timelineKind: timelineContext.viewState.timelineKind, - emojiProvider: timelineContext.viewState.emojiProvider) - .makeActions() - if let actions { - TimelineItemMenu(item: info.item, actions: actions) - .environmentObject(timelineContext) - } - } - } - - @ViewBuilder - private var content: some View { - TimelineView() - .id(timelineContext.viewState.roomID) - .environmentObject(timelineContext) - .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) } } diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index 17910355c..1b1b9a946 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -45,7 +45,7 @@ class TypingMembersObservableObject: ObservableObject { /// extra keyboard handling magic that wasn't playing well with SwiftUI (as of iOS 16.1). /// Also this TableViewController uses a **flipped tableview** class TimelineTableViewController: UIViewController { - private let coordinator: TimelineView.Coordinator + private let coordinator: TimelineViewRepresentable.Coordinator private let tableView = UITableView(frame: .zero, style: .plain) var timelineItemsDictionary = OrderedDictionary() { @@ -168,7 +168,7 @@ class TimelineTableViewController: UIViewController { /// Whether or not the view has been shown on screen yet. private var hasAppearedOnce = false - init(coordinator: TimelineView.Coordinator, + init(coordinator: TimelineViewRepresentable.Coordinator, isScrolledToBottom: Binding, scrollToBottomPublisher: PassthroughSubject) { self.coordinator = coordinator diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 072cae9e6..8cbffded9 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -8,8 +8,51 @@ import SwiftUI import WysiwygComposer +struct TimelineView: View { + @ObservedObject var timelineContext: TimelineViewModel.Context + + var body: some View { + TimelineViewRepresentable() + .id(timelineContext.viewState.roomID) + .environment(\.focussedEventID, timelineContext.viewState.timelineState.focussedEvent?.eventID) + .alert(item: $timelineContext.alertInfo) + .sheet(item: $timelineContext.manageMemberViewModel) { + ManageRoomMemberSheetView(context: $0.context) + } + .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } + .sheet(item: $timelineContext.actionMenuInfo) { info in + let actions = TimelineItemMenuActionProvider(timelineItem: info.item, + canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf, + canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers, + canCurrentUserPin: timelineContext.viewState.canCurrentUserPin, + pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, + isDM: timelineContext.viewState.isDirectOneToOneRoom, + isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, + timelineKind: timelineContext.viewState.timelineKind, + emojiProvider: timelineContext.viewState.emojiProvider) + .makeActions() + if let actions { + TimelineItemMenu(item: info.item, actions: actions) + } + } + .sheet(item: $timelineContext.reactionSummaryInfo) { + ReactionsSummaryView(reactions: $0.reactions, + members: timelineContext.viewState.members, + mediaProvider: timelineContext.mediaProvider, + selectedReactionKey: $0.selectedKey) + .edgesIgnoringSafeArea([.bottom]) + } + .sheet(item: $timelineContext.readReceiptsSummaryInfo) { + ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) + } + // Ensure these are available in the sheets as well. The order is important. + .environmentObject(timelineContext) + .environment(\.timelineContext, timelineContext) + } +} + /// A table view wrapper that displays the timeline of a room. -struct TimelineView: UIViewControllerRepresentable { +struct TimelineViewRepresentable: UIViewControllerRepresentable { @EnvironmentObject private var viewModelContext: TimelineViewModel.Context @Environment(\.openURL) var openURL