mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
implemented navigation for permalinks to threads
also this handles the case where we are permalinking to non existent (or impossible to fetch) event.
This commit is contained in:
@@ -68,6 +68,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private var roomProxy: JoinedRoomProxyProtocol!
|
private var roomProxy: JoinedRoomProxyProtocol!
|
||||||
|
|
||||||
private var roomScreenCoordinator: RoomScreenCoordinator?
|
private var roomScreenCoordinator: RoomScreenCoordinator?
|
||||||
|
private var childThreads: [ThreadTimelineScreenCoordinator] = []
|
||||||
private weak var joinRoomScreenCoordinator: JoinRoomScreenCoordinator?
|
private weak var joinRoomScreenCoordinator: JoinRoomScreenCoordinator?
|
||||||
|
|
||||||
// periphery:ignore - used to avoid deallocation
|
// periphery:ignore - used to avoid deallocation
|
||||||
@@ -163,7 +164,28 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else if roomID != roomProxy.id {
|
} else if roomID != roomProxy.id {
|
||||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||||
} else {
|
} else {
|
||||||
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
showLoadingIndicator(delay: .seconds(0.5))
|
||||||
|
Task {
|
||||||
|
defer { hideLoadingIndicator() }
|
||||||
|
switch await roomProxy.loadOrFetchEvent(for: eventID) {
|
||||||
|
case .success(let event):
|
||||||
|
if flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() {
|
||||||
|
if case .thread(threadRootEventID: threadRootEventID, _) = stateMachine.state, let threadCoordinator = childThreads.last {
|
||||||
|
threadCoordinator.focusOnEvent(eventID: eventID)
|
||||||
|
} else {
|
||||||
|
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: eventID))
|
||||||
|
}
|
||||||
|
} else if !childThreads.isEmpty {
|
||||||
|
// If we are showing a child thread, and we are navigating to a non threaded event
|
||||||
|
// of the same room, we want to push the room on top of the thread
|
||||||
|
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
|
||||||
|
} else {
|
||||||
|
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
|
||||||
|
}
|
||||||
|
case .failure:
|
||||||
|
showError()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .share(let payload):
|
case .share(let payload):
|
||||||
guard let roomID = payload.roomID, roomID == self.roomID else {
|
guard let roomID = payload.roomID, roomID == self.roomID else {
|
||||||
@@ -232,12 +254,27 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
switch room {
|
switch room {
|
||||||
case .joined(let roomProxy):
|
case .joined(let roomProxy):
|
||||||
await storeAndSubscribeToRoomProxy(roomProxy)
|
await storeAndSubscribeToRoomProxy(roomProxy)
|
||||||
|
if case let .eventFocus(focusEvent) = presentationAction {
|
||||||
|
switch await roomProxy.loadOrFetchEvent(for: focusEvent.eventID) {
|
||||||
|
case .success(let event):
|
||||||
|
guard flowParameters.appSettings.threadsEnabled, let threadRootEventID = event.threadRootEventId() else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
stateMachine.tryEvent(.presentRoom(presentationAction: nil), userInfo: EventUserInfo(animated: animated))
|
||||||
|
stateMachine.tryEvent(.presentThread(threadRootEventID: threadRootEventID, focusEventID: focusEvent.eventID), userInfo: EventUserInfo(animated: false))
|
||||||
|
return
|
||||||
|
case .failure:
|
||||||
|
showError()
|
||||||
|
stateMachine.tryEvent(.presentRoom(presentationAction: nil), userInfo: EventUserInfo(animated: animated))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated))
|
||||||
default:
|
default:
|
||||||
stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearRoute(animated: Bool) {
|
func clearRoute(animated: Bool) {
|
||||||
guard stateMachine.state != .initial else {
|
guard stateMachine.state != .initial else {
|
||||||
return
|
return
|
||||||
@@ -305,8 +342,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
// Thread
|
// Thread
|
||||||
|
|
||||||
case (.room, .presentThread(let itemID), .thread):
|
case (_, .presentThread(let threadRootEventID, let focusEventID), .thread):
|
||||||
Task { await self.presentThread(itemID: itemID, animated: animated) }
|
Task { await self.presentThread(threadRootEventID: threadRootEventID, focusEventID: focusEventID, animated: animated) }
|
||||||
|
|
||||||
// Thread + Room
|
// Thread + Room
|
||||||
|
|
||||||
@@ -605,7 +642,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .presentKnockRequestsList:
|
case .presentKnockRequestsList:
|
||||||
stateMachine.tryEvent(.presentKnockRequestsListScreen)
|
stateMachine.tryEvent(.presentKnockRequestsListScreen)
|
||||||
case .presentThread(let itemID):
|
case .presentThread(let itemID):
|
||||||
stateMachine.tryEvent(.presentThread(itemID: itemID))
|
guard let threadRootEventID = itemID.eventID else {
|
||||||
|
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,
|
||||||
@@ -617,7 +657,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return coordinator
|
return coordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentThread(itemID: TimelineItemIdentifier, animated: Bool) async {
|
private func presentThread(threadRootEventID: String, focusEventID: String?, animated: Bool) async {
|
||||||
showLoadingIndicator()
|
showLoadingIndicator()
|
||||||
defer { hideLoadingIndicator() }
|
defer { hideLoadingIndicator() }
|
||||||
|
|
||||||
@@ -625,10 +665,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
|
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
|
||||||
|
|
||||||
guard let threadRootEventID = itemID.eventID else {
|
|
||||||
fatalError("Invalid thread event ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard case let .success(timelineController) = await flowParameters.timelineControllerFactory.buildThreadTimelineController(eventID: threadRootEventID,
|
guard case let .success(timelineController) = await flowParameters.timelineControllerFactory.buildThreadTimelineController(eventID: threadRootEventID,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
timelineItemFactory: timelineItemFactory,
|
timelineItemFactory: timelineItemFactory,
|
||||||
@@ -645,6 +681,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
let coordinator = ThreadTimelineScreenCoordinator(parameters: .init(userSession: userSession,
|
let coordinator = ThreadTimelineScreenCoordinator(parameters: .init(userSession: userSession,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
|
focussedEventID: focusEventID,
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaPlayerProvider: MediaPlayerProvider(),
|
mediaPlayerProvider: MediaPlayerProvider(),
|
||||||
emojiProvider: flowParameters.emojiProvider,
|
emojiProvider: flowParameters.emojiProvider,
|
||||||
@@ -693,9 +730,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||||
self?.stateMachine.tryEvent(.dismissThread)
|
guard let self else { return }
|
||||||
|
stateMachine.tryEvent(.dismissThread)
|
||||||
|
childThreads.removeAll { $0 === coordinator }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
childThreads.append(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentJoinRoomScreen(via: [String], animated: Bool) {
|
private func presentJoinRoomScreen(via: [String], animated: Bool) {
|
||||||
@@ -1579,4 +1620,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private func hideLoadingIndicator() {
|
private func hideLoadingIndicator() {
|
||||||
flowParameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorID)
|
flowParameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showError() {
|
||||||
|
flowParameters.userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ extension RoomFlowCoordinator {
|
|||||||
case initial
|
case initial
|
||||||
case joinRoomScreen
|
case joinRoomScreen
|
||||||
case room
|
case room
|
||||||
case thread(itemID: TimelineItemIdentifier)
|
case thread(threadRootEventID: String, previousState: State)
|
||||||
case roomDetails(isRoot: Bool)
|
case roomDetails(isRoot: Bool)
|
||||||
case roomDetailsEditScreen
|
case roomDetailsEditScreen
|
||||||
case notificationSettings
|
case notificationSettings
|
||||||
@@ -95,7 +95,7 @@ extension RoomFlowCoordinator {
|
|||||||
case presentRoom(presentationAction: PresentationAction?)
|
case presentRoom(presentationAction: PresentationAction?)
|
||||||
case dismissFlow
|
case dismissFlow
|
||||||
|
|
||||||
case presentThread(itemID: TimelineItemIdentifier)
|
case presentThread(threadRootEventID: String, focusEventID: String?)
|
||||||
case dismissThread
|
case dismissThread
|
||||||
|
|
||||||
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
@@ -216,10 +216,12 @@ extension RoomFlowCoordinator {
|
|||||||
return previousState
|
return previousState
|
||||||
|
|
||||||
// Thread
|
// Thread
|
||||||
case (.room, .presentThread(let itemID)):
|
case (.room, .presentThread(let threadRootEventID, _)):
|
||||||
return .thread(itemID: itemID)
|
return .thread(threadRootEventID: threadRootEventID, previousState: fromState)
|
||||||
case (.thread, .dismissThread):
|
case (.thread, .presentThread(let threadRootEventID, _)):
|
||||||
return .room
|
return .thread(threadRootEventID: threadRootEventID, previousState: fromState)
|
||||||
|
case (.thread(_, let previousState), .dismissThread):
|
||||||
|
return previousState
|
||||||
|
|
||||||
case (.thread, .presentReportContent(let itemID, let senderID)):
|
case (.thread, .presentReportContent(let itemID, let senderID)):
|
||||||
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
|
||||||
|
|||||||
@@ -6483,17 +6483,17 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
return threadTimelineEventIDReturnValue
|
return threadTimelineEventIDReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//MARK: - getThreadRootEventID
|
//MARK: - loadOrFetchEvent
|
||||||
|
|
||||||
var getThreadRootEventIDForUnderlyingCallsCount = 0
|
var loadOrFetchEventForUnderlyingCallsCount = 0
|
||||||
var getThreadRootEventIDForCallsCount: Int {
|
var loadOrFetchEventForCallsCount: Int {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return getThreadRootEventIDForUnderlyingCallsCount
|
return loadOrFetchEventForUnderlyingCallsCount
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Int? = nil
|
var returnValue: Int? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = getThreadRootEventIDForUnderlyingCallsCount
|
returnValue = loadOrFetchEventForUnderlyingCallsCount
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@@ -6501,29 +6501,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
getThreadRootEventIDForUnderlyingCallsCount = newValue
|
loadOrFetchEventForUnderlyingCallsCount = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
getThreadRootEventIDForUnderlyingCallsCount = newValue
|
loadOrFetchEventForUnderlyingCallsCount = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var getThreadRootEventIDForCalled: Bool {
|
var loadOrFetchEventForCalled: Bool {
|
||||||
return getThreadRootEventIDForCallsCount > 0
|
return loadOrFetchEventForCallsCount > 0
|
||||||
}
|
}
|
||||||
var getThreadRootEventIDForReceivedEventID: String?
|
var loadOrFetchEventForReceivedEventID: String?
|
||||||
var getThreadRootEventIDForReceivedInvocations: [String] = []
|
var loadOrFetchEventForReceivedInvocations: [String] = []
|
||||||
|
|
||||||
var getThreadRootEventIDForUnderlyingReturnValue: Result<String?, RoomProxyError>!
|
var loadOrFetchEventForUnderlyingReturnValue: Result<TimelineEvent, RoomProxyError>!
|
||||||
var getThreadRootEventIDForReturnValue: Result<String?, RoomProxyError>! {
|
var loadOrFetchEventForReturnValue: Result<TimelineEvent, RoomProxyError>! {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return getThreadRootEventIDForUnderlyingReturnValue
|
return loadOrFetchEventForUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Result<String?, RoomProxyError>? = nil
|
var returnValue: Result<TimelineEvent, RoomProxyError>? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = getThreadRootEventIDForUnderlyingReturnValue
|
returnValue = loadOrFetchEventForUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnValue!
|
return returnValue!
|
||||||
@@ -6531,26 +6531,26 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
getThreadRootEventIDForUnderlyingReturnValue = newValue
|
loadOrFetchEventForUnderlyingReturnValue = newValue
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
getThreadRootEventIDForUnderlyingReturnValue = newValue
|
loadOrFetchEventForUnderlyingReturnValue = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var getThreadRootEventIDForClosure: ((String) async -> Result<String?, RoomProxyError>)?
|
var loadOrFetchEventForClosure: ((String) async -> Result<TimelineEvent, RoomProxyError>)?
|
||||||
|
|
||||||
func getThreadRootEventID(for eventID: String) async -> Result<String?, RoomProxyError> {
|
func loadOrFetchEvent(for eventID: String) async -> Result<TimelineEvent, RoomProxyError> {
|
||||||
getThreadRootEventIDForCallsCount += 1
|
loadOrFetchEventForCallsCount += 1
|
||||||
getThreadRootEventIDForReceivedEventID = eventID
|
loadOrFetchEventForReceivedEventID = eventID
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.getThreadRootEventIDForReceivedInvocations.append(eventID)
|
self.loadOrFetchEventForReceivedInvocations.append(eventID)
|
||||||
}
|
}
|
||||||
if let getThreadRootEventIDForClosure = getThreadRootEventIDForClosure {
|
if let loadOrFetchEventForClosure = loadOrFetchEventForClosure {
|
||||||
return await getThreadRootEventIDForClosure(eventID)
|
return await loadOrFetchEventForClosure(eventID)
|
||||||
} else {
|
} else {
|
||||||
return getThreadRootEventIDForReturnValue
|
return loadOrFetchEventForReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//MARK: - messageFilteredTimeline
|
//MARK: - messageFilteredTimeline
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import WysiwygComposer
|
|||||||
struct ThreadTimelineScreenCoordinatorParameters {
|
struct ThreadTimelineScreenCoordinatorParameters {
|
||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
let roomProxy: JoinedRoomProxyProtocol
|
let roomProxy: JoinedRoomProxyProtocol
|
||||||
|
let focussedEventID: String?
|
||||||
let timelineController: TimelineControllerProtocol
|
let timelineController: TimelineControllerProtocol
|
||||||
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
@@ -58,6 +59,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel = ThreadTimelineScreenViewModel(roomProxy: parameters.roomProxy, userSession: parameters.userSession)
|
viewModel = ThreadTimelineScreenViewModel(roomProxy: parameters.roomProxy, userSession: parameters.userSession)
|
||||||
|
|
||||||
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||||
|
focussedEventID: parameters.focussedEventID,
|
||||||
timelineController: parameters.timelineController,
|
timelineController: parameters.timelineController,
|
||||||
userSession: parameters.userSession,
|
userSession: parameters.userSession,
|
||||||
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
mediaPlayerProvider: parameters.mediaPlayerProvider,
|
||||||
@@ -155,4 +157,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
timelineContext: timelineViewModel.context,
|
timelineContext: timelineViewModel.context,
|
||||||
composerToolbar: composerToolbar))
|
composerToolbar: composerToolbar))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func focusOnEvent(eventID: String) {
|
||||||
|
Task { await timelineViewModel.focusOnEvent(eventID: eventID) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,10 +190,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getThreadRootEventID(for eventID: String) async -> Result<String?, RoomProxyError> {
|
func loadOrFetchEvent(for eventID: String) async -> Result<TimelineEvent, RoomProxyError> {
|
||||||
do {
|
do {
|
||||||
let event = try await room.loadOrFetchEvent(eventId: eventID)
|
let event = try await room.loadOrFetchEvent(eventId: eventID)
|
||||||
return .success(event.threadRootEventId())
|
return .success(event)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed fetching the event with id: \(eventID) with error: \(error)")
|
MXLog.error("Failed fetching the event with id: \(eventID) with error: \(error)")
|
||||||
return .failure(.sdkError(error))
|
return .failure(.sdkError(error))
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
|||||||
|
|
||||||
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError>
|
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError>
|
||||||
|
|
||||||
func getThreadRootEventID(for eventID: String) async -> Result<String?, RoomProxyError>
|
func loadOrFetchEvent(for eventID: String) async -> Result<TimelineEvent, RoomProxyError>
|
||||||
|
|
||||||
func messageFilteredTimeline(focus: TimelineFocus,
|
func messageFilteredTimeline(focus: TimelineFocus,
|
||||||
allowedMessageTypes: [TimelineAllowedMessageType],
|
allowedMessageTypes: [TimelineAllowedMessageType],
|
||||||
|
|||||||
Reference in New Issue
Block a user