Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sharing URLs and text. #3546

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 51 additions & 40 deletions ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,18 @@ private enum PresentationAction: Hashable {
var focusedEvent: FocusEvent? {
switch self {
case .eventFocus(let focusEvent):
return focusEvent
focusEvent
default:
return nil
nil
}
}

var sharedText: String? {
switch self {
case .share(.text(_, let text)):
text
default:
nil
}
}
}
Expand Down Expand Up @@ -196,11 +205,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
}
case .share(let payload):
guard case let .mediaFile(roomID, _) = payload else {
return
}

guard let roomID, roomID == self.roomID else {
guard let roomID = payload.roomID, roomID == self.roomID else {
fatalError("Navigation route doesn't belong to this room flow.")
}

Expand Down Expand Up @@ -615,41 +620,68 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .eventFocus(let focusedEvent):
roomScreenCoordinator?.focusOnEvent(focusedEvent)
case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url))
default:
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated))
case .share(.text(_, let text)):
roomScreenCoordinator?.shareText(text)
case .none:
break
}

return
}
}

Task {
// Flag the room as read on entering, the timeline will take care of the read receipts
await roomProxy.flagAsUnread(false)
}
// Flag the room as read on entering, the timeline will take care of the read receipts
Task { await roomProxy.flagAsUnread(false) }

let userID = userSession.clientProxy.userID
analytics.trackViewRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace)

let coordinator = makeRoomScreenCoordinator(presentationAction: presentationAction)
roomScreenCoordinator = coordinator

if !isChildFlow {
let animated = UIDevice.current.userInterfaceIdiom == .phone ? animated : false
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
self?.stateMachine.tryEvent(.dismissFlow)
}
} else {
if joinRoomScreenCoordinator != nil {
navigationStackCoordinator.pop()
}

navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
self?.stateMachine.tryEvent(.dismissFlow)
}
}

switch presentationAction {
case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated))
case .share(.text), .eventFocus:
break // These are both handled in the coordinator's init.
case .none:
break
}
}

private func makeRoomScreenCoordinator(presentationAction: PresentationAction?) -> RoomScreenCoordinator {
let userID = userSession.clientProxy.userID
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))

let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
initialFocussedEventID: presentationAction?.focusedEvent?.eventID,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider)
self.timelineController = timelineController

analytics.trackViewRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace)

let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy)

let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)

let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
roomProxy: roomProxy,
focussedEvent: presentationAction?.focusedEvent,
sharedText: presentationAction?.sharedText,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
Expand Down Expand Up @@ -697,28 +729,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
self?.stateMachine.tryEvent(.dismissFlow)
}
} else {
if joinRoomScreenCoordinator != nil {
navigationStackCoordinator.pop()
}

navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
self?.stateMachine.tryEvent(.dismissFlow)
}
}

switch presentationAction {
case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated))
default:
break
}
return coordinator
}

private func presentJoinRoomScreen(via: [String], animated: Bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case .settings, .chatBackupSettings:
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
case .share(let payload):
switch payload {
case .mediaFile(let roomID, _):
if let roomID {
stateMachine.processEvent(.selectRoom(roomID: roomID,
via: [],
entryPoint: .share(payload)),
userInfo: .init(animated: animated))
} else {
stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated))
}
if let roomID = payload.roomID {
stateMachine.processEvent(.selectRoom(roomID: roomID,
via: [],
entryPoint: .share(payload)),
userInfo: .init(animated: animated))
} else {
stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated))
}
}
}
Expand Down Expand Up @@ -938,6 +935,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
let sharePayload = switch sharePayload {
case .mediaFile(_, let mediaFile):
ShareExtensionPayload.mediaFile(roomID: roomID, mediaFile: mediaFile)
case .text(_, let text):
ShareExtensionPayload.text(roomID: roomID, text: text)
}

navigationSplitCoordinator.setSheetCoordinator(nil)
Expand Down
15 changes: 14 additions & 1 deletion ElementX/Sources/Other/Extensions/NSItemProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

import Foundation
import UIKit
import SwiftUI
import UniformTypeIdentifiers

