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:
Alfonso Grillo
2023-11-28 20:01:35 +01:00
committed by GitHub
parent fb2bc1c39a
commit e7494164b2
18 changed files with 1212 additions and 1085 deletions
+14 -489
View File
@@ -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)
}
@@ -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
@@ -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)")