diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f05a2a11e6..eccd40b2a9 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3458,4 +3458,13 @@ Apply underline format Toggle full screen mode + + In reply to + sent a file. + sent an audio file. + sent a voice message. + sent an image. + sent a video. + sent a sticker. + created a poll. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 57bd3dbc41..1c09b49298 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -229,11 +229,14 @@ data class Event( return when { isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) isFileMessage() -> "sent a file." + isVoiceMessage() -> "sent a voice message." isAudioMessage() -> "sent an audio file." isImageMessage() -> "sent an image." isVideoMessage() -> "sent a video." - isSticker() -> "sent a sticker" + isSticker() -> "sent a sticker." isPoll() -> getPollQuestion() ?: "created a poll." + isLiveLocation() -> "Live location." + isLocationMessage() -> "has shared their location." else -> text } } @@ -444,7 +447,7 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && content?.toModel()?.membership == Membership.INVITE fun Event.getPollContent(): MessagePollContent? { - return content.toModel() + return getDecryptedContent().toModel() } fun Event.supportsNotification() = diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f4d506fa4b..373410775b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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.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.linkify 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.getFileUrl 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.util.MimeTypes import javax.inject.Inject @@ -139,6 +141,7 @@ class MessageItemFactory @Inject constructor( private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, private val pollItemViewStateFactory: PollItemViewStateFactory, private val voiceBroadcastItemFactory: VoiceBroadcastItemFactory, + private val processBodyOfReplyToEventUseCase: ProcessBodyOfReplyToEventUseCase, ) { // TODO inject this properly? @@ -200,7 +203,7 @@ class MessageItemFactory @Inject constructor( is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, 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) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } @@ -437,7 +440,14 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes ): MessageTextItem? { // 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( @@ -540,7 +550,8 @@ class MessageItemFactory @Inject constructor( ): VectorEpoxyModel<*>? { val matrixFormattedBody = messageContent.matrixFormattedBody return if (matrixFormattedBody != null) { - buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) + val replyToContent = messageContent.relatesTo?.inReplyTo + buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } @@ -552,10 +563,21 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + replyToContent: ReplyToContent?, ): 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 - return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) + return buildMessageTextItem( + renderedFormattedBody, + true, + informationData, + highlight, + callback, + attributes, + ) } private fun buildMessageTextItem( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt index 920f3e3b80..c46112f995 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt @@ -34,22 +34,22 @@ class EventTextRenderer @AssistedInject constructor( @Assisted private val roomId: String?, private val context: Context, private val avatarRenderer: AvatarRenderer, - private val sessionHolder: ActiveSessionHolder + private val activeSessionHolder: ActiveSessionHolder, ) { - /* ========================================================================================== - * Public api - * ========================================================================================== */ - @AssistedFactory interface Factory { 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 { + return renderNotifyEveryone(text) + } + + private fun renderNotifyEveryone(text: CharSequence): CharSequence { return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) { SpannableStringBuilder(text).apply { addNotifyEveryoneSpans(this, roomId) @@ -59,12 +59,8 @@ class EventTextRenderer @AssistedInject constructor( } } - /* ========================================================================================== - * Helper methods - * ========================================================================================== */ - 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( id = roomId, avatarUrl = room?.avatarUrl, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt new file mode 100644 index 0000000000..44fd5a397c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt @@ -0,0 +1,128 @@ +/* + * 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 = "
" +private const val ENDING_BLOCK_QUOTE = "" + +// TODO add unit tests +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.lastIndexOf(BREAKING_LINE) + val endOfBlockQuoteIndex = matrixFormattedBody.lastIndexOf(ENDING_BLOCK_QUOTE) + + // TODO check in other platform how is handled the case of no repliedToEvent fetched + 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 +}