Create the DM when sending an event

This commit is contained in:
Florian Renaud 2022-05-20 18:22:19 +02:00
parent da70d520bc
commit 72896f1c8a
6 changed files with 319 additions and 17 deletions

1
changelog.d/5525.wip Normal file
View file

@ -0,0 +1 @@
Create DM room only on first message - Create the DM and navigate to the new room after sending an event

View file

@ -16,10 +16,12 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
@ -37,12 +39,18 @@ internal class DefaultSendEventTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: EncryptEventTask, private val encryptEventTask: EncryptEventTask,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask,
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver
) : SendEventTask { ) : SendEventTask {
override suspend fun execute(params: SendEventTask.Params): String { override suspend fun execute(params: SendEventTask.Params): String {
try { try {
if (RoomLocalEcho.isLocalEchoId(params.event.roomId.orEmpty())) {
// Room is local, so create a real one and send the event to this new room
return createRoomAndSendEvent(params)
}
// Make sure to load all members in the room before sending the event. // Make sure to load all members in the room before sending the event.
params.event.roomId params.event.roomId
?.takeIf { params.encrypt } ?.takeIf { params.encrypt }
@ -78,6 +86,12 @@ internal class DefaultSendEventTask @Inject constructor(
} }
} }
private suspend fun createRoomAndSendEvent(params: SendEventTask.Params): String {
val roomId = createRoomFromLocalRoomTask.execute(CreateRoomFromLocalRoomTask.Params(params.event.roomId.orEmpty()))
Timber.d("State event: convert local room (${params.event.roomId}) to existing room ($roomId) before sending the event.")
return execute(params.copy(event = params.event.copy(roomId = roomId)))
}
@Throws @Throws
private suspend fun handleEncryption(params: SendEventTask.Params): Event { private suspend fun handleEncryption(params: SendEventTask.Params): Event {
if (params.encrypt && !params.event.isEncrypted()) { if (params.encrypt && !params.event.isEncrypted()) {

View file

@ -44,8 +44,10 @@ import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask
import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
import org.matrix.android.sdk.internal.session.room.delete.DefaultDeleteLocalRoomTask import org.matrix.android.sdk.internal.session.room.delete.DefaultDeleteLocalRoomTask
import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask
@ -213,6 +215,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
@Binds
abstract fun bindCreateRoomFromLocalRoomTask(task: DefaultCreateRoomFromLocalRoomTask): CreateRoomFromLocalRoomTask
@Binds @Binds
abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask

View file

@ -0,0 +1,252 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.create
import android.util.Patterns
import androidx.core.net.toUri
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.extensions.ensurePrefix
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock
import java.util.UUID
import javax.inject.Inject
/**
* Create a Room from a "fake" local room.
* The configuration of the local room will be use to configure the new room.
* The potential local room members will also be invited to this new room.
*
* A "fake" local tombstone event will be created to indicate that the local room has been replacing by the new one.
*/
internal interface CreateRoomFromLocalRoomTask : Task<CreateRoomFromLocalRoomTask.Params, String> {
data class Params(val localRoomId: String)
}
internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor(
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask,
private val stateEventDataSource: StateEventDataSource,
private val clock: Clock,
) : CreateRoomFromLocalRoomTask {
override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String {
val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.NoCondition)
?.content?.toModel<RoomTombstoneContent>()
?.replacementRoomId
if (replacementRoomId != null) {
return replacementRoomId
}
val createRoomParams = getCreateRoomParams(params)
val roomId = createRoomTask.execute(createRoomParams)
createTombstoneEvent(params, roomId)
return roomId
}
/**
* Retrieve the room configuration by parsing the state events related to the local room.
*/
private suspend fun getCreateRoomParams(params: CreateRoomFromLocalRoomTask.Params): CreateRoomParams {
var createRoomParams = CreateRoomParams()
monarchy.awaitTransaction { realm ->
val stateEvents = CurrentStateEventEntity.whereRoomId(realm, params.localRoomId).findAll()
stateEvents.forEach { event ->
createRoomParams = when (event.type) {
EventType.STATE_ROOM_MEMBER -> handleRoomMemberEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> handleRoomHistoryVisibilityEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_ALIASES -> handleRoomAliasesEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_AVATAR -> handleRoomAvatarEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_CANONICAL_ALIAS -> handleRoomCanonicalAliasEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_GUEST_ACCESS -> handleRoomGuestAccessEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_ENCRYPTION -> handleRoomEncryptionEvent(createRoomParams)
EventType.STATE_ROOM_POWER_LEVELS -> handleRoomPowerRoomLevelsEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_NAME -> handleRoomNameEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_TOPIC -> handleRoomTopicEvent(realm, event, createRoomParams)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> handleRoomThirdPartyInviteEvent(event, createRoomParams)
EventType.STATE_ROOM_JOIN_RULES -> handleRoomJoinRulesEvent(realm, event, createRoomParams)
else -> createRoomParams
}
}
}
return createRoomParams
}
/**
* Create a Tombstone event to indicate that the local room has been replaced by a new one.
*/
private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) {
val now = clock.epochMillis()
val event = Event(
type = EventType.STATE_ROOM_TOMBSTONE,
senderId = userId,
originServerTs = now,
stateKey = "",
eventId = UUID.randomUUID().toString(),
content = RoomTombstoneContent(
replacementRoomId = roomId
).toContent()
)
monarchy.awaitTransaction { realm ->
val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
if (event.stateKey != null && event.type != null && event.eventId != null) {
CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply {
eventId = event.eventId
root = eventEntity
}
}
}
}
/* ==========================================================================================
* Local events handling
* ========================================================================================== */
private fun handleRoomMemberEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomMemberContent>(realm, event.eventId) ?: return@apply
invitedUserIds.add(event.stateKey)
if (content.isDirect) {
setDirectMessage()
}
}
private fun handleRoomHistoryVisibilityEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomHistoryVisibilityContent>(realm, event.eventId) ?: return@apply
historyVisibility = content.historyVisibility
}
private fun handleRoomAliasesEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomAliasesContent>(realm, event.eventId) ?: return@apply
roomAliasName = content.aliases.firstOrNull()?.substringAfter("#")?.substringBefore(":")
}
private fun handleRoomAvatarEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomAvatarContent>(realm, event.eventId) ?: return@apply
avatarUri = content.avatarUrl?.toUri()
}
private fun handleRoomCanonicalAliasEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomCanonicalAliasContent>(realm, event.eventId) ?: return@apply
roomAliasName = content.canonicalAlias?.substringAfter("#")?.substringBefore(":")
}
private fun handleRoomGuestAccessEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomGuestAccessContent>(realm, event.eventId) ?: return@apply
guestAccess = content.guestAccess
}
private fun handleRoomEncryptionEvent(params: CreateRoomParams): CreateRoomParams = params.apply {
// Having an encryption event means the room is encrypted, so just enable it again
enableEncryption()
}
private fun handleRoomPowerRoomLevelsEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<PowerLevelsContent>(realm, event.eventId) ?: return@apply
powerLevelContentOverride = content
}
private fun handleRoomNameEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomNameContent>(realm, event.eventId) ?: return@apply
name = content.name
}
private fun handleRoomTopicEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomTopicContent>(realm, event.eventId) ?: return@apply
topic = content.topic
}
private fun handleRoomThirdPartyInviteEvent(event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
when {
event.stateKey.isEmail() -> invite3pids.add(ThreePid.Email(event.stateKey))
event.stateKey.isMsisdn() -> invite3pids.add(ThreePid.Msisdn(event.stateKey))
}
}
private fun handleRoomJoinRulesEvent(realm: Realm, event: CurrentStateEventEntity, params: CreateRoomParams): CreateRoomParams = params.apply {
val content = getEventContent<RoomJoinRulesContent>(realm, event.eventId) ?: return@apply
preset = when {
// If preset has already been set for direct chat, keep it
preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
content.joinRules == RoomJoinRules.PUBLIC -> CreateRoomPreset.PRESET_PUBLIC_CHAT
content.joinRules == RoomJoinRules.INVITE -> CreateRoomPreset.PRESET_PRIVATE_CHAT
else -> null
}
}
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private inline fun <reified T> getEventContent(realm: Realm, eventId: String): T? {
return EventEntity.where(realm, eventId).findFirst()?.asDomain()?.getClearContent().toModel<T>()
}
/**
* Check if a CharSequence is an email.
*/
private fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
/**
* Check if a CharSequence is a phone number.
*/
private fun CharSequence.isMsisdn(): Boolean {
return try {
PhoneNumberUtil.getInstance().parse(ensurePrefix("+"), null)
true
} catch (e: NumberParseException) {
false
}
}
}

