mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Pinned Events Timeline actions and differentiation (#3182)
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum PinnedEventsTimelineFlowCoordinatorAction {
|
||||
case finished
|
||||
case displayUser(userID: String)
|
||||
case forwardedMessageToRoom(roomID: String)
|
||||
case displayRoomScreenWithFocussedPin(eventID: String)
|
||||
}
|
||||
|
||||
class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let userSession: UserSessionProtocol
|
||||
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appMediator: AppMediatorProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineFlowCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<PinnedEventsTimelineFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(navigationStackCoordinator: NavigationStackCoordinator,
|
||||
userSession: UserSessionProtocol,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appMediator: AppMediatorProtocol) {
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
self.userSession = userSession
|
||||
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
||||
self.roomProxy = roomProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appMediator = appMediator
|
||||
}
|
||||
|
||||
func start() {
|
||||
Task { await presentPinnedEventsTimeline() }
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
private func presentPinnedEventsTimeline() async {
|
||||
let userID = userSession.clientProxy.userID
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||
|
||||
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else {
|
||||
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
|
||||
}
|
||||
|
||||
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
mediaPlayerProvider: MediaPlayerProvider(),
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||
appMediator: appMediator))
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.finished)
|
||||
case .displayUser(let userID):
|
||||
actionsSubject.send(.displayUser(userID: userID))
|
||||
case .presentLocationViewer(let geoURI, let description):
|
||||
presentMapNavigator(geoURI: geoURI, description: description)
|
||||
case .displayMessageForwarding(let forwardingItem):
|
||||
presentMessageForwarding(with: forwardingItem)
|
||||
case .displayRoomScreenWithFocussedPin(let eventID):
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentMapNavigator(geoURI: GeoURI, description: String?) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let params = StaticLocationScreenCoordinatorParameters(interactionMode: .viewOnly(geoURI: geoURI, description: description), appMediator: appMediator)
|
||||
let coordinator = StaticLocationScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .selectedLocation:
|
||||
// We don't handle the sending/picker case in this flow
|
||||
break
|
||||
case .close:
|
||||
self.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
stackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
|
||||
}
|
||||
|
||||
private func presentMessageForwarding(with forwardingItem: MessageForwardingItem) {
|
||||
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let parameters = MessageForwardingScreenCoordinatorParameters(forwardingItem: forwardingItem,
|
||||
clientProxy: userSession.clientProxy,
|
||||
roomSummaryProvider: roomSummaryProvider,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
userIndicatorController: userIndicatorController)
|
||||
let coordinator = MessageForwardingScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .sent(let roomID):
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
actionsSubject.send(.forwardedMessageToRoom(roomID: roomID))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
stackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var pinnedEventsTimelineFlowCoordinator: PinnedEventsTimelineFlowCoordinator?
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var childRoomFlowCoordinator: RoomFlowCoordinator?
|
||||
|
||||
private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
|
||||
@@ -166,7 +168,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
} else if roomID != roomProxy.id {
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||
} else {
|
||||
roomScreenCoordinator?.focusOnEvent(eventID: eventID)
|
||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
||||
}
|
||||
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
|
||||
break // These are converted to a room ID route one level above.
|
||||
@@ -194,7 +196,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch room {
|
||||
case .joined(let roomProxy):
|
||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||
stateMachine.tryEvent(.presentRoom(focussedEventID: focussedEventID), userInfo: EventUserInfo(animated: animated))
|
||||
let focussedEvent = focussedEventID.map { FocusEvent(eventID: $0, shouldSetPin: false) }
|
||||
stateMachine.tryEvent(.presentRoom(focussedEvent: focussedEvent), userInfo: EventUserInfo(animated: animated))
|
||||
default:
|
||||
stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated))
|
||||
}
|
||||
@@ -270,16 +273,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .roomDetails(isRoot: false)
|
||||
|
||||
case (.room, .presentRoomMemberDetails(userID: let userID)):
|
||||
return .roomMemberDetails(userID: userID, fromRoomMembersList: false)
|
||||
return .roomMemberDetails(userID: userID, previousState: .room)
|
||||
case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)):
|
||||
return .roomMemberDetails(userID: userID, fromRoomMembersList: true)
|
||||
case (.roomMemberDetails(_, let fromRoomMembersList), .dismissRoomMemberDetails):
|
||||
return fromRoomMembersList ? .roomMembersList : .room
|
||||
return .roomMemberDetails(userID: userID, previousState: .roomMembersList)
|
||||
case (.roomMemberDetails(_, let previousState), .dismissRoomMemberDetails):
|
||||
return previousState
|
||||
|
||||
case (.roomMemberDetails(_, fromRoomMembersList: false), .presentUserProfile(let userID)):
|
||||
return .userProfile(userID: userID)
|
||||
case (.userProfile, .dismissUserProfile):
|
||||
return .room
|
||||
case (.roomMemberDetails(_, let previousState), .presentUserProfile(let userID)):
|
||||
return .userProfile(userID: userID, previousState: previousState)
|
||||
case (.userProfile(_, let previousState), .dismissUserProfile):
|
||||
return previousState
|
||||
|
||||
case (.roomDetails, .presentInviteUsersScreen):
|
||||
return .inviteUsersScreen(fromRoomMembersList: false)
|
||||
@@ -351,6 +354,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .rolesAndPermissions
|
||||
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen):
|
||||
return .roomDetails(isRoot: false)
|
||||
|
||||
case (.roomDetails, .presentRoomMemberDetails(let userID)):
|
||||
return .roomMemberDetails(userID: userID, previousState: fromState)
|
||||
|
||||
// Child flow
|
||||
|
||||
@@ -375,8 +381,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (_, .dismissJoinRoomScreen, .complete):
|
||||
dismissFlow(animated: animated)
|
||||
|
||||
case (_, .presentRoom(let focussedEventID), .room):
|
||||
Task { await self.presentRoom(fromState: context.fromState, focussedEventID: focussedEventID, animated: animated) }
|
||||
case (_, .presentRoom(let focussedEvent), .room):
|
||||
Task { await self.presentRoom(fromState: context.fromState, focussedEvent: focussedEvent, animated: animated) }
|
||||
case (_, .dismissFlow, .complete):
|
||||
dismissFlow(animated: animated)
|
||||
|
||||
@@ -470,7 +476,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||
Task { await self.presentPinnedEventsTimeline() }
|
||||
presentPinnedEventsTimeline()
|
||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
|
||||
break
|
||||
|
||||
@@ -480,7 +486,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||
Task { await self.presentPinnedEventsTimeline() }
|
||||
presentPinnedEventsTimeline()
|
||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
|
||||
break
|
||||
|
||||
@@ -493,6 +499,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
presentRolesAndPermissionsScreen()
|
||||
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen, .roomDetails):
|
||||
rolesAndPermissionsFlowCoordinator = nil
|
||||
|
||||
case (.roomDetails, .presentRoomMemberDetails(let userID), .roomMemberDetails):
|
||||
presentRoomMemberDetails(userID: userID)
|
||||
case (.roomMemberDetails, .dismissRoomMemberDetails, .roomDetails):
|
||||
break
|
||||
|
||||
case (.roomMemberDetails, .dismissUserProfile, .roomDetails):
|
||||
break
|
||||
|
||||
// Child flow
|
||||
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
|
||||
@@ -525,9 +539,9 @@ 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.
|
||||
/// - focussedEvent: An (optional) struct that contains the event ID that the timeline should be focussed around, and a boolean telling if such event should update the pinned events banner
|
||||
/// - animated: whether it should animate the transition
|
||||
private func presentRoom(fromState: State, focussedEventID: String? = nil, animated: Bool) async {
|
||||
private func presentRoom(fromState: State, focussedEvent: FocusEvent?, 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)
|
||||
@@ -545,8 +559,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// The room is already on the stack, no need to present it again
|
||||
|
||||
// Check if we need to focus on an event
|
||||
if let focussedEventID {
|
||||
roomScreenCoordinator?.focusOnEvent(eventID: focussedEventID)
|
||||
if let focussedEvent {
|
||||
roomScreenCoordinator?.focusOnEvent(focussedEvent)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -565,7 +579,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||
|
||||
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
|
||||
initialFocussedEventID: focussedEventID,
|
||||
initialFocussedEventID: focussedEvent?.eventID,
|
||||
timelineItemFactory: timelineItemFactory)
|
||||
self.timelineController = timelineController
|
||||
|
||||
@@ -576,7 +590,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
|
||||
focussedEventID: focussedEventID,
|
||||
focussedEvent: focussedEvent,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
mediaPlayerProvider: MediaPlayerProvider(),
|
||||
@@ -660,7 +674,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) {
|
||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||
stateMachine.tryEvent(.presentRoom(focussedEventID: nil), userInfo: EventUserInfo(animated: animated))
|
||||
stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated))
|
||||
|
||||
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount))
|
||||
} else {
|
||||
@@ -964,42 +978,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentPinnedEventsTimeline() async {
|
||||
let userID = userSession.clientProxy.userID
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||
|
||||
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else {
|
||||
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
|
||||
}
|
||||
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
mediaPlayerProvider: MediaPlayerProvider(),
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||
appMediator: appMediator))
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
stackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentPollForm(mode: PollFormMode) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode))
|
||||
@@ -1345,6 +1323,43 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func presentPinnedEventsTimeline() {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PinnedEventsTimelineFlowCoordinator(navigationStackCoordinator: stackCoordinator,
|
||||
userSession: userSession,
|
||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator)
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case .finished:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .displayUser(let userID):
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
|
||||
case .forwardedMessageToRoom(let roomID):
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
|
||||
case .displayRoomScreenWithFocussedPin(let eventID):
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
stateMachine.tryEvent(.presentRoom(focussedEvent: .init(eventID: eventID, shouldSetPin: true)))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
pinnedEventsTimelineFlowCoordinator = coordinator
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
|
||||
}
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
// MARK: - Child Flow
|
||||
|
||||
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
|
||||
@@ -1405,8 +1420,8 @@ private extension RoomFlowCoordinator {
|
||||
case notificationSettings
|
||||
case globalNotificationSettings
|
||||
case roomMembersList
|
||||
case roomMemberDetails(userID: String, fromRoomMembersList: Bool)
|
||||
case userProfile(userID: String)
|
||||
case roomMemberDetails(userID: String, previousState: State)
|
||||
case userProfile(userID: String, previousState: State)
|
||||
case inviteUsersScreen(fromRoomMembersList: Bool)
|
||||
case mediaUploadPicker(source: MediaPickerScreenSource)
|
||||
case mediaUploadPreview(fileURL: URL)
|
||||
@@ -1434,7 +1449,7 @@ private extension RoomFlowCoordinator {
|
||||
case presentJoinRoomScreen(via: [String])
|
||||
case dismissJoinRoomScreen
|
||||
|
||||
case presentRoom(focussedEventID: String?)
|
||||
case presentRoom(focussedEvent: FocusEvent?)
|
||||
case dismissFlow
|
||||
|
||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||
@@ -1497,12 +1512,6 @@ private extension RoomFlowCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private extension GeoURI {
|
||||
var bodyMessage: String {
|
||||
"Location was shared at \(string)"
|
||||
}
|
||||
}
|
||||
|
||||
private extension Result {
|
||||
var isFailure: Bool {
|
||||
switch self {
|
||||
@@ -1518,3 +1527,10 @@ private enum PinnedEventsTimelineSource: Hashable {
|
||||
case room
|
||||
case details(isRoot: Bool)
|
||||
}
|
||||
|
||||
struct FocusEvent: Hashable {
|
||||
/// The event ID that the timeline should be focussed around
|
||||
let eventID: String
|
||||
/// if the focus is coming from the pinned timeline, this should also update the pin banner
|
||||
let shouldSetPin: Bool
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user