mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
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:
+9
-10
@@ -28,11 +28,10 @@ enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||
case displayUser(userID: String)
|
||||
case presentLocationViewer(geoURI: GeoURI, description: String?)
|
||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case displayRoomScreenWithFocussedPin(eventID: String)
|
||||
case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?)
|
||||
}
|
||||
|
||||
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: PinnedEventsTimelineScreenCoordinatorParameters
|
||||
private let viewModel: PinnedEventsTimelineScreenViewModelProtocol
|
||||
private let timelineViewModel: TimelineViewModelProtocol
|
||||
|
||||
@@ -44,9 +43,10 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(parameters: PinnedEventsTimelineScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = PinnedEventsTimelineScreenViewModel(analyticsService: parameters.analytics)
|
||||
viewModel = PinnedEventsTimelineScreenViewModel(roomProxy: parameters.roomProxy,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: parameters.analytics)
|
||||
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||
timelineController: parameters.timelineController,
|
||||
userSession: parameters.userSession,
|
||||
@@ -68,9 +68,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .displayMessageForwarding(let forwardingItem):
|
||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
||||
case .viewInRoomTimeline(let itemID):
|
||||
guard let eventID = itemID.eventID else { fatalError("A pinned event must have an event ID.") }
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||
case .viewInRoomTimeline(let eventID, let threadRootEventID):
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||
case .dismiss:
|
||||
self.actionsSubject.send(.dismiss)
|
||||
}
|
||||
@@ -90,8 +89,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||
case .displayLocation(_, let geoURI, let description):
|
||||
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
||||
case .viewInRoomTimeline(let eventID):
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||
case .viewInRoomTimeline(let eventID, let threadRootEventID):
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||
// These other actions will not be handled in this view
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
enum PinnedEventsTimelineScreenViewModelAction {
|
||||
case viewInRoomTimeline(itemID: TimelineItemIdentifier)
|
||||
case viewInRoomTimeline(eventID: String, threadRootEventID: String?)
|
||||
case displayMessageForwarding(MessageForwardingItem)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
+29
-3
@@ -12,14 +12,23 @@ import SwiftUI
|
||||
typealias PinnedEventsTimelineScreenViewModelType = StateStoreViewModel<PinnedEventsTimelineScreenViewState, PinnedEventsTimelineScreenViewAction>
|
||||
|
||||
class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelType, PinnedEventsTimelineScreenViewModelProtocol {
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let analyticsService: AnalyticsService
|
||||
|
||||
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(analyticsService: AnalyticsService) {
|
||||
|
||||
init(roomProxy: JoinedRoomProxyProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsService) {
|
||||
self.roomProxy = roomProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appSettings = appSettings
|
||||
self.analyticsService = analyticsService
|
||||
super.init(initialViewState: PinnedEventsTimelineScreenViewState())
|
||||
}
|
||||
@@ -52,7 +61,10 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
||||
self.actionsSubject.send(.displayMessageForwarding(forwardingItem))
|
||||
}
|
||||
case .viewInRoomTimeline(let itemID):
|
||||
actionsSubject.send(.viewInRoomTimeline(itemID: itemID))
|
||||
guard let eventID = itemID.eventID else {
|
||||
return
|
||||
}
|
||||
Task { await self.viewInRoomTimeline(eventID: eventID) }
|
||||
case .dismiss:
|
||||
state.bindings.mediaPreviewViewModel = nil
|
||||
}
|
||||
@@ -61,4 +73,18 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
||||
|
||||
state.bindings.mediaPreviewViewModel = mediaPreviewViewModel
|
||||
}
|
||||
|
||||
private func viewInRoomTimeline(eventID: String) async {
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: eventID) {
|
||||
case .success(let event):
|
||||
let threadRootEventID: String? = if appSettings.threadsEnabled {
|
||||
event.threadRootEventId()
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
actionsSubject.send(.viewInRoomTimeline(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||
case .failure:
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -66,7 +66,11 @@ struct PinnedEventsTimelineScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = PinnedEventsTimelineScreenViewModel(analyticsService: ServiceLocator.shared.analytics)
|
||||
static let viewModel = PinnedEventsTimelineScreenViewModel(roomProxy: JoinedRoomProxyMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: AppSettings(),
|
||||
analyticsService: ServiceLocator.shared.analytics)
|
||||
|
||||
static let emptyTimelineViewModel: TimelineViewModel = {
|
||||
let timelineController = MockTimelineController(timelineKind: .pinned)
|
||||
timelineController.timelineItems = []
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -28,7 +28,7 @@ enum TimelineViewModelAction {
|
||||
case displayThread(itemID: TimelineItemIdentifier)
|
||||
case composer(action: TimelineComposerAction)
|
||||
case hasScrolled(direction: ScrollDirection)
|
||||
case viewInRoomTimeline(eventID: String)
|
||||
case viewInRoomTimeline(eventID: String, threadRootEventID: String?)
|
||||
case displayRoom(roomID: String, via: [String])
|
||||
case displayMediaDetails(item: EventBasedMessageTimelineItemProtocol)
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
case .showDebugInfo(let debugInfo):
|
||||
state.bindings.debugInfo = debugInfo
|
||||
case .viewInRoomTimeline(let eventID):
|
||||
actionsSubject.send(.viewInRoomTimeline(eventID: eventID))
|
||||
Task { await self.viewInRoomTimeline(eventID: eventID) }
|
||||
case .displayThread(let itemID):
|
||||
actionsSubject.send(.displayThread(itemID: itemID))
|
||||
}
|
||||
@@ -503,6 +503,20 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func viewInRoomTimeline(eventID: String) async {
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: eventID) {
|
||||
case .success(let event):
|
||||
let threadRootEventID: String? = if appSettings.threadsEnabled {
|
||||
event.threadRootEventId()
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
actionsSubject.send(.viewInRoomTimeline(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||
case .failure:
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAppSettingsSubscriptions() {
|
||||
appSettings.$sharePresence
|
||||
.weakAssign(to: \.state.showReadReceipts, on: self)
|
||||
|
||||
Reference in New Issue
Block a user