Merge pull request #7125 from vector-im/feature/fre/start_dm_loading

Deferred DM - Add a loading wheel while creating the DM
This commit is contained in:
Florian Renaud 2022-09-19 17:04:56 +02:00 committed by GitHub
commit b4ca16735d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 443 additions and 145 deletions

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

@ -0,0 +1 @@
Create DM room only on first message - Add a spinner when sending the first message

View file

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -46,6 +47,13 @@ class FlowRoom(private val room: Room) {
} }
} }
fun liveLocalRoomSummary(): Flow<Optional<LocalRoomSummary>> {
return room.getLocalRoomSummaryLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.localRoomSummary().toOptional()
}
}
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Flow<List<RoomMemberSummary>> { fun liveRoomMembers(queryParams: RoomMemberQueryParams): Flow<List<RoomMemberSummary>> {
return room.membershipService().getRoomMembersLive(queryParams).asFlow() return room.membershipService().getRoomMembersLive(queryParams).asFlow()
.startWith(room.coroutineDispatchers.io) { .startWith(room.coroutineDispatchers.io) {

View file

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
@ -60,11 +61,22 @@ interface Room {
*/ */
fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
/**
* A live [LocalRoomSummary] associated with the room.
* You can observe this summary to get dynamic data from this room.
*/
fun getLocalRoomSummaryLive(): LiveData<Optional<LocalRoomSummary>>
/** /**
* A current snapshot of [RoomSummary] associated with the room. * A current snapshot of [RoomSummary] associated with the room.
*/ */
fun roomSummary(): RoomSummary? fun roomSummary(): RoomSummary?
/**
* A current snapshot of [LocalRoomSummary] associated with the room.
*/
fun localRoomSummary(): LocalRoomSummary?
/** /**
* Use this room as a Space, if the type is correct. * Use this room as a Space, if the type is correct.
*/ */

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -117,6 +118,12 @@ interface RoomService {
*/ */
fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> fun getRoomSummaryLive(roomId: String): LiveData<Optional<RoomSummary>>
/**
* A live [LocalRoomSummary] associated with the room with id [roomId].
* You can observe this summary to get dynamic data from this room, even if the room is not joined yet
*/
fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>>
/** /**
* Get a snapshot list of room summaries. * Get a snapshot list of room summaries.
* @return the immutable list of [RoomSummary] * @return the immutable list of [RoomSummary]

View file

@ -0,0 +1,24 @@
/*
* 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.api.session.room.model
enum class LocalRoomCreationState {
NOT_CREATED,
CREATING,
FAILURE,
CREATED
}

View file

@ -0,0 +1,46 @@
/*
* 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.api.session.room.model
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
/**
* This class holds some data of a local room.
* It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
*/
data class LocalRoomSummary(
/**
* The roomId of the room.
*/
val roomId: String,
/**
* The room summary of the room.
*/
val roomSummary: RoomSummary?,
/**
* The creation params attached to the room.
*/
val createRoomParams: CreateRoomParams?,
/**
* The roomId of the created room (ie. created on the server), if any.
*/
val replacementRoomId: String?,
/**
* The creation state of the room.
*/
val creationState: LocalRoomCreationState,
)

View file

@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -61,7 +62,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 36L, schemaVersion = 37L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -107,5 +108,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 34) MigrateSessionTo034(realm).perform() if (oldVersion < 34) MigrateSessionTo034(realm).perform()
if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 35) MigrateSessionTo035(realm).perform()
if (oldVersion < 36) MigrateSessionTo036(realm).perform() if (oldVersion < 36) MigrateSessionTo036(realm).perform()
if (oldVersion < 37) MigrateSessionTo037(realm).perform()
} }
} }

View file

