Merge pull request #7397 from vector-im/feature/fre/voice_broadcast_additional_content

Add additional data in voice broadcast events
This commit is contained in:
Florian Renaud 2022-10-19 07:51:01 +02:00 committed by GitHub
commit 13972661e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 176 additions and 69 deletions

1
changelog.d/7397.wip Normal file
View File

@ -0,0 +1 @@
[Voice Broadcast] Add additional data in events

View File

@ -45,18 +45,30 @@ interface SendService {
* @param text the text message to send * @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @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 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] * @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. * Method to send a text message with a formatted body.
* @param text the text message to send * @param text the text message to send
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @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] * @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. * Method to quote an events content.
@ -65,6 +77,7 @@ interface SendService {
* @param formattedText the formatted text message to send * @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 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 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] * @return a [Cancelable]
*/ */
fun sendQuotedTextMessage( fun sendQuotedTextMessage(
@ -73,6 +86,7 @@ interface SendService {
formattedText: String? = null, formattedText: String? = null,
autoMarkdown: Boolean, autoMarkdown: Boolean,
rootThreadEventId: String? = null, rootThreadEventId: String? = null,
additionalContent: Content? = null,
): Cancelable ): 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 * 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 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 relatesTo add a relation content to the media event
* @param additionalContent additional content to put in the event content
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendMedia( fun sendMedia(
@ -91,6 +106,7 @@ interface SendService {
roomIds: Set<String>, roomIds: Set<String>,
rootThreadEventId: String? = null, rootThreadEventId: String? = null,
relatesTo: RelationDefaultContent? = null, relatesTo: RelationDefaultContent? = null,
additionalContent: Content? = null,
): Cancelable ): 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. * @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 * 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 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] * @return a [Cancelable]
*/ */
fun sendMedias( fun sendMedias(
@ -107,6 +124,7 @@ interface SendService {
compressBeforeSending: Boolean, compressBeforeSending: Boolean,
roomIds: Set<String>, roomIds: Set<String>,
rootThreadEventId: String? = null, rootThreadEventId: String? = null,
additionalContent: Content? = null,
): Cancelable ): Cancelable
/** /**
@ -114,31 +132,35 @@ interface SendService {
* @param pollType indicates open or closed polls * @param pollType indicates open or closed polls
* @param question the question * @param question the question
* @param options list of options * @param options list of options
* @param additionalContent additional content to put in the event content
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable fun sendPoll(pollType: PollType, question: String, options: List<String>, additionalContent: Content? = null): Cancelable
/** /**
* Method to send a poll response. * Method to send a poll response.
* @param pollEventId the poll currently replied to * @param pollEventId the poll currently replied to
* @param answerId The id of the answer * @param answerId The id of the answer
* @param additionalContent additional content to put in the event content
* @return a [Cancelable] * @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. * End a poll in the room.
* @param pollEventId event id of the poll * @param pollEventId event id of the poll
* @param additionalContent additional content to put in the event content
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun endPoll(pollEventId: String): Cancelable fun endPoll(pollEventId: String, additionalContent: Content? = null): Cancelable
/** /**
* Redact (delete) the given event. * Redact (delete) the given event.
* @param event The event to redact * @param event The event to redact
* @param reason Optional reason string * @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. * Schedule this message to be resent.

View File

@ -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.listeners.ProgressListener
import org.matrix.android.sdk.api.session.content.ContentAttachmentData 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.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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent 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 newAttachmentAttributes: NewAttachmentAttributes
) { ) {
localEchoRepository.updateEcho(eventId) { _, event -> 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) { val updatedContent = when (messageContent) {
is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes)
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, 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) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize)
else -> messageContent else -> messageContent
} }
event.content = ContentMapper.map(updatedContent.toContent()) event.content = ContentMapper.map(updatedContent.toContent().plus(additionalContent))
} }
} }

View File

@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.session.content.ContentAttachmentData 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.Event
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
@ -88,14 +89,14 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean, additionalContent: Content?): Cancelable {
return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown, additionalContent)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String, additionalContent: Content?): Cancelable {
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType, additionalContent)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }
@ -105,7 +106,8 @@ internal class DefaultSendService @AssistedInject constructor(
text: String, text: String,
formattedText: String?, formattedText: String?,
autoMarkdown: Boolean, autoMarkdown: Boolean,
rootThreadEventId: String? rootThreadEventId: String?,
additionalContent: Content?,
): Cancelable { ): Cancelable {
return localEchoEventFactory.createQuotedTextEvent( return localEchoEventFactory.createQuotedTextEvent(
roomId = roomId, roomId = roomId,
@ -113,33 +115,34 @@ internal class DefaultSendService @AssistedInject constructor(
text = text, text = text,
formattedText = formattedText, formattedText = formattedText,
autoMarkdown = autoMarkdown, autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId rootThreadEventId = rootThreadEventId,
additionalContent = additionalContent,
) )
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable { override fun sendPoll(pollType: PollType, question: String, options: List<String>, additionalContent: Content?): Cancelable {
return localEchoEventFactory.createPollEvent(roomId, pollType, question, options) return localEchoEventFactory.createPollEvent(roomId, pollType, question, options, additionalContent)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun voteToPoll(pollEventId: String, answerId: String): Cancelable { override fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content?): Cancelable {
return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId) return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId, additionalContent)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun endPoll(pollEventId: String): Cancelable { override fun endPoll(pollEventId: String, additionalContent: Content?): Cancelable {
return localEchoEventFactory.createEndPollEvent(roomId, pollEventId) return localEchoEventFactory.createEndPollEvent(roomId, pollEventId, additionalContent)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(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? // TODO manage media/attachements?
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, additionalContent)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
return eventSenderProcessor.postRedaction(redactionEcho, reason) return eventSenderProcessor.postRedaction(redactionEcho, reason)
} }
@ -265,7 +268,8 @@ internal class DefaultSendService @AssistedInject constructor(
attachments: List<ContentAttachmentData>, attachments: List<ContentAttachmentData>,
compressBeforeSending: Boolean, compressBeforeSending: Boolean,
roomIds: Set<String>, roomIds: Set<String>,
rootThreadEventId: String? rootThreadEventId: String?,
additionalContent: Content?,
): Cancelable { ): Cancelable {
return attachments.mapTo(CancelableBag()) { return attachments.mapTo(CancelableBag()) {
sendMedia( sendMedia(
@ -283,6 +287,7 @@ internal class DefaultSendService @AssistedInject constructor(
roomIds: Set<String>, roomIds: Set<String>,
rootThreadEventId: String?, rootThreadEventId: String?,
relatesTo: RelationDefaultContent?, relatesTo: RelationDefaultContent?,
additionalContent: Content?,
): Cancelable { ): Cancelable {
// Ensure that the event will not be send in a thread if we are a different flow. // Ensure that the event will not be send in a thread if we are a different flow.
// Like sending files to multiple rooms // Like sending files to multiple rooms
@ -299,6 +304,7 @@ internal class DefaultSendService @AssistedInject constructor(
attachment = attachment, attachment = attachment,
rootThreadEventId = rootThreadId, rootThreadEventId = rootThreadId,
relatesTo, relatesTo,
additionalContent,
).also { event -> ).also { event ->
createLocalEcho(event) createLocalEcho(event)
} }

View File

@ -95,12 +95,12 @@ internal class LocalEchoEventFactory @Inject constructor(
private val permalinkFactory: PermalinkFactory, private val permalinkFactory: PermalinkFactory,
private val clock: Clock, 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) { 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()) 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 { private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
@ -116,8 +116,8 @@ internal class LocalEchoEventFactory @Inject constructor(
return TextContent(text.toString()) return TextContent(text.toString())
} }
fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String, additionalContent: Content? = null): Event {
return createMessageEvent(roomId, textContent.toMessageTextContent(msgType)) return createMessageEvent(roomId, textContent.toMessageTextContent(msgType), additionalContent)
} }
fun createReplaceTextEvent( fun createReplaceTextEvent(
@ -128,6 +128,7 @@ internal class LocalEchoEventFactory @Inject constructor(
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
msgType: String, msgType: String,
compatibilityText: String, compatibilityText: String,
additionalContent: Content? = null,
): Event { ): Event {
val content = if (newBodyFormattedText != null) { val content = if (newBodyFormattedText != null) {
TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType) TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType)
@ -141,7 +142,8 @@ internal class LocalEchoEventFactory @Inject constructor(
body = compatibilityText, body = compatibilityText,
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
newContent = content, newContent = content,
) ),
additionalContent,
) )
} }
@ -167,6 +169,7 @@ internal class LocalEchoEventFactory @Inject constructor(
targetEventId: String, targetEventId: String,
question: String, question: String,
options: List<String>, options: List<String>,
additionalContent: Content? = null,
): Event { ): Event {
val newContent = MessagePollContent( val newContent = MessagePollContent(
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
@ -179,7 +182,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_START.first(), 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, roomId: String,
pollEventId: String, pollEventId: String,
answerId: String, answerId: String,
additionalContent: Content? = null,
): Event { ): Event {
val content = MessagePollResponseContent( val content = MessagePollResponseContent(
body = answerId, body = answerId,
@ -203,7 +207,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_RESPONSE.first(), type = EventType.POLL_RESPONSE.first(),
content = content.toContent(), content = content.toContent().plus(additionalContent.orEmpty()),
unsignedData = UnsignedData(age = null, transactionId = localId) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -213,6 +217,7 @@ internal class LocalEchoEventFactory @Inject constructor(
pollType: PollType, pollType: PollType,
question: String, question: String,
options: List<String>, options: List<String>,
additionalContent: Content? = null,
): Event { ): Event {
val content = createPollContent(question, options, pollType) val content = createPollContent(question, options, pollType)
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
@ -222,7 +227,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_START.first(), type = EventType.POLL_START.first(),
content = content.toContent(), content = content.toContent().plus(additionalContent.orEmpty()),
unsignedData = UnsignedData(age = null, transactionId = localId) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -230,6 +235,7 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createEndPollEvent( fun createEndPollEvent(
roomId: String, roomId: String,
eventId: String, eventId: String,
additionalContent: Content? = null,
): Event { ): Event {
val content = MessageEndPollContent( val content = MessageEndPollContent(
relatesTo = RelationDefaultContent( relatesTo = RelationDefaultContent(
@ -244,7 +250,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_END.first(), type = EventType.POLL_END.first(),
content = content.toContent(), content = content.toContent().plus(additionalContent.orEmpty()),
unsignedData = UnsignedData(age = null, transactionId = localId) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -255,6 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor(
longitude: Double, longitude: Double,
uncertainty: Double?, uncertainty: Double?,
isUserLocation: Boolean, isUserLocation: Boolean,
additionalContent: Content? = null,
): Event { ): Event {
val geoUri = buildGeoUri(latitude, longitude, uncertainty) val geoUri = buildGeoUri(latitude, longitude, uncertainty)
val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
@ -266,7 +273,7 @@ internal class LocalEchoEventFactory @Inject constructor(
unstableTimestampMillis = clock.epochMillis(), unstableTimestampMillis = clock.epochMillis(),
unstableText = geoUri unstableText = geoUri
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content, additionalContent)
} }
fun createLiveLocationEvent( fun createLiveLocationEvent(
@ -275,6 +282,7 @@ internal class LocalEchoEventFactory @Inject constructor(
latitude: Double, latitude: Double,
longitude: Double, longitude: Double,
uncertainty: Double?, uncertainty: Double?,
additionalContent: Content? = null,
): Event { ): Event {
val geoUri = buildGeoUri(latitude, longitude, uncertainty) val geoUri = buildGeoUri(latitude, longitude, uncertainty)
val content = MessageBeaconLocationDataContent( val content = MessageBeaconLocationDataContent(
@ -293,7 +301,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.BEACON_LOCATION_DATA.first(), type = EventType.BEACON_LOCATION_DATA.first(),
content = content.toContent(), content = content.toContent().plus(additionalContent.orEmpty()),
unsignedData = UnsignedData(age = null, transactionId = localId) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -306,6 +314,7 @@ internal class LocalEchoEventFactory @Inject constructor(
autoMarkdown: Boolean, autoMarkdown: Boolean,
msgType: String, msgType: String,
compatibilityText: String, compatibilityText: String,
additionalContent: Content? = null,
): Event { ): Event {
val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false)
val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: ""
@ -340,7 +349,8 @@ internal class LocalEchoEventFactory @Inject constructor(
formattedBody = replyFormatted formattedBody = replyFormatted
) )
.toContent() .toContent()
) ),
additionalContent,
) )
} }
@ -349,23 +359,32 @@ internal class LocalEchoEventFactory @Inject constructor(
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
rootThreadEventId: String?, rootThreadEventId: String?,
relatesTo: RelationDefaultContent?, relatesTo: RelationDefaultContent?,
additionalContent: Content? = null,
): Event { ): Event {
return when (attachment.type) { return when (attachment.type) {
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId, relatesTo) ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent)
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId, relatesTo) ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent)
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId, relatesTo) ContentAttachmentData.Type.AUDIO -> createAudioEvent(
roomId,
attachment,
isVoiceMessage = false,
rootThreadEventId = rootThreadEventId,
relatesTo,
additionalContent
)
ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent( ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(
roomId, roomId,
attachment, attachment,
isVoiceMessage = true, isVoiceMessage = true,
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
relatesTo, 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( val content = ReactionContent(
ReactionInfo( ReactionInfo(
RelationType.ANNOTATION, RelationType.ANNOTATION,
@ -380,7 +399,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.REACTION, type = EventType.REACTION,
content = content.toContent(), content = content.toContent().plus(additionalContent.orEmpty()),
unsignedData = UnsignedData(age = null, transactionId = localId) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -390,6 +409,7 @@ internal class LocalEchoEventFactory @Inject constructor(
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
rootThreadEventId: String?, rootThreadEventId: String?,
relatesTo: RelationDefaultContent?, relatesTo: RelationDefaultContent?,
additionalContent: Content?,
): Event { ): Event {
var width = attachment.width var width = attachment.width
var height = attachment.height var height = attachment.height
@ -417,7 +437,7 @@ internal class LocalEchoEventFactory @Inject constructor(
url = attachment.queryUri.toString(), url = attachment.queryUri.toString(),
relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content, additionalContent)
} }
private fun createVideoEvent( private fun createVideoEvent(
@ -425,6 +445,7 @@ internal class LocalEchoEventFactory @Inject constructor(
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
rootThreadEventId: String?, rootThreadEventId: String?,
relatesTo: RelationDefaultContent?, relatesTo: RelationDefaultContent?,
additionalContent: Content?,
): Event { ): Event {
val mediaDataRetriever = MediaMetadataRetriever() val mediaDataRetriever = MediaMetadataRetriever()
mediaDataRetriever.setDataSource(context, attachment.queryUri) mediaDataRetriever.setDataSource(context, attachment.queryUri)
@ -459,7 +480,7 @@ internal class LocalEchoEventFactory @Inject constructor(
url = attachment.queryUri.toString(), url = attachment.queryUri.toString(),
relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content, additionalContent)
} }
private fun createAudioEvent( private fun createAudioEvent(
@ -468,6 +489,7 @@ internal class LocalEchoEventFactory @Inject constructor(
isVoiceMessage: Boolean, isVoiceMessage: Boolean,
rootThreadEventId: String?, rootThreadEventId: String?,
relatesTo: RelationDefaultContent?, relatesTo: RelationDefaultContent?,
additionalContent: Content?
): Event { ): Event {
val content = MessageAudioContent( val content = MessageAudioContent(
msgType = MessageType.MSGTYPE_AUDIO, msgType = MessageType.MSGTYPE_AUDIO,
@ -485,7 +507,7 @@ internal class LocalEchoEventFactory @Inject constructor(
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(), voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content, additionalContent)
} }
private fun createFileEvent( private fun createFileEvent(
@ -493,6 +515,7 @@ internal class LocalEchoEventFactory @Inject constructor(
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
rootThreadEventId: String?, rootThreadEventId: String?,
relatesTo: RelationDefaultContent?, relatesTo: RelationDefaultContent?,
additionalContent: Content?
): Event { ): Event {
val content = MessageFileContent( val content = MessageFileContent(
msgType = MessageType.MSGTYPE_FILE, msgType = MessageType.MSGTYPE_FILE,
@ -504,15 +527,16 @@ internal class LocalEchoEventFactory @Inject constructor(
url = attachment.queryUri.toString(), url = attachment.queryUri.toString(),
relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) }
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content, additionalContent)
} }
private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event { private fun createMessageEvent(roomId: String, content: MessageContent, additionalContent: Content?): Event {
return createEvent(roomId, EventType.MESSAGE, content.toContent()) 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 newContent = enhanceStickerIfNeeded(type, content) ?: content
val updatedNewContent = newContent?.plus(additionalContent.orEmpty()) ?: additionalContent
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
roomId = roomId, roomId = roomId,
@ -520,7 +544,7 @@ internal class LocalEchoEventFactory @Inject constructor(
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = type, type = type,
content = newContent, content = updatedNewContent,
unsignedData = UnsignedData(age = null, transactionId = localId) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -555,6 +579,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType: String, msgType: String,
autoMarkdown: Boolean, autoMarkdown: Boolean,
formattedText: String?, formattedText: String?,
additionalContent: Content? = null,
): Event { ): Event {
val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown) val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown)
return createEvent( return createEvent(
@ -564,8 +589,7 @@ internal class LocalEchoEventFactory @Inject constructor(
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = msgType msgType = msgType
) ).toContent().plus(additionalContent.orEmpty())
.toContent()
) )
} }
@ -584,6 +608,7 @@ internal class LocalEchoEventFactory @Inject constructor(
autoMarkdown: Boolean, autoMarkdown: Boolean,
rootThreadEventId: String? = null, rootThreadEventId: String? = null,
showInThread: Boolean, showInThread: Boolean,
additionalContent: Content? = null
): Event? { ): Event? {
// Fallbacks and event representation // Fallbacks and event representation
// TODO Add error/warning logs when any of this is null // TODO Add error/warning logs when any of this is null
@ -621,7 +646,7 @@ internal class LocalEchoEventFactory @Inject constructor(
showInThread = showInThread showInThread = showInThread
) )
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content, additionalContent)
} }
private fun generateThreadRelationContent(rootThreadEventId: String) = 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() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
roomId = roomId, roomId = roomId,
@ -759,7 +784,7 @@ internal class LocalEchoEventFactory @Inject constructor(
eventId = localId, eventId = localId,
type = EventType.REDACTION, type = EventType.REDACTION,
redacts = eventId, 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) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -776,9 +801,14 @@ internal class LocalEchoEventFactory @Inject constructor(
formattedText: String?, formattedText: String?,
autoMarkdown: Boolean, autoMarkdown: Boolean,
rootThreadEventId: String?, rootThreadEventId: String?,
additionalContent: Content? = null,
): Event { ): Event {
val messageContent = quotedEvent.getLastMessageContent() 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 quoteText = legacyRiotQuoteText(textMsg, text)
val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText" val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText"
@ -791,13 +821,15 @@ internal class LocalEchoEventFactory @Inject constructor(
rootThreadEventId = rootThreadEventId, rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = MessageType.MSGTYPE_TEXT msgType = MessageType.MSGTYPE_TEXT
) ),
additionalContent,
) )
} else { } else {
createFormattedTextEvent( createFormattedTextEvent(
roomId, roomId,
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText), markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText),
MessageType.MSGTYPE_TEXT MessageType.MSGTYPE_TEXT,
additionalContent,
) )
} }
} }

View File

@ -16,11 +16,16 @@
package im.vector.app.features.voicebroadcast package im.vector.app.features.voicebroadcast
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
object VoiceBroadcastConstants { object VoiceBroadcastConstants {
/** Voice Broadcast State Event. */ /** Voice Broadcast State Event. */
const val STATE_ROOM_VOICE_BROADCAST_INFO = "io.element.voice_broadcast_info" 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. */ /** Default voice broadcast chunk duration, in seconds. */
const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120 const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120
} }