View file

@ -16,10 +16,12 @@
package org.matrix.android.sdk.internal.session.room.state package org.matrix.android.sdk.internal.session.room.state
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -35,11 +37,16 @@ internal interface SendStateTask : Task<SendStateTask.Params, String> {
internal class DefaultSendStateTask @Inject constructor( internal class DefaultSendStateTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver,
private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask,
) : SendStateTask { ) : SendStateTask {
override suspend fun execute(params: SendStateTask.Params): String { override suspend fun execute(params: SendStateTask.Params): String {
return executeRequest(globalErrorReceiver) { return executeRequest(globalErrorReceiver) {
if (RoomLocalEcho.isLocalEchoId(params.roomId)) {
// Room is local, so create a real one and send the event to this new room
createRoomAndSendEvent(params)
} else {
val response = if (params.stateKey.isEmpty()) { val response = if (params.stateKey.isEmpty()) {
roomAPI.sendStateEvent( roomAPI.sendStateEvent(
roomId = params.roomId, roomId = params.roomId,
@ -59,4 +66,11 @@ internal class DefaultSendStateTask @Inject constructor(
} }
} }
} }
}
private suspend fun createRoomAndSendEvent(params: SendStateTask.Params): String {
val roomId = createRoomFromLocalRoomTask.execute(CreateRoomFromLocalRoomTask.Params(params.roomId))
Timber.d("State event: convert local room (${params.roomId}) to existing room ($roomId) before sending the event.")
return execute(params.copy(roomId = roomId))
}
} }

View file

@ -82,6 +82,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
@ -1269,11 +1270,26 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also {
setState { copy(tombstoneEvent = it) } onRoomTombstoneUpdated(it)
} }
} }
} }
private var roomTombstoneHandled = false
private fun onRoomTombstoneUpdated(tombstoneEvent: Event) = withState { state ->
if (roomTombstoneHandled) return@withState
if (state.isLocalRoom()) {
// Local room has been replaced, so navigate to the new room
val roomId = tombstoneEvent.getClearContent()?.toModel<RoomTombstoneContent>()
?.replacementRoomId
?: return@withState
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true))
roomTombstoneHandled = true
} else {
setState { copy(tombstoneEvent = tombstoneEvent) }
}
}
/** /**
* Navigates to the appropriate event (by paginating the thread timeline until the event is found * Navigates to the appropriate event (by paginating the thread timeline until the event is found
* in the snapshot. The main reason for this function is to support the /relations api * in the snapshot. The main reason for this function is to support the /relations api