@ -0,0 +1,36 @@
/*
* 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.database.mapper
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import javax.inject.Inject
internal class LocalRoomSummaryMapper @Inject constructor(
private val roomSummaryMapper: RoomSummaryMapper,
) {
fun map(localRoomSummaryEntity: LocalRoomSummaryEntity): LocalRoomSummary {
return LocalRoomSummary(
roomId = localRoomSummaryEntity.roomId,
roomSummary = localRoomSummaryEntity.roomSummaryEntity?.let { roomSummaryMapper.map(it) },
createRoomParams = localRoomSummaryEntity.createRoomParams,
replacementRoomId = localRoomSummaryEntity.replacementRoomId,
creationState = localRoomSummaryEntity.creationState
)
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 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.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo037(realm: DynamicRealm) : RealmMigrator(realm, 37) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LocalRoomSummaryEntity")
?.addField(LocalRoomSummaryEntityFields.REPLACEMENT_ROOM_ID, String::class.java)
?.addField(LocalRoomSummaryEntityFields.STATE_STR, String::class.java)
?.transform { obj ->
obj.set(LocalRoomSummaryEntityFields.STATE_STR, LocalRoomCreationState.NOT_CREATED.name)
}
}
}

View file

@ -18,15 +18,24 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.toJSONString import org.matrix.android.sdk.api.session.room.model.create.toJSONString
internal open class LocalRoomSummaryEntity( internal open class LocalRoomSummaryEntity(
@PrimaryKey var roomId: String = "", @PrimaryKey var roomId: String = "",
var roomSummaryEntity: RoomSummaryEntity? = null, var roomSummaryEntity: RoomSummaryEntity? = null,
private var createRoomParamsStr: String? = null var replacementRoomId: String? = null,
) : RealmObject() { ) : RealmObject() {
private var stateStr: String = LocalRoomCreationState.NOT_CREATED.name
var creationState: LocalRoomCreationState
get() = LocalRoomCreationState.valueOf(stateStr)
set(value) {
stateStr = value.name
}
private var createRoomParamsStr: String? = null
var createRoomParams: CreateRoomParams? var createRoomParams: CreateRoomParams?
get() { get() {
return CreateRoomParams.fromJson(createRoomParamsStr) return CreateRoomParams.fromJson(createRoomParamsStr)

View file

@ -22,10 +22,6 @@ import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<LocalRoomSummaryEntity> { internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<LocalRoomSummaryEntity> {
val query = realm.where<LocalRoomSummaryEntity>() return realm.where<LocalRoomSummaryEntity>().equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
if (roomId != null) {
query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
}
return query
} }

View file

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.LocationSharingService
import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.model.relation.RelationService
@ -82,6 +83,14 @@ internal class DefaultRoom(
return roomSummaryDataSource.getRoomSummary(roomId) return roomSummaryDataSource.getRoomSummary(roomId)
} }
override fun getLocalRoomSummaryLive(): LiveData<Optional<LocalRoomSummary>> {
return roomSummaryDataSource.getLocalRoomSummaryLive(roomId)
}
override fun localRoomSummary(): LocalRoomSummary? {
return roomSummaryDataSource.getLocalRoomSummary(roomId)
}
override fun asSpace(): Space? { override fun asSpace(): Space? {
if (roomSummary()?.roomType != RoomType.SPACE) return null if (roomSummary()?.roomType != RoomType.SPACE) return null
return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder) return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder)

View file

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -106,6 +107,10 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getRoomSummaryLive(roomId) return roomSummaryDataSource.getRoomSummaryLive(roomId)
} }
override fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> {
return roomSummaryDataSource.getLocalRoomSummaryLive(roomId)
}
override fun getRoomSummaries( override fun getRoomSummaries(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
sortOrder: RoomSortOrder sortOrder: RoomSortOrder

View file

@ -17,38 +17,23 @@
package org.matrix.android.sdk.internal.session.room.create package org.matrix.android.sdk.internal.session.room.create
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.extensions.orFalse
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.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.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
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.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
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.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.task.Task 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 java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -56,94 +41,100 @@ import javax.inject.Inject
* Create a room on the server from a local room. * Create a room on the server from a local room.
* The configuration of the local room will be use to configure the new 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. * The potential local room members will also be invited to this new room.
*
* A 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> { internal interface CreateRoomFromLocalRoomTask : Task<CreateRoomFromLocalRoomTask.Params, String> {
data class Params(val localRoomId: String) data class Params(val localRoomId: String)
} }
internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor( internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor(
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val stateEventDataSource: StateEventDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val clock: Clock,
) : CreateRoomFromLocalRoomTask { ) : CreateRoomFromLocalRoomTask {
private val realmConfiguration private val realmConfiguration
get() = monarchy.realmConfiguration get() = monarchy.realmConfiguration
override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String { override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String {
val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty) val localRoomSummary = roomSummaryDataSource.getLocalRoomSummary(params.localRoomId)
?.content.toModel<RoomTombstoneContent>() ?: error("## CreateRoomFromLocalRoomTask - Cannot retrieve LocalRoomSummary with roomId ${params.localRoomId}")
?.replacementRoomId
if (replacementRoomId != null) { // If a room has already been created for the given local room, return the existing roomId
return replacementRoomId if (localRoomSummary.replacementRoomId != null) {
return localRoomSummary.replacementRoomId
} }
var createRoomParams: CreateRoomParams? = null if (localRoomSummary.createRoomParams != null && localRoomSummary.roomSummary != null) {
var isEncrypted = false return createRoom(params.localRoomId, localRoomSummary.roomSummary, localRoomSummary.createRoomParams)
monarchy.doWithRealm { realm -> } else {
realm.where<LocalRoomSummaryEntity>() error("## CreateRoomFromLocalRoomTask - Invalid LocalRoomSummary: $localRoomSummary")
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId)
.findFirst()
?.let {
createRoomParams = it.createRoomParams
isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse()
}
} }
val roomId = createRoomTask.execute(createRoomParams!!) }
/**
* Create a room on the server for the given local room.
*
* @param localRoomId the local room identifier.
* @param localRoomSummary the RoomSummary of the local room.
* @param createRoomParams the CreateRoomParams object which was used to configure the local room.
*
* @return the identifier of the created room.
*/
private suspend fun createRoom(localRoomId: String, localRoomSummary: RoomSummary, createRoomParams: CreateRoomParams): String {
updateCreationState(localRoomId, LocalRoomCreationState.CREATING)
val replacementRoomId = runCatching {
createRoomTask.execute(createRoomParams)
}.fold(
{ it },
{
updateCreationState(localRoomId, LocalRoomCreationState.FAILURE)
throw it
}
)
updateReplacementRoomId(localRoomId, replacementRoomId)
waitForRoomEvents(replacementRoomId, localRoomSummary)
updateCreationState(localRoomId, LocalRoomCreationState.CREATED)
return replacementRoomId
}
/**
* Wait for all the room events before triggering the created state.
*
* @param replacementRoomId the identifier of the created room
* @param localRoomSummary the RoomSummary of the local room.
*/
private suspend fun waitForRoomEvents(replacementRoomId: String, localRoomSummary: RoomSummary) {
try { try {
// Wait for all the room events before triggering the replacement room
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomSummaryEntity::class.java) realm.where(RoomSummaryEntity::class.java)
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) .equalTo(RoomSummaryEntityFields.ROOM_ID, replacementRoomId)
.equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0) .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, localRoomSummary.invitedMembersCount)
} }
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
EventEntity.whereRoomId(realm, roomId) EventEntity.whereRoomId(realm, replacementRoomId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY)
} }
if (isEncrypted) { if (localRoomSummary.isEncrypted) {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
EventEntity.whereRoomId(realm, roomId) EventEntity.whereRoomId(realm, replacementRoomId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION) .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
} }
} }
} catch (exception: TimeoutCancellationException) { } catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout(roomId) updateCreationState(localRoomSummary.roomId, LocalRoomCreationState.FAILURE)
throw CreateRoomFailure.CreatedWithTimeout(replacementRoomId)
} }
createTombstoneEvent(params, roomId)
return roomId
} }
/** private fun updateCreationState(roomId: String, creationState: LocalRoomCreationState) {
* Create a Tombstone event to indicate that the local room has been replaced by a new one. monarchy.runTransactionSync { realm ->
*/ LocalRoomSummaryEntity.where(realm, roomId).findFirst()?.creationState = creationState
private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) { }
val now = clock.epochMillis() }
val event = Event(
type = EventType.STATE_ROOM_TOMBSTONE, private fun updateReplacementRoomId(localRoomId: String, replacementRoomId: String) {
senderId = userId, monarchy.runTransactionSync { realm ->
originServerTs = now, LocalRoomSummaryEntity.where(realm, localRoomId).findFirst()?.replacementRoomId = replacementRoomId
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
}
}
} }
} }
} }

