diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 6f47c9662..fc6430721 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -72,6 +72,7 @@ enum TimelineViewAction { case hasSwitchedTimeline case hasScrolled(direction: ScrollDirection) + case setOpenURLAction(OpenURLAction) } enum TimelineComposerAction { @@ -101,6 +102,9 @@ struct TimelineViewState: BindableState { // It's updated from the room info, so it's faster than using the timeline var pinnedEventIDs: Set = [] + /// an openURL closure which opens URLs first using the App's environment rather than skipping out to external apps + var openURL: OpenURLAction? + var bindings: TimelineViewStateBindings /// A closure providing the associated audio player state for an item in the timeline. diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 819f96a1f..c0557862b 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -7,6 +7,7 @@ import Algorithms import Combine +import MatrixRustSDK import OrderedCollections import SwiftUI @@ -167,6 +168,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { Task { state.timelineViewState.isSwitchingTimelines = false } case let .hasScrolled(direction): actionsSubject.send(.hasScrolled(direction: direction)) + case .setOpenURLAction(let action): + state.openURL = action } } @@ -560,7 +563,27 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { displayAlert(.encryptionAuthenticity(authenticityMessage)) } } + + private func slashCommand(message: String) -> SlashCommand? { + for command in SlashCommand.allCases { + if message.starts(with: command.rawValue) { + return command + } + } + return nil + } + private func handleJoinCommand(message: String) { + guard let alias = String(message.dropFirst(SlashCommand.join.rawValue.count)) + .components(separatedBy: .whitespacesAndNewlines) + .first, + let urlString = try? matrixToRoomAliasPermalink(roomAlias: alias), + let url = URL(string: urlString) else { + return + } + state.openURL?(url) + } + private func sendCurrentMessage(_ message: String, html: String?, mode: ComposerMode, intentionalMentions: IntentionalMentions) async { guard !message.isEmpty else { fatalError("This message should never be empty") @@ -580,9 +603,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { html: html, intentionalMentions: intentionalMentions) case .default: - await timelineController.sendMessage(message, - html: html, - intentionalMentions: intentionalMentions) + switch slashCommand(message: message) { + case .join: + handleJoinCommand(message: message) + case .none: + await timelineController.sendMessage(message, + html: html, + intentionalMentions: intentionalMentions) + } case .recordVoiceMessage, .previewVoiceMessage: fatalError("invalid composer mode.") } @@ -862,3 +890,7 @@ extension EnvironmentValues { set { self[FocussedEventID.self] = newValue } } } + +private enum SlashCommand: String, CaseIterable { + case join = "/join " +} diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 6877720e7..c7dbde17d 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -11,11 +11,16 @@ import WysiwygComposer /// A table view wrapper that displays the timeline of a room. struct TimelineView: UIViewControllerRepresentable { @EnvironmentObject private var viewModelContext: TimelineViewModel.Context - + @Environment(\.openURL) var openURL + func makeUIViewController(context: Context) -> TimelineTableViewController { let tableViewController = TimelineTableViewController(coordinator: context.coordinator, isScrolledToBottom: $viewModelContext.isScrolledToBottom, scrollToBottomPublisher: viewModelContext.viewState.timelineViewState.scrollToBottomPublisher) + // Needs to be dispatched on main asynchronously otherwise we get a runtime warning + DispatchQueue.main.async { + viewModelContext.send(viewAction: .setOpenURLAction(openURL)) + } return tableViewController }