mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Add TimelineProxy (update sdk to 0.0.7-november23) (#2178)
* Refactor RoomProxy
* Refactor paginateBackwards
* Refactor sendReadReceipt
* Refactor messageEventContent APIs
* Refactor sendMessage
* Refactor toggleReaction
* Refactor send attachments
* Refactor sendLocation
* Refactor cancel/retry send
* ⚠️ Fix encryption build errors
* Refactor editMessage
* Refactor retryDecryption
* Refactor fetchDetails
* Refactor polls APIs
* Refactor fetchMembers
* Refactor RoomTimelineProviderProtocol
* Update sdk to 0.0.7-november23
* Fix UTs
* Fix comment
* Delete old workaround
* Move TimelineProxyError
* Delete queue warnings
* Fix key listener
* Add pollHistory timeline property
* Refactor room/timeline subscriptions
* Delete unused code
This commit is contained in:
@@ -23,48 +23,35 @@ import MatrixRustSDK
|
||||
class RoomProxy: RoomProxyProtocol {
|
||||
private let roomListItem: RoomListItemProtocol
|
||||
private let room: RoomProtocol
|
||||
let timeline: TimelineProxyProtocol
|
||||
let pollHistoryTimeline: TimelineProxyProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private let backgroundTaskName = "SendRoomEvent"
|
||||
|
||||
private let messageSendingDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.message_sending", qos: .userInitiated)
|
||||
private let userInitiatedDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.user_initiated", qos: .userInitiated)
|
||||
private let lowPriorityDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.low_priority", qos: .utility)
|
||||
|
||||
private var sendMessageBackgroundTask: BackgroundTaskProtocol?
|
||||
|
||||
private(set) var displayName: String?
|
||||
|
||||
private var roomTimelineObservationToken: TaskHandle?
|
||||
private var backPaginationStateObservationToken: TaskHandle?
|
||||
private var roomInfoObservationToken: TaskHandle?
|
||||
private var subscribedForUpdates = false
|
||||
|
||||
private let backPaginationStateSubject = PassthroughSubject<BackPaginationStatus, Never>()
|
||||
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||
var members: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
|
||||
membersSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private var timelineListener: RoomTimelineListener?
|
||||
|
||||
private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>()
|
||||
|
||||
private let stateUpdatesSubject = PassthroughSubject<Void, Never>()
|
||||
var stateUpdatesPublisher: AnyPublisher<Void, Never> {
|
||||
stateUpdatesSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var innerTimelineProvider: RoomTimelineProviderProtocol!
|
||||
var timelineProvider: RoomTimelineProviderProtocol {
|
||||
innerTimelineProvider
|
||||
}
|
||||
|
||||
var ownUserID: String {
|
||||
room.ownUserId()
|
||||
}
|
||||
|
||||
deinit {
|
||||
roomTimelineObservationToken?.cancel()
|
||||
backPaginationStateObservationToken?.cancel()
|
||||
roomListItem.unsubscribe()
|
||||
}
|
||||
|
||||
@@ -74,19 +61,25 @@ class RoomProxy: RoomProxyProtocol {
|
||||
self.roomListItem = roomListItem
|
||||
self.room = room
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
timeline = await TimelineProxy(timeline: room.timeline(), backgroundTaskService: backgroundTaskService)
|
||||
pollHistoryTimeline = await TimelineProxy(timeline: room.pollHistory(), backgroundTaskService: backgroundTaskService)
|
||||
|
||||
Task {
|
||||
await fetchMembers()
|
||||
// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
||||
// This should become automatic on the RustSDK side at some point
|
||||
await room.timeline().fetchMembers()
|
||||
|
||||
await updateMembers()
|
||||
}
|
||||
}
|
||||
|
||||
func subscribeForUpdates() async {
|
||||
guard innerTimelineProvider == nil else {
|
||||
guard !subscribedForUpdates else {
|
||||
MXLog.warning("Room already subscribed for updates")
|
||||
return
|
||||
}
|
||||
|
||||
subscribedForUpdates = true
|
||||
let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""),
|
||||
RequiredState(key: "m.room.topic", value: ""),
|
||||
RequiredState(key: "m.room.avatar", value: ""),
|
||||
@@ -95,26 +88,10 @@ class RoomProxy: RoomProxyProtocol {
|
||||
timelineLimit: UInt32(SlidingSyncConstants.defaultTimelineLimit))
|
||||
roomListItem.subscribe(settings: settings)
|
||||
|
||||
let timelineListener = RoomTimelineListener { [weak self] timelineDiffs in
|
||||
self?.timelineUpdatesSubject.send(timelineDiffs)
|
||||
|
||||
// Workaround for subscribeToRoomStateUpdates creating problems in the timeline
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/issues/2488
|
||||
self?.stateUpdatesSubject.send()
|
||||
}
|
||||
await timeline.subscribeForUpdates()
|
||||
await pollHistoryTimeline.subscribeForUpdates()
|
||||
|
||||
self.timelineListener = timelineListener
|
||||
|
||||
let result = await room.addTimelineListener(listener: timelineListener)
|
||||
roomTimelineObservationToken = result.itemsStream
|
||||
|
||||
subscribeToBackpagination()
|
||||
|
||||
// subscribeToRoomStateUpdates()
|
||||
|
||||
innerTimelineProvider = await RoomTimelineProvider(currentItems: result.items,
|
||||
updatePublisher: timelineUpdatesSubject.eraseToAnyPublisher(),
|
||||
backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher())
|
||||
subscribeToRoomStateUpdates()
|
||||
}
|
||||
|
||||
lazy var id: String = room.id()
|
||||
@@ -216,282 +193,6 @@ class RoomProxy: RoomProxyProtocol {
|
||||
return .failure(.failedRetrievingMemberDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await Task.dispatch(on: .global()) {
|
||||
try self.room.paginateBackwards(opts: .untilNumItems(eventLimit: UInt16(requestSize), items: UInt16(untilNumberOfItems), waitForToken: true))
|
||||
}
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
}
|
||||
|
||||
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: lowPriorityDispatchQueue) {
|
||||
do {
|
||||
try self.room.sendReadReceipt(eventId: eventID)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingReadReceipt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? {
|
||||
try? room.getTimelineEventContentByEventId(eventId: eventID)
|
||||
}
|
||||
|
||||
func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
self.room.send(msg: messageContent)
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String,
|
||||
html: String?,
|
||||
inReplyTo eventID: String? = nil,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let messageContent = buildMessageContentFor(message,
|
||||
html: html,
|
||||
intentionalMentions: intentionalMentions.toRustMentions())
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
do {
|
||||
if let eventID {
|
||||
let replyItem = try self.room.getEventTimelineItemByEventId(eventId: eventID)
|
||||
try self.room.sendReply(msg: messageContent, replyItem: replyItem)
|
||||
} else {
|
||||
self.room.send(msg: messageContent)
|
||||
}
|
||||
} catch {
|
||||
return .failure(.failedSendingMessage)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||
do {
|
||||
try self.room.toggleReaction(eventId: eventID, key: reaction)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingReaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendImage(url: URL,
|
||||
thumbnailURL: URL,
|
||||
imageInfo: ImageInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = room.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendVideo(url: URL,
|
||||
thumbnailURL: URL,
|
||||
videoInfo: VideoInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = room.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendAudio(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = room.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendVoiceMessage(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: [UInt16],
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = room.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendFile(url: URL,
|
||||
fileInfo: FileInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = room.sendFile(url: url.path(percentEncoded: false), fileInfo: fileInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendLocation(body: String,
|
||||
geoURI: GeoURI,
|
||||
description: String?,
|
||||
zoomLevel: UInt8?,
|
||||
assetType: AssetType?) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
.success(self.room.sendLocation(body: body,
|
||||
geoUri: geoURI.string,
|
||||
description: description,
|
||||
zoomLevel: zoomLevel,
|
||||
assetType: assetType))
|
||||
}
|
||||
}
|
||||
|
||||
func retrySend(transactionID: String) async {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
self.room.retrySend(txnId: transactionID)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelSend(transactionID: String) async {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
self.room.cancelSend(txnId: transactionID)
|
||||
}
|
||||
}
|
||||
|
||||
func editMessage(_ message: String,
|
||||
html: String?,
|
||||
original eventID: String,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let messageContent = buildMessageContentFor(message,
|
||||
html: html,
|
||||
intentionalMentions: intentionalMentions.toRustMentions())
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
do {
|
||||
let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID)
|
||||
try self.room.edit(newContent: messageContent, editItem: originalEvent)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedEditingMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
@@ -572,12 +273,6 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func retryDecryption(for sessionID: String) async {
|
||||
await Task.dispatch(on: .global()) { [weak self] in
|
||||
self?.room.retryDecryption(sessionIds: [sessionID])
|
||||
}
|
||||
}
|
||||
|
||||
func leaveRoom() async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
@@ -626,19 +321,6 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchDetails(for eventID: String) {
|
||||
Task {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
MXLog.info("Fetching event details for \(eventID)")
|
||||
try self.room.fetchDetailsForEvent(eventId: eventID)
|
||||
} catch {
|
||||
MXLog.error("Failed fetching event details for \(eventID) with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func invite(userID: String) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
@@ -713,56 +395,6 @@ class RoomProxy: RoomProxyProtocol {
|
||||
return .failure(.failedCheckingPermission)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.room.createPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind)))
|
||||
} catch {
|
||||
MXLog.error("Failed creating a poll: \(error)")
|
||||
return .failure(.failedCreatingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func editPoll(original eventID: String,
|
||||
question: String,
|
||||
answers: [String],
|
||||
pollKind: Poll.Kind) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID)
|
||||
return try .success(self.room.editPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind), editItem: originalEvent))
|
||||
} catch {
|
||||
MXLog.error("Failed editing the poll: \(error), eventID: \(eventID)")
|
||||
return .failure(.failedEditingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.room.sendPollResponse(pollStartId: pollStartID, answers: answers))
|
||||
} catch {
|
||||
MXLog.error("Failed sending a poll vote: \(error), pollStartID: \(pollStartID)")
|
||||
return .failure(.failedSendingPollResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.room.endPoll(pollStartId: pollStartID, text: text))
|
||||
} catch {
|
||||
MXLog.error("Failed ending a poll: \(error), pollStartID: \(pollStartID)")
|
||||
return .failure(.failedEndingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Element Call
|
||||
|
||||
@@ -771,64 +403,6 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildMessageContentFor(_ message: String,
|
||||
html: String?,
|
||||
intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation {
|
||||
let emoteSlashCommand = "/me "
|
||||
let isEmote: Bool = message.starts(with: emoteSlashCommand)
|
||||
|
||||
let content: RoomMessageEventContentWithoutRelation
|
||||
if isEmote {
|
||||
let emoteMessage = String(message.dropFirst(emoteSlashCommand.count))
|
||||
|
||||
var emoteHtml: String?
|
||||
if let html {
|
||||
emoteHtml = String(html.dropFirst(emoteSlashCommand.count))
|
||||
}
|
||||
content = buildEmoteMessageContentFor(emoteMessage, html: emoteHtml)
|
||||
} else {
|
||||
if let html {
|
||||
content = messageEventContentFromHtml(body: message, htmlBody: html)
|
||||
} else {
|
||||
content = messageEventContentFromMarkdown(md: message)
|
||||
}
|
||||
}
|
||||
return content.withMentions(mentions: intentionalMentions)
|
||||
}
|
||||
|
||||
private func buildEmoteMessageContentFor(_ message: String, html: String?) -> RoomMessageEventContentWithoutRelation {
|
||||
if let html {
|
||||
return messageEventContentFromHtmlAsEmote(body: message, htmlBody: html)
|
||||
} else {
|
||||
return messageEventContentFromMarkdownAsEmote(md: message)
|
||||
}
|
||||
}
|
||||
|
||||
/// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
||||
/// This should become automatic on the RustSDK side at some point
|
||||
private func fetchMembers() async {
|
||||
do {
|
||||
try await room.fetchMembers()
|
||||
} catch {
|
||||
MXLog.error("Failed fetching members: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func update(displayName: String) {
|
||||
self.displayName = displayName
|
||||
}
|
||||
|
||||
private func subscribeToBackpagination() {
|
||||
let listener = RoomBackpaginationStatusListener { [weak self] status in
|
||||
self?.backPaginationStateSubject.send(status)
|
||||
}
|
||||
do {
|
||||
backPaginationStateObservationToken = try room.subscribeToBackPaginationStatus(listener: listener)
|
||||
} catch {
|
||||
MXLog.error("Failed to subscribe to back pagination state with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func subscribeToRoomStateUpdates() {
|
||||
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in
|
||||
@@ -838,44 +412,6 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomTimelineListener: TimelineListener {
|
||||
private let onUpdateClosure: ([TimelineDiff]) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping ([TimelineDiff]) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func onUpdate(diff: [TimelineDiff]) {
|
||||
onUpdateClosure(diff)
|
||||
}
|
||||
}
|
||||
|
||||
private final class UploadProgressListener: ProgressWatcher {
|
||||
private let onUpdateClosure: (Double) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping (Double) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func transmissionProgress(progress: TransmissionProgress) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onUpdateClosure(Double(progress.current) / Double(progress.total))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomBackpaginationStatusListener: BackPaginationStatusListener {
|
||||
private let onUpdateClosure: (BackPaginationStatus) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping (BackPaginationStatus) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func onUpdate(status: BackPaginationStatus) {
|
||||
onUpdateClosure(status)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomInfoUpdateListener: RoomInfoListener {
|
||||
private let onUpdateClosure: () -> Void
|
||||
|
||||
@@ -887,14 +423,3 @@ private final class RoomInfoUpdateListener: RoomInfoListener {
|
||||
onUpdateClosure()
|
||||
}
|
||||
}
|
||||
|
||||
private extension MatrixRustSDK.PollKind {
|
||||
init(pollKind: Poll.Kind) {
|
||||
switch pollKind {
|
||||
case .disclosed:
|
||||
self = .disclosed
|
||||
case .undisclosed:
|
||||
self = .undisclosed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,10 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyError: Error, Equatable {
|
||||
case noMoreMessagesToBackPaginate
|
||||
case failedPaginatingBackwards
|
||||
case failedRetrievingMemberAvatarURL
|
||||
case failedRetrievingMemberDisplayName
|
||||
case failedSendingReadReceipt
|
||||
case failedSendingMessage
|
||||
case failedSendingReaction
|
||||
case failedSendingMedia
|
||||
case failedEditingMessage
|
||||
case failedRedactingEvent
|
||||
case failedReportingContent
|
||||
case failedAddingTimelineListener
|
||||
case failedRetrievingMembers
|
||||
case failedRetrievingMember
|
||||
case failedLeavingRoom
|
||||
case failedAcceptingInvite
|
||||
@@ -42,10 +33,6 @@ enum RoomProxyError: Error, Equatable {
|
||||
case failedRemovingAvatar
|
||||
case failedUploadingAvatar
|
||||
case failedCheckingPermission
|
||||
case failedCreatingPoll
|
||||
case failedSendingPollResponse
|
||||
case failedEndingPoll
|
||||
case failedEditingPoll
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
@@ -82,7 +69,10 @@ protocol RoomProxyProtocol {
|
||||
/// The thread on which this publisher sends the output isn't defined.
|
||||
var stateUpdatesPublisher: AnyPublisher<Void, Never> { get }
|
||||
|
||||
var timelineProvider: RoomTimelineProviderProtocol { get }
|
||||
var timeline: TimelineProxyProtocol { get }
|
||||
|
||||
/// A timeline providing just polls related events
|
||||
var pollHistoryTimeline: TimelineProxyProtocol { get }
|
||||
|
||||
func subscribeForUpdates() async
|
||||
|
||||
@@ -90,73 +80,11 @@ protocol RoomProxyProtocol {
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
||||
|
||||
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation?
|
||||
|
||||
func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendMessage(_ message: String,
|
||||
html: String?,
|
||||
inReplyTo eventID: String?,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendImage(url: URL,
|
||||
thumbnailURL: URL,
|
||||
imageInfo: ImageInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendVideo(url: URL,
|
||||
thumbnailURL: URL,
|
||||
videoInfo: VideoInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendAudio(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendFile(url: URL,
|
||||
fileInfo: FileInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendLocation(body: String,
|
||||
geoURI: GeoURI,
|
||||
description: String?,
|
||||
zoomLevel: UInt8?,
|
||||
assetType: AssetType?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendVoiceMessage(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: [UInt16],
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError>
|
||||
|
||||
/// Retries sending a failed message given its transaction ID
|
||||
func retrySend(transactionID: String) async
|
||||
|
||||
/// Cancels a failed message given its transaction ID from the timeline
|
||||
func cancelSend(transactionID: String) async
|
||||
|
||||
func editMessage(_ newMessage: String,
|
||||
html: String?,
|
||||
original eventID: String,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func ignoreUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func retryDecryption(for sessionID: String) async
|
||||
|
||||
func leaveRoom() async -> Result<Void, RoomProxyError>
|
||||
|
||||
@@ -170,8 +98,6 @@ protocol RoomProxyProtocol {
|
||||
|
||||
func acceptInvitation() async -> Result<Void, RoomProxyError>
|
||||
|
||||
func fetchDetails(for eventID: String)
|
||||
|
||||
func invite(userID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func setName(_ name: String) async -> Result<Void, RoomProxyError>
|
||||
@@ -185,14 +111,6 @@ protocol RoomProxyProtocol {
|
||||
func canUserRedact(userID: String) async -> Result<Bool, RoomProxyError>
|
||||
|
||||
func canUserTriggerRoomNotification(userID: String) async -> Result<Bool, RoomProxyError>
|
||||
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
// MARK: - Element Call
|
||||
|
||||
@@ -213,15 +131,6 @@ extension RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String,
|
||||
html: String?,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, RoomProxyError> {
|
||||
await sendMessage(message,
|
||||
html: html,
|
||||
inReplyTo: nil,
|
||||
intentionalMentions: intentionalMentions)
|
||||
}
|
||||
|
||||
// Avoids to duplicate the same logic around in the app
|
||||
// Probably this should be done in rust.
|
||||
var roomTitle: String {
|
||||
|
||||
@@ -96,22 +96,21 @@ class SecureBackupController: SecureBackupControllerProtocol {
|
||||
return .success(key)
|
||||
}
|
||||
|
||||
var keyUploadErrored = false
|
||||
let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in
|
||||
guard let self else { return }
|
||||
|
||||
switch state {
|
||||
case .creatingBackup:
|
||||
recoveryKeyStateSubject.send(.settingUp)
|
||||
case .creatingRecoveryKey:
|
||||
recoveryKeyStateSubject.send(.settingUp)
|
||||
case .backingUp:
|
||||
case .starting, .creatingBackup, .creatingRecoveryKey, .backingUp:
|
||||
recoveryKeyStateSubject.send(.settingUp)
|
||||
case .done:
|
||||
recoveryKeyStateSubject.send(.enabled)
|
||||
case .roomKeyUploadError:
|
||||
keyUploadErrored = true
|
||||
}
|
||||
})
|
||||
|
||||
return .success(recoveryKey)
|
||||
return keyUploadErrored ? .failure(.failedGeneratingRecoveryKey) : .success(recoveryKey)
|
||||
} catch {
|
||||
return .failure(.failedGeneratingRecoveryKey)
|
||||
}
|
||||
@@ -119,7 +118,7 @@ class SecureBackupController: SecureBackupControllerProtocol {
|
||||
|
||||
func confirmRecoveryKey(_ key: String) async -> Result<Void, SecureBackupControllerError> {
|
||||
do {
|
||||
try await encryption.fixRecoveryIssues(recoveryKey: key)
|
||||
try await encryption.recover(recoveryKey: key)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedConfirmingRecoveryKey)
|
||||
@@ -143,7 +142,7 @@ class SecureBackupController: SecureBackupControllerProtocol {
|
||||
case .BackupDisabled:
|
||||
MXLog.error("Key backup disabled, continuing logout.")
|
||||
return .success(())
|
||||
case .Connection, .Laged:
|
||||
case .Connection, .Lagged:
|
||||
MXLog.error("Key backup upload failure: \(error)")
|
||||
return .failure(.failedUploadingForBackup)
|
||||
}
|
||||
|
||||
+3
-3
@@ -52,7 +52,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError> {
|
||||
guard let roomProxy, let eventID = itemID.eventID else { return .failure(.generic) }
|
||||
switch await roomProxy.sendReadReceipt(for: eventID) {
|
||||
switch await roomProxy.timeline.sendReadReceipt(for: eventID) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure:
|
||||
@@ -89,7 +89,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
await roomProxy?.retrySend(transactionID: transactionID)
|
||||
await roomProxy?.timeline.retrySend(transactionID: transactionID)
|
||||
}
|
||||
|
||||
func cancelSending(itemID: TimelineItemIdentifier) async {
|
||||
@@ -97,7 +97,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
await roomProxy?.cancelSend(transactionID: transactionID)
|
||||
await roomProxy?.timeline.cancelSend(transactionID: transactionID)
|
||||
}
|
||||
|
||||
// MARK: - UI Test signalling
|
||||
|
||||
+18
-21
@@ -46,7 +46,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
appSettings: AppSettings,
|
||||
secureBackupController: SecureBackupControllerProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
timelineProvider = roomProxy.timelineProvider
|
||||
timelineProvider = roomProxy.timeline.timelineProvider
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
self.appSettings = appSettings
|
||||
self.secureBackupController = secureBackupController
|
||||
@@ -71,13 +71,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> {
|
||||
MXLog.info("Started back pagination request")
|
||||
switch await roomProxy.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) {
|
||||
switch await roomProxy.timeline.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) {
|
||||
case .success:
|
||||
MXLog.info("Finished back pagination request")
|
||||
return .success(())
|
||||
case .failure(.noMoreMessagesToBackPaginate):
|
||||
MXLog.warning("Back pagination requested when all messages have been loaded.")
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed back pagination request with error: \(error)")
|
||||
return .failure(.generic)
|
||||
@@ -89,7 +86,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let eventID = itemID.eventID
|
||||
else { return .success(()) }
|
||||
|
||||
switch await roomProxy.sendReadReceipt(for: eventID) {
|
||||
switch await roomProxy.timeline.sendReadReceipt(for: eventID) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure:
|
||||
@@ -123,11 +120,11 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
MXLog.error("Send reply in \(roomID) failed: missing event ID")
|
||||
return
|
||||
}
|
||||
|
||||
switch await roomProxy.sendMessage(message,
|
||||
html: html,
|
||||
inReplyTo: inReplyTo,
|
||||
intentionalMentions: intentionalMentions) {
|
||||
|
||||
switch await roomProxy.timeline.sendMessage(message,
|
||||
html: html,
|
||||
inReplyTo: inReplyTo,
|
||||
intentionalMentions: intentionalMentions) {
|
||||
case .success:
|
||||
MXLog.info("Finished sending message")
|
||||
case .failure(let error):
|
||||
@@ -142,7 +139,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch await roomProxy.toggleReaction(reaction, to: eventID) {
|
||||
switch await roomProxy.timeline.toggleReaction(reaction, to: eventID) {
|
||||
case .success:
|
||||
MXLog.info("Finished toggling reaction")
|
||||
case .failure(let error):
|
||||
@@ -162,10 +159,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
await cancelSending(itemID: itemID)
|
||||
await sendMessage(newMessage, html: html, intentionalMentions: intentionalMentions)
|
||||
} else if let eventID = itemID.eventID {
|
||||
switch await roomProxy.editMessage(newMessage,
|
||||
html: html,
|
||||
original: eventID,
|
||||
intentionalMentions: intentionalMentions) {
|
||||
switch await roomProxy.timeline.editMessage(newMessage,
|
||||
html: html,
|
||||
original: eventID,
|
||||
intentionalMentions: intentionalMentions) {
|
||||
case .success:
|
||||
MXLog.info("Finished editing message")
|
||||
case .failure(let error):
|
||||
@@ -207,7 +204,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
func retryDecryption(for sessionID: String) async {
|
||||
await roomProxy.retryDecryption(for: sessionID)
|
||||
await roomProxy.timeline.retryDecryption(for: sessionID)
|
||||
}
|
||||
|
||||
func retrySending(itemID: TimelineItemIdentifier) async {
|
||||
@@ -217,7 +214,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
MXLog.info("Retry sending in \(roomID)")
|
||||
await roomProxy.retrySend(transactionID: transactionID)
|
||||
await roomProxy.timeline.retrySend(transactionID: transactionID)
|
||||
}
|
||||
|
||||
func cancelSending(itemID: TimelineItemIdentifier) async {
|
||||
@@ -227,7 +224,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
MXLog.info("Cancelling send in \(roomID)")
|
||||
await roomProxy.cancelSend(transactionID: transactionID)
|
||||
await roomProxy.timeline.cancelSend(transactionID: transactionID)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -378,10 +375,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
switch timelineItem.replyDetails {
|
||||
case .notLoaded:
|
||||
roomProxy.fetchDetails(for: eventID)
|
||||
roomProxy.timeline.fetchDetails(for: eventID)
|
||||
case .error:
|
||||
if refetchOnError {
|
||||
roomProxy.fetchDetails(for: eventID)
|
||||
roomProxy.timeline.fetchDetails(for: eventID)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
final class TimelineProxy: TimelineProxyProtocol {
|
||||
private let timeline: Timeline
|
||||
private var sendMessageBackgroundTask: BackgroundTaskProtocol?
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private let backgroundTaskName = "SendRoomEvent"
|
||||
private let lowPriorityDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.low_priority", qos: .utility)
|
||||
private let messageSendingDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.message_sending", qos: .userInitiated)
|
||||
private let userInitiatedDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.user_initiated", qos: .userInitiated)
|
||||
|
||||
init(timeline: Timeline, backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.timeline = timeline
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
private var backPaginationStateObservationToken: TaskHandle?
|
||||
private var roomTimelineObservationToken: TaskHandle?
|
||||
private var timelineListener: RoomTimelineListener?
|
||||
|
||||
private let backPaginationStateSubject = PassthroughSubject<BackPaginationStatus, Never>()
|
||||
private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>()
|
||||
|
||||
private var innerTimelineProvider: RoomTimelineProviderProtocol!
|
||||
var timelineProvider: RoomTimelineProviderProtocol {
|
||||
innerTimelineProvider
|
||||
}
|
||||
|
||||
var hasPendingUpdatesSubscription: Bool {
|
||||
innerTimelineProvider != nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
backPaginationStateObservationToken?.cancel()
|
||||
roomTimelineObservationToken?.cancel()
|
||||
}
|
||||
|
||||
func subscribeForUpdates() async {
|
||||
guard innerTimelineProvider == nil else {
|
||||
MXLog.warning("Timeline already subscribed for updates")
|
||||
return
|
||||
}
|
||||
|
||||
let timelineListener = RoomTimelineListener { [weak self] timelineDiffs in
|
||||
self?.timelineUpdatesSubject.send(timelineDiffs)
|
||||
}
|
||||
|
||||
self.timelineListener = timelineListener
|
||||
|
||||
let result = await timeline.addListener(listener: timelineListener)
|
||||
roomTimelineObservationToken = result.itemsStream
|
||||
|
||||
subscribeToBackpagination()
|
||||
|
||||
innerTimelineProvider = await RoomTimelineProvider(currentItems: result.items,
|
||||
updatePublisher: timelineUpdatesSubject.eraseToAnyPublisher(),
|
||||
backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func cancelSend(transactionID: String) async {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
self.timeline.cancelSend(txnId: transactionID)
|
||||
}
|
||||
}
|
||||
|
||||
func editMessage(_ message: String,
|
||||
html: String?,
|
||||
original eventID: String,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let messageContent = buildMessageContentFor(message,
|
||||
html: html,
|
||||
intentionalMentions: intentionalMentions.toRustMentions())
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
do {
|
||||
let originalEvent = try self.timeline.getEventTimelineItemByEventId(eventId: eventID)
|
||||
try self.timeline.edit(newContent: messageContent, editItem: originalEvent)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedEditingMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchDetails(for eventID: String) {
|
||||
Task {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
MXLog.info("Fetching event details for \(eventID)")
|
||||
try self.timeline.fetchDetailsForEvent(eventId: eventID)
|
||||
} catch {
|
||||
MXLog.error("Failed fetching event details for \(eventID) with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? {
|
||||
try? timeline.getTimelineEventContentByEventId(eventId: eventID)
|
||||
}
|
||||
|
||||
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, TimelineProxyError> {
|
||||
do {
|
||||
try await Task.dispatch(on: .global()) {
|
||||
try self.timeline.paginateBackwards(opts: .untilNumItems(eventLimit: UInt16(requestSize), items: UInt16(untilNumberOfItems), waitForToken: true))
|
||||
}
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
}
|
||||
|
||||
func retryDecryption(for sessionID: String) async {
|
||||
await Task.dispatch(on: .global()) { [weak self] in
|
||||
self?.timeline.retryDecryption(sessionIds: [sessionID])
|
||||
}
|
||||
}
|
||||
|
||||
func retrySend(transactionID: String) async {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
self.timeline.retrySend(txnId: transactionID)
|
||||
}
|
||||
}
|
||||
|
||||
func sendAudio(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendFile(url: URL,
|
||||
fileInfo: FileInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendFile(url: url.path(percentEncoded: false), fileInfo: fileInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendImage(url: URL,
|
||||
thumbnailURL: URL,
|
||||
imageInfo: ImageInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendLocation(body: String,
|
||||
geoURI: GeoURI,
|
||||
description: String?,
|
||||
zoomLevel: UInt8?,
|
||||
assetType: AssetType?) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
.success(self.timeline.sendLocation(body: body,
|
||||
geoUri: geoURI.string,
|
||||
description: description,
|
||||
zoomLevel: zoomLevel,
|
||||
assetType: assetType))
|
||||
}
|
||||
}
|
||||
|
||||
func sendVideo(url: URL,
|
||||
thumbnailURL: URL,
|
||||
videoInfo: VideoInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendVoiceMessage(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: [UInt16],
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let handle = timeline.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, progressWatcher: UploadProgressListener { progress in
|
||||
progressSubject?.send(progress)
|
||||
})
|
||||
|
||||
await requestHandle(handle)
|
||||
|
||||
do {
|
||||
try await handle.join()
|
||||
} catch {
|
||||
return .failure(.failedSendingMedia)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String,
|
||||
html: String?,
|
||||
inReplyTo eventID: String? = nil,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
let messageContent = buildMessageContentFor(message,
|
||||
html: html,
|
||||
intentionalMentions: intentionalMentions.toRustMentions())
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
do {
|
||||
if let eventID {
|
||||
let replyItem = try self.timeline.getEventTimelineItemByEventId(eventId: eventID)
|
||||
try self.timeline.sendReply(msg: messageContent, replyItem: replyItem)
|
||||
} else {
|
||||
self.timeline.send(msg: messageContent)
|
||||
}
|
||||
} catch {
|
||||
return .failure(.failedSendingMessage)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: messageSendingDispatchQueue) {
|
||||
self.timeline.send(msg: messageContent)
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
func sendReadReceipt(for eventID: String) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: lowPriorityDispatchQueue) {
|
||||
do {
|
||||
try self.timeline.sendReadReceipt(eventId: eventID)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingReadReceipt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventID: String) async -> Result<Void, TimelineProxyError> {
|
||||
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||
defer {
|
||||
sendMessageBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||
do {
|
||||
try self.timeline.toggleReaction(eventId: eventID, key: reaction)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingReaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.timeline.createPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind)))
|
||||
} catch {
|
||||
MXLog.error("Failed creating a poll: \(error)")
|
||||
return .failure(.failedCreatingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func editPoll(original eventID: String,
|
||||
question: String,
|
||||
answers: [String],
|
||||
pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> {
|
||||
do {
|
||||
let originalEvent = try await Task.dispatch(on: .global()) {
|
||||
try self.timeline.getEventTimelineItemByEventId(eventId: eventID)
|
||||
}
|
||||
return try await .success(timeline.editPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind), editItem: originalEvent))
|
||||
} catch {
|
||||
MXLog.error("Failed editing the poll: \(error), eventID: \(eventID)")
|
||||
return .failure(.failedEditingPoll)
|
||||
}
|
||||
}
|
||||
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.timeline.endPoll(pollStartId: pollStartID, text: text))
|
||||
} catch {
|
||||
MXLog.error("Failed ending a poll: \(error), pollStartID: \(pollStartID)")
|
||||
return .failure(.failedEndingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
return try .success(self.timeline.sendPollResponse(pollStartId: pollStartID, answers: answers))
|
||||
} catch {
|
||||
MXLog.error("Failed sending a poll vote: \(error), pollStartID: \(pollStartID)")
|
||||
return .failure(.failedSendingPollResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildMessageContentFor(_ message: String,
|
||||
html: String?,
|
||||
intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation {
|
||||
let emoteSlashCommand = "/me "
|
||||
let isEmote: Bool = message.starts(with: emoteSlashCommand)
|
||||
|
||||
let content: RoomMessageEventContentWithoutRelation
|
||||
if isEmote {
|
||||
let emoteMessage = String(message.dropFirst(emoteSlashCommand.count))
|
||||
|
||||
var emoteHtml: String?
|
||||
if let html {
|
||||
emoteHtml = String(html.dropFirst(emoteSlashCommand.count))
|
||||
}
|
||||
content = buildEmoteMessageContentFor(emoteMessage, html: emoteHtml)
|
||||
} else {
|
||||
if let html {
|
||||
content = messageEventContentFromHtml(body: message, htmlBody: html)
|
||||
} else {
|
||||
content = messageEventContentFromMarkdown(md: message)
|
||||
}
|
||||
}
|
||||
return content.withMentions(mentions: intentionalMentions)
|
||||
}
|
||||
|
||||
private func buildEmoteMessageContentFor(_ message: String, html: String?) -> RoomMessageEventContentWithoutRelation {
|
||||
if let html {
|
||||
return messageEventContentFromHtmlAsEmote(body: message, htmlBody: html)
|
||||
} else {
|
||||
return messageEventContentFromMarkdownAsEmote(md: message)
|
||||
}
|
||||
}
|
||||
|
||||
private func subscribeToBackpagination() {
|
||||
let listener = RoomBackpaginationStatusListener { [weak self] status in
|
||||
self?.backPaginationStateSubject.send(status)
|
||||
}
|
||||
do {
|
||||
backPaginationStateObservationToken = try timeline.subscribeToBackPaginationStatus(listener: listener)
|
||||
} catch {
|
||||
MXLog.error("Failed to subscribe to back pagination state with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomTimelineListener: TimelineListener {
|
||||
private let onUpdateClosure: ([TimelineDiff]) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping ([TimelineDiff]) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func onUpdate(diff: [TimelineDiff]) {
|
||||
onUpdateClosure(diff)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoomBackpaginationStatusListener: BackPaginationStatusListener {
|
||||
private let onUpdateClosure: (BackPaginationStatus) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping (BackPaginationStatus) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func onUpdate(status: BackPaginationStatus) {
|
||||
onUpdateClosure(status)
|
||||
}
|
||||
}
|
||||
|
||||
private final class UploadProgressListener: ProgressWatcher {
|
||||
private let onUpdateClosure: (Double) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping (Double) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func transmissionProgress(progress: TransmissionProgress) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onUpdateClosure(Double(progress.current) / Double(progress.total))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MatrixRustSDK.PollKind {
|
||||
init(pollKind: Poll.Kind) {
|
||||
switch pollKind {
|
||||
case .disclosed:
|
||||
self = .disclosed
|
||||
case .undisclosed:
|
||||
self = .undisclosed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum TimelineProxyError: Error, Equatable {
|
||||
case failedEditingMessage
|
||||
case failedPaginatingBackwards
|
||||
case failedSendingMessage
|
||||
case failedSendingReaction
|
||||
case failedSendingReadReceipt
|
||||
case failedSendingMedia
|
||||
|
||||
// Polls
|
||||
case failedCreatingPoll
|
||||
case failedEditingPoll
|
||||
case failedEndingPoll
|
||||
case failedSendingPollResponse
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol TimelineProxyProtocol {
|
||||
var timelineProvider: RoomTimelineProviderProtocol { get }
|
||||
func subscribeForUpdates() async
|
||||
|
||||
/// Cancels a failed message given its transaction ID from the timeline
|
||||
func cancelSend(transactionID: String) async
|
||||
|
||||
func editMessage(_ message: String,
|
||||
html: String?,
|
||||
original eventID: String,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func fetchDetails(for eventID: String)
|
||||
|
||||
func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation?
|
||||
|
||||
func retryDecryption(for sessionID: String) async
|
||||
|
||||
/// Retries sending a failed message given its transaction ID
|
||||
func retrySend(transactionID: String) async
|
||||
|
||||
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendAudio(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendFile(url: URL,
|
||||
fileInfo: FileInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendImage(url: URL,
|
||||
thumbnailURL: URL,
|
||||
imageInfo: ImageInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendLocation(body: String,
|
||||
geoURI: GeoURI,
|
||||
description: String?,
|
||||
zoomLevel: UInt8?,
|
||||
assetType: AssetType?) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendVideo(url: URL,
|
||||
thumbnailURL: URL,
|
||||
videoInfo: VideoInfo,
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendVoiceMessage(url: URL,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: [UInt16],
|
||||
progressSubject: CurrentValueSubject<Double, Never>?,
|
||||
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendReadReceipt(for eventID: String) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendMessage(_ message: String,
|
||||
html: String?,
|
||||
inReplyTo eventID: String?,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventID: String) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
// Polls
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func editPoll(original eventID: String,
|
||||
question: String,
|
||||
answers: [String],
|
||||
pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError>
|
||||
}
|
||||
|
||||
extension TimelineProxyProtocol {
|
||||
func sendMessage(_ message: String,
|
||||
html: String?,
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
|
||||
await sendMessage(message,
|
||||
html: html,
|
||||
inReplyTo: nil,
|
||||
intentionalMentions: intentionalMentions)
|
||||
}
|
||||
}
|
||||
@@ -190,10 +190,10 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol {
|
||||
return .failure(.failedSendingVoiceMessage)
|
||||
}
|
||||
|
||||
let result = await roomProxy.sendVoiceMessage(url: oggFile,
|
||||
audioInfo: audioInfo,
|
||||
waveform: waveform,
|
||||
progressSubject: nil) { _ in }
|
||||
let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile,
|
||||
audioInfo: audioInfo,
|
||||
waveform: waveform,
|
||||
progressSubject: nil) { _ in }
|
||||
|
||||
if case .failure(let error) = result {
|
||||
MXLog.error("Failed to send the voice message. \(error)")
|
||||
|
||||
Reference in New Issue
Block a user