mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +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:
@@ -13,7 +13,7 @@ enum PinnedEventsTimelineFlowCoordinatorAction {
|
|||||||
case finished
|
case finished
|
||||||
case displayUser(userID: String)
|
case displayUser(userID: String)
|
||||||
case forwardedMessageToRoom(roomID: String)
|
case forwardedMessageToRoom(roomID: String)
|
||||||
case displayRoomScreenWithFocussedPin(eventID: String)
|
case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||||
@@ -87,8 +87,8 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
presentMapNavigator(geoURI: geoURI, description: description, timelineController: timelineController)
|
presentMapNavigator(geoURI: geoURI, description: description, timelineController: timelineController)
|
||||||
case .displayMessageForwarding(let forwardingItem):
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
presentMessageForwarding(with: forwardingItem)
|
presentMessageForwarding(with: forwardingItem)
|
||||||
case .displayRoomScreenWithFocussedPin(let eventID):
|
case .displayRoomScreenWithFocussedPin(let eventID, let threadRootEventID):
|
||||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -165,10 +165,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
case .thread(let roomID, let threadRootEventID, let focusEventID):
|
case .thread(let roomID, let threadRootEventID, let focusEventID):
|
||||||
Task {
|
Task {
|
||||||
|
let focusEvent: FocusEvent? = if let focusEventID {
|
||||||
|
.init(eventID: focusEventID, shouldSetPin: false)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
await handleRoomRoute(roomID: roomID,
|
await handleRoomRoute(roomID: roomID,
|
||||||
via: [],
|
via: [],
|
||||||
presentationAction: .thread(rootEventID: threadRootEventID,
|
presentationAction: .thread(rootEventID: threadRootEventID,
|
||||||
focusEventID: focusEventID),
|
focusEvent: focusEvent),
|
||||||
animated: animated)
|
animated: animated)
|
||||||
}
|
}
|
||||||
case .event(let eventID, let roomID, let via):
|
case .event(let eventID, let roomID, let via):
|
||||||
@@ -304,7 +309,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch await roomProxy.loadOrFetchEventDetails(for: focusEvent.eventID) {
|
switch await roomProxy.loadOrFetchEventDetails(for: focusEvent.eventID) {
|
||||||
case .success(let event):
|
case .success(let event):
|
||||||
if flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() {
|
if flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() {
|
||||||
stateMachine.tryEvent(.presentRoom(presentationAction: .thread(rootEventID: threadRootEventID, focusEventID: focusEvent.eventID)), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoom(presentationAction: .thread(rootEventID: threadRootEventID,
|
||||||
|
focusEvent: .init(eventID: focusEvent.eventID,
|
||||||
|
shouldSetPin: focusEvent.shouldSetPin))),
|
||||||
|
userInfo: EventUserInfo(animated: animated))
|
||||||
} else {
|
} else {
|
||||||
stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
@@ -570,9 +578,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .share(.text(_, let text)):
|
case .share(.text(_, let text)):
|
||||||
roomScreenCoordinator?.shareText(text)
|
roomScreenCoordinator?.shareText(text)
|
||||||
case .thread(let rootEventID, let focusEventID):
|
case .thread(let rootEventID, let focusEvent):
|
||||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: rootEventID, shouldSetPin: false))
|
if let focusEvent, let roomScreenCoordinator {
|
||||||
stateMachine.tryEvent(.presentThread(threadRootEventID: rootEventID, focusEventID: focusEventID))
|
roomScreenCoordinator.focusOnEvent(.init(eventID: rootEventID, shouldSetPin: false))
|
||||||
|
// Since the root is not the message we want to show in the pinned banner, but the actual event
|
||||||
|
if focusEvent.shouldSetPin {
|
||||||
|
roomScreenCoordinator.setSelectedPin(eventID: focusEvent.eventID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateMachine.tryEvent(.presentThread(threadRootEventID: rootEventID, focusEventID: focusEvent?.eventID))
|
||||||
case .none:
|
case .none:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -607,8 +621,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .share(.mediaFiles(_, let mediaFiles)):
|
case .share(.mediaFiles(_, let mediaFiles)):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url)),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .thread(let rootEventID, let focusEventID):
|
case .thread(let rootEventID, let focusEvent):
|
||||||
stateMachine.tryEvent(.presentThread(threadRootEventID: rootEventID, focusEventID: focusEventID))
|
stateMachine.tryEvent(.presentThread(threadRootEventID: rootEventID, focusEventID: focusEvent?.eventID))
|
||||||
case .share(.text), .eventFocus:
|
case .share(.text), .eventFocus:
|
||||||
break // These are both handled in the coordinator's init.
|
break // These are both handled in the coordinator's init.
|
||||||
case .none:
|
case .none:
|
||||||
@@ -693,11 +707,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
sendHandle: sendHandle))
|
sendHandle: sendHandle))
|
||||||
case .presentKnockRequestsList:
|
case .presentKnockRequestsList:
|
||||||
stateMachine.tryEvent(.presentKnockRequestsListScreen)
|
stateMachine.tryEvent(.presentKnockRequestsListScreen)
|
||||||
case .presentThread(let itemID):
|
case .presentThread(let threadRootEventID, let focussedEventID):
|
||||||
guard let threadRootEventID = itemID.eventID else {
|
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: focussedEventID))
|
||||||
fatalError("A thread root has always an eventID")
|
|
||||||
}
|
|
||||||
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: nil))
|
|
||||||
case .presentRoom(let roomID, let via):
|
case .presentRoom(let roomID, let via):
|
||||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID,
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID,
|
||||||
via: via,
|
via: via,
|
||||||
@@ -1478,9 +1489,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .forwardedMessageToRoom(let roomID):
|
case .forwardedMessageToRoom(let roomID):
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
|
||||||
case .displayRoomScreenWithFocussedPin(let eventID):
|
case .displayRoomScreenWithFocussedPin(let eventID, let threadRootEventID):
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true))))
|
if let threadRootEventID {
|
||||||
|
stateMachine.tryEvent(.presentRoom(presentationAction: .thread(rootEventID: threadRootEventID, focusEvent: .init(eventID: eventID, shouldSetPin: true))))
|
||||||
|
} else {
|
||||||
|
stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -25,18 +25,18 @@ extension RoomFlowCoordinator {
|
|||||||
enum PresentationAction: Hashable {
|
enum PresentationAction: Hashable {
|
||||||
case eventFocus(FocusEvent)
|
case eventFocus(FocusEvent)
|
||||||
case share(ShareExtensionPayload)
|
case share(ShareExtensionPayload)
|
||||||
case thread(rootEventID: String, focusEventID: String?)
|
case thread(rootEventID: String, focusEvent: FocusEvent?)
|
||||||
|
|
||||||
var focusedEvent: FocusEvent? {
|
var focusedEvent: FocusEvent? {
|
||||||
switch self {
|
switch self {
|
||||||
case .eventFocus(let focusEvent):
|
case .eventFocus(let focusEvent):
|
||||||
focusEvent
|
focusEvent
|
||||||
case .thread(let rootEventID, let focusEventID):
|
case .thread(let rootEventID, let focusEvent):
|
||||||
// Since this enum is for the room and not the threaded timeline,
|
// Since this enum is for the room and not the threaded timeline,
|
||||||
// we will focus the thread root event id, and not the event id itself
|
// we will focus the thread root event id, and not the event id itself
|
||||||
// which will be done at the thread presentation level
|
// which will be done at the thread presentation level
|
||||||
if focusEventID != nil {
|
if let focusEvent {
|
||||||
.init(eventID: rootEventID, shouldSetPin: false)
|
.init(eventID: rootEventID, shouldSetPin: focusEvent.shouldSetPin)
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-10
@@ -28,11 +28,10 @@ enum PinnedEventsTimelineScreenCoordinatorAction {
|
|||||||
case displayUser(userID: String)
|
case displayUser(userID: String)
|
||||||
case presentLocationViewer(geoURI: GeoURI, description: String?)
|
case presentLocationViewer(geoURI: GeoURI, description: String?)
|
||||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||||
case displayRoomScreenWithFocussedPin(eventID: String)
|
case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||||
private let parameters: PinnedEventsTimelineScreenCoordinatorParameters
|
|
||||||
private let viewModel: PinnedEventsTimelineScreenViewModelProtocol
|
private let viewModel: PinnedEventsTimelineScreenViewModelProtocol
|
||||||
private let timelineViewModel: TimelineViewModelProtocol
|
private let timelineViewModel: TimelineViewModelProtocol
|
||||||
|
|
||||||
@@ -44,9 +43,10 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(parameters: PinnedEventsTimelineScreenCoordinatorParameters) {
|
init(parameters: PinnedEventsTimelineScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
viewModel = PinnedEventsTimelineScreenViewModel(roomProxy: parameters.roomProxy,
|
||||||
|
userIndicatorController: parameters.userIndicatorController,
|
||||||
viewModel = PinnedEventsTimelineScreenViewModel(analyticsService: parameters.analytics)
|
appSettings: parameters.appSettings,
|
||||||
|
analyticsService: parameters.analytics)
|
||||||
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||||
timelineController: parameters.timelineController,
|
timelineController: parameters.timelineController,
|
||||||
userSession: parameters.userSession,
|
userSession: parameters.userSession,
|
||||||
@@ -68,9 +68,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .displayMessageForwarding(let forwardingItem):
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
|
||||||
case .viewInRoomTimeline(let itemID):
|
case .viewInRoomTimeline(let eventID, let threadRootEventID):
|
||||||
guard let eventID = itemID.eventID else { fatalError("A pinned event must have an event ID.") }
|
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
self.actionsSubject.send(.dismiss)
|
self.actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
@@ -90,8 +89,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||||
case .displayLocation(_, let geoURI, let description):
|
case .displayLocation(_, let geoURI, let description):
|
||||||
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
actionsSubject.send(.presentLocationViewer(geoURI: geoURI, description: description))
|
||||||
case .viewInRoomTimeline(let eventID):
|
case .viewInRoomTimeline(let eventID, let threadRootEventID):
|
||||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||||
// These other actions will not be handled in this view
|
// These other actions will not be handled in this view
|
||||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum PinnedEventsTimelineScreenViewModelAction {
|
enum PinnedEventsTimelineScreenViewModelAction {
|
||||||
case viewInRoomTimeline(itemID: TimelineItemIdentifier)
|
case viewInRoomTimeline(eventID: String, threadRootEventID: String?)
|
||||||
case displayMessageForwarding(MessageForwardingItem)
|
case displayMessageForwarding(MessageForwardingItem)
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-2
@@ -12,6 +12,9 @@ import SwiftUI
|
|||||||
typealias PinnedEventsTimelineScreenViewModelType = StateStoreViewModel<PinnedEventsTimelineScreenViewState, PinnedEventsTimelineScreenViewAction>
|
typealias PinnedEventsTimelineScreenViewModelType = StateStoreViewModel<PinnedEventsTimelineScreenViewState, PinnedEventsTimelineScreenViewAction>
|
||||||
|
|
||||||
class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelType, PinnedEventsTimelineScreenViewModelProtocol {
|
class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelType, PinnedEventsTimelineScreenViewModelProtocol {
|
||||||
|
private let roomProxy: JoinedRoomProxyProtocol
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
private let appSettings: AppSettings
|
||||||
private let analyticsService: AnalyticsService
|
private let analyticsService: AnalyticsService
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenViewModelAction, Never> = .init()
|
||||||
@@ -19,7 +22,13 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
|||||||
actionsSubject.eraseToAnyPublisher()
|
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
|
self.analyticsService = analyticsService
|
||||||
super.init(initialViewState: PinnedEventsTimelineScreenViewState())
|
super.init(initialViewState: PinnedEventsTimelineScreenViewState())
|
||||||
}
|
}
|
||||||
@@ -52,7 +61,10 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
|||||||
self.actionsSubject.send(.displayMessageForwarding(forwardingItem))
|
self.actionsSubject.send(.displayMessageForwarding(forwardingItem))
|
||||||
}
|
}
|
||||||
case .viewInRoomTimeline(let itemID):
|
case .viewInRoomTimeline(let itemID):
|
||||||
actionsSubject.send(.viewInRoomTimeline(itemID: itemID))
|
guard let eventID = itemID.eventID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task { await self.viewInRoomTimeline(eventID: eventID) }
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
state.bindings.mediaPreviewViewModel = nil
|
state.bindings.mediaPreviewViewModel = nil
|
||||||
}
|
}
|
||||||
@@ -61,4 +73,18 @@ class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelTy
|
|||||||
|
|
||||||
state.bindings.mediaPreviewViewModel = mediaPreviewViewModel
|
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
|
// MARK: - Previews
|
||||||
|
|
||||||
struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
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 = {
|
static let emptyTimelineViewModel: TimelineViewModel = {
|
||||||
let timelineController = MockTimelineController(timelineKind: .pinned)
|
let timelineController = MockTimelineController(timelineKind: .pinned)
|
||||||
timelineController.timelineItems = []
|
timelineController.timelineItems = []
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ enum RoomScreenCoordinatorAction {
|
|||||||
case presentPinnedEventsTimeline
|
case presentPinnedEventsTimeline
|
||||||
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||||
case presentKnockRequestsList
|
case presentKnockRequestsList
|
||||||
case presentThread(itemID: TimelineItemIdentifier)
|
case presentThread(threadRootEventID: String, focussedEventID: String?)
|
||||||
case presentRoom(roomID: String, via: [String])
|
case presentRoom(roomID: String, via: [String])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .displayResolveSendFailure(let failure, let sendHandle):
|
case .displayResolveSendFailure(let failure, let sendHandle):
|
||||||
actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
|
actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
|
||||||
case .displayThread(let itemID):
|
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):
|
case .composer(let action):
|
||||||
composerViewModel.process(timelineAction: action)
|
composerViewModel.process(timelineAction: action)
|
||||||
case .hasScrolled(direction: let direction):
|
case .hasScrolled(direction: let direction):
|
||||||
@@ -184,6 +187,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.presentRoom(roomID: roomID, via: via))
|
actionsSubject.send(.presentRoom(roomID: roomID, via: via))
|
||||||
case .displayMessageForwarding(let forwardingItem):
|
case .displayMessageForwarding(let forwardingItem):
|
||||||
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
|
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
|
||||||
|
case .displayThread(let threadRootEventID, let focussedEventID):
|
||||||
|
actionsSubject.send(.presentThread(threadRootEventID: threadRootEventID, focussedEventID: focussedEventID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -201,6 +206,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
Task { await timelineViewModel.focusOnEvent(eventID: eventID) }
|
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) {
|
func shareText(_ string: String) {
|
||||||
composerViewModel.process(timelineAction: .setMode(mode: .default)) // Make sure we're not e.g. replying.
|
composerViewModel.process(timelineAction: .setMode(mode: .default)) // Make sure we're not e.g. replying.
|
||||||
composerViewModel.process(timelineAction: .setText(plainText: string, htmlText: nil))
|
composerViewModel.process(timelineAction: .setText(plainText: string, htmlText: nil))
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import OrderedCollections
|
|||||||
|
|
||||||
enum RoomScreenViewModelAction: Equatable {
|
enum RoomScreenViewModelAction: Equatable {
|
||||||
case focusEvent(eventID: String)
|
case focusEvent(eventID: String)
|
||||||
|
case displayThread(threadRootEventID: String, focussedEventID: String)
|
||||||
case displayPinnedEventsTimeline
|
case displayPinnedEventsTimeline
|
||||||
case displayRoomDetails
|
case displayRoomDetails
|
||||||
case displayCall
|
case displayCall
|
||||||
|
|||||||
@@ -85,11 +85,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
override func process(viewAction: RoomScreenViewAction) {
|
override func process(viewAction: RoomScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .tappedPinnedEventsBanner:
|
case .tappedPinnedEventsBanner:
|
||||||
analyticsService.trackInteraction(name: .PinnedMessageBannerClick)
|
handleTappedPinnedEventsBanner()
|
||||||
if let eventID = state.pinnedEventsBannerState.selectedPinnedEventID {
|
|
||||||
actionsSubject.send(.focusEvent(eventID: eventID))
|
|
||||||
}
|
|
||||||
state.pinnedEventsBannerState.previousPin()
|
|
||||||
case .viewAllPins:
|
case .viewAllPins:
|
||||||
analyticsService.trackInteraction(name: .PinnedMessageBannerViewAllButton)
|
analyticsService.trackInteraction(name: .PinnedMessageBannerViewAllButton)
|
||||||
actionsSubject.send(.displayPinnedEventsTimeline)
|
actionsSubject.send(.displayPinnedEventsTimeline)
|
||||||
@@ -402,6 +398,27 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.handledEventIDs.subtract(failedIDs)
|
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
|
// MARK: Loading indicators
|
||||||
|
|
||||||
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"
|
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ enum TimelineViewModelAction {
|
|||||||
case displayThread(itemID: TimelineItemIdentifier)
|
case displayThread(itemID: TimelineItemIdentifier)
|
||||||
case composer(action: TimelineComposerAction)
|
case composer(action: TimelineComposerAction)
|
||||||
case hasScrolled(direction: ScrollDirection)
|
case hasScrolled(direction: ScrollDirection)
|
||||||
case viewInRoomTimeline(eventID: String)
|
case viewInRoomTimeline(eventID: String, threadRootEventID: String?)
|
||||||
case displayRoom(roomID: String, via: [String])
|
case displayRoom(roomID: String, via: [String])
|
||||||
case displayMediaDetails(item: EventBasedMessageTimelineItemProtocol)
|
case displayMediaDetails(item: EventBasedMessageTimelineItemProtocol)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
case .showDebugInfo(let debugInfo):
|
case .showDebugInfo(let debugInfo):
|
||||||
state.bindings.debugInfo = debugInfo
|
state.bindings.debugInfo = debugInfo
|
||||||
case .viewInRoomTimeline(let eventID):
|
case .viewInRoomTimeline(let eventID):
|
||||||
actionsSubject.send(.viewInRoomTimeline(eventID: eventID))
|
Task { await self.viewInRoomTimeline(eventID: eventID) }
|
||||||
case .displayThread(let itemID):
|
case .displayThread(let itemID):
|
||||||
actionsSubject.send(.displayThread(itemID: itemID))
|
actionsSubject.send(.displayThread(itemID: itemID))
|
||||||
}
|
}
|
||||||
@@ -503,6 +503,20 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
.store(in: &cancellables)
|
.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() {
|
private func setupAppSettingsSubscriptions() {
|
||||||
appSettings.$sharePresence
|
appSettings.$sharePresence
|
||||||
.weakAssign(to: \.state.showReadReceipts, on: self)
|
.weakAssign(to: \.state.showReadReceipts, on: self)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
viewModel = nil
|
viewModel = nil
|
||||||
|
AppSettings.resetAllSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPinnedEventsBanner() async throws {
|
func testPinnedEventsBanner() async throws {
|
||||||
@@ -109,6 +110,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
func testPinnedEventsBannerSelection() async throws {
|
func testPinnedEventsBannerSelection() async throws {
|
||||||
let roomProxyMock = JoinedRoomProxyMock(.init())
|
let roomProxyMock = JoinedRoomProxyMock(.init())
|
||||||
|
roomProxyMock.loadOrFetchEventDetailsForReturnValue = .success(TimelineEventSDKMock())
|
||||||
// setup a way to inject the mock of the pinned events timeline
|
// setup a way to inject the mock of the pinned events timeline
|
||||||
let pinnedTimelineMock = TimelineProxyMock()
|
let pinnedTimelineMock = TimelineProxyMock()
|
||||||
let pinnedTimelineItemProviderMock = TimelineItemProviderMock()
|
let pinnedTimelineItemProviderMock = TimelineItemProviderMock()
|
||||||
@@ -160,6 +162,67 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPinnedEventsBannerThreadedSelection() async throws {
|
||||||
|
ServiceLocator.shared.settings.threadsEnabled = true
|
||||||
|
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init())
|
||||||
|
let eventMock = TimelineEventSDKMock()
|
||||||
|
eventMock.threadRootEventIdReturnValue = "thread"
|
||||||
|
roomProxyMock.loadOrFetchEventDetailsForReturnValue = .success(eventMock)
|
||||||
|
|
||||||
|
// setup a way to inject the mock of the pinned events timeline
|
||||||
|
let pinnedTimelineMock = TimelineProxyMock()
|
||||||
|
let pinnedTimelineItemProviderMock = TimelineItemProviderMock()
|
||||||
|
pinnedTimelineMock.timelineItemProvider = pinnedTimelineItemProviderMock
|
||||||
|
pinnedTimelineItemProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher()
|
||||||
|
pinnedTimelineItemProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))),
|
||||||
|
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))),
|
||||||
|
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))]
|
||||||
|
roomProxyMock.pinnedEventsTimelineReturnValue = .success(pinnedTimelineMock)
|
||||||
|
|
||||||
|
let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()),
|
||||||
|
roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: "test1",
|
||||||
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
appHooks: AppHooks(),
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
// check if the banner is now in a loaded state and is showing the counter
|
||||||
|
var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
|
!viewState.pinnedEventsBannerState.isLoading
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.count, 3)
|
||||||
|
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
|
||||||
|
// And that is actually displaying the `initialSelectedPinEventID` which is gthe first one in the list
|
||||||
|
XCTAssertEqual(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex, 0)
|
||||||
|
|
||||||
|
// check if the banner scrolls when tapping the previous pin
|
||||||
|
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||||
|
viewState.pinnedEventsBannerState.selectedPinnedIndex == 2
|
||||||
|
}
|
||||||
|
let deferredAction1 = deferFulfillment(viewModel.actions) { action in
|
||||||
|
if case let .focusEvent(threadRootEventID) = action {
|
||||||
|
return threadRootEventID == "thread"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let deferredAction2 = deferFulfillment(viewModel.actions) { action in
|
||||||
|
if case let .displayThread(threadRootEventID, focussedEventID) = action {
|
||||||
|
return threadRootEventID == "thread" && focussedEventID == "test1"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.context.send(viewAction: .tappedPinnedEventsBanner)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
try await deferredAction1.fulfill()
|
||||||
|
try await deferredAction2.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
func testRoomInfoUpdate() async throws {
|
func testRoomInfoUpdate() async throws {
|
||||||
var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)
|
var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)
|
||||||
let roomProxyMock = JoinedRoomProxyMock(configuration)
|
let roomProxyMock = JoinedRoomProxyMock(configuration)
|
||||||
|
|||||||
Reference in New Issue
Block a user