View file

@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.ResultBoundaries
import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
@ -43,7 +44,9 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.LocalRoomSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.findByAlias import org.matrix.android.sdk.internal.database.query.findByAlias
@ -57,6 +60,7 @@ import javax.inject.Inject
internal class RoomSummaryDataSource @Inject constructor( internal class RoomSummaryDataSource @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper, private val roomSummaryMapper: RoomSummaryMapper,
private val localRoomSummaryMapper: LocalRoomSummaryMapper,
private val queryStringValueProcessor: QueryStringValueProcessor, private val queryStringValueProcessor: QueryStringValueProcessor,
) { ) {
@ -95,6 +99,25 @@ internal class RoomSummaryDataSource @Inject constructor(
) )
} }
fun getLocalRoomSummary(roomId: String): LocalRoomSummary? {
return monarchy
.fetchCopyMap({
LocalRoomSummaryEntity.where(it, roomId).findFirst()
}, { entity, _ ->
localRoomSummaryMapper.map(entity)
})
}
fun getLocalRoomSummaryLive(roomId: String): LiveData<Optional<LocalRoomSummary>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> LocalRoomSummaryEntity.where(realm, roomId) },
{ localRoomSummaryMapper.map(it) }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
fun getRoomSummariesLive( fun getRoomSummariesLive(
queryParams: RoomSummaryQueryParams, queryParams: RoomSummaryQueryParams,
sortOrder: RoomSortOrder = RoomSortOrder.NONE sortOrder: RoomSortOrder = RoomSortOrder.NONE

View file

@ -22,21 +22,22 @@ import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkAll import io.mockk.unmockkAll
import io.mockk.verify
import io.mockk.verifyOrder
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
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.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity 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.EventEntity
@ -44,29 +45,24 @@ import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore 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.getOrCreate
import org.matrix.android.sdk.internal.util.time.DefaultClock
import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource import org.matrix.android.sdk.test.fakes.FakeRoomSummaryDataSource
private const val A_LOCAL_ROOM_ID = "local.a-local-room-id" private const val A_LOCAL_ROOM_ID = "local.a-local-room-id"
private const val AN_EXISTING_ROOM_ID = "an-existing-room-id" private const val AN_EXISTING_ROOM_ID = "an-existing-room-id"
private const val A_ROOM_ID = "a-room-id" private const val A_ROOM_ID = "a-room-id"
private const val MY_USER_ID = "my-user-id"
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
internal class DefaultCreateRoomFromLocalRoomTaskTest { internal class DefaultCreateRoomFromLocalRoomTaskTest {
private val fakeMonarchy = FakeMonarchy() private val fakeMonarchy = FakeMonarchy()
private val clock = DefaultClock()
private val createRoomTask = mockk<CreateRoomTask>() private val createRoomTask = mockk<CreateRoomTask>()
private val fakeStateEventDataSource = FakeStateEventDataSource() private val fakeRoomSummaryDataSource = FakeRoomSummaryDataSource()
private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask( private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask(
userId = MY_USER_ID,
monarchy = fakeMonarchy.instance, monarchy = fakeMonarchy.instance,
createRoomTask = createRoomTask, createRoomTask = createRoomTask,
stateEventDataSource = fakeStateEventDataSource.instance, roomSummaryDataSource = fakeRoomSummaryDataSource.instance,
clock = clock
) )
@Before @Before
@ -91,13 +87,12 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest {
@Test @Test
fun `given a local room id when execute then the existing room id is kept`() = runTest { fun `given a local room id when execute then the existing room id is kept`() = runTest {
// Given // Given
givenATombstoneEvent( val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true)
Event( givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aCreationState = LocalRoomCreationState.CREATED, aReplacementRoomId = AN_EXISTING_ROOM_ID)
roomId = A_LOCAL_ROOM_ID, val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(
type = EventType.STATE_ROOM_TOMBSTONE, aCreateRoomParams = aCreateRoomParams,
stateKey = "", aCreationState = LocalRoomCreationState.CREATED,
content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent() aReplacementRoomId = AN_EXISTING_ROOM_ID
)
) )
// When // When
@ -105,20 +100,18 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest {
val result = defaultCreateRoomFromLocalRoomTask.execute(params) val result = defaultCreateRoomFromLocalRoomTask.execute(params)
// Then // Then
verifyTombstoneEvent(AN_EXISTING_ROOM_ID) fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID)
result shouldBeEqualTo AN_EXISTING_ROOM_ID result shouldBeEqualTo AN_EXISTING_ROOM_ID
aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo AN_EXISTING_ROOM_ID
aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED
} }
@Test @Test
fun `given a local room id when execute then it is correctly executed`() = runTest { fun `given a local room id when execute then it is correctly executed`() = runTest {
// Given // Given
val aCreateRoomParams = mockk<CreateRoomParams>() val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true)
val aLocalRoomSummaryEntity = mockk<LocalRoomSummaryEntity> { givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
every { roomSummaryEntity } returns mockk(relaxed = true) val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
every { createRoomParams } returns aCreateRoomParams
}
givenATombstoneEvent(null)
givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity)
coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID
@ -127,32 +120,84 @@ internal class DefaultCreateRoomFromLocalRoomTaskTest {
val result = defaultCreateRoomFromLocalRoomTask.execute(params) val result = defaultCreateRoomFromLocalRoomTask.execute(params)
// Then // Then
verifyTombstoneEvent(null) fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID)
// CreateRoomTask has been called with the initial CreateRoomParams // CreateRoomTask has been called with the initial CreateRoomParams
coVerify { createRoomTask.execute(aCreateRoomParams) } coVerify { createRoomTask.execute(aCreateRoomParams) }
// The resulting roomId matches the roomId returned by the createRoomTask // The resulting roomId matches the roomId returned by the createRoomTask
result shouldBeEqualTo A_ROOM_ID result shouldBeEqualTo A_ROOM_ID
// A tombstone state event has been created // The room creation state has correctly been updated
coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) } verifyOrder {
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATED
}
// The local room summary has been updated with the created room id
verify { aLocalRoomSummaryEntity.replacementRoomId = A_ROOM_ID }
aLocalRoomSummaryEntity.replacementRoomId shouldBeEqualTo A_ROOM_ID
aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.CREATED
} }
private fun givenATombstoneEvent(event: Event?) { @Test
fakeStateEventDataSource.givenGetStateEventReturns(event) fun `given a local room id when execute with an exception then the creation state is correctly updated`() = runTest {
// Given
val aCreateRoomParams = mockk<CreateRoomParams>(relaxed = true)
givenALocalRoomSummary(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
val aLocalRoomSummaryEntity = givenALocalRoomSummaryEntity(aCreateRoomParams = aCreateRoomParams, aReplacementRoomId = null)
coEvery { createRoomTask.execute(any()) }.throws(mockk())
// When
val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
tryOrNull { defaultCreateRoomFromLocalRoomTask.execute(params) }
// Then
fakeRoomSummaryDataSource.verifyGetLocalRoomSummary(A_LOCAL_ROOM_ID)
// CreateRoomTask has been called with the initial CreateRoomParams
coVerify { createRoomTask.execute(aCreateRoomParams) }
// The room creation state has correctly been updated
verifyOrder {
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.CREATING
aLocalRoomSummaryEntity.creationState = LocalRoomCreationState.FAILURE
}
// The local room summary has been updated with the created room id
aLocalRoomSummaryEntity.replacementRoomId.shouldBeNull()
aLocalRoomSummaryEntity.creationState shouldBeEqualTo LocalRoomCreationState.FAILURE
} }
private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) { private fun givenALocalRoomSummary(
aCreateRoomParams: CreateRoomParams,
aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED,
aReplacementRoomId: String? = null
): LocalRoomSummary {
val aLocalRoomSummary = LocalRoomSummary(
roomId = A_LOCAL_ROOM_ID,
roomSummary = mockk(relaxed = true),
createRoomParams = aCreateRoomParams,
creationState = aCreationState,
replacementRoomId = aReplacementRoomId,
)
fakeRoomSummaryDataSource.givenGetLocalRoomSummaryReturns(A_LOCAL_ROOM_ID, aLocalRoomSummary)
return aLocalRoomSummary
}
private fun givenALocalRoomSummaryEntity(
aCreateRoomParams: CreateRoomParams,
aCreationState: LocalRoomCreationState = LocalRoomCreationState.NOT_CREATED,
aReplacementRoomId: String? = null
): LocalRoomSummaryEntity {
val aLocalRoomSummaryEntity = spyk(LocalRoomSummaryEntity(
roomId = A_LOCAL_ROOM_ID,
roomSummaryEntity = mockk(relaxed = true),
replacementRoomId = aReplacementRoomId,
).apply {
createRoomParams = aCreateRoomParams
creationState = aCreationState
})
every { every {
fakeMonarchy.fakeRealm.instance fakeMonarchy.fakeRealm.instance
.where<LocalRoomSummaryEntity>() .where<LocalRoomSummaryEntity>()
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID) .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID)
.findFirst() .findFirst()
} returns localRoomSummaryEntity } returns aLocalRoomSummaryEntity
} return aLocalRoomSummaryEntity
private fun verifyTombstoneEvent(expectedRoomId: String?) {
fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
?.content.toModel<RoomTombstoneContent>()
?.replacementRoomId shouldBeEqualTo expectedRoomId
} }
} }

