From 0e06908a48ef42f6f5734c6251db62b28b90aa15 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 27 May 2019 11:55:20 +0200 Subject: [PATCH] Design update + Reply + Better preview in action menu --- .../android/api/session/events/model/Event.kt | 9 +- .../matrix/android/api/session/room/Room.kt | 4 +- .../room/model/annotation/ReactionInfo.kt | 4 +- .../room/model/annotation/RelationContent.kt | 1 + .../annotation/RelationDefaultContent.kt | 3 +- ...{ReactionService.kt => RelationService.kt} | 8 +- .../room/model/annotation/ReplyToContent.kt | 25 ++++++ .../android/internal/di/MatrixModule.kt | 4 + .../internal/session/room/DefaultRoom.kt | 6 +- .../internal/session/room/RoomFactory.kt | 6 +- .../internal/session/room/RoomModule.kt | 5 +- ...onService.kt => DefaultRelationService.kt} | 48 +++++++++- .../room/send/LocalEchoEventFactory.kt | 87 +++++++++++++++++-- .../session/room/send/SendEventWorker.kt | 8 +- .../android/internal/util/StringProvider.kt | 55 ++++++++++++ .../src/main/res/values/strings_riotX.xml | 9 ++ .../riotredesign/features/home/HomeModule.kt | 2 +- .../home/room/detail/RoomDetailActions.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 16 +++- .../home/room/detail/RoomDetailViewModel.kt | 54 ++++++++---- .../home/room/detail/RoomDetailViewState.kt | 3 +- .../action/MessageActionsViewModel.kt | 19 +++- .../timeline/action/MessageMenuViewModel.kt | 9 +- .../timeline/factory/MessageItemFactory.kt | 8 +- ...{ic_corner_down_right.xml => ic_reply.xml} | 12 +-- vector/src/main/res/values/strings_riotX.xml | 3 + 26 files changed, 350 insertions(+), 59 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/{ReactionService.kt => RelationService.kt} (88%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/{DefaultReactionService.kt => DefaultRelationService.kt} (77%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt create mode 100644 matrix-sdk-android/src/main/res/values/strings_riotX.xml rename vector/src/main/res/drawable/{ic_corner_down_right.xml => ic_reply.xml} (72%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 7c6ca39b..8603cb79 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -18,8 +18,10 @@ package im.vector.matrix.android.api.session.events.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.squareup.moshi.JsonDataException import com.squareup.moshi.Types import im.vector.matrix.android.internal.di.MoshiProvider +import timber.log.Timber import java.lang.reflect.ParameterizedType typealias Content = Map @@ -31,7 +33,12 @@ inline fun Content?.toModel(): T? { return this?.let { val moshi = MoshiProvider.providesMoshi() val moshiAdapter = moshi.adapter(T::class.java) - return moshiAdapter.fromJsonValue(it) + try { + return moshiAdapter.fromJsonValue(it) + } catch (e: JsonDataException) { + Timber.e(e, "Failed to parse content") + return null + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index ae890eaf..1e05d71a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.model.annotation.ReactionService +import im.vector.matrix.android.api.session.room.model.annotation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService @@ -28,7 +28,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService /** * This interface defines methods to interact within a room. */ -interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , ReactionService{ +interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , RelationService{ /** * The roomId of this room diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt index bf573744..dcb7328d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt @@ -7,5 +7,7 @@ import com.squareup.moshi.JsonClass data class ReactionInfo( @Json(name = "rel_type") override val type: String?, @Json(name = "event_id") override val eventId: String, - val key: String + val key: String, + //always null for reaction + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null ) : RelationContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt index d9e23b30..3327fc45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt @@ -3,4 +3,5 @@ package im.vector.matrix.android.api.session.room.model.annotation interface RelationContent { val type: String? val eventId: String? + val inReplyTo: ReplyToContent? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt index 7137e86a..5ccfca11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt @@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class RelationDefaultContent( @Json(name = "rel_type") override val type: String?, - @Json(name = "event_id") override val eventId: String? + @Json(name = "event_id") override val eventId: String?, + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null ) : RelationContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt similarity index 88% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt index 3f8fec00..5185babf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt @@ -15,10 +15,13 @@ */ package im.vector.matrix.android.api.session.room.model.annotation +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable //TODO rename in relationService? -interface ReactionService { +interface RelationService { /** @@ -59,4 +62,7 @@ interface ReactionService { */ fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String = "* $newBodyText"): Cancelable + + fun replyToMessage(eventReplied: Event, replyText: String) : Cancelable? + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt new file mode 100644 index 00000000..54226545 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.matrix.android.api.session.room.model.annotation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReplyToContent( + @Json(name = "event_id") val eventId: String +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index f750c981..693c08f2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -20,6 +20,7 @@ import android.content.Context import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.StringProvider import kotlinx.coroutines.Dispatchers import org.koin.dsl.module.module @@ -39,6 +40,9 @@ class MatrixModule(private val context: Context) { single { TaskExecutor(get()) } + single { + StringProvider(context.resources) + } single { BackgroundDetectionObserver() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 80d89659..7ee1ea28 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -22,7 +22,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.model.annotation.ReactionService +import im.vector.matrix.android.api.session.room.model.annotation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService @@ -40,14 +40,14 @@ internal class DefaultRoom( private val sendService: SendService, private val stateService: StateService, private val readService: ReadService, - private val reactionService: ReactionService, + private val relationService: RelationService, private val roomMembersService: MembershipService ) : Room, TimelineService by timelineService, SendService by sendService, StateService by stateService, ReadService by readService, - ReactionService by reactionService, + RelationService by relationService, MembershipService by roomMembersService { override val roomSummary: LiveData by lazy { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index c59e3f8d..1b544c00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -18,10 +18,10 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService +import im.vector.matrix.android.internal.session.room.annotation.DefaultRelationService import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.annotation.DefaultReactionService import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask @@ -58,7 +58,7 @@ internal class RoomFactory(private val monarchy: Monarchy, val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) - val reactionService = DefaultReactionService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, taskExecutor) + val reactionService = DefaultRelationService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, monarchy, taskExecutor) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 952909ef..b1799bd2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -39,6 +39,7 @@ import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.* +import im.vector.matrix.android.internal.util.StringProvider import org.koin.dsl.module.module import retrofit2.Retrofit @@ -73,7 +74,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - LocalEchoEventFactory(get()) + LocalEchoEventFactory(get(), get()) } scope(DefaultSession.SCOPE) { @@ -109,7 +110,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultPruneEventTask(get(),get()) as PruneEventTask + DefaultPruneEventTask(get(), get()) as PruneEventTask } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt similarity index 77% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt index fdb75a8f..ec36f683 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt @@ -16,11 +16,17 @@ package im.vector.matrix.android.internal.session.room.annotation import androidx.work.* +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.annotation.ReactionService +import im.vector.matrix.android.api.session.room.model.annotation.RelationService import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.database.helper.addSendingEvent +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RedactEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker @@ -28,6 +34,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory +import im.vector.matrix.android.internal.util.tryTransactionAsync import java.util.concurrent.TimeUnit private const val REACTION_WORK = "REACTION_WORK" @@ -37,12 +44,13 @@ private val WORK_CONSTRAINTS = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() -internal class DefaultReactionService(private val roomId: String, +internal class DefaultRelationService(private val roomId: String, private val eventFactory: LocalEchoEventFactory, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val updateQuickReactionTask: UpdateQuickReactionTask, + private val monarchy: Monarchy, private val taskExecutor: TaskExecutor) - : ReactionService { + : RelationService { override fun sendReaction(reaction: String, targetEventId: String): Cancelable { @@ -170,4 +178,38 @@ internal class DefaultReactionService(private val roomId: String, return CancelableWork(workRequest.id) } + + /** + * Reply to an event in the timeline + * Users may wish to reference another message when forming their own message + * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 + */ + override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { + val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also { + saveLocalEcho(it) + } ?: return null + val sendContentWorkerParams = SendEventWorker.Params(roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + val workRequest = OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(sendWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + return CancelableWork(workRequest.id) + } + + private fun saveLocalEcho(event: Event) { + monarchy.tryTransactionAsync { realm -> + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + ?: return@tryTransactionAsync + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) + ?: return@tryTransactionAsync + + roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index e1cc9c74..6b1cdb11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,19 +17,20 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever +import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.RelationType -import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.annotation.ReplyToContent import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.session.content.ThumbnailExtractor +import im.vector.matrix.android.internal.util.StringProvider -internal class LocalEchoEventFactory(private val credentials: Credentials) { +internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) { fun createTextEvent(roomId: String, msgType: String, text: String): Event { val content = MessageTextContent(type = msgType, body = text) @@ -183,4 +184,80 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { private fun dummyEventId(roomId: String): String { return roomId + "-" + dummyOriginServerTs() } + + fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { + //Fallbacks and event representation + //TODO Add error/warning logs when any of this is null + val permalink = PermalinkFactory.createPermalink(eventReplied) ?: return null + val userId = eventReplied.sender ?: return null + val userLink = PermalinkFactory.createPermalink(userId) ?: return null +// +//
+// In reply to +// @alice:example.org +//
+// +//
+//
+// This is where the reply goes. + val body = bodyForReply(eventReplied.content.toModel()) + val replyFallbackTemplateFormatted = """ + +
+ ${stringProvider.getString(R.string.in_reply_to)} + %s +
+ %s +
+
+ %s + """.trim().format(permalink, userLink, userId, body.second ?: body.first, replyText) +// +// > <@alice:example.org> This is the original body +// +// This is where the reply goes + val lines = body.first.split("\n") + val plainTextBody = StringBuffer("><${userId}>") + lines.firstOrNull()?.also { plainTextBody.append(" $it") } + lines.forEachIndexed { index, s -> + if (index > 0) { + plainTextBody.append("\n>$s") + } + } + plainTextBody.append("\n\n").append(replyText) + + val eventId = eventReplied.eventId ?: return null + val content = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = plainTextBody.toString(), + formattedBody = replyFallbackTemplateFormatted, + relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) + ) + return createEvent(roomId, content) + } + + private fun bodyForReply(content: MessageContent?): Pair { + when (content?.type) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE -> { + //If we already have formatted body, return it? + var formattedText: String? = null + if (content is MessageTextContent) { + if (content.format == MessageType.FORMAT_MATRIX_HTML) { + formattedText = content.formattedBody + } + } + return content.body to formattedText + } + MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.sent_a_file) to null + MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.sent_an_audio_file) to null + MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.sent_an_image) to null + MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.sent_a_video) to null + else -> return (content?.body ?: "") to null + + } + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 864ef13e..73330770 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.network.executeRequest @@ -57,6 +58,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) localEvent.content ) } - return result.fold({ Result.retry() }, { Result.success() }) + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> Result.failure() + } + }, { Result.success() }) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt new file mode 100644 index 00000000..8b95a633 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 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.matrix.android.internal.util + +import android.content.res.Resources +import androidx.annotation.NonNull +import androidx.annotation.StringRes + +class StringProvider(private val resources: Resources) { + + /** + * Returns a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + * @return The string data associated with the resource, stripped of styled + * text information. + */ + @NonNull + fun getString(@StringRes resId: Int): String { + return resources.getString(resId) + } + + /** + * Returns a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for + * substitution. + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + */ + @NonNull + fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { + return resources.getString(resId, *formatArgs) + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values/strings_riotX.xml b/matrix-sdk-android/src/main/res/values/strings_riotX.xml new file mode 100644 index 00000000..2fea241b --- /dev/null +++ b/matrix-sdk-android/src/main/res/values/strings_riotX.xml @@ -0,0 +1,9 @@ + + + + In reply to + sent a file. + sent an image. + sent a video. + sent an audio file. + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 1474d2da..4ccbb0ae 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -60,7 +60,7 @@ class HomeModule { val timelineDateFormatter = TimelineDateFormatter(get()) val timelineMediaSizeProvider = TimelineMediaSizeProvider() val colorProvider = ColorProvider(fragment.requireContext()) - val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer) + val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer,get()) val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory, roomNameItemFactory = RoomNameItemFactory(get()), diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 190c37de..7835c2da 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -38,6 +38,7 @@ sealed class RoomDetailActions { data class EnterEditMode(val eventId: String) : RoomDetailActions() data class EnterQuoteMode(val eventId: String) : RoomDetailActions() + data class EnterReplyMode(val eventId: String) : RoomDetailActions() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 30f5746e..4b2a010d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -208,7 +208,8 @@ class RoomDetailFragment : composerLayout.collapse() } SendMode.EDIT, - SendMode.QUOTE -> { + SendMode.QUOTE, + SendMode.REPLY -> { commandAutocompletePolicy.enabled = false if (event == null) { //we should ignore? can this happen? @@ -233,9 +234,12 @@ class RoomDetailFragment : if (mode == SendMode.EDIT) { composerLayout.composerEditText.setText(eventTextBody) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit)) - } else { + } else if (mode == SendMode.QUOTE) { composerLayout.composerEditText.setText("") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote)) + } else if (mode == SendMode.REPLY) { + composerLayout.composerEditText.setText("") + composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply)) } AvatarRenderer.render(event.senderAvatar, event.root.sender @@ -673,13 +677,17 @@ class RoomDetailFragment : } } MessageMenuViewModel.ACTION_EDIT -> { - val eventId = actionData.data.toString() ?: return@let + val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) } MessageMenuViewModel.ACTION_QUOTE -> { - val eventId = actionData.data.toString() ?: return@let + val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId)) } + MessageMenuViewModel.ACTION_REPLY -> { + val eventId = actionData.data.toString() + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } else -> { Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index f5664efc..40bb323d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -96,6 +96,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action) is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) } } @@ -208,24 +209,34 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) } SendMode.QUOTE -> { - withState { state -> - val messageContent: MessageContent? = - state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() - ?: state.selectedEvent?.root?.content.toModel() - val textMsg = messageContent?.body + val messageContent: MessageContent? = + state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() + ?: state.selectedEvent?.root?.content.toModel() + val textMsg = messageContent?.body - val finalText = legacyRiotQuoteText(textMsg, action.text) + val finalText = legacyRiotQuoteText(textMsg, action.text) - //TODO Refactor this, just temporary for quotes - val parser = Parser.builder().build() - val document = parser.parse(finalText) - val renderer = HtmlRenderer.builder().build() - val htmlText = renderer.render(document) - if (TextUtils.equals(finalText, htmlText)) { - room.sendTextMessage(finalText) - } else { - room.sendFormattedTextMessage(finalText, htmlText) - } + //TODO Refactor this, just temporary for quotes + val parser = Parser.builder().build() + val document = parser.parse(finalText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (TextUtils.equals(finalText, htmlText)) { + room.sendTextMessage(finalText) + } else { + room.sendFormattedTextMessage(finalText, htmlText) + } + setState { + copy( + sendMode = SendMode.REGULAR, + selectedEvent = null + ) + } + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + SendMode.REPLY -> { + state.selectedEvent?.let { + room.replyToMessage(it.root, action.text) setState { copy( sendMode = SendMode.REGULAR, @@ -377,6 +388,17 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } } } + private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { + room.getTimeLineEvent(action.eventId)?.let { + setState { + copy( + sendMode = SendMode.REPLY, + selectedEvent = it + ) + } + } + } + private fun observeEventDisplayedActions() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index 00ce0b47..c0a83ad2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -36,7 +36,8 @@ import im.vector.matrix.android.api.session.user.model.User enum class SendMode { REGULAR, QUOTE, - EDIT + EDIT, + REPLY } data class RoomDetailViewState( diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index a35199a0..9198f8ee 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -21,8 +21,14 @@ import com.airbnb.mvrx.ViewModelContext import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.riotredesign.core.platform.VectorViewModel +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer import org.koin.android.ext.android.get +import ru.noties.markwon.Markwon +import ru.noties.markwon.html.HtmlPlugin import timber.log.Timber import java.text.SimpleDateFormat import java.util.* @@ -31,7 +37,7 @@ import java.util.* data class MessageActionState( val userId: String, val senderName: String, - val messageBody: String, + val messageBody: CharSequence, val ts: String?, val senderAvatarPath: String? = null) : MvRxState @@ -54,10 +60,19 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() ?: event.root.content.toModel() val originTs = event.root.originServerTs + var body: CharSequence = messageContent?.body ?: "" + if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + val parser = Parser.builder().build() + val document = parser.parse(messageContent.formattedBody ?: messageContent.body) + // val renderer = HtmlRenderer.builder().build() + body = Markwon.builder(viewModelContext.activity) + .usePlugin(HtmlPlugin.create()).build().render(document) +// body = renderer.render(document) + } MessageActionState( event.root.sender ?: "", parcel.informationData.memberName.toString(), - messageContent?.body ?: "", + body, dateFormat.format(Date(originTs ?: 0)), currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) ) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 8d32a7a4..d225b510 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -57,7 +57,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel + android:viewportHeight="13"> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 556bd50c..5f2aabe1 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -17,4 +17,7 @@ Event moderated by room admin Last edited by %s on %s + + Malformed event, cannot display + \ No newline at end of file