mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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:
+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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user