extension NSItemProvider {
Expand All @@ -15,6 +15,19 @@ extension NSItemProvider {
let fileExtension: String
}

func loadTransferable<T: Transferable>(type transferableType: T.Type) async -> T? {
try? await withCheckedContinuation { continuation in
_ = loadTransferable(type: T.self) { result in
continuation.resume(returning: result)
}
}
.get()
}

func loadString() async -> String? {
try? await loadItem(forTypeIdentifier: UTType.text.identifier) as? String
}

func storeData() async -> URL? {
guard let contentType = preferredContentType else {
MXLog.error("Invalid NSItemProvider: \(self)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import WysiwygComposer
typealias ComposerToolbarViewModelType = StateStoreViewModel<ComposerToolbarViewState, ComposerToolbarViewAction>

final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerToolbarViewModelProtocol {
private var initialText: String?
private let wysiwygViewModel: WysiwygComposerViewModel
private let completionSuggestionService: CompletionSuggestionServiceProtocol
private let analyticsService: AnalyticsService
Expand All @@ -41,12 +42,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool

private var replyLoadingTask: Task<Void, Never>?

init(wysiwygViewModel: WysiwygComposerViewModel,
init(initialText: String? = nil,
wysiwygViewModel: WysiwygComposerViewModel,
completionSuggestionService: CompletionSuggestionServiceProtocol,
mediaProvider: MediaProviderProtocol,
mentionDisplayHelper: MentionDisplayHelper,
analyticsService: AnalyticsService,
composerDraftService: ComposerDraftServiceProtocol) {
self.initialText = initialText
self.wysiwygViewModel = wysiwygViewModel
self.completionSuggestionService = completionSuggestionService
self.analyticsService = analyticsService
Expand Down Expand Up @@ -206,6 +209,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
} else {
set(text: plainText)
}
case .setFocus:
state.bindings.composerFocused = false
case .removeFocus:
state.bindings.composerFocused = false
pixlwave marked this conversation as resolved.
Show resolved Hide resolved
case .clear:
Expand All @@ -219,8 +224,12 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
}
}

func loadDraft() {
Task {
func loadDraft() async {
if let initialText {
set(text: initialText)
set(mode: .default)
state.bindings.composerFocused = true
} else {
guard case let .success(draft) = await draftService.loadDraft(),
let draft else {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ protocol ComposerToolbarViewModelProtocol {
var keyCommands: [WysiwygKeyCommand] { get }

func process(timelineAction: TimelineComposerAction)
func loadDraft()
func loadDraft() async
func saveDraft()
}
12 changes: 10 additions & 2 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct RoomScreenCoordinatorParameters {
let clientProxy: ClientProxyProtocol
let roomProxy: JoinedRoomProxyProtocol
var focussedEvent: FocusEvent?
var sharedText: String?
let timelineController: RoomTimelineControllerProtocol
let mediaProvider: MediaProviderProtocol
let mediaPlayerProvider: MediaPlayerProviderProtocol
Expand Down Expand Up @@ -88,7 +89,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
maxCompressedHeight: ComposerConstant.maxHeight,
maxExpandedHeight: ComposerConstant.maxHeight,
parserStyle: .elementX)
let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider,
mentionDisplayHelper: ComposerMentionDisplayHelper(timelineContext: timelineViewModel.context),
Expand Down Expand Up @@ -172,7 +174,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
.store(in: &cancellables)

// Loading the draft requires the subscriptions to be set up first otherwise the room won't be be able to propagate the information to the composer.
composerViewModel.loadDraft()
Task { await composerViewModel.loadDraft() }
}

func focusOnEvent(_ focussedEvent: FocusEvent) {
Expand All @@ -183,6 +185,12 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
Task { await timelineViewModel.focusOnEvent(eventID: eventID) }
}

func shareText(_ string: String) {
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: .setFocus)
}

func stop() {
composerViewModel.saveDraft()
timelineViewModel.stop()
Expand Down
1 change: 1 addition & 0 deletions ElementX/Sources/Screens/Timeline/TimelineModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ enum TimelineViewAction {
enum TimelineComposerAction {
case setMode(mode: ComposerMode)
case setText(plainText: String, htmlText: String?)
case setFocus
case removeFocus
case clear
}
Expand Down
9 changes: 9 additions & 0 deletions ElementX/Sources/ShareExtension/ShareExtensionModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ enum ShareExtensionConstants {

enum ShareExtensionPayload: Hashable, Codable {
case mediaFile(roomID: String?, mediaFile: ShareExtensionMediaFile)
case text(roomID: String?, text: String)

var roomID: String? {
switch self {
case .mediaFile(let roomID, _),
.text(let roomID, _):
roomID
}
}
}

struct ShareExtensionMediaFile: Hashable, Codable {
Expand Down
16 changes: 10 additions & 6 deletions ShareExtension/Sources/ShareExtensionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@
return nil
}

guard let fileURL = await itemProvider.storeData() else {
MXLog.error("Failed storing NSItemProvider data \(itemProvider)")
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier

if let fileURL = await itemProvider.storeData() {

Check warning on line 47 in ShareExtension/Sources/ShareExtensionViewController.swift

View workflow job for this annotation

GitHub Actions / Tests (Enterprise)

passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode

Check warning on line 47 in ShareExtension/Sources/ShareExtensionViewController.swift

View workflow job for this annotation

GitHub Actions / Tests

passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
} else if let url = await itemProvider.loadTransferable(type: URL.self) {

Check warning on line 49 in ShareExtension/Sources/ShareExtensionViewController.swift

View workflow job for this annotation

GitHub Actions / Tests (Enterprise)

passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode

Check warning on line 49 in ShareExtension/Sources/ShareExtensionViewController.swift

View workflow job for this annotation

GitHub Actions / Tests

passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode
return .text(roomID: roomID, text: url.absoluteString)
} else if let string = await itemProvider.loadString() {

Check warning on line 51 in ShareExtension/Sources/ShareExtensionViewController.swift

View workflow job for this annotation

GitHub Actions / Tests (Enterprise)

passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode

Check warning on line 51 in ShareExtension/Sources/ShareExtensionViewController.swift

View workflow job for this annotation

GitHub Actions / Tests

passing argument of non-sendable type 'NSItemProvider' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode
return .text(roomID: roomID, text: string)
} else {
MXLog.error("Failed loading NSItemProvider data: \(itemProvider)")
return nil
}

let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier

return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
}

private func openMainApp(payload: ShareExtensionPayload) async {
Expand Down
4 changes: 4 additions & 0 deletions ShareExtension/SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<integer>1</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
Expand Down
Loading