View file

@ -47,6 +47,11 @@ internal class FakeMonarchy {
} coAnswers { } coAnswers {
firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance) firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance)
} }
coEvery {
instance.runTransactionSync(any())
} coAnswers {
firstArg<Realm.Transaction>().execute(fakeRealm.instance)
}
every { instance.realmConfiguration } returns mockk() every { instance.realmConfiguration } returns mockk()
} }

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 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.test.fakes
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
internal class FakeRoomSummaryDataSource {
val instance: RoomSummaryDataSource = mockk()
fun givenGetLocalRoomSummaryReturns(roomId: String?, localRoomSummary: LocalRoomSummary?) {
every { instance.getLocalRoomSummary(roomId = roomId ?: any()) } returns localRoomSummary
}
fun verifyGetLocalRoomSummary(roomId: String) {
verify { instance.getLocalRoomSummary(roomId) }
}
}

View file

@ -51,7 +51,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object OpenRoomProfile : RoomDetailViewEvents() object OpenRoomProfile : RoomDetailViewEvents()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents()
object ShowWaitingView : RoomDetailViewEvents() data class ShowWaitingView(val text: String? = null) : RoomDetailViewEvents()
object HideWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents()
data class DownloadFileState( data class DownloadFileState(

View file

@ -493,7 +493,7 @@ class TimelineFragment :
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference() RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() is RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView(it.text)
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)

View file

@ -83,7 +83,6 @@ 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
@ -100,9 +99,11 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.LocalRoomCreationState
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
@ -185,6 +186,7 @@ class TimelineViewModel @AssistedInject constructor(
init { init {
// This method will take care of a null room to update the state. // This method will take care of a null room to update the state.
observeRoomSummary() observeRoomSummary()
observeLocalRoomSummary()
if (room == null) { if (room == null) {
timeline = null timeline = null
} else { } else {
@ -617,7 +619,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView) _viewEvents.post(RoomDetailViewEvents.ShowWaitingView())
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo) val widget = jitsiService.createJitsiWidget(initialState.roomId, action.withVideo)
@ -637,7 +639,7 @@ class TimelineViewModel @AssistedInject constructor(
if (isJitsiWidget) { if (isJitsiWidget) {
setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) } setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) }
} else { } else {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView) _viewEvents.post(RoomDetailViewEvents.ShowWaitingView())
} }
session.widgetService().destroyRoomWidget(initialState.roomId, widgetId) session.widgetService().destroyRoomWidget(initialState.roomId, widgetId)
// local echo // local echo
@ -1231,6 +1233,28 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun observeLocalRoomSummary() {
if (room != null && RoomLocalEcho.isLocalEchoId(room.roomId)) {
room.flow().liveLocalRoomSummary()
.unwrap()
.map { it.creationState }
.distinctUntilChanged()
.onEach { creationState ->
when (creationState) {
LocalRoomCreationState.NOT_CREATED -> Unit
LocalRoomCreationState.CREATING ->
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView(stringProvider.getString(R.string.creating_direct_room)))
LocalRoomCreationState.FAILURE -> {
_viewEvents.post(RoomDetailViewEvents.HideWaitingView)
}
LocalRoomCreationState.CREATED ->
_viewEvents.post(RoomDetailViewEvents.OpenRoom(room.localRoomSummary()?.replacementRoomId!!, true))
}
}
.launchIn(viewModelScope)
}
}
private fun getUnreadState() { private fun getUnreadState() {
if (room == null) return if (room == null) return
combine( combine(
@ -1322,26 +1346,11 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also { room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also {
onRoomTombstoneUpdated(it) setState { copy(tombstoneEvent = 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