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 99479d87..a93b3e7d 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 @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.di.MoshiProvider import timber.log.Timber import java.util.* +import kotlin.collections.HashMap typealias Content = JsonDict @@ -146,21 +147,27 @@ data class Event( val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent) - } - mClearEvent?.apply { - mSenderCurve25519Key = decryptionResult.senderCurve25519Key - mClaimedEd25519Key = decryptionResult.claimedEd25519Key - mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain - try { - // Add "m.relates_to" data from e2e event to the unencrypted event - // TODO - //if (getWireContent().getAsJsonObject().has("m.relates_to")) { - // clearEvent!!.getContentAsJsonObject() - // .add("m.relates_to", getWireContent().getAsJsonObject().get("m.relates_to")) - //} - } catch (e: Exception) { - Timber.e(e, "Unable to restore 'm.relates_to' the clear event") + if (mClearEvent != null) { + mSenderCurve25519Key = decryptionResult.senderCurve25519Key + mClaimedEd25519Key = decryptionResult.claimedEd25519Key + mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain + + // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back + // in the clear event + try { + content?.get("m.relates_to")?.let { clearRelates -> + mClearEvent = mClearEvent?.copy( + content = HashMap(mClearEvent!!.content).apply { + this["m.relates_to"] = clearRelates + } + ) + } + } catch (e: Exception) { + Timber.e(e, "Unable to restore 'm.relates_to' the clear event") + } } + + } } mCryptoError = null 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 cd7be990..38935f19 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 @@ -69,7 +69,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) - val relationService = DefaultRelationService(context, credentials, roomId, eventFactory, findReactionEventForUndoTask, monarchy, taskExecutor) + val relationService = DefaultRelationService(context, credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, monarchy, taskExecutor) return DefaultRoom( roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 9b07bb28..cf0b3f1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -21,6 +21,7 @@ import androidx.work.OneTimeWorkRequest import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.message.MessageType @@ -33,6 +34,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE 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.EncryptEventWorker 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 @@ -49,12 +51,12 @@ internal class DefaultRelationService @Inject constructor(private val context: C private val credentials: Credentials, private val roomId: String, private val eventFactory: LocalEchoEventFactory, + private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor) : RelationService { - override fun sendReaction(reaction: String, targetEventId: String): Cancelable { val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) .also { @@ -65,13 +67,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C return CancelableWork(context, sendRelationWork.id) } - private fun createSendRelationWork(event: Event): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return TimelineSendEventWorkCommon.createWork(sendWorkData) - + return createSendEventWork(event) } override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { @@ -119,31 +116,44 @@ internal class DefaultRelationService @Inject constructor(private val context: C val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also { saveLocalEcho(it) } - val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - //TODO use relation API? - - val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + val workRequest = createSendEventWork(event) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) return CancelableWork(context, workRequest.id) } - override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also { saveLocalEcho(it) } ?: return null - val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event) + TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) + return CancelableWork(context, encryptWork.id) + + } else { + val workRequest = createSendEventWork(event) + TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) + return CancelableWork(context, workRequest.id) + } - val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) - TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - return CancelableWork(context, workRequest.id) } + private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(credentials.userId, roomId, event, keepKeys) + val sendWorkData = WorkerParamsFactory.toData(params) + return TimelineSendEventWorkCommon.createWork(sendWorkData) + } + + private fun createSendEventWork(event: Event): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + return workRequest + } override fun getEventSummaryLive(eventId: String): LiveData> { return monarchy.findAllMappedWithChanges( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 359eb181..9e68f6d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -39,7 +39,9 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) internal data class Params( override val userId: String, val roomId: String, - val event: Event + val event: Event, + /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + val keepKeys: List? = null ) : SessionWorkerParams @Inject lateinit var crypto: CryptoService @@ -65,8 +67,13 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) var result: MXEncryptEventContentResult? = null var error: Throwable? = null + val localMutableContent = HashMap(localEvent.content) + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + try { - crypto.encryptEventContent(localEvent.content!!, localEvent.type, params.roomId, object : MatrixCallback { + crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, object : MatrixCallback { override fun onSuccess(data: MXEncryptEventContentResult) { result = data latch.countDown() @@ -83,15 +90,24 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } latch.await() - val safeResult = result - if (safeResult != null) { + if (result != null) { + var modifiedContent = HashMap(result?.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + //put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result!!.copy(eventContent = modifiedContent) val encryptedEvent = localEvent.copy( type = safeResult.eventType, content = safeResult.eventContent ) val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) + } + val safeError = error val sendState = when (safeError) { is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/JsonCanonicalizer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/JsonCanonicalizer.kt index 9c8c78a3..f7fd3cf4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/JsonCanonicalizer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/JsonCanonicalizer.kt @@ -100,7 +100,7 @@ object JsonCanonicalizer { return result.toString() } - is String -> return "\"" + src.toString() + "\"" + is String -> return JSONObject.quote(src) else -> return src.toString() } } 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 f7a3b964..081c83a6 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 @@ -247,7 +247,7 @@ class RoomDetailFragment : //TODO this is used at several places, find way to refactor? val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.content.toModel() + ?: event.root.getClearContent().toModel() val nonFormattedBody = messageContent?.body ?: "" var formattedBody: CharSequence? = null if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 9ae2e4e5..391a680e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -45,8 +45,10 @@ import javax.inject.Inject */ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { - @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory - @Inject lateinit var avatarRenderer: AvatarRenderer + @Inject + lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory + @Inject + lateinit var avatarRenderer: AvatarRenderer private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) private lateinit var actionHandlerModel: ActionsHandler @@ -124,17 +126,18 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { } override fun invalidate() = withState(viewModel) { - if (it.showPreview) { + val body = viewModel.resolveBody(it) + if (body != null) { bottom_sheet_message_preview.isVisible = true - senderNameTextView.text = it.senderName - messageBodyTextView.text = it.messageBody - messageTimestampText.text = it.ts - avatarRenderer.render(it.senderAvatarPath, it.userId, it.senderName, senderAvatarImageView) + senderNameTextView.text = it.senderName() + messageBodyTextView.text = body + messageTimestampText.text = it.time() + avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView) } else { bottom_sheet_message_preview.isVisible = false } - quickReactBottomDivider.isVisible = it.canReact - bottom_sheet_quick_reaction_container.isVisible = it.canReact + quickReactBottomDivider.isVisible = it.canReact() + bottom_sheet_quick_reaction_container.isVisible = it.canReact() return@withState } 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 16dd5d1b..37e38a5f 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 @@ -27,6 +27,8 @@ 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.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.di.HasScreenInjector import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData @@ -35,21 +37,46 @@ import java.text.SimpleDateFormat import java.util.* +val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) + data class MessageActionState( val roomId: String, val eventId: String, val informationData: MessageInformationData, - val userId: String = "", - val senderName: String = "", - val messageBody: CharSequence? = null, - val ts: String? = null, - val showPreview: Boolean = false, - val canReact: Boolean = false, - val senderAvatarPath: String? = null) - : MvRxState { + val timelineEvent: TimelineEvent? +) : MvRxState { - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) + fun senderName(): String = informationData.memberName?.toString() ?: "" + fun time(): String? = timelineEvent?.root?.originServerTs?.let { dateFormat.format(Date(it)) } + ?: "" + + fun canReact(): Boolean = timelineEvent?.root?.type == EventType.MESSAGE && timelineEvent.sendState.isSent() + + fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? { + return when (timelineEvent?.root?.getClearType()) { + EventType.MESSAGE -> { + val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent?.toModel() + ?: timelineEvent.root.getClearContent().toModel() + if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + eventHtmlRenderer?.render(messageContent.formattedBody + ?: messageContent.body) + } else { + messageContent?.body + } + } + EventType.STATE_ROOM_NAME, + EventType.STATE_ROOM_TOPIC, + EventType.STATE_ROOM_MEMBER, + EventType.STATE_HISTORY_VISIBILITY, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER -> { + noticeEventFormatter?.format(timelineEvent) + } + else -> null + } + } } /** @@ -62,10 +89,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private val noticeEventFormatter: NoticeEventFormatter ) : VectorViewModel(initialState) { - private val roomId = initialState.roomId - private val eventId = initialState.eventId - private val informationData = initialState.informationData - @AssistedInject.Factory interface Factory { fun create(initialState: MessageActionState): MessageActionsViewModel @@ -77,47 +100,23 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() return fragment.messageActionViewModelFactory.create(state) } - } - - init { - setState { reduceState(this) } - } - - private fun reduceState(state: MessageActionState): MessageActionState { - val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) - val event = session.getRoom(roomId)?.getTimeLineEvent(eventId) ?: return state - var body: CharSequence? = null - val originTs = event.root.originServerTs - when (event.root.getClearType()) { - EventType.MESSAGE -> { - val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.getClearContent().toModel() - body = messageContent?.body - if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { - body = eventHtmlRenderer.render(messageContent.formattedBody - ?: messageContent.body) - } - } - EventType.STATE_ROOM_NAME, - EventType.STATE_ROOM_TOPIC, - EventType.STATE_ROOM_MEMBER, - EventType.STATE_HISTORY_VISIBILITY, - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> { - body = noticeEventFormatter.format(event) - } + override fun initialState(viewModelContext: ViewModelContext): MessageActionState? { + val session = (viewModelContext.activity as HasScreenInjector).injector().session() + val args: TimelineEventFragmentArgs = viewModelContext.args() + val event = session.getRoom(args.roomId)?.getTimeLineEvent(args.eventId) + return MessageActionState( + args.roomId, + args.eventId, + args.informationData, + event + ) } - return state.copy( - userId = event.root.senderId ?: "", - senderName = informationData.memberName?.toString() ?: "", - messageBody = body, - ts = dateFormat.format(Date(originTs ?: 0)), - showPreview = body != null, - canReact = event.root.type == EventType.MESSAGE && event.sendState.isSent(), - senderAvatarPath = informationData.avatarUrl - ) + + } + + fun resolveBody(state: MessageActionState): CharSequence? { + return state.messageBody(eventHtmlRenderer, noticeEventFormatter) } } \ No newline at end of file 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 30cc1d1e..c0f8baa9 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 @@ -33,11 +33,10 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.core.utils.isSingleEmoji import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import org.json.JSONObject -import im.vector.riotredesign.core.utils.isSingleEmoji - data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null) @@ -95,7 +94,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M val event = session.getRoom(state.roomId)?.getTimeLineEvent(state.eventId) ?: return state val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.content.toModel() + ?: event.root.getClearContent().toModel() val type = messageContent?.type val actions = if (!event.sendState.isSent()) {