Merge pull request #7566 from vector-im/fix/mna/missing-translation-reply-to

Missing translations on "replyTo" messages (PSG-978)
This commit is contained in:
Maxime NATUREL 2022-11-18 14:39:18 +01:00 committed by GitHub
commit 14de485c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 456 additions and 42 deletions

1
changelog.d/7555.bugfix Normal file
View File

@ -0,0 +1 @@
Missing translations on "replyTo" messages

View File

@ -3464,4 +3464,13 @@
<string name="rich_text_editor_format_underline">Apply underline format</string> <string name="rich_text_editor_format_underline">Apply underline format</string>
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string> <string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
<!-- ReplyTo events -->
<string name="message_reply_to_prefix">In reply to</string>
<string name="message_reply_to_sender_sent_file">sent a file.</string>
<string name="message_reply_to_sender_sent_audio_file">sent an audio file.</string>
<string name="message_reply_to_sender_sent_voice_message">sent a voice message.</string>
<string name="message_reply_to_sender_sent_image">sent an image.</string>
<string name="message_reply_to_sender_sent_video">sent a video.</string>
<string name="message_reply_to_sender_sent_sticker">sent a sticker.</string>
<string name="message_reply_to_sender_created_poll">created a poll.</string>
</resources> </resources>

View File

@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageStickerConte
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.relation.isReply
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.threads.ThreadDetails import org.matrix.android.sdk.api.session.threads.ThreadDetails
@ -228,11 +229,14 @@ data class Event(
return when { return when {
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
isFileMessage() -> "sent a file." isFileMessage() -> "sent a file."
isVoiceMessage() -> "sent a voice message."
isAudioMessage() -> "sent an audio file." isAudioMessage() -> "sent an audio file."
isImageMessage() -> "sent an image." isImageMessage() -> "sent an image."
isVideoMessage() -> "sent a video." isVideoMessage() -> "sent a video."
isSticker() -> "sent a sticker" isSticker() -> "sent a sticker."
isPoll() -> getPollQuestion() ?: "created a poll." isPoll() -> getPollQuestion() ?: "created a poll."
isLiveLocation() -> "Live location."
isLocationMessage() -> "has shared their location."
else -> text else -> text
} }
} }
@ -420,7 +424,7 @@ fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
getRelationContent()?.takeIf { it.type == type } getRelationContent()?.takeIf { it.type == type }
fun Event.isReply(): Boolean { fun Event.isReply(): Boolean {
return getRelationContent()?.inReplyTo?.eventId != null return getRelationContent().isReply()
} }
fun Event.isReplyRenderedInThread(): Boolean { fun Event.isReplyRenderedInThread(): Boolean {
@ -443,7 +447,7 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
fun Event.getPollContent(): MessagePollContent? { fun Event.getPollContent(): MessagePollContent? {
return content.toModel<MessagePollContent>() return getClearContent().toModel<MessagePollContent>()
} }
fun Event.supportsNotification() = fun Event.supportsNotification() =

View File

@ -47,10 +47,9 @@ interface LocationSharingService {
/** /**
* Starts sharing live location in the room. * Starts sharing live location in the room.
* @param timeoutMillis timeout of the live in milliseconds * @param timeoutMillis timeout of the live in milliseconds
* @param description description of the live for text fallback
* @return the result of the update of the live * @return the result of the update of the live
*/ */
suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
/** /**
* Stops sharing live location in the room. * Stops sharing live location in the room.

View File

@ -28,3 +28,5 @@ data class RelationDefaultContent(
) : RelationContent ) : RelationContent
fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
fun RelationDefaultContent?.isReply(): Boolean = this?.inReplyTo?.eventId != null

View File

@ -73,7 +73,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
return sendLiveLocationTask.execute(params) return sendLiveLocationTask.execute(params)
} }
override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult { override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
// Ensure to stop any active live before starting a new one // Ensure to stop any active live before starting a new one
if (checkIfExistingActiveLive()) { if (checkIfExistingActiveLive()) {
val result = stopLiveLocationShare() val result = stopLiveLocationShare()
@ -84,7 +84,6 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
val params = StartLiveLocationShareTask.Params( val params = StartLiveLocationShareTask.Params(
roomId = roomId, roomId = roomId,
timeoutMillis = timeoutMillis, timeoutMillis = timeoutMillis,
description = description
) )
return startLiveLocationShareTask.execute(params) return startLiveLocationShareTask.execute(params)
} }

View File

@ -30,7 +30,6 @@ internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.
data class Params( data class Params(
val roomId: String, val roomId: String,
val timeoutMillis: Long, val timeoutMillis: Long,
val description: String,
) )
} }
@ -42,7 +41,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult { override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
val beaconContent = MessageBeaconInfoContent( val beaconContent = MessageBeaconInfoContent(
body = params.description, body = "Live location",
timeout = params.timeoutMillis, timeout = params.timeoutMillis,
isLive = true, isLive = true,
unstableTimestampMillis = clock.epochMillis() unstableTimestampMillis = clock.epochMillis()

View File

@ -53,7 +53,6 @@ private const val A_LATITUDE = 1.4
private const val A_LONGITUDE = 40.0 private const val A_LONGITUDE = 40.0
private const val AN_UNCERTAINTY = 5.0 private const val AN_UNCERTAINTY = 5.0
private const val A_TIMEOUT = 15_000L private const val A_TIMEOUT = 15_000L
private const val A_DESCRIPTION = "description"
private const val A_REASON = "reason" private const val A_REASON = "reason"
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@ -143,7 +142,7 @@ internal class DefaultLocationSharingServiceTest {
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id") coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@ -157,7 +156,6 @@ internal class DefaultLocationSharingServiceTest {
val expectedStartParams = StartLiveLocationShareTask.Params( val expectedStartParams = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT, timeoutMillis = A_TIMEOUT,
description = A_DESCRIPTION
) )
coVerify { startLiveLocationShareTask.execute(expectedStartParams) } coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
} }
@ -168,7 +166,7 @@ internal class DefaultLocationSharingServiceTest {
val error = Throwable() val error = Throwable()
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error) coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error) result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@ -186,7 +184,7 @@ internal class DefaultLocationSharingServiceTest {
coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@ -196,7 +194,6 @@ internal class DefaultLocationSharingServiceTest {
val expectedStartParams = StartLiveLocationShareTask.Params( val expectedStartParams = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT, timeoutMillis = A_TIMEOUT,
description = A_DESCRIPTION
) )
coVerify { startLiveLocationShareTask.execute(expectedStartParams) } coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
} }

View File

@ -34,7 +34,6 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask
private const val A_USER_ID = "user-id" private const val A_USER_ID = "user-id"
private const val A_ROOM_ID = "room-id" private const val A_ROOM_ID = "room-id"
private const val AN_EVENT_ID = "event-id" private const val AN_EVENT_ID = "event-id"
private const val A_DESCRIPTION = "description"
private const val A_TIMEOUT = 15_000L private const val A_TIMEOUT = 15_000L
private const val AN_EPOCH = 1655210176L private const val AN_EPOCH = 1655210176L
@ -60,7 +59,6 @@ internal class DefaultStartLiveLocationShareTaskTest {
val params = StartLiveLocationShareTask.Params( val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT, timeoutMillis = A_TIMEOUT,
description = A_DESCRIPTION
) )
fakeClock.givenEpoch(AN_EPOCH) fakeClock.givenEpoch(AN_EPOCH)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
@ -69,7 +67,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val expectedBeaconContent = MessageBeaconInfoContent( val expectedBeaconContent = MessageBeaconInfoContent(
body = A_DESCRIPTION, body = "Live location",
timeout = params.timeoutMillis, timeout = params.timeoutMillis,
isLive = true, isLive = true,
unstableTimestampMillis = AN_EPOCH unstableTimestampMillis = AN_EPOCH
@ -91,7 +89,6 @@ internal class DefaultStartLiveLocationShareTaskTest {
val params = StartLiveLocationShareTask.Params( val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT, timeoutMillis = A_TIMEOUT,
description = A_DESCRIPTION
) )
fakeClock.givenEpoch(AN_EPOCH) fakeClock.givenEpoch(AN_EPOCH)
fakeSendStateTask.givenExecuteRetryReturns("") fakeSendStateTask.givenExecuteRetryReturns("")
@ -106,7 +103,6 @@ internal class DefaultStartLiveLocationShareTaskTest {
val params = StartLiveLocationShareTask.Params( val params = StartLiveLocationShareTask.Params(
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT, timeoutMillis = A_TIMEOUT,
description = A_DESCRIPTION
) )
fakeClock.givenEpoch(AN_EPOCH) fakeClock.givenEpoch(AN_EPOCH)
val error = Throwable() val error = Throwable()

View File

@ -65,6 +65,7 @@ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
import im.vector.app.features.home.room.detail.timeline.render.EventTextRenderer import im.vector.app.features.home.room.detail.timeline.render.EventTextRenderer
import im.vector.app.features.home.room.detail.timeline.render.ProcessBodyOfReplyToEventUseCase
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
@ -106,6 +107,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.MimeTypes
import javax.inject.Inject import javax.inject.Inject
@ -139,6 +141,7 @@ class MessageItemFactory @Inject constructor(
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
private val pollItemViewStateFactory: PollItemViewStateFactory, private val pollItemViewStateFactory: PollItemViewStateFactory,
private val voiceBroadcastItemFactory: VoiceBroadcastItemFactory, private val voiceBroadcastItemFactory: VoiceBroadcastItemFactory,
private val processBodyOfReplyToEventUseCase: ProcessBodyOfReplyToEventUseCase,
) { ) {
// TODO inject this properly? // TODO inject this properly?
@ -200,7 +203,7 @@ class MessageItemFactory @Inject constructor(
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes)
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes) is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
@ -437,7 +440,14 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes attributes: AbsMessageItem.Attributes
): MessageTextItem? { ): MessageTextItem? {
// For compatibility reason we should display the body // For compatibility reason we should display the body
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) return buildMessageTextItem(
messageContent.body,
false,
informationData,
highlight,
callback,
attributes,
)
} }
private fun buildImageMessageItem( private fun buildImageMessageItem(
@ -540,7 +550,8 @@ class MessageItemFactory @Inject constructor(
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
val matrixFormattedBody = messageContent.matrixFormattedBody val matrixFormattedBody = messageContent.matrixFormattedBody
return if (matrixFormattedBody != null) { return if (matrixFormattedBody != null) {
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) val replyToContent = messageContent.relatesTo?.inReplyTo
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent)
} else { } else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
} }
@ -552,10 +563,21 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
replyToContent: ReplyToContent?,
): MessageTextItem? { ): MessageTextItem? {
val compressed = htmlCompressor.compress(matrixFormattedBody) val processedBody = replyToContent
?.let { processBodyOfReplyToEventUseCase.execute(roomId, matrixFormattedBody, it) }
?: matrixFormattedBody
val compressed = htmlCompressor.compress(processedBody)
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) return buildMessageTextItem(
renderedFormattedBody,
true,
informationData,
highlight,
callback,
attributes,
)
} }
private fun buildMessageTextItem( private fun buildMessageTextItem(

View File

@ -34,22 +34,22 @@ class EventTextRenderer @AssistedInject constructor(
@Assisted private val roomId: String?, @Assisted private val roomId: String?,
private val context: Context, private val context: Context,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder private val activeSessionHolder: ActiveSessionHolder,
) { ) {
/* ==========================================================================================
* Public api
* ========================================================================================== */
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String?): EventTextRenderer fun create(roomId: String?): EventTextRenderer
} }
/** /**
* @param text the text you want to render * @param text the text to be rendered
*/ */
fun render(text: CharSequence): CharSequence { fun render(text: CharSequence): CharSequence {
return renderNotifyEveryone(text)
}
private fun renderNotifyEveryone(text: CharSequence): CharSequence {
return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) { return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) {
SpannableStringBuilder(text).apply { SpannableStringBuilder(text).apply {
addNotifyEveryoneSpans(this, roomId) addNotifyEveryoneSpans(this, roomId)
@ -59,12 +59,8 @@ class EventTextRenderer @AssistedInject constructor(
} }
} }
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) { private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId) val room: RoomSummary? = activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId)
val matrixItem = MatrixItem.EveryoneInRoomItem( val matrixItem = MatrixItem.EveryoneInRoomItem(
id = roomId, id = roomId,
avatarUrl = room?.avatarUrl, avatarUrl = room?.avatarUrl,

View File

@ -0,0 +1,126 @@
/*
* 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.home.room.detail.timeline.render
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.events.model.getPollQuestion
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isLiveLocation
import org.matrix.android.sdk.api.session.events.model.isPoll
import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
import javax.inject.Inject
private const val IN_REPLY_TO = "In reply to"
private const val BREAKING_LINE = "<br />"
private const val ENDING_BLOCK_QUOTE = "</blockquote>"
class ProcessBodyOfReplyToEventUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider,
) {
fun execute(roomId: String, matrixFormattedBody: String, replyToContent: ReplyToContent): String {
val repliedToEvent = replyToContent.eventId?.let { getEvent(it, roomId) }
val breakingLineIndex = matrixFormattedBody.indexOf(BREAKING_LINE)
val endOfBlockQuoteIndex = matrixFormattedBody.lastIndexOf(ENDING_BLOCK_QUOTE)
val withTranslatedContent = if (repliedToEvent != null && breakingLineIndex != -1 && endOfBlockQuoteIndex != -1) {
val afterBreakingLineIndex = breakingLineIndex + BREAKING_LINE.length
when {
repliedToEvent.isFileMessage() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.message_reply_to_sender_sent_file)
)
}
repliedToEvent.isVoiceMessage() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.message_reply_to_sender_sent_voice_message)
)
}
repliedToEvent.isAudioMessage() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.message_reply_to_sender_sent_audio_file)
)
}
repliedToEvent.isImageMessage() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.message_reply_to_sender_sent_image)
)
}
repliedToEvent.isVideoMessage() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.message_reply_to_sender_sent_video)
)
}
repliedToEvent.isSticker() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.message_reply_to_sender_sent_sticker)
)
}
repliedToEvent.isPoll() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
repliedToEvent.getPollQuestion() ?: stringProvider.getString(R.string.message_reply_to_sender_created_poll)
)
}
repliedToEvent.isLiveLocation() -> {
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
stringProvider.getString(R.string.live_location_description)
)
}
else -> matrixFormattedBody
}
} else {
matrixFormattedBody
}
return withTranslatedContent.replace(
IN_REPLY_TO,
stringProvider.getString(R.string.message_reply_to_prefix)
)
}
private fun getEvent(eventId: String, roomId: String) =
activeSessionHolder.getSafeActiveSession()
?.getRoom(roomId)
?.getTimelineEvent(eventId)
?.root
}

View File

@ -21,7 +21,6 @@ import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.services.VectorAndroidService
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
@ -125,10 +124,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
val updateLiveResult = session val updateLiveResult = session
.getRoom(roomArgs.roomId) .getRoom(roomArgs.roomId)
?.locationSharingService() ?.locationSharingService()
?.startLiveLocationShare( ?.startLiveLocationShare(roomArgs.durationMillis)
timeoutMillis = roomArgs.durationMillis,
description = getString(R.string.live_location_description)
)
updateLiveResult updateLiveResult
?.let { result -> ?.let { result ->

View File

@ -0,0 +1,268 @@
/*
* 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.home.room.detail.timeline.render
import android.annotation.StringRes
import im.vector.app.R
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeStringProvider
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.getPollQuestion
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isLiveLocation
import org.matrix.android.sdk.api.session.events.model.isPoll
import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
private const val A_ROOM_ID = "room-id"
private const val AN_EVENT_ID = "event-id"
private const val A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY =
"<mx-reply>" +
"<blockquote>" +
"<a href=\"matrixToLink\">In reply to</a> " +
"<a href=\"matrixToLink\">@user:matrix.org</a>" +
"<br />" +
"Message content" +
"</blockquote>" +
"</mx-reply>" +
"Reply text"
private const val A_NEW_PREFIX = "new-prefix"
private const val A_NEW_CONTENT = "new-content"
private const val PREFIX_PROCESSED_ONLY_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY =
"<mx-reply>" +
"<blockquote>" +
"<a href=\"matrixToLink\">$A_NEW_PREFIX</a> " +
"<a href=\"matrixToLink\">@user:matrix.org</a>" +
"<br />" +
"Message content" +
"</blockquote>" +
"</mx-reply>" +
"Reply text"
private const val FULLY_PROCESSED_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY =
"<mx-reply>" +
"<blockquote>" +
"<a href=\"matrixToLink\">$A_NEW_PREFIX</a> " +
"<a href=\"matrixToLink\">@user:matrix.org</a>" +
"<br />" +
A_NEW_CONTENT +
"</blockquote>" +
"</mx-reply>" +
"Reply text"
class ProcessBodyOfReplyToEventUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeStringProvider = FakeStringProvider()
private val fakeReplyToContent = ReplyToContent(eventId = AN_EVENT_ID)
private val fakeRepliedEvent = givenARepliedEvent()
private val processBodyOfReplyToEventUseCase = ProcessBodyOfReplyToEventUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
stringProvider = fakeStringProvider.instance,
)
@Before
fun setup() {
givenNewPrefix()
mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a replied event of type file message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isFileMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_sent_file)
executeAndAssertResult()
}
@Test
fun `given a replied event of type voice message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isVoiceMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_sent_voice_message)
executeAndAssertResult()
}
@Test
fun `given a replied event of type audio message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isAudioMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_sent_audio_file)
executeAndAssertResult()
}
@Test
fun `given a replied event of type image message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isImageMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_sent_image)
executeAndAssertResult()
}
@Test
fun `given a replied event of type video message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isVideoMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_sent_video)
executeAndAssertResult()
}
@Test
fun `given a replied event of type sticker message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isStickerMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_sent_sticker)
executeAndAssertResult()
}
@Test
fun `given a replied event of type poll message with null question when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
every { fakeRepliedEvent.getPollQuestion() } returns null
executeAndAssertResult()
}
@Test
fun `given a replied event of type poll message with existing question when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT
executeAndAssertResult()
}
@Test
fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent(isLiveLocationMessage = true)
givenNewContentForId(R.string.live_location_description)
executeAndAssertResult()
}
@Test
fun `given a replied event of type not handled when process the formatted body only prefix is replaced by correct string`() {
// Given
givenTypeOfRepliedEvent()
// When
val result = processBodyOfReplyToEventUseCase.execute(
roomId = A_ROOM_ID,
matrixFormattedBody = A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY,
replyToContent = fakeReplyToContent,
)
// Then
result shouldBeEqualTo PREFIX_PROCESSED_ONLY_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY
}
@Test
fun `given no replied event found when process the formatted body then only prefix is replaced by correct string`() {
// Given
givenARepliedEvent(timelineEvent = null)
// When
val result = processBodyOfReplyToEventUseCase.execute(
roomId = A_ROOM_ID,
matrixFormattedBody = A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY,
replyToContent = fakeReplyToContent,
)
// Then
result shouldBeEqualTo PREFIX_PROCESSED_ONLY_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY
}
private fun executeAndAssertResult() {
// When
val result = processBodyOfReplyToEventUseCase.execute(
roomId = A_ROOM_ID,
matrixFormattedBody = A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY,
replyToContent = fakeReplyToContent,
)
// Then
result shouldBeEqualTo FULLY_PROCESSED_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY
}
private fun givenARepliedEvent(timelineEvent: TimelineEvent? = mockk()): Event {
val event = mockk<Event>()
timelineEvent?.let { every { it.root } returns event }
fakeActiveSessionHolder
.fakeSession
.roomService()
.getRoom(A_ROOM_ID)
.timelineService()
.givenTimelineEvent(timelineEvent)
return event
}
private fun givenTypeOfRepliedEvent(
isFileMessage: Boolean = false,
isVoiceMessage: Boolean = false,
isAudioMessage: Boolean = false,
isImageMessage: Boolean = false,
isVideoMessage: Boolean = false,
isStickerMessage: Boolean = false,
isPollMessage: Boolean = false,
isLiveLocationMessage: Boolean = false,
) {
every { fakeRepliedEvent.isFileMessage() } returns isFileMessage
every { fakeRepliedEvent.isVoiceMessage() } returns isVoiceMessage
every { fakeRepliedEvent.isAudioMessage() } returns isAudioMessage
every { fakeRepliedEvent.isImageMessage() } returns isImageMessage
every { fakeRepliedEvent.isVideoMessage() } returns isVideoMessage
every { fakeRepliedEvent.isSticker() } returns isStickerMessage
every { fakeRepliedEvent.isPoll() } returns isPollMessage
every { fakeRepliedEvent.isLiveLocation() } returns isLiveLocationMessage
}
private fun givenNewPrefix() {
fakeStringProvider.given(R.string.message_reply_to_prefix, A_NEW_PREFIX)
}
private fun givenNewContentForId(@StringRes resId: Int) {
fakeStringProvider.given(resId, A_NEW_CONTENT)
}
}

View File

@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
class FakeTimelineService : TimelineService by mockk() { class FakeTimelineService : TimelineService by mockk() {
fun givenTimelineEvent(event: TimelineEvent) { fun givenTimelineEvent(event: TimelineEvent?) {
every { getTimelineEvent(any()) } returns event every { getTimelineEvent(any()) } returns event
} }
} }