diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 575c3726a..62d02fa1a 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -206,6 +206,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } else { handleAppRoute(.childRoomAlias(alias)) } + case .event(let roomID, let eventID): + if isExternalURL { + handleAppRoute(route) + } else { + handleAppRoute(.childEvent(roomID: roomID, eventID: eventID)) + } + case .eventOnRoomAlias(let alias, let eventID): + if isExternalURL { + handleAppRoute(route) + } else { + handleAppRoute(.childEventOnRoomAlias(alias: alias, eventID: eventID)) + } default: break } diff --git a/ElementX/Sources/Application/Navigation/AppRoutes.swift b/ElementX/Sources/Application/Navigation/AppRoutes.swift index 7d3679936..4dc1c729e 100644 --- a/ElementX/Sources/Application/Navigation/AppRoutes.swift +++ b/ElementX/Sources/Application/Navigation/AppRoutes.swift @@ -24,16 +24,24 @@ enum AppRoute: Equatable { case roomList /// A room, shown as the root of the stack (popping any child rooms). case room(roomID: String) - /// A room, shown as the root of the stack (popping any child rooms). + /// The same as ``room`` but using a room alias. case roomAlias(String) /// A room, pushed as a child of any existing rooms on the stack. case childRoom(roomID: String) - /// A room, pushed as a child of any existing rooms on the stack. + /// The same as ``childRoom`` but using a room alias. case childRoomAlias(String) /// The information about a particular room. case roomDetails(roomID: String) /// The profile of a member within the current room. case roomMemberDetails(userID: String) + /// An event within a room, shown as the root of the stack (popping any child rooms). + case event(roomID: String, eventID: String) + /// The same as ``event`` but using a room alias. + case eventOnRoomAlias(alias: String, eventID: String) + /// An event within a room, either within the last child on the stack or pushing a new child if needed. + case childEvent(roomID: String, eventID: String) + /// The same as ``childEvent`` but using a room alias. + case childEventOnRoomAlias(alias: String, eventID: String) /// The profile of a matrix user (outside of a room). case userProfile(userID: String) /// An Element Call link generated outside of a chat room. @@ -133,6 +141,12 @@ struct MatrixPermalinkParser: URLParser { return .roomAlias(alias) case .user(let id): return .userProfile(userID: id) + case .eventOnRoomId(let roomID, let eventID): + // return .event(roomID: roomID, eventID: eventID) + return nil + case .eventOnRoomAlias(let alias, let eventID): + // return .eventOnRoomAlias(alias: alias, eventID: eventID) + return nil default: return nil } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 7c9deb53f..0d825dc0b 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -39,6 +39,16 @@ enum RoomFlowCoordinatorAction: Equatable { } } +/// A value that represents where the room flow will be started. +enum RoomFlowCoordinatorEntryPoint: Hashable { + /// The flow will start by showing the room directly. + case room + /// The flow will start by showing the room, focussing on the supplied event ID. + case eventID(String) + /// The flow will start by showing the room's details. + case roomDetails +} + // swiftlint:disable:next type_body_length class RoomFlowCoordinator: FlowCoordinatorProtocol { private let roomID: String @@ -54,6 +64,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private var roomProxy: RoomProxyProtocol! + private var roomScreenCoordinator: RoomScreenCoordinator? private weak var joinRoomScreenCoordinator: JoinRoomScreenCoordinator? // periphery:ignore - used to avoid deallocation @@ -118,7 +129,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator { childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated) } else if roomID != roomProxy.id { - stateMachine.tryEvent(.startChildFlow(roomID: roomID), userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room), userInfo: EventUserInfo(animated: animated)) } else { MXLog.info("Ignoring presentation of the same room as a child of this one.") } @@ -143,12 +154,24 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } else { stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated)) } - case .roomAlias, .childRoomAlias, .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings: - break + case .event(let roomID, let eventID): + Task { await handleRoomRoute(roomID: roomID, focussedEventID: eventID, animated: animated) } + case .childEvent(let roomID, let eventID): + if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator { + childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated) + } else if roomID != roomProxy.id { + stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated)) + } else { + roomScreenCoordinator?.focusOnEvent(eventID: eventID) + } + case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: + break // These are converted to a room ID route one level above. + case .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings: + break // These routes can't be handled. } } - private func handleRoomRoute(roomID: String, animated: Bool) async { + private func handleRoomRoute(roomID: String, focussedEventID: String? = nil, animated: Bool) async { guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") } guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else { @@ -159,7 +182,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { switch roomProxy.membership { case .joined: await storeAndSubscribeToRoomProxy(roomProxy) - stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.presentRoom(focussedEventID: focussedEventID), userInfo: EventUserInfo(animated: animated)) default: stateMachine.tryEvent(.presentJoinRoomScreen, userInfo: EventUserInfo(animated: animated)) } @@ -307,7 +330,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { // Child flow - case (_, .startChildFlow(let roomID)): + case (_, .startChildFlow(let roomID, _)): return .presentingChild(childRoomID: roomID, previousState: fromState) case (.presentingChild(_, let previousState), .dismissChildFlow): return previousState @@ -328,8 +351,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (_, .dismissJoinRoomScreen, .complete): dismissFlow(animated: animated) - case (_, .presentRoom, .room): - Task { await self.presentRoom(fromState: context.fromState, animated: animated) } + case (_, .presentRoom(let focussedEventID), .room): + Task { await self.presentRoom(fromState: context.fromState, focussedEventID: focussedEventID, animated: animated) } case (_, .dismissFlow, .complete): dismissFlow(animated: animated) @@ -438,8 +461,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { rolesAndPermissionsFlowCoordinator = nil // Child flow - case (_, .startChildFlow(let roomID), .presentingChild): - Task { await self.startChildFlow(for: roomID) } + case (_, .startChildFlow(let roomID, let entryPoint), .presentingChild): + Task { await self.startChildFlow(for: roomID, entryPoint: entryPoint) } case (.presentingChild, .dismissChildFlow, _): childRoomFlowCoordinator = nil @@ -467,8 +490,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { /// Updates the navigation stack so it displays the timeline for the given room /// - Parameters: + /// - fromState: The state that asked for the room presentation. + /// - focussedEventID: An (optional) event ID that the timeline should be focussed around. /// - animated: whether it should animate the transition - private func presentRoom(fromState: State, animated: Bool) async { + private func presentRoom(fromState: State, focussedEventID: String? = nil, animated: Bool) async { // If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine // through the correct states before presenting the room navigationStackCoordinator.setSheetCoordinator(nil) @@ -507,6 +532,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy) let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, + focussedEventID: focussedEventID, timelineController: timelineController, mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), @@ -548,6 +574,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } .store(in: &cancellables) + roomScreenCoordinator = coordinator if !isChildFlow { let animated = UIDevice.current.userInterfaceIdiom == .phone ? animated : false navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in @@ -584,7 +611,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { if let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) { await storeAndSubscribeToRoomProxy(roomProxy) - stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.presentRoom(focussedEventID: nil), userInfo: EventUserInfo(animated: animated)) analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount)) } else { @@ -1022,7 +1049,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .openUserProfile: stateMachine.tryEvent(.presentUserProfile(userID: userID)) case .openDirectChat(let roomID): - stateMachine.tryEvent(.startChildFlow(roomID: roomID)) + stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room)) } } .store(in: &cancellables) @@ -1045,7 +1072,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { switch action { case .openDirectChat(let roomID): - stateMachine.tryEvent(.startChildFlow(roomID: roomID)) + stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room)) case .dismiss: break // Not supported when pushed. } @@ -1115,7 +1142,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { // We don't need to worry about passing in the room proxy as timelines are // cached. The local echo will be visible when fetching the room by its ID. - stateMachine.tryEvent(.startChildFlow(roomID: roomID)) + stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room)) } private func presentNotificationSettingsScreen() { @@ -1249,7 +1276,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { // MARK: - Child Flow - private func startChildFlow(for roomID: String) async { + private func startChildFlow(for roomID: String, entryPoint: RoomFlowCoordinatorEntryPoint) async { let coordinator = await RoomFlowCoordinator(roomID: roomID, userSession: userSession, isChildFlow: true, @@ -1275,7 +1302,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) childRoomFlowCoordinator = coordinator - coordinator.handleAppRoute(.room(roomID: roomID), animated: true) + switch entryPoint { + case .room: + coordinator.handleAppRoute(.room(roomID: roomID), animated: true) + case .eventID(let eventID): + coordinator.handleAppRoute(.event(roomID: roomID, eventID: eventID), animated: true) + case .roomDetails: + coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true) + } } } @@ -1329,7 +1363,7 @@ private extension RoomFlowCoordinator { case presentJoinRoomScreen case dismissJoinRoomScreen - case presentRoom + case presentRoom(focussedEventID: String?) case dismissFlow case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) @@ -1384,7 +1418,7 @@ private extension RoomFlowCoordinator { case dismissRolesAndPermissionsScreen // Child room flow events - case startChildFlow(roomID: String) + case startChildFlow(roomID: String, entryPoint: RoomFlowCoordinatorEntryPoint) case dismissChildFlow } } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 259fbc906..2cd780d67 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -196,39 +196,39 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { switch appRoute { case .room(let roomID): - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated)) + stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room), userInfo: .init(animated: animated)) case .roomAlias(let alias): - guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { - return - } - - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated)) + guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return } + await asyncHandleAppRoute(.room(roomID: roomID), animated: animated) case .childRoom(let roomID): if let roomFlowCoordinator { roomFlowCoordinator.handleAppRoute(appRoute, animated: animated) } else { - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated)) + stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room), userInfo: .init(animated: animated)) } case .childRoomAlias(let alias): - guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { - return - } - - if let roomFlowCoordinator { - roomFlowCoordinator.handleAppRoute(.childRoom(roomID: roomID), animated: animated) - } else { - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated)) - } + guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return } + await asyncHandleAppRoute(.childRoom(roomID: roomID), animated: animated) case .roomDetails(let roomID): if stateMachine.state.selectedRoomID == roomID { roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) } else { - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated)) + stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .roomDetails), userInfo: .init(animated: animated)) } case .roomList: roomFlowCoordinator?.clearRoute(animated: animated) case .roomMemberDetails: roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) + case .event(let roomID, let eventID): + stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .eventID(eventID)), userInfo: .init(animated: animated)) + case .eventOnRoomAlias(let alias, let eventID): + guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return } + await asyncHandleAppRoute(.event(roomID: roomID, eventID: eventID), animated: animated) + case .childEvent: + roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) + case .childEventOnRoomAlias(let alias, let eventID): + guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return } + await asyncHandleAppRoute(.childEvent(roomID: roomID, eventID: eventID), animated: animated) case .userProfile(let userID): stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated)) case .genericCallLink(let url): @@ -266,14 +266,18 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case (.initial, .start, .roomList): presentHomeScreen() attemptStartingOnboarding() - - case(.roomList(let selectedRoomID), .selectRoom(let roomID, let showingRoomDetails), .roomList): + case(.roomList(let selectedRoomID), .selectRoom(let roomID, let entryPoint), .roomList): if selectedRoomID == roomID { if let roomFlowCoordinator { - roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: animated) + let route: AppRoute = switch entryPoint { + case .room: .room(roomID: roomID) + case .eventID(let eventID): .event(roomID: roomID, eventID: eventID) + case .roomDetails: .roomDetails(roomID: roomID) + } + roomFlowCoordinator.handleAppRoute(route, animated: animated) } } else { - Task { await self.startRoomFlow(roomID: roomID, showingRoomDetails: showingRoomDetails, animated: animated) } + Task { await self.startRoomFlow(roomID: roomID, entryPoint: entryPoint, animated: animated) } } case(.roomList, .deselectRoom, .roomList): dismissRoomFlow(animated: animated) @@ -442,7 +446,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { // MARK: Room Flow - private func startRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async { + private func startRoomFlow(roomID: String, + entryPoint: RoomFlowCoordinatorEntryPoint, + animated: Bool) async { let coordinator = await RoomFlowCoordinator(roomID: roomID, userSession: userSession, isChildFlow: false, @@ -461,7 +467,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .finished: stateMachine.processEvent(.deselectRoom) case .presentRoom(let roomID): - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false)) + stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room)) case .presentCallScreen(let roomProxy): presentCallScreen(roomProxy: roomProxy) } @@ -474,10 +480,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator, animated: animated) } - if showingRoomDetails { - coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated) - } else { + switch entryPoint { + case .room: coordinator.handleAppRoute(.room(roomID: roomID), animated: animated) + case .eventID(let eventID): + coordinator.handleAppRoute(.event(roomID: roomID, eventID: eventID), animated: animated) + case .roomDetails: + coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated) } let availableInvitesCount = userSession.clientProxy.inviteSummaryProvider?.roomListPublisher.value.count ?? 0 @@ -518,7 +527,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { self.navigationSplitCoordinator.setSheetCoordinator(nil) case .openRoom(let roomID): self.navigationSplitCoordinator.setSheetCoordinator(nil) - self.stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false)) + self.stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room)) } } .store(in: &cancellables) @@ -540,7 +549,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { .sink { [weak self] action in switch action { case .openRoom(let roomID): - self?.stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false)) + self?.stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room)) } } .store(in: &cancellables) @@ -691,7 +700,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { switch action { case .openDirectChat(let roomID): navigationSplitCoordinator.setSheetCoordinator(nil) - stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false)) + stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room)) case .dismiss: navigationSplitCoordinator.setSheetCoordinator(nil) } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index 57551f471..3e58b554d 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -75,8 +75,9 @@ class UserSessionFlowCoordinatorStateMachine { case start /// Request presentation for a particular room - /// - Parameter roomID:the room identifier - case selectRoom(roomID: String, showingRoomDetails: Bool) + /// - Parameter roomID: The room identifier + /// - Parameter entryPoint: The starting point for the presented room. + case selectRoom(roomID: String, entryPoint: RoomFlowCoordinatorEntryPoint) /// The room screen has been dismissed case deselectRoom diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index b910a1f0d..faf38faf2 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -21,6 +21,7 @@ import WysiwygComposer struct RoomScreenCoordinatorParameters { let roomProxy: RoomProxyProtocol + var focussedEventID: String? let timelineController: RoomTimelineControllerProtocol let mediaProvider: MediaProviderProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol @@ -59,6 +60,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { init(parameters: RoomScreenCoordinatorParameters) { viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, + focussedEventID: parameters.focussedEventID, timelineController: parameters.timelineController, mediaProvider: parameters.mediaProvider, mediaPlayerProvider: parameters.mediaPlayerProvider, @@ -129,6 +131,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { .store(in: &cancellables) } + func focusOnEvent(eventID: String) { + Task { await viewModel.focusOnEvent(eventID: eventID) } + } + func stop() { viewModel.stop() } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index f18bd72f7..6b82eaa56 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -49,6 +49,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private var paginateBackwardsTask: Task? init(roomProxy: RoomProxyProtocol, + focussedEventID: String? = nil, timelineController: RoomTimelineControllerProtocol, mediaProvider: MediaProviderProtocol, mediaPlayerProvider: MediaPlayerProviderProtocol, @@ -90,6 +91,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol bindings: .init(reactionsCollapsed: [:])), imageProvider: mediaProvider) + // This may change to load the detached timeline directly. + if let focussedEventID { + Task { await focusOnEvent(eventID: focussedEventID) } + } + setupSubscriptions() setupDirectRoomSubscriptionsIfNeeded() @@ -208,6 +214,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } + func focusOnEvent(eventID: String) async { } + // MARK: - Private private func attach(_ attachment: ComposerAttachmentType) { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift index 96d1ffd9f..30792cad1 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift @@ -23,5 +23,7 @@ protocol RoomScreenViewModelProtocol { var actions: AnyPublisher { get } var context: RoomScreenViewModelType.Context { get } func process(composerAction: ComposerToolbarViewModelAction) + /// Updates the timeline to show and highlight the item with the corresponding event ID. + func focusOnEvent(eventID: String) async func stop() }