From fbf242756ef384326a705c4fcdf7375ca252c7b7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 14:34:31 +0200 Subject: [PATCH 1/8] Allow additional content when sending an event --- .../sdk/api/session/room/send/SendService.kt | 34 ++++-- .../session/content/UploadContentWorker.kt | 8 +- .../session/room/send/DefaultSendService.kt | 36 ++++--- .../room/send/LocalEchoEventFactory.kt | 102 ++++++++++++------ 4 files changed, 122 insertions(+), 58 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 53b49129c4..6a6fadc95a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -45,18 +45,30 @@ interface SendService { * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + fun sendTextMessage( + text: CharSequence, + msgType: String = MessageType.MSGTYPE_TEXT, + autoMarkdown: Boolean = false, + additionalContent: Content? = null, + ): Cancelable /** * Method to send a text message with a formatted body. * @param text the text message to send * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + fun sendFormattedTextMessage( + text: String, + formattedText: String, + msgType: String = MessageType.MSGTYPE_TEXT, + additionalContent: Content? = null, + ): Cancelable /** * Method to quote an events content. @@ -65,6 +77,7 @@ interface SendService { * @param formattedText the formatted text message to send * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param rootThreadEventId when this param is not null, the message will be sent in this specific thread + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ fun sendQuotedTextMessage( @@ -73,6 +86,7 @@ interface SendService { formattedText: String? = null, autoMarkdown: Boolean, rootThreadEventId: String? = null, + additionalContent: Content? = null, ): Cancelable /** @@ -83,6 +97,7 @@ interface SendService { * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread * @param relatesTo add a relation content to the media event + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ fun sendMedia( @@ -91,6 +106,7 @@ interface SendService { roomIds: Set, rootThreadEventId: String? = null, relatesTo: RelationDefaultContent? = null, + additionalContent: Content? = null, ): Cancelable /** @@ -100,6 +116,7 @@ interface SendService { * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ fun sendMedias( @@ -107,6 +124,7 @@ interface SendService { compressBeforeSending: Boolean, roomIds: Set, rootThreadEventId: String? = null, + additionalContent: Content? = null, ): Cancelable /** @@ -114,31 +132,35 @@ interface SendService { * @param pollType indicates open or closed polls * @param question the question * @param options list of options + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendPoll(pollType: PollType, question: String, options: List): Cancelable + fun sendPoll(pollType: PollType, question: String, options: List, additionalContent: Content? = null): Cancelable /** * Method to send a poll response. * @param pollEventId the poll currently replied to * @param answerId The id of the answer + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun voteToPoll(pollEventId: String, answerId: String): Cancelable + fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content? = null): Cancelable /** * End a poll in the room. * @param pollEventId event id of the poll + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun endPoll(pollEventId: String): Cancelable + fun endPoll(pollEventId: String, additionalContent: Content? = null): Cancelable /** * Redact (delete) the given event. * @param event The event to redact * @param reason Optional reason string + * @param additionalContent additional content to put in the event content */ - fun redactEvent(event: Event, reason: String?): Cancelable + fun redactEvent(event: Event, reason: String?, additionalContent: Content? = null): Cancelable /** * Schedule this message to be resent. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 1e62b5d7f5..db1cd1b33b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent @@ -407,7 +408,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter newAttachmentAttributes: NewAttachmentAttributes ) { localEchoRepository.updateEcho(eventId) { _, event -> - val messageContent: MessageContent? = event.asDomain().content.toModel() + val content: Content? = event.asDomain().content + val messageContent: MessageContent? = content.toModel() + // Retrieve potential additional content from the original event + val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) @@ -415,7 +419,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) else -> messageContent } - event.content = ContentMapper.map(updatedContent.toContent()) + event.content = ContentMapper.map(updatedContent.toContent().plus(additionalContent)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index aa305e6067..9cdbc7ff46 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage @@ -88,14 +89,14 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { - return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) + override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { - return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } @@ -105,7 +106,8 @@ internal class DefaultSendService @AssistedInject constructor( text: String, formattedText: String?, autoMarkdown: Boolean, - rootThreadEventId: String? + rootThreadEventId: String?, + additionalContent: Content?, ): Cancelable { return localEchoEventFactory.createQuotedTextEvent( roomId = roomId, @@ -113,33 +115,34 @@ internal class DefaultSendService @AssistedInject constructor( text = text, formattedText = formattedText, autoMarkdown = autoMarkdown, - rootThreadEventId = rootThreadEventId + rootThreadEventId = rootThreadEventId, + additionalContent = additionalContent, ) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable { - return localEchoEventFactory.createPollEvent(roomId, pollType, question, options) + override fun sendPoll(pollType: PollType, question: String, options: List, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createPollEvent(roomId, pollType, question, options, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun voteToPoll(pollEventId: String, answerId: String): Cancelable { - return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId) + override fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun endPoll(pollEventId: String): Cancelable { - return localEchoEventFactory.createEndPollEvent(roomId, pollEventId) + override fun endPoll(pollEventId: String, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createEndPollEvent(roomId, pollEventId, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun redactEvent(event: Event, reason: String?): Cancelable { + override fun redactEvent(event: Event, reason: String?, additionalContent: Content?): Cancelable { // TODO manage media/attachements? - val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, additionalContent) .also { createLocalEcho(it) } return eventSenderProcessor.postRedaction(redactionEcho, reason) } @@ -265,7 +268,8 @@ internal class DefaultSendService @AssistedInject constructor( attachments: List, compressBeforeSending: Boolean, roomIds: Set, - rootThreadEventId: String? + rootThreadEventId: String?, + additionalContent: Content?, ): Cancelable { return attachments.mapTo(CancelableBag()) { sendMedia( @@ -283,6 +287,7 @@ internal class DefaultSendService @AssistedInject constructor( roomIds: Set, rootThreadEventId: String?, relatesTo: RelationDefaultContent?, + additionalContent: Content?, ): Cancelable { // Ensure that the event will not be send in a thread if we are a different flow. // Like sending files to multiple rooms @@ -299,6 +304,7 @@ internal class DefaultSendService @AssistedInject constructor( attachment = attachment, rootThreadEventId = rootThreadId, relatesTo, + additionalContent, ).also { event -> createLocalEcho(event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 1d7f624eba..7d8605c2bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -95,12 +95,12 @@ internal class LocalEchoEventFactory @Inject constructor( private val permalinkFactory: PermalinkFactory, private val clock: Clock, ) { - fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean, additionalContent: Content? = null): Event { if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { - return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) + return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType, additionalContent) } val content = MessageTextContent(msgType = msgType, body = text.toString()) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { @@ -116,8 +116,8 @@ internal class LocalEchoEventFactory @Inject constructor( return TextContent(text.toString()) } - fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { - return createMessageEvent(roomId, textContent.toMessageTextContent(msgType)) + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String, additionalContent: Content? = null): Event { + return createMessageEvent(roomId, textContent.toMessageTextContent(msgType), additionalContent) } fun createReplaceTextEvent( @@ -128,6 +128,7 @@ internal class LocalEchoEventFactory @Inject constructor( newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String, + additionalContent: Content? = null, ): Event { val content = if (newBodyFormattedText != null) { TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType) @@ -141,7 +142,8 @@ internal class LocalEchoEventFactory @Inject constructor( body = compatibilityText, relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), newContent = content, - ) + ), + additionalContent, ) } @@ -167,6 +169,7 @@ internal class LocalEchoEventFactory @Inject constructor( targetEventId: String, question: String, options: List, + additionalContent: Content? = null, ): Event { val newContent = MessagePollContent( relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), @@ -179,7 +182,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_START.first(), - content = newContent.toContent() + content = newContent.toContent().plus(additionalContent.orEmpty()) ) } @@ -187,6 +190,7 @@ internal class LocalEchoEventFactory @Inject constructor( roomId: String, pollEventId: String, answerId: String, + additionalContent: Content? = null, ): Event { val content = MessagePollResponseContent( body = answerId, @@ -203,7 +207,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_RESPONSE.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -213,6 +217,7 @@ internal class LocalEchoEventFactory @Inject constructor( pollType: PollType, question: String, options: List, + additionalContent: Content? = null, ): Event { val content = createPollContent(question, options, pollType) val localId = LocalEcho.createLocalEchoId() @@ -222,7 +227,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_START.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -230,6 +235,7 @@ internal class LocalEchoEventFactory @Inject constructor( fun createEndPollEvent( roomId: String, eventId: String, + additionalContent: Content? = null, ): Event { val content = MessageEndPollContent( relatesTo = RelationDefaultContent( @@ -244,7 +250,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_END.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -255,6 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor( longitude: Double, uncertainty: Double?, isUserLocation: Boolean, + additionalContent: Content? = null, ): Event { val geoUri = buildGeoUri(latitude, longitude, uncertainty) val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN @@ -266,7 +273,7 @@ internal class LocalEchoEventFactory @Inject constructor( unstableTimestampMillis = clock.epochMillis(), unstableText = geoUri ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } fun createLiveLocationEvent( @@ -275,6 +282,7 @@ internal class LocalEchoEventFactory @Inject constructor( latitude: Double, longitude: Double, uncertainty: Double?, + additionalContent: Content? = null, ): Event { val geoUri = buildGeoUri(latitude, longitude, uncertainty) val content = MessageBeaconLocationDataContent( @@ -293,7 +301,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.BEACON_LOCATION_DATA.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -306,6 +314,7 @@ internal class LocalEchoEventFactory @Inject constructor( autoMarkdown: Boolean, msgType: String, compatibilityText: String, + additionalContent: Content? = null, ): Event { val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" @@ -340,7 +349,8 @@ internal class LocalEchoEventFactory @Inject constructor( formattedBody = replyFormatted ) .toContent() - ) + ), + additionalContent, ) } @@ -349,23 +359,32 @@ internal class LocalEchoEventFactory @Inject constructor( attachment: ContentAttachmentData, rootThreadEventId: String?, relatesTo: RelationDefaultContent?, + additionalContent: Content? = null, ): Event { return when (attachment.type) { - ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId, relatesTo) - ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId, relatesTo) - ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId, relatesTo) + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent) + ContentAttachmentData.Type.AUDIO -> createAudioEvent( + roomId, + attachment, + isVoiceMessage = false, + rootThreadEventId = rootThreadEventId, + relatesTo, + additionalContent + ) ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent( roomId, attachment, isVoiceMessage = true, rootThreadEventId = rootThreadEventId, relatesTo, + additionalContent, ) - ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId, relatesTo) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent) } } - fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event { + fun createReactionEvent(roomId: String, targetEventId: String, reaction: String, additionalContent: Content? = null): Event { val content = ReactionContent( ReactionInfo( RelationType.ANNOTATION, @@ -380,7 +399,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.REACTION, - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -390,6 +409,7 @@ internal class LocalEchoEventFactory @Inject constructor( attachment: ContentAttachmentData, rootThreadEventId: String?, relatesTo: RelationDefaultContent?, + additionalContent: Content?, ): Event { var width = attachment.width var height = attachment.height @@ -417,7 +437,7 @@ internal class LocalEchoEventFactory @Inject constructor( url = attachment.queryUri.toString(), relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun createVideoEvent( @@ -425,6 +445,7 @@ internal class LocalEchoEventFactory @Inject constructor( attachment: ContentAttachmentData, rootThreadEventId: String?, relatesTo: RelationDefaultContent?, + additionalContent: Content?, ): Event { val mediaDataRetriever = MediaMetadataRetriever() mediaDataRetriever.setDataSource(context, attachment.queryUri) @@ -459,7 +480,7 @@ internal class LocalEchoEventFactory @Inject constructor( url = attachment.queryUri.toString(), relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun createAudioEvent( @@ -468,6 +489,7 @@ internal class LocalEchoEventFactory @Inject constructor( isVoiceMessage: Boolean, rootThreadEventId: String?, relatesTo: RelationDefaultContent?, + additionalContent: Content? ): Event { val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, @@ -485,7 +507,7 @@ internal class LocalEchoEventFactory @Inject constructor( voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(), relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun createFileEvent( @@ -493,6 +515,7 @@ internal class LocalEchoEventFactory @Inject constructor( attachment: ContentAttachmentData, rootThreadEventId: String?, relatesTo: RelationDefaultContent?, + additionalContent: Content? ): Event { val content = MessageFileContent( msgType = MessageType.MSGTYPE_FILE, @@ -504,15 +527,16 @@ internal class LocalEchoEventFactory @Inject constructor( url = attachment.queryUri.toString(), relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } - private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event { - return createEvent(roomId, EventType.MESSAGE, content.toContent()) + private fun createMessageEvent(roomId: String, content: MessageContent, additionalContent: Content?): Event { + return createEvent(roomId, EventType.MESSAGE, content.toContent(), additionalContent) } - fun createEvent(roomId: String, type: String, content: Content?): Event { + fun createEvent(roomId: String, type: String, content: Content?, additionalContent: Content? = null): Event { val newContent = enhanceStickerIfNeeded(type, content) ?: content + val updatedNewContent = newContent?.plus(additionalContent.orEmpty()) ?: additionalContent val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -520,7 +544,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = type, - content = newContent, + content = updatedNewContent, unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -555,6 +579,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType: String, autoMarkdown: Boolean, formattedText: String?, + additionalContent: Content? = null, ): Event { val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown) return createEvent( @@ -564,8 +589,7 @@ internal class LocalEchoEventFactory @Inject constructor( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = msgType - ) - .toContent() + ).toContent().plus(additionalContent.orEmpty()) ) } @@ -584,6 +608,7 @@ internal class LocalEchoEventFactory @Inject constructor( autoMarkdown: Boolean, rootThreadEventId: String? = null, showInThread: Boolean, + additionalContent: Content? = null ): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null @@ -621,7 +646,7 @@ internal class LocalEchoEventFactory @Inject constructor( showInThread = showInThread ) ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun generateThreadRelationContent(rootThreadEventId: String) = @@ -750,7 +775,7 @@ internal class LocalEchoEventFactory @Inject constructor( } } */ - fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + fun createRedactEvent(roomId: String, eventId: String, reason: String?, additionalContent: Content? = null): Event { val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -759,7 +784,7 @@ internal class LocalEchoEventFactory @Inject constructor( eventId = localId, type = EventType.REDACTION, redacts = eventId, - content = reason?.let { mapOf("reason" to it).toContent() }, + content = reason?.let { mapOf("reason" to it).toContent().plus(additionalContent.orEmpty()) } ?: additionalContent, unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -776,9 +801,14 @@ internal class LocalEchoEventFactory @Inject constructor( formattedText: String?, autoMarkdown: Boolean, rootThreadEventId: String?, + additionalContent: Content? = null, ): Event { val messageContent = quotedEvent.getLastMessageContent() - val textMsg = if (messageContent is MessageContentWithFormattedBody) { messageContent.formattedBody } else { messageContent?.body } + val textMsg = if (messageContent is MessageContentWithFormattedBody) { + messageContent.formattedBody + } else { + messageContent?.body + } val quoteText = legacyRiotQuoteText(textMsg, text) val quoteFormattedText = "
$textMsg
$formattedText" @@ -791,13 +821,15 @@ internal class LocalEchoEventFactory @Inject constructor( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = MessageType.MSGTYPE_TEXT - ) + ), + additionalContent, ) } else { createFormattedTextEvent( roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText), - MessageType.MSGTYPE_TEXT + MessageType.MSGTYPE_TEXT, + additionalContent, ) } } From 1647fe233f5211457d6f5d872fbd40cc33def2ff Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 15:45:44 +0200 Subject: [PATCH 2/8] Voice Broadcast - Introduce io.element.voice_broadcast_chunk key in voice messages --- .../voicebroadcast/VoiceBroadcastConstants.kt | 7 +++++- .../VoiceBroadcastExtensions.kt | 18 +++++++------ .../voicebroadcast/VoiceBroadcastPlayer.kt | 2 +- .../model/VoiceBroadcastChunk.kt | 25 +++++++++++++++++++ .../usecase/StartVoiceBroadcastUseCase.kt | 6 ++++- 5 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcastChunk.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt index d445dfdfbe..7b937f740e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt @@ -16,11 +16,16 @@ package im.vector.app.features.voicebroadcast +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent + object VoiceBroadcastConstants { /** Voice Broadcast State Event. */ const val STATE_ROOM_VOICE_BROADCAST_INFO = "io.element.voice_broadcast_info" + /** Custom key passed to the [MessageAudioContent] with Voice Broadcast information. */ + const val VOICE_BROADCAST_CHUNK_KEY = "io.element.voice_broadcast_chunk" + /** Default voice broadcast chunk duration, in seconds. */ - const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120 + const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 10 } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index d016703968..fe449c679e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -16,14 +16,18 @@ package im.vector.app.features.voicebroadcast -import org.matrix.android.sdk.api.session.events.model.RelationType +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent -fun MessageAudioEvent?.isVoiceBroadcast() = this?.getVoiceBroadcastEventId() != null +fun MessageAudioEvent?.isVoiceBroadcast() = this?.root?.getClearContent()?.get(VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY) != null +fun MessageAudioEvent.getVoiceBroadcastEventId(): String? = if (isVoiceBroadcast()) root.getRelationContent()?.eventId else null -fun MessageAudioEvent.getVoiceBroadcastEventId(): String? = - // TODO Improve this condition by checking the referenced event type - root.takeIf { content.voiceMessageIndicator != null } - ?.getRelationContent()?.takeIf { it.type == RelationType.REFERENCE } - ?.eventId +fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? { + @Suppress("UNCHECKED_CAST") + return (root.getClearContent()?.get(VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY) as? Content).toModel() +} + +val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index 41ed1ff84d..dfd50ea5cb 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -85,7 +85,7 @@ class VoiceBroadcastPlayer @Inject constructor( private fun updatePlaylist(room: Room, eventId: String) { val timelineEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId) val audioEvents = timelineEvents.mapNotNull { it.root.asMessageAudioEvent() } - playlist = audioEvents.sortedBy { it.root.originServerTs } + playlist = audioEvents.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } } private fun startPlayback() { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcastChunk.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcastChunk.kt new file mode 100644 index 0000000000..e0f6e6e7b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcastChunk.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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. + */ + +package im.vector.app.features.voicebroadcast.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class VoiceBroadcastChunk( + @Json(name = "sequence") val sequence: Int? = null +) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt index 780150d5e7..ba1799c520 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt @@ -23,6 +23,7 @@ import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.lib.multipicker.utils.toMultiPickerAudioType @@ -98,7 +99,10 @@ class StartVoiceBroadcastUseCase @Inject constructor( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), compressBeforeSending = false, roomIds = emptySet(), - relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId) + relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId), + additionalContent = mapOf( + VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY to VoiceBroadcastChunk(sequence = sequence).toContent() + ) ) } } From 64456860e239529f71f944f670c4e7c4a58f87bd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 16:00:23 +0200 Subject: [PATCH 3/8] Voice Broadcast - Add deviceId in state event content --- .../voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt | 2 ++ .../voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt | 1 + 2 files changed, 3 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt index 5044bb5c34..a9db63c538 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/MessageVoiceBroadcastInfoContent.kt @@ -38,6 +38,8 @@ data class MessageVoiceBroadcastInfoContent( @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, + /** The device from which the broadcast has been started. */ + @Json(name = "device_id") val deviceId: String? = null, /** The [VoiceBroadcastState] value. **/ @Json(name = "state") val voiceBroadcastStateStr: String = "", /** The length of the voice chunks in seconds. **/ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt index ba1799c520..7e43439b45 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt @@ -71,6 +71,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, stateKey = session.myUserId, body = MessageVoiceBroadcastInfoContent( + deviceId = session.sessionParams.deviceId, voiceBroadcastStateStr = VoiceBroadcastState.STARTED.value, chunkLength = chunkLength, ).toContent() From 5438c7e089e724675ed6dffca5678641e934c2dd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 16:39:13 +0200 Subject: [PATCH 4/8] Add changelog --- changelog.d/7397.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7397.wip diff --git a/changelog.d/7397.wip b/changelog.d/7397.wip new file mode 100644 index 0000000000..1a7d1866a6 --- /dev/null +++ b/changelog.d/7397.wip @@ -0,0 +1 @@ +[Voice Broadcast] Add additional data in events From 5004db07fb49548a8166fd749f94d73a6780d526 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 17:22:44 +0200 Subject: [PATCH 5/8] Remove legacy comment --- .../voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt index 7e43439b45..7cb66cd9e5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt @@ -95,7 +95,6 @@ class StartVoiceBroadcastUseCase @Inject constructor( "Voice Broadcast Part ($sequence).${voiceMessageFile.extension}" ) val audioType = outputFileUri.toMultiPickerAudioType(context) ?: return - // TODO put sequence in event content room.sendService().sendMedia( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), compressBeforeSending = false, From a658e7727afd556fe266d2e4d7f143b440a5ae22 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 17:39:06 +0200 Subject: [PATCH 6/8] Voice Broadcast - Update chunk length to 120 sec --- .../app/features/voicebroadcast/VoiceBroadcastConstants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt index 7b937f740e..551eaa4dac 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt @@ -27,5 +27,5 @@ object VoiceBroadcastConstants { const val VOICE_BROADCAST_CHUNK_KEY = "io.element.voice_broadcast_chunk" /** Default voice broadcast chunk duration, in seconds. */ - const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 10 + const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120 } From 0781ee84d96c117b8904df01aafac4a88ed2a5b1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 17:39:55 +0200 Subject: [PATCH 7/8] Reformat file --- .../app/features/voicebroadcast/VoiceBroadcastExtensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index fe449c679e..f9da2e76b1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent fun MessageAudioEvent?.isVoiceBroadcast() = this?.root?.getClearContent()?.get(VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY) != null + fun MessageAudioEvent.getVoiceBroadcastEventId(): String? = if (isVoiceBroadcast()) root.getRelationContent()?.eventId else null fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? { From e4a98378a1c12886998eb6b8d3ee1304e8a3db4f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 18 Oct 2022 21:11:03 +0200 Subject: [PATCH 8/8] Fix unit test --- .../src/test/java/im/vector/app/test/fakes/FakeSendService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt index 04b9b95ce1..425a485561 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSendService.kt @@ -17,6 +17,7 @@ package im.vector.app.test.fakes import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.util.Cancelable @@ -25,5 +26,5 @@ class FakeSendService : SendService by mockk() { private val cancelable = mockk() - override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable = cancelable + override fun sendPoll(pollType: PollType, question: String, options: List, additionalContent: Content?): Cancelable = cancelable }