Merge pull request #243 from vector-im/feature/reply_e2e

Reply in e2e room
This commit is contained in:
Valere 2019-06-28 16:07:08 +02:00 committed by GitHub
commit 419ef7b46f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 104 deletions

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.HashMap


typealias Content = JsonDict typealias Content = JsonDict


@ -146,21 +147,27 @@ data class Event(
val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent) mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)


} if (mClearEvent != null) {
mClearEvent?.apply { mSenderCurve25519Key = decryptionResult.senderCurve25519Key
mSenderCurve25519Key = decryptionResult.senderCurve25519Key mClaimedEd25519Key = decryptionResult.claimedEd25519Key
mClaimedEd25519Key = decryptionResult.claimedEd25519Key mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
try { // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
// Add "m.relates_to" data from e2e event to the unencrypted event // in the clear event
// TODO try {
//if (getWireContent().getAsJsonObject().has("m.relates_to")) { content?.get("m.relates_to")?.let { clearRelates ->
// clearEvent!!.getContentAsJsonObject() mClearEvent = mClearEvent?.copy(
// .add("m.relates_to", getWireContent().getAsJsonObject().get("m.relates_to")) 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") )
}
} catch (e: Exception) {
Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
}
} }


} }
} }
mCryptoError = null mCryptoError = null

View File

@ -69,7 +69,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) 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( return DefaultRoom(
roomId, roomId,

View File

@ -21,6 +21,7 @@ import androidx.work.OneTimeWorkRequest
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials 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.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageType 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.model.RoomEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where 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.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker import im.vector.matrix.android.internal.session.room.send.RedactEventWorker
import im.vector.matrix.android.internal.session.room.send.SendEventWorker 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 credentials: Credentials,
private val roomId: String, private val roomId: String,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: RelationService { : RelationService {



override fun sendReaction(reaction: String, targetEventId: String): Cancelable { override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { .also {
@ -65,13 +67,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C
return CancelableWork(context, sendRelationWork.id) return CancelableWork(context, sendRelationWork.id)
} }



private fun createSendRelationWork(event: Event): OneTimeWorkRequest { private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) return createSendEventWork(event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)

return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)

} }


override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { 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 { val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val workRequest = createSendEventWork(event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)

//TODO use relation API?

val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id) return CancelableWork(context, workRequest.id)


} }



override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also { val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also {
saveLocalEcho(it) saveLocalEcho(it)
} ?: return null } ?: 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<SendEventWorker>(sendWorkData)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
} }


private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
// Same parameter
val params = EncryptEventWorker.Params(credentials.userId, roomId, event, keepKeys)
val sendWorkData = WorkerParamsFactory.toData(params)
return TimelineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData)
}

private fun createSendEventWork(event: Event): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
return workRequest
}


