diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 05b1549ae1..c15c4b323f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -17,8 +17,20 @@ package io.element.android.libraries.matrix.api.timeline import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MediaUploadHandler +import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.room.Mention +import io.element.android.libraries.matrix.api.room.location.AssetType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import java.io.File interface Timeline : AutoCloseable { @@ -39,4 +51,119 @@ interface Timeline : AutoCloseable { suspend fun paginate(direction: PaginationDirection): Result fun paginationStatus(direction: PaginationDirection): StateFlow val timelineItems: Flow> + + + suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result + + suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List): Result + + suspend fun enterSpecialMode(eventId: EventId?): Result + + suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result + + suspend fun sendImage( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback? + ): Result + + suspend fun sendVideo( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback? + ): Result + + suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result + + suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result + + suspend fun toggleReaction(emoji: String, eventId: EventId): Result + + suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + + suspend fun retrySendMessage(transactionId: TransactionId): Result + + suspend fun cancelSend(transactionId: TransactionId): Result + + /** + * Share a location message in the room. + * + * @param body A human readable textual representation of the location. + * @param geoUri A geo URI (RFC 5870) representing the location e.g. `geo:51.5008,0.1247;u=35`. + * Respectively: latitude, longitude, and (optional) uncertainty. + * @param description Optional description of the location to display to the user. + * @param zoomLevel Optional zoom level to display the map at. + * @param assetType Optional type of the location asset. + * Set to SENDER if sharing own location. Set to PIN if sharing any location. + */ + suspend fun sendLocation( + body: String, + geoUri: String, + description: String? = null, + zoomLevel: Int? = null, + assetType: AssetType? = null, + ): Result + + /** + * Create a poll in the room. + * + * @param question The question to ask. + * @param answers The list of answers. + * @param maxSelections The maximum number of answers that can be selected. + * @param pollKind The kind of poll to create. + */ + suspend fun createPoll( + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result + + /** + * Edit a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param question The question to ask. + * @param answers The list of answers. + * @param maxSelections The maximum number of answers that can be selected. + * @param pollKind The kind of poll to create. + */ + suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result + + /** + * Send a response to a poll. + * + * @param pollStartId The event ID of the poll start event. + * @param answers The list of answer ids to send. + */ + suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result + + /** + * Ends a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param text Fallback text of the poll end event. + */ + suspend fun endPoll(pollStartId: EventId, text: String): Result + + suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback? + ): Result + + } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index a6e51fefbc..c99c9a8c21 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -43,17 +43,12 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings -import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl -import io.element.android.libraries.matrix.impl.media.map -import io.element.android.libraries.matrix.impl.media.toMSC3246range import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService -import io.element.android.libraries.matrix.impl.poll.toInner -import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper @@ -80,8 +75,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem @@ -94,10 +87,8 @@ import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.use -import timber.log.Timber import uniffi.matrix_sdk.RoomPowerLevelChanges import java.io.File -import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody import org.matrix.rustcomponents.sdk.Room as InnerRoom import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @@ -198,7 +189,6 @@ class RustMatrixRoom( liveTimeline.close() innerRoom.destroy() roomListItem.destroy() - specialModeEventTimelineItem?.destroy() } override val name: String? @@ -332,12 +322,8 @@ class RustMatrixRoom( } } - override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result = withContext(roomDispatcher) { - messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> - runCatching { - innerTimeline.send(content) - } - } + override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result { + return liveTimeline.sendMessage(body, htmlBody, mentions) } override suspend fun editMessage( @@ -346,45 +332,16 @@ class RustMatrixRoom( body: String, htmlBody: String?, mentions: List, - ): Result = - withContext(roomDispatcher) { - if (originalEventId != null) { - runCatching { - val editedEvent = specialModeEventTimelineItem ?: innerTimeline.getEventTimelineItemByEventId(originalEventId.value) - editedEvent.use { - innerTimeline.edit( - newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), - editItem = it, - ) - } - specialModeEventTimelineItem = null - } - } else { - runCatching { - transactionId?.let { cancelSend(it) } - innerTimeline.send(messageEventContentFromParts(body, htmlBody)) - } - } - } - - private var specialModeEventTimelineItem: EventTimelineItem? = null - - override suspend fun enterSpecialMode(eventId: EventId?): Result = withContext(roomDispatcher) { - runCatching { - specialModeEventTimelineItem?.destroy() - specialModeEventTimelineItem = null - specialModeEventTimelineItem = eventId?.let { innerTimeline.getEventTimelineItemByEventId(it.value) } - } + ): Result { + return liveTimeline.editMessage(originalEventId, transactionId, body, htmlBody, mentions) } - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result = withContext(roomDispatcher) { - runCatching { - val inReplyTo = specialModeEventTimelineItem ?: innerTimeline.getEventTimelineItemByEventId(eventId.value) - inReplyTo.use { eventTimelineItem -> - innerTimeline.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) - } - specialModeEventTimelineItem = null - } + override suspend fun enterSpecialMode(eventId: EventId?): Result { + return liveTimeline.enterSpecialMode(eventId) + } + + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result{ + return liveTimeline.replyMessage(eventId, body, htmlBody, mentions) } override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) { @@ -467,18 +424,7 @@ class RustMatrixRoom( formattedBody: String?, progressCallback: ProgressCallback?, ): Result { - return sendAttachment(listOfNotNull(file, thumbnailFile)) { - innerTimeline.sendImage( - url = file.path, - thumbnailUrl = thumbnailFile?.path, - imageInfo = imageInfo.map(), - caption = body, - formattedCaption = formattedBody?.let { - RustFormattedBody(body = it, format = MessageFormat.Html) - }, - progressWatcher = progressCallback?.toProgressWatcher() - ) - } + return liveTimeline.sendImage(file, thumbnailFile, imageInfo, body, formattedBody, progressCallback) } override suspend fun sendVideo( @@ -489,63 +435,31 @@ class RustMatrixRoom( formattedBody: String?, progressCallback: ProgressCallback?, ): Result { - return sendAttachment(listOfNotNull(file, thumbnailFile)) { - innerTimeline.sendVideo( - url = file.path, - thumbnailUrl = thumbnailFile?.path, - videoInfo = videoInfo.map(), - caption = body, - formattedCaption = formattedBody?.let { - RustFormattedBody(body = it, format = MessageFormat.Html) - }, - progressWatcher = progressCallback?.toProgressWatcher() - ) - } + return liveTimeline.sendVideo(file, thumbnailFile, videoInfo, body, formattedBody, progressCallback) } override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { - return sendAttachment(listOf(file)) { - innerTimeline.sendAudio( - url = file.path, - audioInfo = audioInfo.map(), - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, - progressWatcher = progressCallback?.toProgressWatcher() - ) - } + return liveTimeline.sendAudio(file, audioInfo, progressCallback) } override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { - return sendAttachment(listOf(file)) { - innerTimeline.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) - } + return liveTimeline.sendFile(file, fileInfo, progressCallback) } - override suspend fun toggleReaction(emoji: String, eventId: EventId): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.toggleReaction(key = emoji, eventId = eventId.value) - } + override suspend fun toggleReaction(emoji: String, eventId: EventId): Result{ + return liveTimeline.toggleReaction(emoji, eventId) } - override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(roomDispatcher) { - runCatching { - roomContentForwarder.forward(fromTimeline = innerTimeline, eventId = eventId, toRoomIds = roomIds) - }.onFailure { - Timber.e(it) - } + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result{ + return liveTimeline.forwardEvent(eventId, roomIds) } - override suspend fun retrySendMessage(transactionId: TransactionId): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.retrySend(transactionId.value) - } + override suspend fun retrySendMessage(transactionId: TransactionId): Result { + return liveTimeline.retrySendMessage(transactionId) } - override suspend fun cancelSend(transactionId: TransactionId): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.cancelSend(transactionId.value) - } + override suspend fun cancelSend(transactionId: TransactionId): Result{ + return liveTimeline.cancelSend(transactionId) } override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(roomDispatcher) { @@ -623,16 +537,8 @@ class RustMatrixRoom( description: String?, zoomLevel: Int?, assetType: AssetType?, - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.sendLocation( - body = body, - geoUri = geoUri, - description = description, - zoomLevel = zoomLevel?.toUByte(), - assetType = assetType?.toInner(), - ) - } + ): Result { + return liveTimeline.sendLocation(body, geoUri, description, zoomLevel, assetType) } override suspend fun createPoll( @@ -640,15 +546,8 @@ class RustMatrixRoom( answers: List, maxSelections: Int, pollKind: PollKind, - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.createPoll( - question = question, - answers = answers, - maxSelections = maxSelections.toUByte(), - pollKind = pollKind.toInner(), - ) - } + ): Result { + return liveTimeline.createPoll(question, answers, maxSelections, pollKind) } override suspend fun editPoll( @@ -657,46 +556,22 @@ class RustMatrixRoom( answers: List, maxSelections: Int, pollKind: PollKind, - ): Result = withContext(roomDispatcher) { - runCatching { - val pollStartEvent = - innerTimeline.getEventTimelineItemByEventId( - eventId = pollStartId.value - ) - pollStartEvent.use { - innerTimeline.editPoll( - question = question, - answers = answers, - maxSelections = maxSelections.toUByte(), - pollKind = pollKind.toInner(), - editItem = pollStartEvent, - ) - } - } + ): Result { + return liveTimeline.editPoll(pollStartId, question, answers, maxSelections, pollKind) } override suspend fun sendPollResponse( pollStartId: EventId, answers: List - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.sendPollResponse( - pollStartId = pollStartId.value, - answers = answers, - ) - } + ): Result { + return liveTimeline.sendPollResponse(pollStartId, answers) } override suspend fun endPoll( pollStartId: EventId, text: String - ): Result = withContext(roomDispatcher) { - runCatching { - innerTimeline.endPoll( - pollStartId = pollStartId.value, - text = text, - ) - } + ): Result { + return liveTimeline.endPoll(pollStartId, text) } override suspend fun sendVoiceMessage( @@ -704,16 +579,8 @@ class RustMatrixRoom( audioInfo: AudioInfo, waveform: List, progressCallback: ProgressCallback?, - ): Result = sendAttachment(listOf(file)) { - innerTimeline.sendVoiceMessage( - url = file.path, - audioInfo = audioInfo.map(), - waveform = waveform.toMSC3246range(), - // Maybe allow a caption in the future? - caption = null, - formattedCaption = null, - progressWatcher = progressCallback?.toProgressWatcher(), - ) + ): Result{ + return liveTimeline.sendVoiceMessage(file, audioInfo, waveform, progressCallback) } override suspend fun typingNotice(isTyping: Boolean) = runCatching { @@ -749,12 +616,6 @@ class RustMatrixRoom( innerRoom.matrixToEventPermalink(eventId.value) } - private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { - return runCatching { - MediaUploadHandlerImpl(files, handle()) - } - } - private fun createTimeline( timeline: InnerTimeline, isLive: Boolean = true, @@ -769,19 +630,9 @@ class RustMatrixRoom( dispatcher = roomDispatcher, lastLoginTimestamp = sessionData.loginTimestamp, onNewSyncedEvent = onNewSyncedEvent, + roomContentForwarder = roomContentForwarder, inner = timeline, - fetchDetailsForEvent = { eventId -> - runCatching { - innerTimeline.getEventTimelineItemByEventId(eventId.value) - } - } ) } - private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = - if (htmlBody != null) { - messageEventContentFromHtml(body, htmlBody) - } else { - messageEventContentFromMarkdown(body) - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index a3c121fd97..43c53dcf0d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -17,11 +17,30 @@ package io.element.android.libraries.matrix.impl.timeline import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.TransactionId +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MediaUploadHandler +import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.Mention +import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineException +import io.element.android.libraries.matrix.impl.core.toProgressWatcher +import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl +import io.element.android.libraries.matrix.impl.media.map +import io.element.android.libraries.matrix.impl.media.toMSC3246range +import io.element.android.libraries.matrix.impl.poll.toInner +import io.element.android.libraries.matrix.impl.room.RoomContentForwarder +import io.element.android.libraries.matrix.impl.room.location.toInner +import io.element.android.libraries.matrix.impl.room.map import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper @@ -48,10 +67,19 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.FormattedBody +import org.matrix.rustcomponents.sdk.MessageFormat +import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation +import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem +import org.matrix.rustcomponents.sdk.messageEventContentFromHtml +import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.use import timber.log.Timber import uniffi.matrix_sdk_ui.EventItemOrigin +import java.io.File import java.util.Date import java.util.concurrent.atomic.AtomicBoolean import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @@ -68,7 +96,7 @@ class RustTimeline( private val matrixRoom: MatrixRoom, private val dispatcher: CoroutineDispatcher, private val lastLoginTimestamp: Date?, - private val fetchDetailsForEvent: suspend (EventId) -> Result, + private val roomContentForwarder: RoomContentForwarder, private val onNewSyncedEvent: () -> Unit, ) : Timeline { @@ -90,7 +118,7 @@ class RustTimeline( private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive) private val timelineItemFactory = MatrixTimelineItemMapper( - fetchDetailsForEvent = fetchDetailsForEvent, + fetchDetailsForEvent = this::fetchDetailsForEvent, roomCoroutineScope = roomCoroutineScope, virtualTimelineItemMapper = VirtualTimelineItemMapper(), eventTimelineItemMapper = EventTimelineItemMapper( @@ -138,7 +166,7 @@ class RustTimeline( } } - private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus)->Timeline.PaginationStatus){ + private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) { when (direction) { Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update) Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update) @@ -204,6 +232,7 @@ class RustTimeline( override fun close() { inner.close() + specialModeEventTimelineItem?.destroy() } private suspend fun fetchMembers() = withContext(dispatcher) { @@ -229,4 +258,266 @@ class RustTimeline( initLatch.await() timelineDiffProcessor.postDiffs(diffs) } + + override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result = withContext(dispatcher) { + messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> + runCatching { + inner.send(content) + } + } + } + + override suspend fun editMessage( + originalEventId: EventId?, + transactionId: TransactionId?, + body: String, + htmlBody: String?, + mentions: List, + ): Result = + withContext(dispatcher) { + if (originalEventId != null) { + runCatching { + val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value) + editedEvent.use { + inner.edit( + newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), + editItem = it, + ) + } + specialModeEventTimelineItem = null + } + } else { + runCatching { + transactionId?.let { cancelSend(it) } + inner.send(messageEventContentFromParts(body, htmlBody)) + } + } + } + + private var specialModeEventTimelineItem: EventTimelineItem? = null + + override suspend fun enterSpecialMode(eventId: EventId?): Result = withContext(dispatcher) { + runCatching { + specialModeEventTimelineItem?.destroy() + specialModeEventTimelineItem = null + specialModeEventTimelineItem = eventId?.let { inner.getEventTimelineItemByEventId(it.value) } + } + } + + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result = withContext(dispatcher) { + runCatching { + val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value) + inReplyTo.use { eventTimelineItem -> + inner.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) + } + specialModeEventTimelineItem = null + } + } + + override suspend fun sendImage( + file: File, + thumbnailFile: File?, + imageInfo: ImageInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result { + return sendAttachment(listOfNotNull(file, thumbnailFile)) { + inner.sendImage( + url = file.path, + thumbnailUrl = thumbnailFile?.path, + imageInfo = imageInfo.map(), + caption = body, + formattedCaption = formattedBody?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, + progressWatcher = progressCallback?.toProgressWatcher() + ) + } + } + + override suspend fun sendVideo( + file: File, + thumbnailFile: File?, + videoInfo: VideoInfo, + body: String?, + formattedBody: String?, + progressCallback: ProgressCallback?, + ): Result { + return sendAttachment(listOfNotNull(file, thumbnailFile)) { + inner.sendVideo( + url = file.path, + thumbnailUrl = thumbnailFile?.path, + videoInfo = videoInfo.map(), + caption = body, + formattedCaption = formattedBody?.let { + FormattedBody(body = it, format = MessageFormat.Html) + }, + progressWatcher = progressCallback?.toProgressWatcher() + ) + } + } + + override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { + return sendAttachment(listOf(file)) { + inner.sendAudio( + url = file.path, + audioInfo = audioInfo.map(), + // Maybe allow a caption in the future? + caption = null, + formattedCaption = null, + progressWatcher = progressCallback?.toProgressWatcher() + ) + } + } + + override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { + return sendAttachment(listOf(file)) { + inner.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) + } + } + + override suspend fun toggleReaction(emoji: String, eventId: EventId): Result = withContext(dispatcher) { + runCatching { + inner.toggleReaction(key = emoji, eventId = eventId.value) + } + } + + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(dispatcher) { + runCatching { + roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) + }.onFailure { + Timber.e(it) + } + } + + override suspend fun retrySendMessage(transactionId: TransactionId): Result = withContext(dispatcher) { + runCatching { + inner.retrySend(transactionId.value) + } + } + + override suspend fun cancelSend(transactionId: TransactionId): Result = withContext(dispatcher) { + runCatching { + inner.cancelSend(transactionId.value) + } + } + + override suspend fun sendLocation( + body: String, + geoUri: String, + description: String?, + zoomLevel: Int?, + assetType: AssetType?, + ): Result = withContext(dispatcher) { + runCatching { + inner.sendLocation( + body = body, + geoUri = geoUri, + description = description, + zoomLevel = zoomLevel?.toUByte(), + assetType = assetType?.toInner(), + ) + } + } + + override suspend fun createPoll( + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = withContext(dispatcher) { + runCatching { + inner.createPoll( + question = question, + answers = answers, + maxSelections = maxSelections.toUByte(), + pollKind = pollKind.toInner(), + ) + } + } + + override suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = withContext(dispatcher) { + runCatching { + val pollStartEvent = + inner.getEventTimelineItemByEventId( + eventId = pollStartId.value + ) + pollStartEvent.use { + inner.editPoll( + question = question, + answers = answers, + maxSelections = maxSelections.toUByte(), + pollKind = pollKind.toInner(), + editItem = pollStartEvent, + ) + } + } + } + + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List + ): Result = withContext(dispatcher) { + runCatching { + inner.sendPollResponse( + pollStartId = pollStartId.value, + answers = answers, + ) + } + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String + ): Result = withContext(dispatcher) { + runCatching { + inner.endPoll( + pollStartId = pollStartId.value, + text = text, + ) + } + } + + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + ): Result = sendAttachment(listOf(file)) { + inner.sendVoiceMessage( + url = file.path, + audioInfo = audioInfo.map(), + waveform = waveform.toMSC3246range(), + // Maybe allow a caption in the future? + caption = null, + formattedCaption = null, + progressWatcher = progressCallback?.toProgressWatcher(), + ) + } + + private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = + if (htmlBody != null) { + messageEventContentFromHtml(body, htmlBody) + } else { + messageEventContentFromMarkdown(body) + } + + private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { + return runCatching { + MediaUploadHandlerImpl(files, handle()) + } + } + + private fun fetchDetailsForEvent(eventId: EventId): Result { + return runCatching { + inner.getEventTimelineItemByEventId(eventId.value) + } + } }