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
@@ -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)
}
}