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
@@ -301,8 +301,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (_, .presentReportContent, .reportContent(let itemID, let senderID, _)):
presentReportContent(for: itemID, from: senderID)
case (_, .presentMediaUploadPicker, .mediaUploadPicker(let source, _)):
presentMediaUploadPickerWithSource(source)
case (_, .presentMediaUploadPicker, .mediaUploadPicker(let source, let threadRootEventID, _)):
presentMediaUploadPickerWithSource(source, threadRootEventID: threadRootEventID)
case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)):
presentEmojiPicker(for: itemID, selectedEmoji: selectedEmoji)
@@ -387,8 +387,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm):
presentPollForm(mode: mode)
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, _)):
presentMediaUploadPreviewScreen(for: fileURL, animated: animated)
case (_, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL, let threadRootEventID, _)):
presentMediaUploadPreviewScreen(for: fileURL, threadRootEventID: threadRootEventID, animated: animated)
case (_, .presentInviteUsersScreen, .inviteUsersScreen):
presentInviteUsersScreen()
@@ -441,7 +441,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .eventFocus(let focusedEvent):
roomScreenCoordinator?.focusOnEvent(focusedEvent)
case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated))
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url, threadRootEventID: nil),
userInfo: EventUserInfo(animated: animated))
case .share(.text(_, let text)):
roomScreenCoordinator?.shareText(text)
case .none:
@@ -477,7 +478,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch presentationAction {
case .share(.mediaFile(_, let mediaFile)):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated))
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url, threadRootEventID: nil),
userInfo: EventUserInfo(animated: animated))
case .share(.text), .eventFocus:
break // These are both handled in the coordinator's init.
case .none:
@@ -528,9 +530,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentReportContent(let itemID, let senderID):
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
case .presentMediaUploadPicker(let source):
stateMachine.tryEvent(.presentMediaUploadPicker(source: source))
stateMachine.tryEvent(.presentMediaUploadPicker(source: source, threadRootEventID: nil))
case .presentMediaUploadPreviewScreen(let url):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url))
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url, threadRootEventID: nil))
case .presentEmojiPicker(let itemID, let selectedEmojis):
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
case .presentLocationPicker:
@@ -605,10 +607,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .presentReportContent(let itemID, let senderID):
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
case .presentMediaUploadPicker(let source):
stateMachine.tryEvent(.presentMediaUploadPicker(source: source))
case .presentMediaUploadPreviewScreen(let url):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url))
case .presentMediaUploadPicker(let source, let threadRootEventID):
stateMachine.tryEvent(.presentMediaUploadPicker(source: source,
threadRootEventID: threadRootEventID))
case .presentMediaUploadPreviewScreen(let url, let threadRootEventID):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url,
threadRootEventID: threadRootEventID))
case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker))
case .presentPollForm(let mode):
@@ -867,7 +871,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource) {
private func presentMediaUploadPickerWithSource(_ source: MediaPickerScreenSource,
threadRootEventID: String?) {
let stackCoordinator = NavigationStackCoordinator()
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController,
@@ -880,7 +885,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
case .selectMediaAtURL(let url):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url))
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url, threadRootEventID: threadRootEventID))
}
}
@@ -893,7 +898,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentMediaUploadPreviewScreen(for url: URL, animated: Bool) {
private func presentMediaUploadPreviewScreen(for url: URL,
threadRootEventID: String?,
animated: Bool) {
let stackCoordinator = NavigationStackCoordinator()
let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController,
@@ -901,6 +908,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings),
title: url.lastPathComponent,
url: url,
threadRootEventID: threadRootEventID,
shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning)
let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters)
@@ -966,11 +974,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .selectedLocation(let geoURI, let isUserLocation):
Task {
#warning("Allow sending locations within threads when the SDK allows it")
_ = await self.roomProxy.timeline.sendLocation(body: geoURI.bodyMessage,
geoURI: geoURI,
description: nil,
zoomLevel: 15,
assetType: isUserLocation ? .sender : .pin)
assetType: isUserLocation ? .sender : .pin,
threadRootEventID: nil)
self.navigationStackCoordinator.setSheetCoordinator(nil)
}
@@ -1028,7 +1038,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func createPoll(question: String, options: [String], pollKind: Poll.Kind) {
Task {
let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind)
#warning("Allow sending polls within threads when the SDK allows it")
let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind, threadRootEventID: nil)
self.analytics.trackComposer(inThread: false,
isEditing: false,
@@ -57,8 +57,8 @@ extension RoomFlowCoordinator {
case roomMemberDetails(userID: String, previousState: State)
case userProfile(userID: String, previousState: State)
case inviteUsersScreen(previousState: State)
case mediaUploadPicker(source: MediaPickerScreenSource, previousState: State)
case mediaUploadPreview(fileURL: URL, previousState: State)
case mediaUploadPicker(source: MediaPickerScreenSource, threadRootEventID: String?, previousState: State)
case mediaUploadPreview(fileURL: URL, threadRootEventID: String?, previousState: State)
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State)
case mapNavigator(previousState: State)
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
@@ -122,10 +122,10 @@ extension RoomFlowCoordinator {
case presentInviteUsersScreen
case dismissInviteUsersScreen
case presentMediaUploadPicker(source: MediaPickerScreenSource)
case presentMediaUploadPicker(source: MediaPickerScreenSource, threadRootEventID: String?)
case dismissMediaUploadPicker
case presentMediaUploadPreview(fileURL: URL)
case presentMediaUploadPreview(fileURL: URL, threadRootEventID: String?)
case dismissMediaUploadPreview
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
@@ -184,11 +184,11 @@ extension RoomFlowCoordinator {
case (.room, .presentReportContent(let itemID, let senderID)):
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
case (.room, .presentMediaUploadPicker(let source)):
return .mediaUploadPicker(source: source, previousState: fromState)
case (.room, .presentMediaUploadPicker(let source, let threadRootEventID)):
return .mediaUploadPicker(source: source, threadRootEventID: threadRootEventID, previousState: fromState)
case (.room, .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState)
case (.room, .presentMediaUploadPreview(let fileURL, let threadRootEventID)):
return .mediaUploadPreview(fileURL: fileURL, threadRootEventID: threadRootEventID, previousState: fromState)
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
@@ -221,11 +221,11 @@ extension RoomFlowCoordinator {
case (.thread, .presentReportContent(let itemID, let senderID)):
return .reportContent(itemID: itemID, senderID: senderID, previousState: fromState)
case (.thread, .presentMediaUploadPicker(let source)):
return .mediaUploadPicker(source: source, previousState: fromState)
case (.thread, .presentMediaUploadPicker(let source, let threadRootEventID)):
return .mediaUploadPicker(source: source, threadRootEventID: threadRootEventID, previousState: fromState)
case (.thread, .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(fileURL: fileURL, previousState: fromState)
case (.thread, .presentMediaUploadPreview(let fileURL, let threadRootEventID)):
return .mediaUploadPreview(fileURL: fileURL, threadRootEventID: threadRootEventID, previousState: fromState)
case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)):
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
@@ -244,7 +244,7 @@ extension RoomFlowCoordinator {
// Room + Thread
case (.mediaUploadPicker(_, let previousState), .dismissMediaUploadPicker):
case (.mediaUploadPicker(_, _, let previousState), .dismissMediaUploadPicker):
return previousState
case (.emojiPicker(_, _, let previouState), .dismissEmojiPicker):
@@ -343,7 +343,7 @@ extension RoomFlowCoordinator {
case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen):
return previousState
case (.mediaUploadPreview(_, let previousState), .dismissMediaUploadPreview):
case (.mediaUploadPreview(_, _, let previousState), .dismissMediaUploadPreview):
return previousState
case (.notificationSettings, .presentGlobalNotificationSettingsScreen):
@@ -361,8 +361,8 @@ extension RoomFlowCoordinator {
case (.pollsHistoryForm, .dismissPollForm):
return .pollsHistory
case (.mediaUploadPicker(_, let previousMediaUploadPickerState), .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(fileURL: fileURL, previousState: previousMediaUploadPickerState)
case (.mediaUploadPicker(_, _, let previousMediaUploadPickerState), .presentMediaUploadPreview(let fileURL, let threadRootEventID)):
return .mediaUploadPreview(fileURL: fileURL, threadRootEventID: threadRootEventID, previousState: previousMediaUploadPickerState)
case (_, .presentInviteUsersScreen):
return .inviteUsersScreen(previousState: fromState)
@@ -16153,15 +16153,15 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
//MARK: - sendAudio
var sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = 0
var sendAudioUrlAudioInfoCaptionRequestHandleCallsCount: Int {
var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get {
if Thread.isMainThread {
return sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount
return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount
returnValue = sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
}
return returnValue!
@@ -16169,27 +16169,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
}
}
}
}
var sendAudioUrlAudioInfoCaptionRequestHandleCalled: Bool {
return sendAudioUrlAudioInfoCaptionRequestHandleCallsCount > 0
var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
}
var sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendAudioUrlAudioInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue
return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue
returnValue = sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
}
return returnValue!
@@ -16197,35 +16197,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
}
}
}
}
var sendAudioUrlAudioInfoCaptionRequestHandleClosure: ((URL, AudioInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
var sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, AudioInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendAudio(url: URL, audioInfo: AudioInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendAudioUrlAudioInfoCaptionRequestHandleCallsCount += 1
if let sendAudioUrlAudioInfoCaptionRequestHandleClosure = sendAudioUrlAudioInfoCaptionRequestHandleClosure {
return await sendAudioUrlAudioInfoCaptionRequestHandleClosure(url, audioInfo, caption, requestHandle)
func sendAudio(url: URL, audioInfo: AudioInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure = sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure(url, audioInfo, caption, threadRootEventID, requestHandle)
} else {
return sendAudioUrlAudioInfoCaptionRequestHandleReturnValue
return sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleReturnValue
}
}
//MARK: - sendFile
var sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = 0
var sendFileUrlFileInfoCaptionRequestHandleCallsCount: Int {
var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get {
if Thread.isMainThread {
return sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount
return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount
returnValue = sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
}
return returnValue!
@@ -16233,27 +16233,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
}
}
}
}
var sendFileUrlFileInfoCaptionRequestHandleCalled: Bool {
return sendFileUrlFileInfoCaptionRequestHandleCallsCount > 0
var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
}
var sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendFileUrlFileInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue
return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue
returnValue = sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
}
return returnValue!
@@ -16261,35 +16261,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
}
}
}
}
var sendFileUrlFileInfoCaptionRequestHandleClosure: ((URL, FileInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
var sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, FileInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendFile(url: URL, fileInfo: FileInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendFileUrlFileInfoCaptionRequestHandleCallsCount += 1
if let sendFileUrlFileInfoCaptionRequestHandleClosure = sendFileUrlFileInfoCaptionRequestHandleClosure {
return await sendFileUrlFileInfoCaptionRequestHandleClosure(url, fileInfo, caption, requestHandle)
func sendFile(url: URL, fileInfo: FileInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure = sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure(url, fileInfo, caption, threadRootEventID, requestHandle)
} else {
return sendFileUrlFileInfoCaptionRequestHandleReturnValue
return sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleReturnValue
}
}
//MARK: - sendImage
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = 0
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount: Int {
var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get {
if Thread.isMainThread {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount
return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount
returnValue = sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
}
return returnValue!
@@ -16297,27 +16297,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
}
}
}
}
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCalled: Bool {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount > 0
var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
}
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue
return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue
returnValue = sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
}
return returnValue!
@@ -16325,35 +16325,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
}
}
}
}
var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure: ((URL, URL, ImageInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
var sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, URL, ImageInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount += 1
if let sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure {
return await sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure(url, thumbnailURL, imageInfo, caption, requestHandle)
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure = sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure(url, thumbnailURL, imageInfo, caption, threadRootEventID, requestHandle)
} else {
return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleReturnValue
return sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleReturnValue
}
}
//MARK: - sendLocation
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount = 0
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount: Int {
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount = 0
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCallsCount: Int {
get {
if Thread.isMainThread {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount
returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount
}
return returnValue!
@@ -16361,29 +16361,29 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount = newValue
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingCallsCount = newValue
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingCallsCount = newValue
}
}
}
}
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCalled: Bool {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount > 0
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCalled: Bool {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCallsCount > 0
}
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments: (body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)?
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations: [(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)] = []
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedArguments: (body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?, threadRootEventID: String?)?
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedInvocations: [(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?, threadRootEventID: String?)] = []
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue: Result<Void, TimelineProxyError>! {
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue
returnValue = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue
}
return returnValue!
@@ -16391,39 +16391,39 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue = newValue
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeUnderlyingReturnValue = newValue
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDUnderlyingReturnValue = newValue
}
}
}
}
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure: ((String, GeoURI, String?, UInt8?, AssetType?) async -> Result<Void, TimelineProxyError>)?
var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure: ((String, GeoURI, String?, UInt8?, AssetType?, String?) async -> Result<Void, TimelineProxyError>)?
func sendLocation(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?) async -> Result<Void, TimelineProxyError> {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount += 1
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments = (body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType)
func sendLocation(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?, threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDCallsCount += 1
sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedArguments = (body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType, threadRootEventID: threadRootEventID)
DispatchQueue.main.async {
self.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations.append((body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType))
self.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReceivedInvocations.append((body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType, threadRootEventID: threadRootEventID))
}
if let sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure {
return await sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure(body, geoURI, description, zoomLevel, assetType)
if let sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure {
return await sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDClosure(body, geoURI, description, zoomLevel, assetType, threadRootEventID)
} else {
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue
return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeThreadRootEventIDReturnValue
}
}
//MARK: - sendVideo
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = 0
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount: Int {
var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCallsCount: Int {
get {
if Thread.isMainThread {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount
return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount
returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount
}
return returnValue!
@@ -16431,27 +16431,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = newValue
sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
}
}
}
}
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCalled: Bool {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount > 0
var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCalled: Bool {
return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCallsCount > 0
}
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue
return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue
returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue
}
return returnValue!
@@ -16459,35 +16459,35 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue = newValue
sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
}
}
}
}
var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure: ((URL, URL, VideoInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
var sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure: ((URL, URL, VideoInfo, String?, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount += 1
if let sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure {
return await sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure(url, thumbnailURL, videoInfo, caption, requestHandle)
func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, caption: String?, threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleCallsCount += 1
if let sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure {
return await sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure(url, thumbnailURL, videoInfo, caption, threadRootEventID, requestHandle)
} else {
return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleReturnValue
return sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleReturnValue
}
}
//MARK: - sendVoiceMessage
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = 0
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount: Int {
var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount = 0
var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCallsCount: Int {
get {
if Thread.isMainThread {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount
return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount
returnValue = sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount
}
return returnValue!
@@ -16495,27 +16495,27 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = newValue
sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = newValue
sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingCallsCount = newValue
}
}
}
}
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled: Bool {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount > 0
var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCalled: Bool {
return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCallsCount > 0
}
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue
return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue
returnValue = sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue
}
return returnValue!
@@ -16523,22 +16523,22 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue = newValue
sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue = newValue
sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleUnderlyingReturnValue = newValue
}
}
}
}
var sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure: ((URL, AudioInfo, [UInt16], @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
var sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure: ((URL, AudioInfo, [UInt16], String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>)?
func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount += 1
if let sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure {
return await sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure(url, audioInfo, waveform, requestHandle)
func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], threadRootEventID: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCallsCount += 1
if let sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure {
return await sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure(url, audioInfo, waveform, threadRootEventID, requestHandle)
} else {
return sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue
return sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue
}
}
//MARK: - sendReadReceipt
@@ -16823,15 +16823,15 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
//MARK: - createPoll
var createPollQuestionAnswersPollKindUnderlyingCallsCount = 0
var createPollQuestionAnswersPollKindCallsCount: Int {
var createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount = 0
var createPollQuestionAnswersPollKindThreadRootEventIDCallsCount: Int {
get {
if Thread.isMainThread {
return createPollQuestionAnswersPollKindUnderlyingCallsCount
return createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = createPollQuestionAnswersPollKindUnderlyingCallsCount
returnValue = createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount
}
return returnValue!
@@ -16839,29 +16839,29 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
createPollQuestionAnswersPollKindUnderlyingCallsCount = newValue
createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
createPollQuestionAnswersPollKindUnderlyingCallsCount = newValue
createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingCallsCount = newValue
}
}
}
}
var createPollQuestionAnswersPollKindCalled: Bool {
return createPollQuestionAnswersPollKindCallsCount > 0
var createPollQuestionAnswersPollKindThreadRootEventIDCalled: Bool {
return createPollQuestionAnswersPollKindThreadRootEventIDCallsCount > 0
}
var createPollQuestionAnswersPollKindReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)?
var createPollQuestionAnswersPollKindReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] = []
var createPollQuestionAnswersPollKindThreadRootEventIDReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?)?
var createPollQuestionAnswersPollKindThreadRootEventIDReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?)] = []
var createPollQuestionAnswersPollKindUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var createPollQuestionAnswersPollKindReturnValue: Result<Void, TimelineProxyError>! {
var createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var createPollQuestionAnswersPollKindThreadRootEventIDReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return createPollQuestionAnswersPollKindUnderlyingReturnValue
return createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = createPollQuestionAnswersPollKindUnderlyingReturnValue
returnValue = createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue
}
return returnValue!
@@ -16869,26 +16869,26 @@ class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable {
}
set {
if Thread.isMainThread {
createPollQuestionAnswersPollKindUnderlyingReturnValue = newValue
createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
createPollQuestionAnswersPollKindUnderlyingReturnValue = newValue
createPollQuestionAnswersPollKindThreadRootEventIDUnderlyingReturnValue = newValue
}
}
}
}
var createPollQuestionAnswersPollKindClosure: ((String, [String], Poll.Kind) async -> Result<Void, TimelineProxyError>)?
var createPollQuestionAnswersPollKindThreadRootEventIDClosure: ((String, [String], Poll.Kind, String?) async -> Result<Void, TimelineProxyError>)?
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> {
createPollQuestionAnswersPollKindCallsCount += 1
createPollQuestionAnswersPollKindReceivedArguments = (question: question, answers: answers, pollKind: pollKind)
func createPoll(question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
createPollQuestionAnswersPollKindThreadRootEventIDCallsCount += 1
createPollQuestionAnswersPollKindThreadRootEventIDReceivedArguments = (question: question, answers: answers, pollKind: pollKind, threadRootEventID: threadRootEventID)
DispatchQueue.main.async {
self.createPollQuestionAnswersPollKindReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind))
self.createPollQuestionAnswersPollKindThreadRootEventIDReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind, threadRootEventID: threadRootEventID))
}
if let createPollQuestionAnswersPollKindClosure = createPollQuestionAnswersPollKindClosure {
return await createPollQuestionAnswersPollKindClosure(question, answers, pollKind)
if let createPollQuestionAnswersPollKindThreadRootEventIDClosure = createPollQuestionAnswersPollKindThreadRootEventIDClosure {
return await createPollQuestionAnswersPollKindThreadRootEventIDClosure(question, answers, pollKind, threadRootEventID)
} else {
return createPollQuestionAnswersPollKindReturnValue
return createPollQuestionAnswersPollKindThreadRootEventIDReturnValue
}
}
//MARK: - editPoll
@@ -18819,15 +18819,15 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl
}
//MARK: - sendVoiceMessage
var sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount = 0
var sendVoiceMessageInRoomAudioConverterCallsCount: Int {
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = 0
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount: Int {
get {
if Thread.isMainThread {
return sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount
return sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount
returnValue = sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount
}
return returnValue!
@@ -18835,29 +18835,29 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl
}
set {
if Thread.isMainThread {
sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount = newValue
sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendVoiceMessageInRoomAudioConverterUnderlyingCallsCount = newValue
sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingCallsCount = newValue
}
}
}
}
var sendVoiceMessageInRoomAudioConverterCalled: Bool {
return sendVoiceMessageInRoomAudioConverterCallsCount > 0
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDCalled: Bool {
return sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount > 0
}
var sendVoiceMessageInRoomAudioConverterReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol)?
var sendVoiceMessageInRoomAudioConverterReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol)] = []
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?)?
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?)] = []
var sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue: Result<Void, VoiceMessageRecorderError>!
var sendVoiceMessageInRoomAudioConverterReturnValue: Result<Void, VoiceMessageRecorderError>! {
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue: Result<Void, VoiceMessageRecorderError>!
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDReturnValue: Result<Void, VoiceMessageRecorderError>! {
get {
if Thread.isMainThread {
return sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue
return sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue
} else {
var returnValue: Result<Void, VoiceMessageRecorderError>? = nil
DispatchQueue.main.sync {
returnValue = sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue
returnValue = sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue
}
return returnValue!
@@ -18865,26 +18865,26 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unchecked Sendabl
}
set {
if Thread.isMainThread {
sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue = newValue
sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
sendVoiceMessageInRoomAudioConverterUnderlyingReturnValue = newValue
sendVoiceMessageInRoomAudioConverterThreadRootEventIDUnderlyingReturnValue = newValue
}
}
}
}
var sendVoiceMessageInRoomAudioConverterClosure: ((JoinedRoomProxyProtocol, AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError>)?
var sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure: ((JoinedRoomProxyProtocol, AudioConverterProtocol, String?) async -> Result<Void, VoiceMessageRecorderError>)?
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError> {
sendVoiceMessageInRoomAudioConverterCallsCount += 1
sendVoiceMessageInRoomAudioConverterReceivedArguments = (roomProxy: roomProxy, audioConverter: audioConverter)
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol, threadRootEventID: String?) async -> Result<Void, VoiceMessageRecorderError> {
sendVoiceMessageInRoomAudioConverterThreadRootEventIDCallsCount += 1
sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedArguments = (roomProxy: roomProxy, audioConverter: audioConverter, threadRootEventID: threadRootEventID)
DispatchQueue.main.async {
self.sendVoiceMessageInRoomAudioConverterReceivedInvocations.append((roomProxy: roomProxy, audioConverter: audioConverter))
self.sendVoiceMessageInRoomAudioConverterThreadRootEventIDReceivedInvocations.append((roomProxy: roomProxy, audioConverter: audioConverter, threadRootEventID: threadRootEventID))
}
if let sendVoiceMessageInRoomAudioConverterClosure = sendVoiceMessageInRoomAudioConverterClosure {
return await sendVoiceMessageInRoomAudioConverterClosure(roomProxy, audioConverter)
if let sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure = sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure {
return await sendVoiceMessageInRoomAudioConverterThreadRootEventIDClosure(roomProxy, audioConverter, threadRootEventID)
} else {
return sendVoiceMessageInRoomAudioConverterReturnValue
return sendVoiceMessageInRoomAudioConverterThreadRootEventIDReturnValue
}
}
}
@@ -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):
@@ -229,15 +229,22 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendAudio(url: URL,
audioInfo: AudioInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending audio")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do {
let handle = try timeline.sendAudio(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption,
formattedCaption: nil, // Rust will build this from the caption's markdown.
mentions: nil,
replyParams: nil,
replyParams: replyParameters,
useSendQueue: true),
audioInfo: audioInfo,
progressWatcher: nil)
@@ -257,15 +264,22 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendFile(url: URL,
fileInfo: FileInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending file")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do {
let handle = try timeline.sendFile(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption,
formattedCaption: nil, // Rust will build this from the caption's markdown.
mentions: nil,
replyParams: nil,
replyParams: replyParameters,
useSendQueue: true),
fileInfo: fileInfo,
progressWatcher: nil)
@@ -286,15 +300,22 @@ final class TimelineProxy: TimelineProxyProtocol {
thumbnailURL: URL,
imageInfo: ImageInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending image")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do {
let handle = try timeline.sendImage(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption,
formattedCaption: nil, // Rust will build this from the caption's markdown.
mentions: nil,
replyParams: nil,
replyParams: replyParameters,
useSendQueue: true),
thumbnailPath: thumbnailURL.path(percentEncoded: false),
imageInfo: imageInfo,
@@ -316,7 +337,8 @@ final class TimelineProxy: TimelineProxyProtocol {
geoURI: GeoURI,
description: String?,
zoomLevel: UInt8?,
assetType: AssetType?) async -> Result<Void, TimelineProxyError> {
assetType: AssetType?,
threadRootEventID: String?) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending location")
await timeline.sendLocation(body: body,
@@ -334,15 +356,22 @@ final class TimelineProxy: TimelineProxyProtocol {
thumbnailURL: URL,
videoInfo: VideoInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending video")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do {
let handle = try timeline.sendVideo(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: caption,
formattedCaption: nil,
mentions: nil,
replyParams: nil,
replyParams: replyParameters,
useSendQueue: true),
thumbnailPath: thumbnailURL.path(percentEncoded: false),
videoInfo: videoInfo,
@@ -363,15 +392,22 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendVoiceMessage(url: URL,
audioInfo: AudioInfo,
waveform: [UInt16],
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
MXLog.info("Sending voice message")
let replyParameters: ReplyParameters? = if let threadRootEventID {
ReplyParameters(eventId: threadRootEventID, enforceThread: true, replyWithinThread: false)
} else {
nil
}
do {
let handle = try timeline.sendVoiceMessage(params: .init(source: .file(filename: url.path(percentEncoded: false)),
caption: nil,
formattedCaption: nil,
mentions: nil,
replyParams: nil,
replyParams: replyParameters,
useSendQueue: true),
audioInfo: audioInfo,
waveform: waveform,
@@ -481,7 +517,9 @@ final class TimelineProxy: TimelineProxyProtocol {
// MARK: - Polls
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> {
func createPoll(question: String, answers: [String],
pollKind: Poll.Kind,
threadRootEventID: String? = nil) async -> Result<Void, TimelineProxyError> {
MXLog.info("Creating poll")
do {
@@ -82,34 +82,40 @@ protocol TimelineProxyProtocol {
func sendAudio(url: URL,
audioInfo: AudioInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendFile(url: URL,
fileInfo: FileInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendImage(url: URL,
thumbnailURL: URL,
imageInfo: ImageInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendLocation(body: String,
geoURI: GeoURI,
description: String?,
zoomLevel: UInt8?,
assetType: AssetType?) async -> Result<Void, TimelineProxyError>
assetType: AssetType?,
threadRootEventID: String?) async -> Result<Void, TimelineProxyError>
func sendVideo(url: URL,
thumbnailURL: URL,
videoInfo: VideoInfo,
caption: String?,
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendVoiceMessage(url: URL,
audioInfo: AudioInfo,
waveform: [UInt16],
threadRootEventID: String?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result<Void, TimelineProxyError>
@@ -124,7 +130,7 @@ protocol TimelineProxyProtocol {
func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async -> Result<Void, TimelineProxyError>
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError>
func createPoll(question: String, answers: [String], pollKind: Poll.Kind, threadRootEventID: String?) async -> Result<Void, TimelineProxyError>
func editPoll(original eventID: String,
question: String,
@@ -144,7 +144,9 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
return .success(waveformData)
}
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError> {
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol,
audioConverter: AudioConverterProtocol,
threadRootEventID: String?) async -> Result<Void, VoiceMessageRecorderError> {
guard let url = audioRecorder.audioFileURL else {
return .failure(VoiceMessageRecorderError.missingRecordingFile)
}
@@ -178,7 +180,8 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile,
audioInfo: audioInfo,
waveform: waveform) { _ in }
waveform: waveform,
threadRootEventID: threadRootEventID) { _ in }
if case .failure(let error) = result {
MXLog.error("Failed to send the voice message. \(error)")
@@ -38,7 +38,9 @@ protocol VoiceMessageRecorderProtocol {
func seekPlayback(to progress: Double) async
func deleteRecording() async
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol, audioConverter: AudioConverterProtocol) async -> Result<Void, VoiceMessageRecorderError>
func sendVoiceMessage(inRoom roomProxy: JoinedRoomProxyProtocol,
audioConverter: AudioConverterProtocol,
threadRootEventID: String?) async -> Result<Void, VoiceMessageRecorderError>
}
// sourcery: AutoMockable
@@ -106,16 +106,16 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
private func setUpViewModel(url: URL, expectedCaption: String?) {
timelineProxy = TimelineProxyMock(.init())
timelineProxy.sendAudioUrlAudioInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in
timelineProxy.sendAudioUrlAudioInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
}
timelineProxy.sendFileUrlFileInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in
timelineProxy.sendFileUrlFileInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
}
timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in
timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
}
timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in
timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionThreadRootEventIDRequestHandleClosure = { [weak self] _, _, _, caption, _, _ in
self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown))
}
@@ -126,6 +126,7 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
title: "Some File",
url: url,
threadRootEventID: nil,
shouldShowCaptionWarning: true)
}
@@ -196,7 +196,9 @@ class VoiceMessageRecorderTests: XCTestCase {
// If there is no recording file, an error is expected
audioRecorder.audioFileURL = nil
guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
guard case .failure(.missingRecordingFile) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected")
return
}
@@ -208,7 +210,9 @@ class VoiceMessageRecorderTests: XCTestCase {
audioConverter.convertToOpusOggSourceURLDestinationURLThrowableError = AudioConverterError.conversionFailed(nil)
let roomProxy = JoinedRoomProxyMock()
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected")
return
}
@@ -227,8 +231,10 @@ class VoiceMessageRecorderTests: XCTestCase {
let timelineProxy = TimelineProxyMock()
let roomProxy = JoinedRoomProxyMock()
roomProxy.timeline = timelineProxy
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected")
return
}
@@ -248,8 +254,10 @@ class VoiceMessageRecorderTests: XCTestCase {
let timelineProxy = TimelineProxyMock()
let roomProxy = JoinedRoomProxyMock()
roomProxy.timeline = timelineProxy
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected")
return
}
@@ -271,8 +279,10 @@ class VoiceMessageRecorderTests: XCTestCase {
let timelineProxy = TimelineProxyMock()
let roomProxy = JoinedRoomProxyMock()
roomProxy.timeline = timelineProxy
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleReturnValue = .failure(.sdkError(SDKError.generic))
guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy,
audioConverter: audioConverter,
threadRootEventID: nil) else {
XCTFail("An error is expected")
return
}
@@ -307,7 +317,7 @@ class VoiceMessageRecorderTests: XCTestCase {
XCTAssertEqual(destination.pathExtension, "ogg")
}
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = { url, audioInfo, waveform, _ in
timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleClosure = { url, audioInfo, waveform, _, _ in
XCTAssertEqual(url, convertedFileURL)
XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime)
XCTAssertEqual(audioInfo.size, convertedFileSize)
@@ -317,13 +327,13 @@ class VoiceMessageRecorderTests: XCTestCase {
return .success(())
}
guard case .success = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else {
guard case .success = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter, threadRootEventID: nil) else {
XCTFail("A success is expected")
return
}
XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled)
XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled)
XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformThreadRootEventIDRequestHandleCalled)
// the converted file must have been deleted
if let convertedFileURL {