override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> { override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(

View File

@ -39,7 +39,9 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
internal data class Params( internal data class Params(
override val userId: String, override val userId: String,
val roomId: 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<String>? = null
) : SessionWorkerParams ) : SessionWorkerParams


@Inject lateinit var crypto: CryptoService @Inject lateinit var crypto: CryptoService
@ -65,8 +67,13 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
var result: MXEncryptEventContentResult? = null var result: MXEncryptEventContentResult? = null
var error: Throwable? = null var error: Throwable? = null


val localMutableContent = HashMap(localEvent.content)
params.keepKeys?.forEach {
localMutableContent.remove(it)
}

try { try {
crypto.encryptEventContent(localEvent.content!!, localEvent.type, params.roomId, object : MatrixCallback<MXEncryptEventContentResult> { crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, object : MatrixCallback<MXEncryptEventContentResult> {
override fun onSuccess(data: MXEncryptEventContentResult) { override fun onSuccess(data: MXEncryptEventContentResult) {
result = data result = data
latch.countDown() latch.countDown()
@ -83,15 +90,24 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
} }
latch.await() latch.await()


val safeResult = result if (result != null) {
if (safeResult != 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( val encryptedEvent = localEvent.copy(
type = safeResult.eventType, type = safeResult.eventType,
content = safeResult.eventContent content = safeResult.eventContent
) )
val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent) val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, encryptedEvent)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) return Result.success(WorkerParamsFactory.toData(nextWorkerParams))

} }

val safeError = error val safeError = error
val sendState = when (safeError) { val sendState = when (safeError) {
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES

View File

@ -100,7 +100,7 @@ object JsonCanonicalizer {


return result.toString() return result.toString()
} }
is String -> return "\"" + src.toString() + "\"" is String -> return JSONObject.quote(src)
else -> return src.toString() else -> return src.toString()
} }
} }

View File

@ -247,7 +247,7 @@ class RoomDetailFragment :
//TODO this is used at several places, find way to refactor? //TODO this is used at several places, find way to refactor?
val messageContent: MessageContent? = val messageContent: MessageContent? =
event.annotations?.editSummary?.aggregatedContent?.toModel() event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel() ?: event.root.getClearContent().toModel()
val nonFormattedBody = messageContent?.body ?: "" val nonFormattedBody = messageContent?.body ?: ""
var formattedBody: CharSequence? = null var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {

View File

@ -45,8 +45,10 @@ import javax.inject.Inject
*/ */
class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {


@Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory @Inject
@Inject lateinit var avatarRenderer: AvatarRenderer lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
@Inject
lateinit var avatarRenderer: AvatarRenderer
private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)


private lateinit var actionHandlerModel: ActionsHandler private lateinit var actionHandlerModel: ActionsHandler
@ -124,17 +126,18 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }


override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
if (it.showPreview) { val body = viewModel.resolveBody(it)
if (body != null) {
bottom_sheet_message_preview.isVisible = true bottom_sheet_message_preview.isVisible = true
senderNameTextView.text = it.senderName senderNameTextView.text = it.senderName()
messageBodyTextView.text = it.messageBody messageBodyTextView.text = body
messageTimestampText.text = it.ts messageTimestampText.text = it.time()
avatarRenderer.render(it.senderAvatarPath, it.userId, it.senderName, senderAvatarImageView) avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView)
} else { } else {
bottom_sheet_message_preview.isVisible = false bottom_sheet_message_preview.isVisible = false
} }
quickReactBottomDivider.isVisible = it.canReact quickReactBottomDivider.isVisible = it.canReact()
bottom_sheet_quick_reaction_container.isVisible = it.canReact bottom_sheet_quick_reaction_container.isVisible = it.canReact()
return@withState return@withState
} }



View File

@ -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.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent 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.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.core.platform.VectorViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
@ -35,21 +37,46 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*




val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())

data class MessageActionState( data class MessageActionState(
val roomId: String, val roomId: String,
val eventId: String, val eventId: String,
val informationData: MessageInformationData, val informationData: MessageInformationData,
val userId: String = "", val timelineEvent: TimelineEvent?
val senderName: String = "", ) : MvRxState {
val messageBody: CharSequence? = null,
val ts: String? = null,
val showPreview: Boolean = false,
val canReact: Boolean = false,
val senderAvatarPath: String? = null)
: 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 private val noticeEventFormatter: NoticeEventFormatter
) : VectorViewModel<MessageActionState>(initialState) { ) : VectorViewModel<MessageActionState>(initialState) {


private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val informationData = initialState.informationData

@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel fun create(initialState: MessageActionState): MessageActionsViewModel
@ -77,47 +100,23 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageActionViewModelFactory.create(state) return fragment.messageActionViewModelFactory.create(state)
} }
}



override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
init { val session = (viewModelContext.activity as HasScreenInjector).injector().session()
setState { reduceState(this) } val args: TimelineEventFragmentArgs = viewModelContext.args()
} val event = session.getRoom(args.roomId)?.getTimeLineEvent(args.eventId)

return MessageActionState(
private fun reduceState(state: MessageActionState): MessageActionState { args.roomId,
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) args.eventId,
val event = session.getRoom(roomId)?.getTimeLineEvent(eventId) ?: return state args.informationData,
var body: CharSequence? = null event
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)
}
} }
return state.copy(
userId = event.root.senderId ?: "", }
senderName = informationData.memberName?.toString() ?: "",
messageBody = body, fun resolveBody(state: MessageActionState): CharSequence? {
ts = dateFormat.format(Date(originTs ?: 0)), return state.messageBody(eventHtmlRenderer, noticeEventFormatter)
showPreview = body != null,
canReact = event.root.type == EventType.MESSAGE && event.sendState.isSent(),
senderAvatarPath = informationData.avatarUrl
)
} }


} }

View File

@ -33,11 +33,10 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.resources.StringProvider 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 im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import org.json.JSONObject 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) 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 event = session.getRoom(state.roomId)?.getTimeLineEvent(state.eventId) ?: return state


val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel() ?: event.root.getClearContent().toModel()
val type = messageContent?.type val type = messageContent?.type


val actions = if (!event.sendState.isSent()) { val actions = if (!event.sendState.isSent()) {