Handle threaded pinned events (#4704)

* implemented focussing a threaded event through the banner tap

* view in timeline implementation for threaded events

* added a test and improved the existing one

* pr suggestions

* put the logic for fetching the event in the view models
This commit is contained in:
Mauro
2025-11-10 12:36:42 +01:00
committed by GitHub
parent 3e922ba80e
commit 2ccbe52bc6
13 changed files with 194 additions and 45 deletions
@@ -47,7 +47,7 @@ enum RoomScreenCoordinatorAction {
case presentPinnedEventsTimeline
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case presentKnockRequestsList
case presentThread(itemID: TimelineItemIdentifier)
case presentThread(threadRootEventID: String, focussedEventID: String?)
case presentRoom(roomID: String, via: [String])
}
@@ -142,7 +142,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
case .displayResolveSendFailure(let failure, let sendHandle):
actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
case .displayThread(let itemID):
actionsSubject.send(.presentThread(itemID: itemID))
guard let eventID = itemID.eventID else {
fatalError("A thread root has always an eventID")
}
actionsSubject.send(.presentThread(threadRootEventID: eventID, focussedEventID: nil))
case .composer(let action):
composerViewModel.process(timelineAction: action)
case .hasScrolled(direction: let direction):
@@ -184,6 +187,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentRoom(roomID: roomID, via: via))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
case .displayThread(let threadRootEventID, let focussedEventID):
actionsSubject.send(.presentThread(threadRootEventID: threadRootEventID, focussedEventID: focussedEventID))
}
}
.store(in: &cancellables)
@@ -201,6 +206,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
Task { await timelineViewModel.focusOnEvent(eventID: eventID) }
}
/// Sets the banner to selection to a specific event ID, even if not visible in the main timeline (like a threaded event).
func setSelectedPin(eventID: String) {
roomViewModel.setSelectedPinnedEventID(eventID)
}
func shareText(_ string: String) {
composerViewModel.process(timelineAction: .setMode(mode: .default)) // Make sure we're not e.g. replying.
composerViewModel.process(timelineAction: .setText(plainText: string, htmlText: nil))
@@ -11,6 +11,7 @@ import OrderedCollections
enum RoomScreenViewModelAction: Equatable {
case focusEvent(eventID: String)
case displayThread(threadRootEventID: String, focussedEventID: String)
case displayPinnedEventsTimeline
case displayRoomDetails
case displayCall
@@ -85,11 +85,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
override func process(viewAction: RoomScreenViewAction) {
switch viewAction {
case .tappedPinnedEventsBanner:
analyticsService.trackInteraction(name: .PinnedMessageBannerClick)
if let eventID = state.pinnedEventsBannerState.selectedPinnedEventID {
actionsSubject.send(.focusEvent(eventID: eventID))
}
state.pinnedEventsBannerState.previousPin()
handleTappedPinnedEventsBanner()
case .viewAllPins:
analyticsService.trackInteraction(name: .PinnedMessageBannerViewAllButton)
actionsSubject.send(.displayPinnedEventsTimeline)
@@ -402,6 +398,27 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.handledEventIDs.subtract(failedIDs)
}
private func handleTappedPinnedEventsBanner() {
analyticsService.trackInteraction(name: .PinnedMessageBannerClick)
if let eventID = state.pinnedEventsBannerState.selectedPinnedEventID {
Task {
switch await roomProxy.loadOrFetchEventDetails(for: eventID) {
case .success(let event):
if appSettings.threadsEnabled,
let threadRootEventID = event.threadRootEventId() {
actionsSubject.send(.focusEvent(eventID: threadRootEventID))
actionsSubject.send(.displayThread(threadRootEventID: threadRootEventID, focussedEventID: eventID))
} else {
actionsSubject.send(.focusEvent(eventID: eventID))
}
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
}
state.pinnedEventsBannerState.previousPin()
}
// MARK: Loading indicators
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"