Skip to content

Various timeline code improvements #4144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 26 additions & 39 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15297,7 +15297,7 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
}
}
}
class TimelineProviderMock: TimelineProviderProtocol, @unchecked Sendable {
class TimelineItemProviderMock: TimelineItemProviderProtocol, @unchecked Sendable {
var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> {
get { return underlyingUpdatePublisher }
set(value) { underlyingUpdatePublisher = value }
Expand All @@ -15322,11 +15322,11 @@ class TimelineProviderMock: TimelineProviderProtocol, @unchecked Sendable {

}
class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
var timelineProvider: TimelineProviderProtocol {
get { return underlyingTimelineProvider }
set(value) { underlyingTimelineProvider = value }
var timelineItemProvider: TimelineItemProviderProtocol {
get { return underlyingTimelineItemProvider }
set(value) { underlyingTimelineItemProvider = value }
}
var underlyingTimelineProvider: TimelineProviderProtocol!
var underlyingTimelineItemProvider: TimelineItemProviderProtocol!

//MARK: - subscribeForUpdates

Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Mocks/TimelineProviderMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK

@MainActor
class AutoUpdatingTimelineProviderMock: TimelineProvider {
class AutoUpdatingTimelineItemProviderMock: TimelineItemProvider {
static var timelineListener: TimelineListener?

private let innerPaginationStatePublisher: PassthroughSubject<PaginationState, Never>
Expand Down
10 changes: 5 additions & 5 deletions ElementX/Sources/Mocks/TimelineProxyMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ extension TimelineProxyMock {
sendReadReceiptForTypeReturnValue = .success(())

if configuration.isAutoUpdating {
underlyingTimelineProvider = AutoUpdatingTimelineProviderMock()
underlyingTimelineItemProvider = AutoUpdatingTimelineItemProviderMock()
} else {
let timelineProvider = TimelineProviderMock()
timelineProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached)
timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
underlyingTimelineProvider = timelineProvider
let timelineItemProvider = TimelineItemProviderMock()
timelineItemProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached)
timelineItemProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
underlyingTimelineItemProvider = timelineItemProvider
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class NetworkMonitor: NetworkMonitorProtocol {
}

init() {
queue = DispatchQueue(label: "io.element.elementx.networkmonitor", qos: .background)
queue = DispatchQueue(label: "io.element.elementx.network_monitor", qos: .background)
pathMonitor = NWPathMonitor()
reachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)

Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Other/SDKListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ extension SDKListener: ProgressWatcher where T == Double {
}
}

// MARK: TimelineProvider
// MARK: TimelineItemProvider

extension SDKListener: TimelineListener where T == [TimelineDiff] {
func onUpdate(diff: [TimelineDiff]) { onUpdateClosure(diff) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
}
.store(in: &cancellables)

roomProxy.timeline.timelineProvider.membershipChangePublisher.sink { [roomProxy] in
roomProxy.timeline.timelineItemProvider.membershipChangePublisher.sink { [roomProxy] in
Task { await roomProxy.updateMembers() }
}
.store(in: &cancellables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private let attributedStringBuilder: AttributedStringBuilderProtocol
private let appSettings: AppSettings

private var pinnedEventsTimelineProvider: TimelineProviderProtocol? {
private var pinnedEventsTimelineItemProvider: TimelineItemProviderProtocol? {
didSet {
guard let pinnedEventsTimelineProvider else {
guard let pinnedEventsTimelineItemProvider else {
return
}

state.pinnedEventsActionState = .loaded(numberOfItems: pinnedEventsTimelineProvider.itemProxies.filter(\.isEvent).count)
state.pinnedEventsActionState = .loaded(numberOfItems: pinnedEventsTimelineItemProvider.itemProxies.filter(\.isEvent).count)

pinnedEventsTimelineProvider.updatePublisher
pinnedEventsTimelineItemProvider.updatePublisher
// When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.sink { [weak self] updatedItems, _ in
Expand Down Expand Up @@ -86,7 +86,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.filter { $0 == .reachable }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.setupPinnedEventsTimelineProviderIfNeeded()
self?.setupPinnedEventsTimelineItemProviderIfNeeded()
}
.store(in: &cancellables)

Expand Down Expand Up @@ -439,8 +439,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}

private func setupPinnedEventsTimelineProviderIfNeeded() {
guard pinnedEventsTimelineProvider == nil else {
private func setupPinnedEventsTimelineItemProviderIfNeeded() {
guard pinnedEventsTimelineItemProvider == nil else {
return
}

Expand All @@ -449,8 +449,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
return
}

if pinnedEventsTimelineProvider == nil {
pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider
if pinnedEventsTimelineItemProvider == nil {
pinnedEventsTimelineItemProvider = pinnedEventsTimeline.timelineItemProvider
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
}
.store(in: &cancellables)

roomProxy.timeline.timelineProvider.membershipChangePublisher.sink { [roomProxy] _ in
roomProxy.timeline.timelineItemProvider.membershipChangePublisher.sink { [roomProxy] _ in
Task { await roomProxy.updateMembers() }
}
.store(in: &cancellables)
Expand Down
18 changes: 9 additions & 9 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
actionsSubject.eraseToAnyPublisher()
}

private var pinnedEventsTimelineProvider: TimelineProviderProtocol? {
private var pinnedEventsTimelineItemProvider: TimelineItemProviderProtocol? {
didSet {
guard let pinnedEventsTimelineProvider else {
guard let pinnedEventsTimelineItemProvider else {
return
}

buildPinnedEventContents(timelineItems: pinnedEventsTimelineProvider.itemProxies)
pinnedEventsTimelineProvider.updatePublisher
buildPinnedEventContents(timelineItems: pinnedEventsTimelineItemProvider.itemProxies)
pinnedEventsTimelineItemProvider.updatePublisher
// When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.sink { [weak self] updatedItems, _ in
Expand Down Expand Up @@ -193,7 +193,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
.filter { $0 == .reachable }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.setupPinnedEventsTimelineProviderIfNeeded()
self?.setupPinnedEventsTimelineItemProviderIfNeeded()
}
.store(in: &cancellables)

Expand Down Expand Up @@ -347,8 +347,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.canBan = await (try? roomProxy.canUserBan(userID: ownUserID).get()) == true
}

private func setupPinnedEventsTimelineProviderIfNeeded() {
guard pinnedEventsTimelineProvider == nil else {
private func setupPinnedEventsTimelineItemProviderIfNeeded() {
guard pinnedEventsTimelineItemProvider == nil else {
return
}

Expand All @@ -357,8 +357,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
return
}

if pinnedEventsTimelineProvider == nil {
pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider
if pinnedEventsTimelineItemProvider == nil {
pinnedEventsTimelineItemProvider = pinnedEventsTimeline.timelineItemProvider
}
}
}
Expand Down
47 changes: 4 additions & 43 deletions ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ enum TimelineInteractionHandlerAction {
case viewInRoomTimeline(eventID: String)
}

/// The interaction handler groups logic for dealing with various actions the user can take on a timeline's
/// view that would've normally been part of the ``TimelineViewModel``
@MainActor
class TimelineInteractionHandler {
private let roomProxy: JoinedRoomProxyProtocol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimelineItemIdentifier.UniqueID, RoomTimelineItemViewState>() {
Expand Down Expand Up @@ -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<Bool>,
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
self.coordinator = coordinator
Expand Down
Loading
Loading