View File

@ -16,14 +16,19 @@
package im.vector.app.features.voicebroadcast 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.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent 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? = fun MessageAudioEvent.getVoiceBroadcastEventId(): String? = if (isVoiceBroadcast()) root.getRelationContent()?.eventId else null
// TODO Improve this condition by checking the referenced event type
root.takeIf { content.voiceMessageIndicator != null } fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? {
?.getRelationContent()?.takeIf { it.type == RelationType.REFERENCE } @Suppress("UNCHECKED_CAST")
?.eventId return (root.getClearContent()?.get(VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY) as? Content).toModel<VoiceBroadcastChunk>()
}
val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence

View File

@ -85,7 +85,7 @@ class VoiceBroadcastPlayer @Inject constructor(
private fun updatePlaylist(room: Room, eventId: String) { private fun updatePlaylist(room: Room, eventId: String) {
val timelineEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId) val timelineEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId)
val audioEvents = timelineEvents.mapNotNull { it.root.asMessageAudioEvent() } 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() { private fun startPlayback() {

View File

@ -38,6 +38,8 @@ data class MessageVoiceBroadcastInfoContent(
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = 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. **/ /** The [VoiceBroadcastState] value. **/
@Json(name = "state") val voiceBroadcastStateStr: String = "", @Json(name = "state") val voiceBroadcastStateStr: String = "",
/** The length of the voice chunks in seconds. **/ /** The length of the voice chunks in seconds. **/

View File

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

View File

@ -23,6 +23,7 @@ import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent 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.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.lib.multipicker.utils.toMultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType
@ -70,6 +71,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = session.myUserId, stateKey = session.myUserId,
body = MessageVoiceBroadcastInfoContent( body = MessageVoiceBroadcastInfoContent(
deviceId = session.sessionParams.deviceId,
voiceBroadcastStateStr = VoiceBroadcastState.STARTED.value, voiceBroadcastStateStr = VoiceBroadcastState.STARTED.value,
chunkLength = chunkLength, chunkLength = chunkLength,
).toContent() ).toContent()
@ -93,12 +95,14 @@ class StartVoiceBroadcastUseCase @Inject constructor(
"Voice Broadcast Part ($sequence).${voiceMessageFile.extension}" "Voice Broadcast Part ($sequence).${voiceMessageFile.extension}"
) )
val audioType = outputFileUri.toMultiPickerAudioType(context) ?: return val audioType = outputFileUri.toMultiPickerAudioType(context) ?: return
// TODO put sequence in event content
room.sendService().sendMedia( room.sendService().sendMedia(
attachment = audioType.toContentAttachmentData(isVoiceMessage = true), attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
compressBeforeSending = false, compressBeforeSending = false,
roomIds = emptySet(), roomIds = emptySet(),
relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId) relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId),
additionalContent = mapOf(
VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY to VoiceBroadcastChunk(sequence = sequence).toContent()
)
) )
} }
} }

View File

@ -17,6 +17,7 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import io.mockk.mockk 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.model.message.PollType
import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -25,5 +26,5 @@ class FakeSendService : SendService by mockk() {
private val cancelable = mockk<Cancelable>() private val cancelable = mockk<Cancelable>()
override fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable = cancelable override fun sendPoll(pollType: PollType, question: String, options: List<String>, additionalContent: Content?): Cancelable = cancelable
} }