Add support for sending media within threads.

This patch add the `threadRootEventID` as a parameter to media sending functions and forwards the timeline thread root between various application states to the place where its needed.
The 2 missing items are Locations and Polls which currently don't have any support on the Rust side for being sent within threads. That will come in a separate PR.
This commit is contained in:
Stefan Ceriu
2025-06-10 17:18:32 +03:00
committed by Stefan Ceriu
parent 6481093107
commit 558cdf5be3
18 changed files with 340 additions and 247 deletions
@@ -14,6 +14,7 @@ struct MediaUploadPreviewScreenCoordinatorParameters {
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
let title: String?
let url: URL
let threadRootEventID: String?
let shouldShowCaptionWarning: Bool
}
@@ -36,6 +37,7 @@ final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol {
mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor,
title: parameters.title,
url: parameters.url,
threadRootEventID: parameters.threadRootEventID,
shouldShowCaptionWarning: parameters.shouldShowCaptionWarning)
}
@@ -16,6 +16,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
private let roomProxy: JoinedRoomProxyProtocol
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
private let url: URL
private let threadRootEventID: String?
private var processingTask: Task<Result<MediaInfo, MediaUploadingPreprocessorError>, Never>
private var requestHandle: SendAttachmentJoinHandleProtocol?
@@ -31,11 +32,13 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
mediaUploadingPreprocessor: MediaUploadingPreprocessor,
title: String?,
url: URL,
threadRootEventID: String?,
shouldShowCaptionWarning: Bool) {
self.userIndicatorController = userIndicatorController
self.roomProxy = roomProxy
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
self.url = url
self.threadRootEventID = threadRootEventID
// Start processing the media whilst the user is reviewing it/adding a caption.
processingTask = Task { await mediaUploadingPreprocessor.processMedia(at: url) }
@@ -57,7 +60,9 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
Task {
switch await processingTask.value {
case .success(let mediaInfo):
switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) {
switch await sendAttachment(mediaInfo: mediaInfo,
caption: caption,
threadRootEventID: threadRootEventID) {
case .success:
actionsSubject.send(.dismiss)
case .failure(let error):
@@ -85,7 +90,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
// MARK: - Private
private func sendAttachment(mediaInfo: MediaInfo, caption: String?) async -> Result<Void, TimelineProxyError> {
private func sendAttachment(mediaInfo: MediaInfo, caption: String?, threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in
self?.requestHandle = handle
}
@@ -96,22 +101,26 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
thumbnailURL: thumbnailURL,
imageInfo: imageInfo,
caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle)
case let .video(videoURL, thumbnailURL, videoInfo):
return await roomProxy.timeline.sendVideo(url: videoURL,
thumbnailURL: thumbnailURL,
videoInfo: videoInfo,
caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle)
case let .audio(audioURL, audioInfo):
return await roomProxy.timeline.sendAudio(url: audioURL,
audioInfo: audioInfo,
caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle)
case let .file(fileURL, fileInfo):
return await roomProxy.timeline.sendFile(url: fileURL,
fileInfo: fileInfo,
caption: caption,
threadRootEventID: threadRootEventID,
requestHandle: requestHandle)
}
}
@@ -232,6 +232,7 @@ struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview {
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
title: "App Icon.png",
url: snapshotURL,
threadRootEventID: nil,
shouldShowCaptionWarning: true)
static var previews: some View {
NavigationStack {
@@ -80,6 +80,8 @@ struct ComposerToolbarViewState: BindableState {
var keyCommands: [WysiwygKeyCommand] = []
var isInThread: Bool
var bindings: ComposerToolbarViewStateBindings
var isUploading: Bool {
@@ -46,6 +46,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
init(initialText: String? = nil,
roomProxy: JoinedRoomProxyProtocol,
isInThread: Bool = false,
wysiwygViewModel: WysiwygComposerViewModel,
completionSuggestionService: CompletionSuggestionServiceProtocol,
mediaProvider: MediaProviderProtocol,
@@ -68,6 +69,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
audioRecorderState: .init(),
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled,
isInThread: isInThread,
bindings: .init()),
mediaProvider: mediaProvider)
@@ -40,14 +40,16 @@ struct RoomAttachmentPicker: View {
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting)
Button {
context.send(viewAction: .attach(.poll))
} label: {
Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls)
if !context.viewState.isInThread {
Button {
context.send(viewAction: .attach(.poll))
} label: {
Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll)
if context.viewState.isLocationSharingEnabled {
if !context.viewState.isInThread, context.viewState.isLocationSharingEnabled {
Button {
context.send(viewAction: .attach(.location))
} label: {
@@ -98,6 +98,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
parserStyle: .elementX)
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText,
roomProxy: parameters.roomProxy,
isInThread: false,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider,
@@ -27,8 +27,8 @@ struct ThreadTimelineScreenCoordinatorParameters {
enum ThreadTimelineScreenCoordinatorAction {
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case presentMediaUploadPicker(MediaPickerScreenSource)
case presentMediaUploadPreviewScreen(URL)
case presentMediaUploadPicker(MediaPickerScreenSource, threadRootEventID: String?)
case presentMediaUploadPreviewScreen(url: URL, threadRootEventID: String?)
case presentLocationPicker
case presentPollForm(mode: PollFormMode)
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
@@ -78,6 +78,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
composerViewModel = ComposerToolbarViewModel(initialText: nil,
roomProxy: parameters.roomProxy,
isInThread: true,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider,
@@ -98,11 +99,11 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
case .displayReportContent(let itemID, let senderID):
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
case .displayCameraPicker:
actionsSubject.send(.presentMediaUploadPicker(.camera))
actionsSubject.send(.presentMediaUploadPicker(.camera, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displayMediaPicker:
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displayDocumentPicker:
actionsSubject.send(.presentMediaUploadPicker(.documents))
actionsSubject.send(.presentMediaUploadPicker(.documents, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displayMediaPreview(let mediaPreviewViewModel):
viewModel.displayMediaPreview(mediaPreviewViewModel)
case .displayLocationPicker:
@@ -110,7 +111,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
case .displayPollForm(let mode):
actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
actionsSubject.send(.presentMediaUploadPreviewScreen(url: url, threadRootEventID: parameters.timelineController.timelineKind.threadRootEventID))
case .displaySenderDetails(userID: let userID):
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let forwardingItem):
@@ -362,7 +362,9 @@ class TimelineInteractionHandler {
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: true))))
await voiceMessageRecorder.stopPlayback()
switch await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: AudioConverter()) {
switch await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: AudioConverter(),
threadRootEventID: timelineController.timelineKind.threadRootEventID) {
case .success:
await deleteCurrentVoiceMessage()
case .failure(let error):