Merge remote-tracking branch 'origin/develop' into feature/eric/new-layout-navigation
# Conflicts: # vector/src/main/java/im/vector/app/features/spaces/NewSpaceSummaryController.kt
This commit is contained in:
commit
95b37e2838
1
changelog.d/5525.wip
Normal file
1
changelog.d/5525.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
Create DM room only on first message - Create the DM and navigate to the new room after sending an event
|
1
changelog.d/6889.wip
Normal file
1
changelog.d/6889.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[App Layout] new room invites screen
|
1
changelog.d/6907.wip
Normal file
1
changelog.d/6907.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[New Layout] Changes space sheet to accordion-style with expandable subspaces
|
1
changelog.d/6926.misc
Normal file
1
changelog.d/6926.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Focus input field when editing homeserver address to speed up login and registration.
|
@ -104,6 +104,7 @@ ext.libs = [
|
|||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
|
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
|
||||||
'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
|
'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
|
||||||
|
'moshiAdapters' : "com.squareup.moshi:moshi-adapters:$moshi",
|
||||||
'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit",
|
'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit",
|
||||||
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"
|
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"
|
||||||
],
|
],
|
||||||
|
@ -163,6 +163,7 @@ dependencies {
|
|||||||
implementation 'com.squareup.okhttp3:logging-interceptor'
|
implementation 'com.squareup.okhttp3:logging-interceptor'
|
||||||
|
|
||||||
implementation libs.squareup.moshi
|
implementation libs.squareup.moshi
|
||||||
|
implementation libs.squareup.moshiAdapters
|
||||||
kapt libs.squareup.moshiKotlin
|
kapt libs.squareup.moshiKotlin
|
||||||
|
|
||||||
api "com.atlassian.commonmark:commonmark:0.13.0"
|
api "com.atlassian.commonmark:commonmark:0.13.0"
|
||||||
|
@ -70,6 +70,9 @@ object EventType {
|
|||||||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||||
const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
|
const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
|
||||||
|
|
||||||
|
// This type is for local purposes, it should never be processed by the server
|
||||||
|
const val LOCAL_STATE_ROOM_THIRD_PARTY_INVITE = "local.room.third_party_invite"
|
||||||
|
|
||||||
// Call Events
|
// Call Events
|
||||||
const val CALL_INVITE = "m.call.invite"
|
const val CALL_INVITE = "m.call.invite"
|
||||||
const val CALL_CANDIDATES = "m.call.candidates"
|
const val CALL_CANDIDATES = "m.call.candidates"
|
||||||
|
@ -18,10 +18,14 @@ package org.matrix.android.sdk.api.session.identity
|
|||||||
|
|
||||||
import com.google.i18n.phonenumbers.NumberParseException
|
import com.google.i18n.phonenumbers.NumberParseException
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier
|
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier
|
||||||
|
|
||||||
sealed class ThreePid(open val value: String) {
|
sealed class ThreePid(open val value: String) {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Email(val email: String) : ThreePid(email)
|
data class Email(val email: String) : ThreePid(email)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class Msisdn(val msisdn: String) : ThreePid(msisdn)
|
data class Msisdn(val msisdn: String) : ThreePid(msisdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,13 +17,16 @@
|
|||||||
package org.matrix.android.sdk.api.session.room.model.create
|
package org.matrix.android.sdk.api.session.room.model.create
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
open class CreateRoomParams {
|
open class CreateRoomParams {
|
||||||
/**
|
/**
|
||||||
* A public visibility indicates that the room will be shown in the published room list.
|
* A public visibility indicates that the room will be shown in the published room list.
|
||||||
@ -61,12 +64,12 @@ open class CreateRoomParams {
|
|||||||
* A list of user IDs to invite to the room.
|
* A list of user IDs to invite to the room.
|
||||||
* This will tell the server to invite everyone in the list to the newly created room.
|
* This will tell the server to invite everyone in the list to the newly created room.
|
||||||
*/
|
*/
|
||||||
val invitedUserIds = mutableListOf<String>()
|
var invitedUserIds = mutableListOf<String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of objects representing third party IDs to invite into the room.
|
* A list of objects representing third party IDs to invite into the room.
|
||||||
*/
|
*/
|
||||||
val invite3pids = mutableListOf<ThreePid>()
|
var invite3pids = mutableListOf<ThreePid>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial Guest Access.
|
* Initial Guest Access.
|
||||||
@ -99,14 +102,14 @@ open class CreateRoomParams {
|
|||||||
* The server will clobber the following keys: creator.
|
* The server will clobber the following keys: creator.
|
||||||
* Future versions of the specification may allow the server to clobber other keys.
|
* Future versions of the specification may allow the server to clobber other keys.
|
||||||
*/
|
*/
|
||||||
val creationContent = mutableMapOf<String, Any>()
|
var creationContent = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of state events to set in the new room. This allows the user to override the default state events
|
* A list of state events to set in the new room. This allows the user to override the default state events
|
||||||
* set in the new room. The expected format of the state events are an object with type, state_key and content keys set.
|
* set in the new room. The expected format of the state events are an object with type, state_key and content keys set.
|
||||||
* Takes precedence over events set by preset, but gets overridden by name and topic keys.
|
* Takes precedence over events set by preset, but gets overridden by name and topic keys.
|
||||||
*/
|
*/
|
||||||
val initialStates = mutableListOf<CreateRoomStateEvent>()
|
var initialStates = mutableListOf<CreateRoomStateEvent>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true to disable federation of this room.
|
* Set to true to disable federation of this room.
|
||||||
@ -151,7 +154,7 @@ open class CreateRoomParams {
|
|||||||
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM.
|
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM.
|
||||||
*/
|
*/
|
||||||
var algorithm: String? = null
|
var algorithm: String? = null
|
||||||
private set
|
internal set
|
||||||
|
|
||||||
var historyVisibility: RoomHistoryVisibility? = null
|
var historyVisibility: RoomHistoryVisibility? = null
|
||||||
|
|
||||||
@ -161,10 +164,18 @@ open class CreateRoomParams {
|
|||||||
|
|
||||||
var roomVersion: String? = null
|
var roomVersion: String? = null
|
||||||
|
|
||||||
var featurePreset: RoomFeaturePreset? = null
|
@Transient var featurePreset: RoomFeaturePreset? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
|
internal const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
|
||||||
private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
|
internal const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
|
||||||
|
|
||||||
|
fun fromJson(json: String?): CreateRoomParams? {
|
||||||
|
return json?.let { MoshiProvider.providesMoshi().adapter(CreateRoomParams::class.java).fromJson(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun CreateRoomParams.toJSONString(): String {
|
||||||
|
return MoshiProvider.providesMoshi().adapter(CreateRoomParams::class.java).toJson(this)
|
||||||
|
}
|
||||||
|
@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.create
|
package org.matrix.android.sdk.api.session.room.model.create
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class CreateRoomStateEvent(
|
data class CreateRoomStateEvent(
|
||||||
/**
|
/**
|
||||||
* Required. The type of event to send.
|
* Required. The type of event to send.
|
||||||
|
@ -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,17 @@ 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 (params.event.isLocalRoomEvent) {
|
||||||
|
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 +85,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()) {
|
||||||
@ -91,4 +104,7 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
return params.event
|
return params.event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val Event.isLocalRoomEvent
|
||||||
|
get() = RoomLocalEcho.isLocalEchoId(roomId.orEmpty())
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
|
|||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
|
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.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
|
||||||
@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
private val normalizer: Normalizer
|
private val normalizer: Normalizer
|
||||||
) : MatrixRealmMigration(
|
) : MatrixRealmMigration(
|
||||||
dbName = "Session",
|
dbName = "Session",
|
||||||
schemaVersion = 35L,
|
schemaVersion = 36L,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||||
@ -105,5 +106,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
|
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.database.model.LocalRoomSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||||
|
|
||||||
|
internal class MigrateSessionTo036(realm: DynamicRealm) : RealmMigrator(realm, 36) {
|
||||||
|
|
||||||
|
override fun doMigrate(realm: DynamicRealm) {
|
||||||
|
realm.schema.create("LocalRoomSummaryEntity")
|
||||||
|
.addField(LocalRoomSummaryEntityFields.ROOM_ID, String::class.java)
|
||||||
|
.addPrimaryKey(LocalRoomSummaryEntityFields.ROOM_ID)
|
||||||
|
.setRequired(LocalRoomSummaryEntityFields.ROOM_ID, true)
|
||||||
|
.addField(LocalRoomSummaryEntityFields.CREATE_ROOM_PARAMS_STR, String::class.java)
|
||||||
|
.addRealmObjectField(LocalRoomSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.PrimaryKey
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.create.toJSONString
|
||||||
|
|
||||||
|
internal open class LocalRoomSummaryEntity(
|
||||||
|
@PrimaryKey var roomId: String = "",
|
||||||
|
var roomSummaryEntity: RoomSummaryEntity? = null,
|
||||||
|
private var createRoomParamsStr: String? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
var createRoomParams: CreateRoomParams?
|
||||||
|
get() {
|
||||||
|
return CreateRoomParams.fromJson(createRoomParamsStr)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
createRoomParamsStr = value?.toJSONString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
|
|||||||
ReadReceiptEntity::class,
|
ReadReceiptEntity::class,
|
||||||
RoomEntity::class,
|
RoomEntity::class,
|
||||||
RoomSummaryEntity::class,
|
RoomSummaryEntity::class,
|
||||||
|
LocalRoomSummaryEntity::class,
|
||||||
RoomTagEntity::class,
|
RoomTagEntity::class,
|
||||||
SyncEntity::class,
|
SyncEntity::class,
|
||||||
PendingThreePidEntity::class,
|
PendingThreePidEntity::class,
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.query
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
|
||||||
|
|
||||||
|
internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<LocalRoomSummaryEntity> {
|
||||||
|
val query = realm.where<LocalRoomSummaryEntity>()
|
||||||
|
if (roomId != null) {
|
||||||
|
query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
@ -17,6 +17,8 @@
|
|||||||
package org.matrix.android.sdk.internal.di
|
package org.matrix.android.sdk.internal.di
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||||
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageDefaultContent
|
||||||
@ -60,6 +62,12 @@ internal object MoshiProvider {
|
|||||||
.registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE)
|
.registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE)
|
||||||
)
|
)
|
||||||
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
|
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
|
||||||
|
.add(
|
||||||
|
PolymorphicJsonAdapterFactory.of(ThreePid::class.java, "type")
|
||||||
|
.withSubtype(ThreePid.Email::class.java, "email")
|
||||||
|
.withSubtype(ThreePid.Msisdn::class.java, "msisdn")
|
||||||
|
.withDefaultValue(null)
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun providesMoshi(): Moshi {
|
fun providesMoshi(): Moshi {
|
||||||
|
@ -43,9 +43,13 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAli
|
|||||||
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
|
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.CreateLocalRoomStateEventsTask
|
||||||
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.DefaultCreateLocalRoomStateEventsTask
|
||||||
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 +217,12 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
|
abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindCreateLocalRoomStateEventsTask(task: DefaultCreateLocalRoomStateEventsTask): CreateLocalRoomStateEventsTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindCreateRoomFromLocalRoomTask(task: DefaultCreateRoomFromLocalRoomTask): CreateRoomFromLocalRoomTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
|
abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
|
||||||
|
|
||||||
|
@ -0,0 +1,299 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
|
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.LocalEcho
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
|
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.RoomHistoryVisibility
|
||||||
|
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.RoomThirdPartyInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.banOrDefault
|
||||||
|
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.create.RoomCreateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.inviteOrDefault
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.kickOrDefault
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.redactOrDefault
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
|
||||||
|
import org.matrix.android.sdk.api.session.user.UserService
|
||||||
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomStateEventsTask.Params
|
||||||
|
import org.matrix.android.sdk.internal.session.room.membership.threepid.toThreePid
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a list of local state events from the given [CreateRoomBody].
|
||||||
|
* The states events are generated according to the given configuration and following the matrix specification.
|
||||||
|
* This list reflects as much as possible a list of state events related to a real room configured and got from the server.
|
||||||
|
*
|
||||||
|
* Ref: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
|
||||||
|
*/
|
||||||
|
internal interface CreateLocalRoomStateEventsTask : Task<Params, List<Event>> {
|
||||||
|
data class Params(val createRoomBody: CreateRoomBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultCreateLocalRoomStateEventsTask @Inject constructor(
|
||||||
|
@UserId private val myUserId: String,
|
||||||
|
private val userService: UserService,
|
||||||
|
private val clock: Clock,
|
||||||
|
) : CreateLocalRoomStateEventsTask {
|
||||||
|
|
||||||
|
private lateinit var createRoomBody: CreateRoomBody
|
||||||
|
|
||||||
|
override suspend fun execute(params: Params): List<Event> {
|
||||||
|
createRoomBody = params.createRoomBody
|
||||||
|
|
||||||
|
// Build the list of the state events following the priorities from the matrix specification
|
||||||
|
// Changing the order of the events might break the correct display of the room on the client side
|
||||||
|
return buildList {
|
||||||
|
createRoomCreateEvent()
|
||||||
|
createRoomMemberEvents(listOf(myUserId))
|
||||||
|
createRoomPowerLevelsEvent()
|
||||||
|
createRoomAliasEvent()
|
||||||
|
createRoomPresetEvents()
|
||||||
|
createRoomInitialStateEvents()
|
||||||
|
createRoomNameAndTopicStateEvents()
|
||||||
|
createRoomMemberEvents(createRoomBody.invitedUserIds.orEmpty())
|
||||||
|
createRoomThreePidEvents()
|
||||||
|
createRoomDefaultEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the create state event related to this room.
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomCreateEvent() {
|
||||||
|
val roomCreateEvent = createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_CREATE,
|
||||||
|
content = RoomCreateContent(
|
||||||
|
creator = myUserId,
|
||||||
|
roomVersion = createRoomBody.roomVersion,
|
||||||
|
type = (createRoomBody.creationContent as? Map<*, *>)?.get(CreateRoomParams.CREATION_CONTENT_KEY_ROOM_TYPE) as? String
|
||||||
|
|
||||||
|
).toContent(),
|
||||||
|
)
|
||||||
|
add(roomCreateEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the create state event related to the power levels using the given overridden values or the default values according to the specification.
|
||||||
|
* Ref: https://spec.matrix.org/latest/client-server-api/#mroompower_levels
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomPowerLevelsEvent() {
|
||||||
|
val powerLevelsContent = createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_POWER_LEVELS,
|
||||||
|
content = (createRoomBody.powerLevelContentOverride ?: PowerLevelsContent()).let {
|
||||||
|
it.copy(
|
||||||
|
ban = it.banOrDefault(),
|
||||||
|
eventsDefault = it.eventsDefaultOrDefault(),
|
||||||
|
invite = it.inviteOrDefault(),
|
||||||
|
kick = it.kickOrDefault(),
|
||||||
|
redact = it.redactOrDefault(),
|
||||||
|
stateDefault = it.stateDefaultOrDefault(),
|
||||||
|
usersDefault = it.usersDefaultOrDefault(),
|
||||||
|
)
|
||||||
|
}.toContent(),
|
||||||
|
)
|
||||||
|
add(powerLevelsContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local room member state events related to the given user ids, if any.
|
||||||
|
*/
|
||||||
|
private suspend fun MutableList<Event>.createRoomMemberEvents(userIds: List<String>) {
|
||||||
|
val memberEvents = userIds
|
||||||
|
.mapNotNull { tryOrNull { userService.resolveUser(it) } }
|
||||||
|
.map { user ->
|
||||||
|
createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_MEMBER,
|
||||||
|
content = RoomMemberContent(
|
||||||
|
isDirect = createRoomBody.isDirect.takeUnless { user.userId == myUserId }.orFalse(),
|
||||||
|
membership = if (user.userId == myUserId) Membership.JOIN else Membership.INVITE,
|
||||||
|
displayName = user.displayName,
|
||||||
|
avatarUrl = user.avatarUrl
|
||||||
|
).toContent(),
|
||||||
|
stateKey = user.userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addAll(memberEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local state events related to the given third party invites, if any.
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomThreePidEvents() {
|
||||||
|
createRoomBody.invite3pids.orEmpty().forEach { body ->
|
||||||
|
val localThirdPartyInviteEvent = createLocalStateEvent(
|
||||||
|
type = EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
content = LocalRoomThirdPartyInviteContent(
|
||||||
|
isDirect = createRoomBody.isDirect.orFalse(),
|
||||||
|
membership = Membership.INVITE,
|
||||||
|
displayName = body.address,
|
||||||
|
thirdPartyInvite = body.toThreePid()
|
||||||
|
).toContent(),
|
||||||
|
)
|
||||||
|
val thirdPartyInviteEvent = createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
content = RoomThirdPartyInviteContent(
|
||||||
|
displayName = body.address,
|
||||||
|
keyValidityUrl = null,
|
||||||
|
publicKey = null,
|
||||||
|
publicKeys = null
|
||||||
|
).toContent(),
|
||||||
|
)
|
||||||
|
add(localThirdPartyInviteEvent)
|
||||||
|
add(thirdPartyInviteEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local state event related to the given alias, if any.
|
||||||
|
*/
|
||||||
|
fun MutableList<Event>.createRoomAliasEvent() {
|
||||||
|
if (createRoomBody.roomAliasName != null) {
|
||||||
|
val canonicalAliasContent = createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||||
|
content = RoomCanonicalAliasContent(
|
||||||
|
canonicalAlias = "${createRoomBody.roomAliasName}:${myUserId.getServerName()}"
|
||||||
|
).toContent(),
|
||||||
|
)
|
||||||
|
add(canonicalAliasContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local state events related to the given [CreateRoomPreset].
|
||||||
|
* Ref: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomPresetEvents() {
|
||||||
|
val preset = createRoomBody.preset ?: return
|
||||||
|
|
||||||
|
var joinRules: RoomJoinRules? = null
|
||||||
|
var historyVisibility: RoomHistoryVisibility? = null
|
||||||
|
var guestAccess: GuestAccess? = null
|
||||||
|
when (preset) {
|
||||||
|
CreateRoomPreset.PRESET_PRIVATE_CHAT,
|
||||||
|
CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> {
|
||||||
|
joinRules = RoomJoinRules.INVITE
|
||||||
|
historyVisibility = RoomHistoryVisibility.SHARED
|
||||||
|
guestAccess = GuestAccess.CanJoin
|
||||||
|
}
|
||||||
|
CreateRoomPreset.PRESET_PUBLIC_CHAT -> {
|
||||||
|
joinRules = RoomJoinRules.PUBLIC
|
||||||
|
historyVisibility = RoomHistoryVisibility.SHARED
|
||||||
|
guestAccess = GuestAccess.Forbidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(createLocalStateEvent(EventType.STATE_ROOM_JOIN_RULES, RoomJoinRulesContent(joinRules.value).toContent()))
|
||||||
|
add(createLocalStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, RoomHistoryVisibilityContent(historyVisibility.value).toContent()))
|
||||||
|
add(createLocalStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, RoomGuestAccessContent(guestAccess.value).toContent()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local state events related to the given initial states, if any.
|
||||||
|
* The given initial state events override the potential existing ones of the same type.
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomInitialStateEvents() {
|
||||||
|
val initialStates = createRoomBody.initialStates ?: return
|
||||||
|
|
||||||
|
val initialStateEvents = initialStates.map { createLocalStateEvent(it.type, it.content, it.stateKey) }
|
||||||
|
// Erase existing events of the same type
|
||||||
|
removeAll { event -> event.type in initialStateEvents.map { it.type } }
|
||||||
|
// Add the initial state events to the list
|
||||||
|
addAll(initialStateEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local events related to the given room name and topic, if any.
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomNameAndTopicStateEvents() {
|
||||||
|
if (createRoomBody.name != null) {
|
||||||
|
add(createLocalStateEvent(EventType.STATE_ROOM_NAME, RoomNameContent(createRoomBody.name).toContent()))
|
||||||
|
}
|
||||||
|
if (createRoomBody.topic != null) {
|
||||||
|
add(createLocalStateEvent(EventType.STATE_ROOM_TOPIC, RoomTopicContent(createRoomBody.topic).toContent()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the local events which have not been set and are in that case provided by the server with default values.
|
||||||
|
* Default events:
|
||||||
|
* - m.room.history_visibility (https://spec.matrix.org/latest/client-server-api/#server-behaviour-5)
|
||||||
|
* - m.room.guest_access (https://spec.matrix.org/latest/client-server-api/#mroomguest_access)
|
||||||
|
*/
|
||||||
|
private fun MutableList<Event>.createRoomDefaultEvents() {
|
||||||
|
// HistoryVisibility
|
||||||
|
if (none { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }) {
|
||||||
|
add(
|
||||||
|
createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
|
content = RoomHistoryVisibilityContent(RoomHistoryVisibility.SHARED.value).toContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// GuestAccess
|
||||||
|
if (none { it.type == EventType.STATE_ROOM_GUEST_ACCESS }) {
|
||||||
|
add(
|
||||||
|
createLocalStateEvent(
|
||||||
|
type = EventType.STATE_ROOM_GUEST_ACCESS,
|
||||||
|
content = RoomGuestAccessContent(GuestAccess.Forbidden.value).toContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a local state event from the given parameters.
|
||||||
|
*
|
||||||
|
* @param type the event type, see [EventType]
|
||||||
|
* @param content the content of the event
|
||||||
|
* @param stateKey the stateKey, if any
|
||||||
|
*
|
||||||
|
* @return a local state event
|
||||||
|
*/
|
||||||
|
private fun createLocalStateEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
|
||||||
|
return Event(
|
||||||
|
type = type,
|
||||||
|
senderId = myUserId,
|
||||||
|
stateKey = stateKey,
|
||||||
|
content = content,
|
||||||
|
originServerTs = clock.epochMillis(),
|
||||||
|
eventId = LocalEcho.createLocalEchoId()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -21,26 +21,15 @@ import io.realm.Realm
|
|||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
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.toContent
|
|
||||||
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.GuestAccess
|
|
||||||
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.RoomDirectoryVisibility
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
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.RoomCreateContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
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.api.session.sync.model.RoomSyncSummary
|
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
|
||||||
import org.matrix.android.sdk.api.session.user.UserService
|
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||||
import org.matrix.android.sdk.api.session.user.model.User
|
|
||||||
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
|
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
|
||||||
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
@ -48,6 +37,7 @@ import org.matrix.android.sdk.internal.database.mapper.toEntity
|
|||||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||||
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.EventInsertType
|
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.RoomEntity
|
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
|
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
@ -56,7 +46,6 @@ 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.database.query.getOrNull
|
import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||||
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.events.getFixedRoomMemberContent
|
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
|
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
|
||||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||||
@ -70,22 +59,22 @@ import javax.inject.Inject
|
|||||||
internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
|
internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
|
||||||
|
|
||||||
internal class DefaultCreateLocalRoomTask @Inject constructor(
|
internal class DefaultCreateLocalRoomTask @Inject constructor(
|
||||||
@UserId private val userId: String,
|
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
private val roomMemberEventHandler: RoomMemberEventHandler,
|
private val roomMemberEventHandler: RoomMemberEventHandler,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||||
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
|
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
|
||||||
private val userService: UserService,
|
private val cryptoService: DefaultCryptoService,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
private val createLocalRoomStateEventsTask: CreateLocalRoomStateEventsTask,
|
||||||
) : CreateLocalRoomTask {
|
) : CreateLocalRoomTask {
|
||||||
|
|
||||||
override suspend fun execute(params: CreateRoomParams): String {
|
override suspend fun execute(params: CreateRoomParams): String {
|
||||||
val createRoomBody = createRoomBodyBuilder.build(params.withDefault())
|
val createRoomBody = createRoomBodyBuilder.build(params)
|
||||||
val roomId = RoomLocalEcho.createLocalEchoId()
|
val roomId = RoomLocalEcho.createLocalEchoId()
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
createLocalRoomEntity(realm, roomId, createRoomBody)
|
createLocalRoomEntity(realm, roomId, createRoomBody)
|
||||||
createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
|
createLocalRoomSummaryEntity(realm, roomId, params, createRoomBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for room to be created in DB
|
// Wait for room to be created in DB
|
||||||
@ -114,14 +103,29 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
|
private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomParams: CreateRoomParams, createRoomBody: CreateRoomBody) {
|
||||||
|
// Create the room summary entity
|
||||||
|
val roomSummaryEntity = realm.createObject<RoomSummaryEntity>(roomId).apply {
|
||||||
val otherUserId = createRoomBody.getDirectUserId()
|
val otherUserId = createRoomBody.getDirectUserId()
|
||||||
if (otherUserId != null) {
|
if (otherUserId != null) {
|
||||||
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
|
||||||
isDirect = true
|
isDirect = true
|
||||||
directUserId = otherUserId
|
directUserId = otherUserId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the createRoomParams from the potential feature preset before saving
|
||||||
|
createRoomParams.featurePreset?.let { featurePreset ->
|
||||||
|
featurePreset.updateRoomParams(createRoomParams)
|
||||||
|
createRoomParams.initialStates.addAll(featurePreset.setupInitialStates().orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a LocalRoomSummaryEntity decorated by the related RoomSummaryEntity and the updated CreateRoomParams
|
||||||
|
realm.createObject<LocalRoomSummaryEntity>(roomId).also {
|
||||||
|
it.roomSummaryEntity = roomSummaryEntity
|
||||||
|
it.createRoomParams = createRoomParams
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the RoomSummaryEntity by simulating a fake sync response
|
||||||
roomSummaryUpdater.update(
|
roomSummaryUpdater.update(
|
||||||
realm = realm,
|
realm = realm,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
@ -150,7 +154,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
|
|||||||
isLastForward = true
|
isLastForward = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val eventList = createLocalRoomEvents(createRoomBody)
|
val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
|
||||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
|
||||||
for (event in eventList) {
|
for (event in eventList) {
|
||||||
@ -169,6 +173,9 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
|
|||||||
roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
|
roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
|
||||||
roomMemberEventHandler.handle(realm, roomId, event, false)
|
roomMemberEventHandler.handle(realm, roomId, event, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give info to crypto module
|
||||||
|
cryptoService.onStateEvent(roomId, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomMemberContentsByUser.getOrPut(event.senderId) {
|
roomMemberContentsByUser.getOrPut(event.senderId) {
|
||||||
@ -187,81 +194,4 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
|
|||||||
|
|
||||||
return chunkEntity
|
return chunkEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the list of the events related to the room creation params.
|
|
||||||
*
|
|
||||||
* @param createRoomBody the room creation params
|
|
||||||
*
|
|
||||||
* @return the list of events
|
|
||||||
*/
|
|
||||||
private suspend fun createLocalRoomEvents(createRoomBody: CreateRoomBody): List<Event> {
|
|
||||||
val myUser = userService.getUser(userId) ?: User(userId)
|
|
||||||
val invitedUsers = createRoomBody.invitedUserIds.orEmpty()
|
|
||||||
.mapNotNull { tryOrNull { userService.resolveUser(it) } }
|
|
||||||
|
|
||||||
val createRoomEvent = createLocalEvent(
|
|
||||||
type = EventType.STATE_ROOM_CREATE,
|
|
||||||
content = RoomCreateContent(
|
|
||||||
creator = userId
|
|
||||||
).toContent()
|
|
||||||
)
|
|
||||||
val myRoomMemberEvent = createLocalEvent(
|
|
||||||
type = EventType.STATE_ROOM_MEMBER,
|
|
||||||
content = RoomMemberContent(
|
|
||||||
membership = Membership.JOIN,
|
|
||||||
displayName = myUser.displayName,
|
|
||||||
avatarUrl = myUser.avatarUrl
|
|
||||||
).toContent(),
|
|
||||||
stateKey = userId
|
|
||||||
)
|
|
||||||
val roomMemberEvents = invitedUsers.map {
|
|
||||||
createLocalEvent(
|
|
||||||
type = EventType.STATE_ROOM_MEMBER,
|
|
||||||
content = RoomMemberContent(
|
|
||||||
isDirect = createRoomBody.isDirect.orFalse(),
|
|
||||||
membership = Membership.INVITE,
|
|
||||||
displayName = it.displayName,
|
|
||||||
avatarUrl = it.avatarUrl
|
|
||||||
).toContent(),
|
|
||||||
stateKey = it.userId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildList {
|
|
||||||
add(createRoomEvent)
|
|
||||||
add(myRoomMemberEvent)
|
|
||||||
addAll(createRoomBody.initialStates.orEmpty().map { createLocalEvent(it.type, it.content, it.stateKey) })
|
|
||||||
addAll(roomMemberEvents)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a local event from the given parameters.
|
|
||||||
*
|
|
||||||
* @param type the event type, see [EventType]
|
|
||||||
* @param content the content of the Event
|
|
||||||
* @param stateKey the stateKey, if any
|
|
||||||
*
|
|
||||||
* @return a fake event
|
|
||||||
*/
|
|
||||||
private fun createLocalEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
|
|
||||||
return Event(
|
|
||||||
type = type,
|
|
||||||
senderId = userId,
|
|
||||||
stateKey = stateKey,
|
|
||||||
content = content,
|
|
||||||
originServerTs = clock.epochMillis(),
|
|
||||||
eventId = LocalEcho.createLocalEchoId()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup default values to the CreateRoomParams as the room is created locally (the default values will not be defined by the server).
|
|
||||||
*/
|
|
||||||
private fun CreateRoomParams.withDefault() = this.apply {
|
|
||||||
if (visibility == null) visibility = RoomDirectoryVisibility.PRIVATE
|
|
||||||
if (historyVisibility == null) historyVisibility = RoomHistoryVisibility.SHARED
|
|
||||||
if (guestAccess == null) guestAccess = GuestAccess.Forbidden
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
|
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,7 +120,13 @@ internal data class CreateRoomBody(
|
|||||||
*/
|
*/
|
||||||
@Json(name = "room_version")
|
@Json(name = "room_version")
|
||||||
val roomVersion: String?
|
val roomVersion: String?
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromJson(json: String?): CreateRoomBody? {
|
||||||
|
return json?.let { MoshiProvider.providesMoshi().adapter(CreateRoomBody::class.java).fromJson(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if the created room can be a direct chat one.
|
* Tells if the created room can be a direct chat one.
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
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.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.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.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.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.LocalRoomSummaryEntityFields
|
||||||
|
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.query.copyToRealmOrIgnore
|
||||||
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
|
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 java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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> {
|
||||||
|
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 {
|
||||||
|
|
||||||
|
private val realmConfiguration
|
||||||
|
get() = monarchy.realmConfiguration
|
||||||
|
|
||||||
|
override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String {
|
||||||
|
val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
|
||||||
|
?.content.toModel<RoomTombstoneContent>()
|
||||||
|
?.replacementRoomId
|
||||||
|
|
||||||
|
if (replacementRoomId != null) {
|
||||||
|
return replacementRoomId
|
||||||
|
}
|
||||||
|
|
||||||
|
var createRoomParams: CreateRoomParams? = null
|
||||||
|
var isEncrypted = false
|
||||||
|
monarchy.doWithRealm { realm ->
|
||||||
|
realm.where<LocalRoomSummaryEntity>()
|
||||||
|
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId)
|
||||||
|
.findFirst()
|
||||||
|
?.let {
|
||||||
|
createRoomParams = it.createRoomParams
|
||||||
|
isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val roomId = createRoomTask.execute(createRoomParams!!)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for all the room events before triggering the replacement room
|
||||||
|
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
|
||||||
|
realm.where(RoomSummaryEntity::class.java)
|
||||||
|
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0)
|
||||||
|
}
|
||||||
|
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
|
||||||
|
EventEntity.whereRoomId(realm, roomId)
|
||||||
|
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY)
|
||||||
|
}
|
||||||
|
if (isEncrypted) {
|
||||||
|
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
|
||||||
|
EventEntity.whereRoomId(realm, roomId)
|
||||||
|
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception: TimeoutCancellationException) {
|
||||||
|
throw CreateRoomFailure.CreatedWithTimeout(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
createTombstoneEvent(params, roomId)
|
||||||
|
return roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,8 +54,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||||||
private val directChatsHelper: DirectChatsHelper,
|
private val directChatsHelper: DirectChatsHelper,
|
||||||
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
||||||
private val readMarkersTask: SetReadMarkersTask,
|
private val readMarkersTask: SetReadMarkersTask,
|
||||||
@SessionDatabase
|
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||||
private val realmConfiguration: RealmConfiguration,
|
|
||||||
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
|
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
@ -71,7 +70,6 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val createRoomBody = createRoomBodyBuilder.build(params)
|
val createRoomBody = createRoomBodyBuilder.build(params)
|
||||||
|
|
||||||
val createRoomResponse = try {
|
val createRoomResponse = try {
|
||||||
executeRequest(globalErrorReceiver) {
|
executeRequest(globalErrorReceiver) {
|
||||||
roomAPI.createRoom(createRoomBody)
|
roomAPI.createRoom(createRoomBody)
|
||||||
@ -90,6 +88,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomId = createRoomResponse.roomId
|
val roomId = createRoomResponse.roomId
|
||||||
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
|
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
|
||||||
try {
|
try {
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing the EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE state event content
|
||||||
|
* This class is only used to store the third party invite data of a local room.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class LocalRoomThirdPartyInviteContent(
|
||||||
|
@Json(name = "membership") val membership: Membership,
|
||||||
|
@Json(name = "displayname") val displayName: String? = null,
|
||||||
|
@Json(name = "is_direct") val isDirect: Boolean = false,
|
||||||
|
@Json(name = "third_party_invite") val thirdPartyInvite: ThreePid? = null,
|
||||||
|
)
|
@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
|||||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||||
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
|
||||||
|
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
@ -70,6 +71,9 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
|
|||||||
RoomEntity.where(realm, roomId = roomId).findAll()
|
RoomEntity.where(realm, roomId = roomId).findAll()
|
||||||
?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") }
|
?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") }
|
||||||
?.deleteAllFromRealm()
|
?.deleteAllFromRealm()
|
||||||
|
LocalRoomSummaryEntity.where(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - LocalRoomSummaryEntity - delete ${it.size} entries") }
|
||||||
|
?.deleteAllFromRealm()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room")
|
Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room")
|
||||||
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.membership.threepid
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
|
import org.matrix.android.sdk.internal.auth.data.ThreePidMedium
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class ThreePidInviteBody(
|
internal data class ThreePidInviteBody(
|
||||||
@ -43,3 +45,9 @@ internal data class ThreePidInviteBody(
|
|||||||
@Json(name = "address")
|
@Json(name = "address")
|
||||||
val address: String
|
val address: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun ThreePidInviteBody.toThreePid() = when (medium) {
|
||||||
|
ThreePidMedium.EMAIL -> ThreePid.Email(address)
|
||||||
|
ThreePidMedium.MSISDN -> ThreePid.Msisdn(address)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,462 @@
|
|||||||
|
/*
|
||||||
|
* 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.session.room.create
|
||||||
|
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldNotBeNull
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
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.content.EncryptionEventContent
|
||||||
|
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.GuestAccess
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
|
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.RoomHistoryVisibility
|
||||||
|
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.RoomThirdPartyInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.powerlevels.Role
|
||||||
|
import org.matrix.android.sdk.api.session.user.UserService
|
||||||
|
import org.matrix.android.sdk.api.session.user.model.User
|
||||||
|
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_EMAIL
|
||||||
|
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_MSISDN
|
||||||
|
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||||
|
import org.matrix.android.sdk.internal.session.room.membership.threepid.toThreePid
|
||||||
|
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
||||||
|
|
||||||
|
private const val MY_USER_ID = "my-user-id"
|
||||||
|
private const val MY_USER_DISPLAY_NAME = "my-user-display-name"
|
||||||
|
private const val MY_USER_AVATAR = "my-user-avatar"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
internal class DefaultCreateLocalRoomStateEventsTaskTest {
|
||||||
|
|
||||||
|
private val clock = DefaultClock()
|
||||||
|
private val userService = mockk<UserService>()
|
||||||
|
|
||||||
|
private val defaultCreateLocalRoomStateEventsTask = DefaultCreateLocalRoomStateEventsTask(
|
||||||
|
myUserId = MY_USER_ID,
|
||||||
|
userService = userService,
|
||||||
|
clock = clock
|
||||||
|
)
|
||||||
|
|
||||||
|
lateinit var createRoomBody: CreateRoomBody
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
createRoomBody = mockk {
|
||||||
|
every { roomVersion } returns null
|
||||||
|
every { creationContent } returns null
|
||||||
|
every { roomAliasName } returns null
|
||||||
|
every { topic } returns null
|
||||||
|
every { name } returns null
|
||||||
|
every { powerLevelContentOverride } returns null
|
||||||
|
every { initialStates } returns null
|
||||||
|
every { invite3pids } returns null
|
||||||
|
every { preset } returns null
|
||||||
|
every { isDirect } returns null
|
||||||
|
every { invitedUserIds } returns null
|
||||||
|
}
|
||||||
|
coEvery { userService.resolveUser(any()) } answers { User(firstArg()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct room create state event`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aRoomVersion = "a_room_version"
|
||||||
|
|
||||||
|
every { createRoomBody.roomVersion } returns aRoomVersion
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val roomCreateEvent = result.find { it.type == EventType.STATE_ROOM_CREATE }
|
||||||
|
val roomCreateContent = roomCreateEvent?.content.toModel<RoomCreateContent>()
|
||||||
|
|
||||||
|
roomCreateContent?.creator shouldBeEqualTo MY_USER_ID
|
||||||
|
roomCreateContent?.roomVersion shouldBeEqualTo aRoomVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct name and topic state events`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aRoomName = "a_room_name"
|
||||||
|
val aRoomTopic = "a_room_topic"
|
||||||
|
|
||||||
|
every { createRoomBody.name } returns aRoomName
|
||||||
|
every { createRoomBody.topic } returns aRoomTopic
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val roomNameEvent = result.find { it.type == EventType.STATE_ROOM_NAME }
|
||||||
|
val roomTopicEvent = result.find { it.type == EventType.STATE_ROOM_TOPIC }
|
||||||
|
|
||||||
|
roomNameEvent?.content.toModel<RoomNameContent>()?.name shouldBeEqualTo aRoomName
|
||||||
|
roomTopicEvent?.content.toModel<RoomTopicContent>()?.topic shouldBeEqualTo aRoomTopic
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct room member events`() = runTest {
|
||||||
|
// Given
|
||||||
|
data class RoomMember(val user: User, val membership: Membership)
|
||||||
|
|
||||||
|
val aRoomMemberList: List<RoomMember> = listOf(
|
||||||
|
RoomMember(User(MY_USER_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR), Membership.JOIN),
|
||||||
|
RoomMember(User("userA_id", "userA_display_name", "userA_avatar"), Membership.INVITE),
|
||||||
|
RoomMember(User("userB_id", "userB_display_name", "userB_avatar"), Membership.INVITE)
|
||||||
|
)
|
||||||
|
|
||||||
|
every { createRoomBody.invitedUserIds } returns aRoomMemberList.filter { it.membership == Membership.INVITE }.map { it.user.userId }
|
||||||
|
coEvery { userService.resolveUser(any()) } answers {
|
||||||
|
aRoomMemberList.map { it.user }.find { it.userId == firstArg() } ?: User(firstArg())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val roomMemberEvents = result.filter { it.type == EventType.STATE_ROOM_MEMBER }
|
||||||
|
|
||||||
|
roomMemberEvents.map { it.stateKey } shouldBeEqualTo aRoomMemberList.map { it.user.userId }
|
||||||
|
roomMemberEvents.forEach { event ->
|
||||||
|
val roomMemberContent = event.content.toModel<RoomMemberContent>()
|
||||||
|
val roomMember = aRoomMemberList.find { it.user.userId == event.stateKey }
|
||||||
|
|
||||||
|
roomMember.shouldNotBeNull()
|
||||||
|
roomMemberContent?.avatarUrl shouldBeEqualTo roomMember.user.avatarUrl
|
||||||
|
roomMemberContent?.displayName shouldBeEqualTo roomMember.user.displayName
|
||||||
|
roomMemberContent?.membership shouldBeEqualTo roomMember.membership
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct power levels event`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aPowerLevelsContent = PowerLevelsContent(
|
||||||
|
ban = 1,
|
||||||
|
kick = 2,
|
||||||
|
invite = 3,
|
||||||
|
redact = 4,
|
||||||
|
eventsDefault = 5,
|
||||||
|
events = null,
|
||||||
|
usersDefault = 6,
|
||||||
|
users = null,
|
||||||
|
stateDefault = 7,
|
||||||
|
notifications = null
|
||||||
|
)
|
||||||
|
|
||||||
|
every { createRoomBody.powerLevelContentOverride } returns aPowerLevelsContent
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val roomPowerLevelsEvent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }
|
||||||
|
roomPowerLevelsEvent?.content.toModel<PowerLevelsContent>() shouldBeEqualTo aPowerLevelsContent
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct canonical alias event`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aRoomAlias = "a_room_alias"
|
||||||
|
val expectedCanonicalAlias = "$aRoomAlias:${MY_USER_ID.getServerName()}"
|
||||||
|
|
||||||
|
every { createRoomBody.roomAliasName } returns aRoomAlias
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val roomPowerLevelsEvent = result.find { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
|
||||||
|
roomPowerLevelsEvent?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias shouldBeEqualTo expectedCanonicalAlias
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct preset related events`() = runTest {
|
||||||
|
data class ExpectedResult(val joinRules: RoomJoinRules, val historyVisibility: RoomHistoryVisibility, val guestAccess: GuestAccess)
|
||||||
|
data class Case(val preset: CreateRoomPreset, val expectedResult: ExpectedResult)
|
||||||
|
|
||||||
|
CreateRoomPreset.values().forEach { aRoomPreset ->
|
||||||
|
// Given
|
||||||
|
val case = when (aRoomPreset) {
|
||||||
|
CreateRoomPreset.PRESET_PRIVATE_CHAT -> Case(
|
||||||
|
CreateRoomPreset.PRESET_PRIVATE_CHAT,
|
||||||
|
ExpectedResult(RoomJoinRules.INVITE, RoomHistoryVisibility.SHARED, GuestAccess.CanJoin)
|
||||||
|
)
|
||||||
|
CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> Case(
|
||||||
|
CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
|
||||||
|
ExpectedResult(RoomJoinRules.INVITE, RoomHistoryVisibility.SHARED, GuestAccess.CanJoin)
|
||||||
|
)
|
||||||
|
CreateRoomPreset.PRESET_PUBLIC_CHAT -> Case(
|
||||||
|
CreateRoomPreset.PRESET_PUBLIC_CHAT,
|
||||||
|
ExpectedResult(RoomJoinRules.PUBLIC, RoomHistoryVisibility.SHARED, GuestAccess.Forbidden)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
every { createRoomBody.preset } returns case.preset
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_JOIN_RULES }
|
||||||
|
?.content.toModel<RoomJoinRulesContent>()
|
||||||
|
?.joinRules shouldBeEqualTo case.expectedResult.joinRules
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||||
|
?.content.toModel<RoomHistoryVisibilityContent>()
|
||||||
|
?.historyVisibility shouldBeEqualTo case.expectedResult.historyVisibility
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
|
||||||
|
?.content.toModel<RoomGuestAccessContent>()
|
||||||
|
?.guestAccess shouldBeEqualTo case.expectedResult.guestAccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the initial state events`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aListOfInitialStateEvents = listOf(
|
||||||
|
Event(
|
||||||
|
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||||
|
stateKey = "",
|
||||||
|
content = EncryptionEventContent(MXCRYPTO_ALGORITHM_MEGOLM).toContent()
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
type = "a_custom_type",
|
||||||
|
content = mapOf("a_custom_map_to_integer" to 42),
|
||||||
|
stateKey = "a_state_key"
|
||||||
|
),
|
||||||
|
Event(
|
||||||
|
type = "another_custom_type",
|
||||||
|
content = mapOf("a_custom_map_to_boolean" to false),
|
||||||
|
stateKey = "another_state_key"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
every { createRoomBody.initialStates } returns aListOfInitialStateEvents
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
aListOfInitialStateEvents.forEach { expected ->
|
||||||
|
val found = result.find { it.type == expected.type }
|
||||||
|
found.shouldNotBeNull()
|
||||||
|
found.content shouldBeEqualTo expected.content
|
||||||
|
found.stateKey shouldBeEqualTo expected.stateKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct third party invite events`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aListOfThreePids = listOf(
|
||||||
|
ThreePid.Email("bob@matrix.org"),
|
||||||
|
ThreePid.Msisdn("+11111111111"),
|
||||||
|
ThreePid.Email("alice@matrix.org"),
|
||||||
|
ThreePid.Msisdn("+22222222222"),
|
||||||
|
)
|
||||||
|
val aListOf3pids = aListOfThreePids.mapIndexed { index, threePid ->
|
||||||
|
ThreePidInviteBody(
|
||||||
|
idServer = "an_id_server_$index",
|
||||||
|
idAccessToken = "an_id_access_token_$index",
|
||||||
|
medium = when (threePid) {
|
||||||
|
is ThreePid.Email -> MEDIUM_EMAIL
|
||||||
|
is ThreePid.Msisdn -> MEDIUM_MSISDN
|
||||||
|
},
|
||||||
|
address = threePid.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
every { createRoomBody.invite3pids } returns aListOf3pids
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val thirdPartyInviteEvents = result.filter { it.type == EventType.STATE_ROOM_THIRD_PARTY_INVITE }
|
||||||
|
val thirdPartyInviteContents = thirdPartyInviteEvents.map { it.content.toModel<RoomThirdPartyInviteContent>() }
|
||||||
|
val localThirdPartyInviteEvents = result.filter { it.type == EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE }
|
||||||
|
val localThirdPartyInviteContents = localThirdPartyInviteEvents.map { it.content.toModel<LocalRoomThirdPartyInviteContent>() }
|
||||||
|
|
||||||
|
thirdPartyInviteEvents.size shouldBeEqualTo aListOf3pids.size
|
||||||
|
localThirdPartyInviteEvents.size shouldBeEqualTo aListOf3pids.size
|
||||||
|
|
||||||
|
aListOf3pids.forEach { expected ->
|
||||||
|
thirdPartyInviteContents.find { it?.displayName == expected.address }.shouldNotBeNull()
|
||||||
|
|
||||||
|
val localThirdPartyInviteContent = localThirdPartyInviteContents.find { it?.thirdPartyInvite == expected.toThreePid() }
|
||||||
|
localThirdPartyInviteContent.shouldNotBeNull()
|
||||||
|
localThirdPartyInviteContent.membership shouldBeEqualTo Membership.INVITE
|
||||||
|
localThirdPartyInviteContent.isDirect shouldBeEqualTo createRoomBody.isDirect.orFalse()
|
||||||
|
localThirdPartyInviteContent.displayName shouldBeEqualTo expected.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody with default values when execute then the resulting list of events is correct`() = runTest {
|
||||||
|
// Given
|
||||||
|
// map of expected event types to occurrences
|
||||||
|
val expectedEventTypes = mapOf(
|
||||||
|
EventType.STATE_ROOM_CREATE to 1,
|
||||||
|
EventType.STATE_ROOM_POWER_LEVELS to 1,
|
||||||
|
EventType.STATE_ROOM_MEMBER to 1,
|
||||||
|
EventType.STATE_ROOM_GUEST_ACCESS to 1,
|
||||||
|
EventType.STATE_ROOM_HISTORY_VISIBILITY to 1,
|
||||||
|
)
|
||||||
|
coEvery { userService.resolveUser(any()) } answers {
|
||||||
|
if (firstArg<String>() == MY_USER_ID) User(MY_USER_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR) else User(firstArg())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.size shouldBeEqualTo expectedEventTypes.values.sum()
|
||||||
|
result.map { it.type }.toSet() shouldBeEqualTo expectedEventTypes.keys
|
||||||
|
|
||||||
|
// Room create
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_CREATE }.shouldNotBeNull()
|
||||||
|
// Room member
|
||||||
|
result.singleOrNull { it.type == EventType.STATE_ROOM_MEMBER }?.stateKey shouldBeEqualTo MY_USER_ID
|
||||||
|
// Power levels
|
||||||
|
val powerLevelsContent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }?.content.toModel<PowerLevelsContent>()
|
||||||
|
powerLevelsContent.shouldNotBeNull()
|
||||||
|
powerLevelsContent.ban shouldBeEqualTo Role.Moderator.value
|
||||||
|
powerLevelsContent.kick shouldBeEqualTo Role.Moderator.value
|
||||||
|
powerLevelsContent.invite shouldBeEqualTo Role.Moderator.value
|
||||||
|
powerLevelsContent.redact shouldBeEqualTo Role.Moderator.value
|
||||||
|
powerLevelsContent.eventsDefault shouldBeEqualTo Role.Default.value
|
||||||
|
powerLevelsContent.usersDefault shouldBeEqualTo Role.Default.value
|
||||||
|
powerLevelsContent.stateDefault shouldBeEqualTo Role.Moderator.value
|
||||||
|
// Guest access
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
|
||||||
|
?.content.toModel<RoomGuestAccessContent>()?.guestAccess shouldBeEqualTo GuestAccess.Forbidden
|
||||||
|
// History visibility
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||||
|
?.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility shouldBeEqualTo RoomHistoryVisibility.SHARED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a CreateRoomBody when execute then the resulting list of events is correctly ordered with the right values`() = runTest {
|
||||||
|
// Given
|
||||||
|
val expectedIsDirect = true
|
||||||
|
val expectedHistoryVisibility = RoomHistoryVisibility.WORLD_READABLE
|
||||||
|
|
||||||
|
every { createRoomBody.roomVersion } returns "a_room_version"
|
||||||
|
every { createRoomBody.roomAliasName } returns "a_room_alias_name"
|
||||||
|
every { createRoomBody.name } returns "a_name"
|
||||||
|
every { createRoomBody.topic } returns "a_topic"
|
||||||
|
every { createRoomBody.powerLevelContentOverride } returns PowerLevelsContent(
|
||||||
|
ban = 1,
|
||||||
|
kick = 2,
|
||||||
|
invite = 3,
|
||||||
|
redact = 4,
|
||||||
|
eventsDefault = 5,
|
||||||
|
events = null,
|
||||||
|
usersDefault = 6,
|
||||||
|
users = null,
|
||||||
|
stateDefault = 7,
|
||||||
|
notifications = null
|
||||||
|
)
|
||||||
|
every { createRoomBody.invite3pids } returns listOf(
|
||||||
|
ThreePidInviteBody(
|
||||||
|
idServer = "an_id_server",
|
||||||
|
idAccessToken = "an_id_access_token",
|
||||||
|
medium = MEDIUM_EMAIL,
|
||||||
|
address = "an_email@example.org"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
every { createRoomBody.preset } returns CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||||
|
every { createRoomBody.initialStates } returns listOf(
|
||||||
|
Event(type = "a_custom_type", stateKey = ""),
|
||||||
|
// override the value from the preset
|
||||||
|
Event(
|
||||||
|
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
|
stateKey = "",
|
||||||
|
content = RoomHistoryVisibilityContent(expectedHistoryVisibility.value).toContent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
every { createRoomBody.isDirect } returns expectedIsDirect
|
||||||
|
every { createRoomBody.invitedUserIds } returns listOf("a_user_id")
|
||||||
|
|
||||||
|
val orderedExpectedEventType = listOf(
|
||||||
|
EventType.STATE_ROOM_CREATE,
|
||||||
|
EventType.STATE_ROOM_MEMBER,
|
||||||
|
EventType.STATE_ROOM_POWER_LEVELS,
|
||||||
|
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||||
|
EventType.STATE_ROOM_JOIN_RULES,
|
||||||
|
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||||
|
"a_custom_type",
|
||||||
|
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
|
EventType.STATE_ROOM_NAME,
|
||||||
|
EventType.STATE_ROOM_TOPIC,
|
||||||
|
EventType.STATE_ROOM_MEMBER,
|
||||||
|
EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
|
||||||
|
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.map { it.type } shouldBeEqualTo orderedExpectedEventType
|
||||||
|
result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||||
|
?.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility shouldBeEqualTo expectedHistoryVisibility
|
||||||
|
result.lastOrNull { it.type == EventType.STATE_ROOM_MEMBER }
|
||||||
|
?.content.toModel<RoomMemberContent>()?.isDirect shouldBeEqualTo expectedIsDirect
|
||||||
|
result.lastOrNull { it.type == EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE }
|
||||||
|
?.content.toModel<LocalRoomThirdPartyInviteContent>()?.isDirect shouldBeEqualTo expectedIsDirect
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* 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.session.room.create
|
||||||
|
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
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.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.model.CurrentStateEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
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.query.copyToRealmOrIgnore
|
||||||
|
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.FakeStateEventDataSource
|
||||||
|
|
||||||
|
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 A_ROOM_ID = "a-room-id"
|
||||||
|
private const val MY_USER_ID = "my-user-id"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
internal class DefaultCreateRoomFromLocalRoomTaskTest {
|
||||||
|
|
||||||
|
private val fakeMonarchy = FakeMonarchy()
|
||||||
|
private val clock = DefaultClock()
|
||||||
|
private val createRoomTask = mockk<CreateRoomTask>()
|
||||||
|
private val fakeStateEventDataSource = FakeStateEventDataSource()
|
||||||
|
|
||||||
|
private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask(
|
||||||
|
userId = MY_USER_ID,
|
||||||
|
monarchy = fakeMonarchy.instance,
|
||||||
|
createRoomTask = createRoomTask,
|
||||||
|
stateEventDataSource = fakeStateEventDataSource.instance,
|
||||||
|
clock = clock
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic("org.matrix.android.sdk.internal.database.RealmQueryLatchKt")
|
||||||
|
coJustRun { awaitNotEmptyResult<Any>(realmConfiguration = any(), timeoutMillis = any(), builder = any()) }
|
||||||
|
|
||||||
|
mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt")
|
||||||
|
coEvery { any<EventEntity>().copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, any()) } answers { firstArg() }
|
||||||
|
|
||||||
|
mockkStatic("org.matrix.android.sdk.internal.database.query.CurrentStateEventEntityQueriesKt")
|
||||||
|
every { CurrentStateEventEntity.getOrCreate(fakeMonarchy.fakeRealm.instance, any(), any(), any()) } answers {
|
||||||
|
CurrentStateEventEntity(roomId = arg(2), stateKey = arg(3), type = arg(4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a local room id when execute then the existing room id is kept`() = runTest {
|
||||||
|
// Given
|
||||||
|
givenATombstoneEvent(
|
||||||
|
Event(
|
||||||
|
roomId = A_LOCAL_ROOM_ID,
|
||||||
|
type = EventType.STATE_ROOM_TOMBSTONE,
|
||||||
|
stateKey = "",
|
||||||
|
content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
|
||||||
|
val result = defaultCreateRoomFromLocalRoomTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verifyTombstoneEvent(AN_EXISTING_ROOM_ID)
|
||||||
|
result shouldBeEqualTo AN_EXISTING_ROOM_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a local room id when execute then it is correctly executed`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aCreateRoomParams = mockk<CreateRoomParams>()
|
||||||
|
val aLocalRoomSummaryEntity = mockk<LocalRoomSummaryEntity> {
|
||||||
|
every { roomSummaryEntity } returns mockk(relaxed = true)
|
||||||
|
every { createRoomParams } returns aCreateRoomParams
|
||||||
|
}
|
||||||
|
givenATombstoneEvent(null)
|
||||||
|
givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity)
|
||||||
|
|
||||||
|
coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID
|
||||||
|
|
||||||
|
// When
|
||||||
|
val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
|
||||||
|
val result = defaultCreateRoomFromLocalRoomTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verifyTombstoneEvent(null)
|
||||||
|
// CreateRoomTask has been called with the initial CreateRoomParams
|
||||||
|
coVerify { createRoomTask.execute(aCreateRoomParams) }
|
||||||
|
// The resulting roomId matches the roomId returned by the createRoomTask
|
||||||
|
result shouldBeEqualTo A_ROOM_ID
|
||||||
|
// A tombstone state event has been created
|
||||||
|
coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenATombstoneEvent(event: Event?) {
|
||||||
|
fakeStateEventDataSource.givenGetStateEventReturns(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) {
|
||||||
|
every {
|
||||||
|
fakeMonarchy.fakeRealm.instance
|
||||||
|
.where<LocalRoomSummaryEntity>()
|
||||||
|
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID)
|
||||||
|
.findFirst()
|
||||||
|
} returns localRoomSummaryEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.test.runTest
|
|||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
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.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.toContent
|
||||||
@ -69,7 +70,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest {
|
|||||||
fakeStateEventDataSource.verifyGetStateEvent(
|
fakeStateEventDataSource.verifyGetStateEvent(
|
||||||
roomId = params.roomId,
|
roomId = params.roomId,
|
||||||
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
|
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
|
||||||
stateKey = A_USER_ID
|
stateKey = QueryStringValue.Equals(A_USER_ID)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction
|
|||||||
internal class FakeMonarchy {
|
internal class FakeMonarchy {
|
||||||
|
|
||||||
val instance = mockk<Monarchy>()
|
val instance = mockk<Monarchy>()
|
||||||
private val fakeRealm = FakeRealm()
|
val fakeRealm = FakeRealm()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
|
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
|
||||||
@ -42,6 +42,12 @@ internal class FakeMonarchy {
|
|||||||
} coAnswers {
|
} coAnswers {
|
||||||
secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
|
secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
|
||||||
}
|
}
|
||||||
|
coEvery {
|
||||||
|
instance.doWithRealm(any())
|
||||||
|
} coAnswers {
|
||||||
|
firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance)
|
||||||
|
}
|
||||||
|
every { instance.realmConfiguration } returns mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {
|
inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {
|
||||||
|
@ -19,7 +19,7 @@ package org.matrix.android.sdk.test.fakes
|
|||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStateEventValue
|
||||||
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.internal.session.room.state.StateEventDataSource
|
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
||||||
|
|
||||||
@ -37,12 +37,12 @@ internal class FakeStateEventDataSource {
|
|||||||
} returns event
|
} returns event
|
||||||
}
|
}
|
||||||
|
|
||||||
fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) {
|
fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: QueryStateEventValue) {
|
||||||
verify {
|
verify {
|
||||||
instance.getStateEvent(
|
instance.getStateEvent(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventType = eventType,
|
eventType = eventType,
|
||||||
stateKey = QueryStringValue.Equals(stateKey)
|
stateKey = stateKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -349,6 +349,7 @@
|
|||||||
<activity android:name=".features.location.live.map.LiveLocationMapViewActivity" />
|
<activity android:name=".features.location.live.map.LiveLocationMapViewActivity" />
|
||||||
<activity android:name=".features.settings.font.FontScaleSettingActivity"/>
|
<activity android:name=".features.settings.font.FontScaleSettingActivity"/>
|
||||||
<activity android:name=".features.call.dialpad.PstnDialActivity" />
|
<activity android:name=".features.call.dialpad.PstnDialActivity" />
|
||||||
|
<activity android:name=".features.home.room.list.home.invites.InvitesActivity"/>
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsV
|
|||||||
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel
|
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel
|
||||||
import im.vector.app.features.home.room.list.RoomListViewModel
|
import im.vector.app.features.home.room.list.RoomListViewModel
|
||||||
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
|
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
|
||||||
|
import im.vector.app.features.home.room.list.home.invites.InvitesViewModel
|
||||||
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
|
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
|
||||||
import im.vector.app.features.invite.InviteUsersToRoomViewModel
|
import im.vector.app.features.invite.InviteUsersToRoomViewModel
|
||||||
import im.vector.app.features.location.LocationSharingViewModel
|
import im.vector.app.features.location.LocationSharingViewModel
|
||||||
@ -618,4 +619,9 @@ interface MavericksViewModelModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@MavericksViewModelKey(HomeRoomListViewModel::class)
|
@MavericksViewModelKey(HomeRoomListViewModel::class)
|
||||||
fun homeRoomListViewModel(factory: HomeRoomListViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
fun homeRoomListViewModel(factory: HomeRoomListViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@MavericksViewModelKey(InvitesViewModel::class)
|
||||||
|
fun invitesViewModel(factory: InvitesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -229,8 +229,9 @@ class TimelineEventVisibilityHelper @Inject constructor(
|
|||||||
|
|
||||||
// Hide fake events for local rooms
|
// Hide fake events for local rooms
|
||||||
if (RoomLocalEcho.isLocalEchoId(roomId) &&
|
if (RoomLocalEcho.isLocalEchoId(roomId) &&
|
||||||
root.getClearType() == EventType.STATE_ROOM_MEMBER ||
|
(root.getClearType() == EventType.STATE_ROOM_MEMBER ||
|
||||||
root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY) {
|
root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY ||
|
||||||
|
root.getClearType() == EventType.STATE_ROOM_THIRD_PARTY_INVITE)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.list.home
|
package im.vector.app.features.home.room.list.home
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -48,6 +49,8 @@ import im.vector.app.features.home.room.list.actions.RoomListSharedAction
|
|||||||
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
|
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
|
||||||
import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController
|
import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController
|
||||||
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
|
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
|
||||||
|
import im.vector.app.features.home.room.list.home.invites.InvitesActivity
|
||||||
|
import im.vector.app.features.home.room.list.home.invites.InvitesCounterController
|
||||||
import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController
|
import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController
|
||||||
import im.vector.app.features.spaces.SpaceListBottomSheet
|
import im.vector.app.features.spaces.SpaceListBottomSheet
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -66,6 +69,7 @@ class HomeRoomListFragment :
|
|||||||
@Inject lateinit var roomSummaryItemFactory: RoomSummaryItemFactory
|
@Inject lateinit var roomSummaryItemFactory: RoomSummaryItemFactory
|
||||||
@Inject lateinit var userPreferencesProvider: UserPreferencesProvider
|
@Inject lateinit var userPreferencesProvider: UserPreferencesProvider
|
||||||
@Inject lateinit var recentRoomCarouselController: RecentRoomCarouselController
|
@Inject lateinit var recentRoomCarouselController: RecentRoomCarouselController
|
||||||
|
@Inject lateinit var invitesCounterController: InvitesCounterController
|
||||||
|
|
||||||
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
|
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
|
||||||
private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel
|
private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel
|
||||||
@ -266,7 +270,17 @@ class HomeRoomListFragment :
|
|||||||
controller.submitList(list)
|
controller.submitList(list)
|
||||||
}
|
}
|
||||||
}.adapter
|
}.adapter
|
||||||
|
is HomeRoomSection.InvitesCountData -> invitesCounterController.also { controller ->
|
||||||
|
controller.clickListener = ::onInvitesCounterClicked
|
||||||
|
section.count.observe(viewLifecycleOwner) { count ->
|
||||||
|
controller.submitData(count)
|
||||||
}
|
}
|
||||||
|
}.adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInvitesCounterClicked() {
|
||||||
|
startActivity(Intent(activity, InvitesActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRoomFilterChanged(filter: HomeRoomFilter) {
|
private fun onRoomFilterChanged(filter: HomeRoomFilter) {
|
||||||
@ -285,6 +299,7 @@ class HomeRoomListFragment :
|
|||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
views.roomListView.cleanup()
|
views.roomListView.cleanup()
|
||||||
recentRoomCarouselController.listener = null
|
recentRoomCarouselController.listener = null
|
||||||
|
invitesCounterController.clickListener = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.list.home
|
package im.vector.app.features.home.room.list.home
|
||||||
|
|
||||||
|
import androidx.lifecycle.map
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import arrow.core.toOption
|
import arrow.core.toOption
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
@ -100,9 +101,9 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun configureSections() = viewModelScope.launch {
|
private fun configureSections() = viewModelScope.launch {
|
||||||
val newSections = mutableSetOf<HomeRoomSection>()
|
val newSections = mutableSetOf<HomeRoomSection>()
|
||||||
|
newSections.add(getInvitesCountSection())
|
||||||
|
|
||||||
val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first()
|
val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first()
|
||||||
|
|
||||||
if (areSettingsEnabled) {
|
if (areSettingsEnabled) {
|
||||||
newSections.add(getRecentRoomsSection())
|
newSections.add(getRecentRoomsSection())
|
||||||
}
|
}
|
||||||
@ -127,6 +128,19 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getInvitesCountSection(): HomeRoomSection.InvitesCountData {
|
||||||
|
val builder = RoomSummaryQueryParams.Builder().also {
|
||||||
|
it.memberships = listOf(Membership.INVITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val liveCount = session.roomService().getRoomSummariesLive(
|
||||||
|
builder.build(),
|
||||||
|
RoomSortOrder.ACTIVITY
|
||||||
|
).map { it.count() }
|
||||||
|
|
||||||
|
return HomeRoomSection.InvitesCountData(liveCount)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
|
private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
|
||||||
val builder = RoomSummaryQueryParams.Builder().also {
|
val builder = RoomSummaryQueryParams.Builder().also {
|
||||||
it.memberships = listOf(Membership.JOIN)
|
it.memberships = listOf(Membership.JOIN)
|
||||||
|
@ -32,4 +32,8 @@ sealed class HomeRoomSection {
|
|||||||
data class RecentRoomsData(
|
data class RecentRoomsData(
|
||||||
val list: LiveData<List<RoomSummary>>
|
val list: LiveData<List<RoomSummary>>
|
||||||
) : HomeRoomSection()
|
) : HomeRoomSection()
|
||||||
|
|
||||||
|
data class InvitesCountData(
|
||||||
|
val count: LiveData<Int>
|
||||||
|
) : HomeRoomSection()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
|
||||||
|
@EpoxyModelClass
|
||||||
|
abstract class InviteCounterItem : VectorEpoxyModel<InviteCounterItem.Holder>(R.layout.item_invites_count) {
|
||||||
|
|
||||||
|
@EpoxyAttribute var invitesCount: Int = 0
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.view.setOnClickListener(listener)
|
||||||
|
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(invitesCount, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.invites_count_badge)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
sealed class InvitesAction : VectorViewModelAction {
|
||||||
|
data class AcceptInvitation(val roomSummary: RoomSummary) : InvitesAction()
|
||||||
|
data class RejectInvitation(val roomSummary: RoomSummary) : InvitesAction()
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.core.extensions.addFragment
|
||||||
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
|
import im.vector.app.databinding.ActivitySimpleBinding
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class InvitesActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
||||||
|
|
||||||
|
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
override fun initUiAndData() {
|
||||||
|
if (isFirstCreation()) {
|
||||||
|
addFragment(views.simpleFragmentContainer, InvitesFragment::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
|
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||||
|
import im.vector.app.core.utils.createUIHandler
|
||||||
|
import im.vector.app.features.home.RoomListDisplayMode
|
||||||
|
import im.vector.app.features.home.room.list.RoomListListener
|
||||||
|
import im.vector.app.features.home.room.list.RoomSummaryItemFactory
|
||||||
|
import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_
|
||||||
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class InvitesController @Inject constructor(
|
||||||
|
private val roomSummaryItemFactory: RoomSummaryItemFactory,
|
||||||
|
) : PagedListEpoxyController<RoomSummary>(
|
||||||
|
// Important it must match the PageList builder notify Looper
|
||||||
|
modelBuildingHandler = createUIHandler()
|
||||||
|
) {
|
||||||
|
|
||||||
|
var roomChangeMembershipStates: Map<String, ChangeMembershipState>? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
requestForcedModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener: RoomListListener? = null
|
||||||
|
|
||||||
|
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
|
||||||
|
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
|
||||||
|
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class InvitesCounterController @Inject constructor(
|
||||||
|
val stringProvider: StringProvider
|
||||||
|
) : EpoxyController() {
|
||||||
|
|
||||||
|
private var count = 0
|
||||||
|
var clickListener: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
val host = this
|
||||||
|
if (count != 0) {
|
||||||
|
inviteCounterItem {
|
||||||
|
id("invites_counter")
|
||||||
|
invitesCount(host.count)
|
||||||
|
listener { host.clickListener?.invoke() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitData(count: Int?) {
|
||||||
|
this.count = count ?: 0
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.core.extensions.configureWith
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.databinding.FragmentInvitesBinding
|
||||||
|
import im.vector.app.features.analytics.plan.ViewRoom
|
||||||
|
import im.vector.app.features.home.room.list.RoomListListener
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class InvitesFragment : VectorBaseFragment<FragmentInvitesBinding>(), RoomListListener {
|
||||||
|
|
||||||
|
@Inject lateinit var controller: InvitesController
|
||||||
|
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||||
|
|
||||||
|
private val viewModel by fragmentViewModel(InvitesViewModel::class)
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentInvitesBinding {
|
||||||
|
return FragmentInvitesBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
setupToolbar(views.invitesToolbar)
|
||||||
|
.allowBack()
|
||||||
|
|
||||||
|
views.invitesRecycler.configureWith(controller)
|
||||||
|
controller.listener = this
|
||||||
|
|
||||||
|
viewModel.onEach(InvitesViewState::roomMembershipChanges) {
|
||||||
|
controller.roomChangeMembershipStates = it
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is InvitesViewEvents.Failure -> showFailure(it.throwable)
|
||||||
|
is InvitesViewEvents.OpenRoom -> handleOpenRoom(it.roomSummary, it.shouldCloseInviteView)
|
||||||
|
InvitesViewEvents.Close -> handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleClose() {
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOpenRoom(roomSummary: RoomSummary, shouldCloseInviteView: Boolean) {
|
||||||
|
navigator.openRoom(
|
||||||
|
context = requireActivity(),
|
||||||
|
roomId = roomSummary.roomId,
|
||||||
|
isInviteAlreadyAccepted = true,
|
||||||
|
trigger = ViewRoom.Trigger.RoomList // #6508
|
||||||
|
)
|
||||||
|
if (shouldCloseInviteView) {
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate(): Unit = withState(viewModel) { state ->
|
||||||
|
super.invalidate()
|
||||||
|
|
||||||
|
state.pagedList?.observe(viewLifecycleOwner) { list ->
|
||||||
|
controller.submitList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRejectRoomInvitation(room: RoomSummary) {
|
||||||
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
|
||||||
|
viewModel.handle(InvitesAction.RejectInvitation(room))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAcceptRoomInvitation(room: RoomSummary) {
|
||||||
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) }
|
||||||
|
viewModel.handle(InvitesAction.AcceptInvitation(room))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onJoinSuggestedRoom(room: SpaceChildInfo) = Unit
|
||||||
|
|
||||||
|
override fun onSuggestedRoomClicked(room: SpaceChildInfo) = Unit
|
||||||
|
|
||||||
|
override fun onRoomClicked(room: RoomSummary) = Unit
|
||||||
|
|
||||||
|
override fun onRoomLongClicked(room: RoomSummary): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
sealed class InvitesViewEvents : VectorViewEvents {
|
||||||
|
data class Failure(val throwable: Throwable) : InvitesViewEvents()
|
||||||
|
data class OpenRoom(val roomSummary: RoomSummary, val shouldCloseInviteView: Boolean) : InvitesViewEvents()
|
||||||
|
object Close : InvitesViewEvents()
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
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.members.ChangeMembershipState
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class InvitesViewModel @AssistedInject constructor(
|
||||||
|
@Assisted val initialState: InvitesViewState,
|
||||||
|
private val session: Session,
|
||||||
|
) : VectorViewModel<InvitesViewState, InvitesAction, InvitesViewEvents>(initialState) {
|
||||||
|
|
||||||
|
private val pagedListConfig = PagedList.Config.Builder()
|
||||||
|
.setPageSize(10)
|
||||||
|
.setInitialLoadSizeHint(20)
|
||||||
|
.setEnablePlaceholders(true)
|
||||||
|
.setPrefetchDistance(10)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : MavericksAssistedViewModelFactory<InvitesViewModel, InvitesViewState> {
|
||||||
|
override fun create(initialState: InvitesViewState): InvitesViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<InvitesViewModel, InvitesViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeInvites()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: InvitesAction) {
|
||||||
|
when (action) {
|
||||||
|
is InvitesAction.AcceptInvitation -> handleAcceptInvitation(action)
|
||||||
|
is InvitesAction.RejectInvitation -> handleRejectInvitation(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRejectInvitation(action: InvitesAction.RejectInvitation) = withState { state ->
|
||||||
|
val roomId = action.roomSummary.roomId
|
||||||
|
val roomMembershipChange = state.roomMembershipChanges[roomId]
|
||||||
|
if (roomMembershipChange?.isInProgress().orFalse()) {
|
||||||
|
// Request already sent, should not happen
|
||||||
|
Timber.w("Try to left an already leaving or joining room. Should not happen")
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
|
||||||
|
val shouldCloseInviteView = state.pagedList?.value?.size == 1
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
session.roomService().leaveRoom(roomId)
|
||||||
|
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
|
||||||
|
// Instead, we wait for the room to be rejected
|
||||||
|
// Known bug: if the user is invited again (after rejecting the first invitation), the loading will be displayed instead of the buttons.
|
||||||
|
// If we update the state, the button will be displayed again, so it's not ideal...
|
||||||
|
if (shouldCloseInviteView) {
|
||||||
|
_viewEvents.post(InvitesViewEvents.Close)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
// Notify the user
|
||||||
|
_viewEvents.post(InvitesViewEvents.Failure(failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAcceptInvitation(action: InvitesAction.AcceptInvitation) = withState { state ->
|
||||||
|
val roomId = action.roomSummary.roomId
|
||||||
|
val roomMembershipChange = state.roomMembershipChanges[roomId]
|
||||||
|
if (roomMembershipChange?.isInProgress().orFalse()) {
|
||||||
|
// Request already sent, should not happen
|
||||||
|
Timber.w("Try to join an already joining room. Should not happen")
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
// close invites view when navigate to a room from the last one invite
|
||||||
|
|
||||||
|
val shouldCloseInviteView = state.pagedList?.value?.size == 1
|
||||||
|
|
||||||
|
_viewEvents.post(InvitesViewEvents.OpenRoom(action.roomSummary, shouldCloseInviteView))
|
||||||
|
|
||||||
|
// quick echo
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
roomMembershipChanges = roomMembershipChanges.mapValues {
|
||||||
|
if (it.key == roomId) {
|
||||||
|
ChangeMembershipState.Joining
|
||||||
|
} else {
|
||||||
|
it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeInvites() {
|
||||||
|
val builder = RoomSummaryQueryParams.Builder().also {
|
||||||
|
it.memberships = listOf(Membership.INVITE)
|
||||||
|
}
|
||||||
|
val pagedList = session.roomService().getPagedRoomSummariesLive(
|
||||||
|
queryParams = builder.build(),
|
||||||
|
pagedListConfig = pagedListConfig,
|
||||||
|
sortOrder = RoomSortOrder.ACTIVITY
|
||||||
|
)
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(pagedList = pagedList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.list.home.invites
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
data class InvitesViewState(
|
||||||
|
val pagedList: LiveData<PagedList<RoomSummary>>? = null,
|
||||||
|
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
|
||||||
|
) : MavericksState
|
@ -28,6 +28,7 @@ import im.vector.app.core.extensions.content
|
|||||||
import im.vector.app.core.extensions.editText
|
import im.vector.app.core.extensions.editText
|
||||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
|
import im.vector.app.core.extensions.showKeyboard
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.core.utils.ensureProtocol
|
import im.vector.app.core.utils.ensureProtocol
|
||||||
import im.vector.app.core.utils.ensureTrailingSlash
|
import im.vector.app.core.utils.ensureTrailingSlash
|
||||||
@ -91,6 +92,9 @@ class FtueAuthCombinedServerSelectionFragment :
|
|||||||
val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure() ?: viewModel.getDefaultHomeserverUrl()
|
val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure() ?: viewModel.getDefaultHomeserverUrl()
|
||||||
views.chooseServerInput.editText().setText(userUrlInput)
|
views.chooseServerInput.editText().setText(userUrlInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views.chooseServerInput.editText().selectAll()
|
||||||
|
views.chooseServerInput.editText().showKeyboard(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(throwable: Throwable) {
|
override fun onError(throwable: Throwable) {
|
||||||
|
@ -22,6 +22,7 @@ import im.vector.app.core.resources.StringProvider
|
|||||||
import im.vector.app.features.grouplist.newHomeSpaceSummaryItem
|
import im.vector.app.features.grouplist.newHomeSpaceSummaryItem
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
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.SpaceChildInfo
|
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
||||||
@ -51,6 +52,7 @@ class NewSpaceSummaryController @Inject constructor(
|
|||||||
nonNullViewState.selectedSpace,
|
nonNullViewState.selectedSpace,
|
||||||
nonNullViewState.rootSpacesOrdered,
|
nonNullViewState.rootSpacesOrdered,
|
||||||
nonNullViewState.homeAggregateCount,
|
nonNullViewState.homeAggregateCount,
|
||||||
|
nonNullViewState.expandedStates,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,20 +61,13 @@ class NewSpaceSummaryController @Inject constructor(
|
|||||||
selectedSpace: RoomSummary?,
|
selectedSpace: RoomSummary?,
|
||||||
rootSpaces: List<RoomSummary>?,
|
rootSpaces: List<RoomSummary>?,
|
||||||
homeCount: RoomAggregateNotificationCount,
|
homeCount: RoomAggregateNotificationCount,
|
||||||
|
expandedStates: Map<String, Boolean>,
|
||||||
) {
|
) {
|
||||||
val host = this
|
val host = this
|
||||||
|
|
||||||
if (selectedSpace != null) {
|
addHomeItem(selectedSpace == null, homeCount)
|
||||||
addSubSpaces(selectedSpace, spaceSummaries, homeCount)
|
addSpaces(spaceSummaries, selectedSpace, rootSpaces, expandedStates)
|
||||||
} else {
|
addCreateItem()
|
||||||
addHomeItem(true, homeCount)
|
|
||||||
addRootSpaces(rootSpaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
newSpaceAddItem {
|
|
||||||
id("create")
|
|
||||||
listener { host.callback?.onAddSpaceSelected() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addHomeItem(selected: Boolean, homeCount: RoomAggregateNotificationCount) {
|
private fun addHomeItem(selected: Boolean, homeCount: RoomAggregateNotificationCount) {
|
||||||
@ -86,53 +81,87 @@ class NewSpaceSummaryController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addSubSpaces(
|
private fun addSpaces(
|
||||||
selectedSpace: RoomSummary,
|
|
||||||
spaceSummaries: List<RoomSummary>?,
|
spaceSummaries: List<RoomSummary>?,
|
||||||
homeCount: RoomAggregateNotificationCount,
|
selectedSpace: RoomSummary?,
|
||||||
|
rootSpaces: List<RoomSummary>?,
|
||||||
|
expandedStates: Map<String, Boolean>,
|
||||||
) {
|
) {
|
||||||
val host = this
|
val host = this
|
||||||
val spaceChildren = selectedSpace.spaceChildren
|
|
||||||
var subSpacesAdded = false
|
|
||||||
|
|
||||||
spaceChildren?.sortedWith(subSpaceComparator)?.forEach { spaceChild ->
|
rootSpaces?.filter { it.membership != Membership.INVITE }
|
||||||
val subSpaceSummary = spaceSummaries?.firstOrNull { it.roomId == spaceChild.childRoomId } ?: return@forEach
|
?.forEach { spaceSummary ->
|
||||||
|
val subSpaces = spaceSummary.spaceChildren?.filter { spaceChild -> spaceSummaries.containsSpaceId(spaceChild.childRoomId) }
|
||||||
|
val hasChildren = (subSpaces?.size ?: 0) > 0
|
||||||
|
val isSelected = spaceSummary.roomId == selectedSpace?.roomId
|
||||||
|
val expanded = expandedStates[spaceSummary.roomId] == true
|
||||||
|
|
||||||
if (subSpaceSummary.membership != Membership.INVITE) {
|
|
||||||
subSpacesAdded = true
|
|
||||||
newSpaceSummaryItem {
|
newSpaceSummaryItem {
|
||||||
|
id(spaceSummary.roomId)
|
||||||
avatarRenderer(host.avatarRenderer)
|
avatarRenderer(host.avatarRenderer)
|
||||||
id(subSpaceSummary.roomId)
|
countState(UnreadCounterBadgeView.State(spaceSummary.notificationCount, spaceSummary.highlightCount > 0))
|
||||||
matrixItem(subSpaceSummary.toMatrixItem())
|
expanded(expanded)
|
||||||
selected(false)
|
hasChildren(hasChildren)
|
||||||
listener { host.callback?.onSpaceSelected(subSpaceSummary) }
|
matrixItem(spaceSummary.toMatrixItem())
|
||||||
countState(
|
onLongClickListener { host.callback?.onSpaceSettings(spaceSummary) }
|
||||||
UnreadCounterBadgeView.State(
|
onSpaceSelectedListener { host.callback?.onSpaceSelected(spaceSummary) }
|
||||||
subSpaceSummary.notificationCount,
|
onToggleExpandListener { host.callback?.onToggleExpand(spaceSummary) }
|
||||||
subSpaceSummary.highlightCount > 0
|
selected(isSelected)
|
||||||
)
|
}
|
||||||
)
|
|
||||||
|
if (hasChildren && expanded) {
|
||||||
|
subSpaces?.forEach { child ->
|
||||||
|
addSubSpace(spaceSummary.roomId, spaceSummaries, expandedStates, selectedSpace, child, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subSpacesAdded) {
|
private fun List<RoomSummary>?.containsSpaceId(spaceId: String) = this?.any { it.roomId == spaceId }.orFalse()
|
||||||
addHomeItem(false, homeCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRootSpaces(rootSpaces: List<RoomSummary>?) {
|
private fun addSubSpace(
|
||||||
|
idPrefix: String,
|
||||||
|
spaceSummaries: List<RoomSummary>?,
|
||||||
|
expandedStates: Map<String, Boolean>,
|
||||||
|
selectedSpace: RoomSummary?,
|
||||||
|
info: SpaceChildInfo,
|
||||||
|
depth: Int,
|
||||||
|
) {
|
||||||
val host = this
|
val host = this
|
||||||
rootSpaces
|
val childSummary = spaceSummaries?.firstOrNull { it.roomId == info.childRoomId } ?: return
|
||||||
?.filter { it.membership != Membership.INVITE }
|
val id = "$idPrefix:${childSummary.roomId}"
|
||||||
?.forEach { roomSummary ->
|
val countState = UnreadCounterBadgeView.State(childSummary.notificationCount, childSummary.highlightCount > 0)
|
||||||
newSpaceSummaryItem {
|
val expanded = expandedStates[childSummary.roomId] == true
|
||||||
|
val isSelected = childSummary.roomId == selectedSpace?.roomId
|
||||||
|
val subSpaces = childSummary.spaceChildren?.filter { childSpace -> spaceSummaries.containsSpaceId(childSpace.childRoomId) }
|
||||||
|
?.sortedWith(subSpaceComparator)
|
||||||
|
|
||||||
|
newSubSpaceSummaryItem {
|
||||||
|
id(id)
|
||||||
avatarRenderer(host.avatarRenderer)
|
avatarRenderer(host.avatarRenderer)
|
||||||
id(roomSummary.roomId)
|
countState(countState)
|
||||||
matrixItem(roomSummary.toMatrixItem())
|
expanded(expanded)
|
||||||
listener { host.callback?.onSpaceSelected(roomSummary) }
|
hasChildren(!subSpaces.isNullOrEmpty())
|
||||||
countState(UnreadCounterBadgeView.State(roomSummary.notificationCount, roomSummary.highlightCount > 0))
|
indent(depth)
|
||||||
|
matrixItem(childSummary.toMatrixItem())
|
||||||
|
onLongClickListener { host.callback?.onSpaceSettings(childSummary) }
|
||||||
|
onSubSpaceSelectedListener { host.callback?.onSpaceSelected(childSummary) }
|
||||||
|
onToggleExpandListener { host.callback?.onToggleExpand(childSummary) }
|
||||||
|
selected(isSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
subSpaces?.forEach {
|
||||||
|
addSubSpace(id, spaceSummaries, expandedStates, selectedSpace, it, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCreateItem() {
|
||||||
|
val host = this
|
||||||
|
newSpaceAddItem {
|
||||||
|
id("create")
|
||||||
|
listener { host.callback?.onAddSpaceSelected() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +169,7 @@ class NewSpaceSummaryController @Inject constructor(
|
|||||||
fun onSpaceSelected(spaceSummary: RoomSummary?)
|
fun onSpaceSelected(spaceSummary: RoomSummary?)
|
||||||
fun onSpaceInviteSelected(spaceSummary: RoomSummary)
|
fun onSpaceInviteSelected(spaceSummary: RoomSummary)
|
||||||
fun onSpaceSettings(spaceSummary: RoomSummary)
|
fun onSpaceSettings(spaceSummary: RoomSummary)
|
||||||
|
fun onToggleExpand(spaceSummary: RoomSummary)
|
||||||
fun onAddSpaceSelected()
|
fun onAddSpaceSelected()
|
||||||
fun sendFeedBack()
|
fun sendFeedBack()
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.spaces
|
|||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
@ -34,16 +35,30 @@ import org.matrix.android.sdk.api.util.MatrixItem
|
|||||||
abstract class NewSpaceSummaryItem : VectorEpoxyModel<NewSpaceSummaryItem.Holder>(R.layout.item_new_space) {
|
abstract class NewSpaceSummaryItem : VectorEpoxyModel<NewSpaceSummaryItem.Holder>(R.layout.item_new_space) {
|
||||||
|
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
||||||
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
|
||||||
@EpoxyAttribute var selected: Boolean = false
|
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null
|
|
||||||
@EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
|
@EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
|
||||||
|
@EpoxyAttribute var expanded: Boolean = false
|
||||||
|
@EpoxyAttribute var hasChildren: Boolean = false
|
||||||
|
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onLongClickListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onSpaceSelectedListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onToggleExpandListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute var selected: Boolean = false
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.rootView.onClick(listener)
|
val context = holder.root.context
|
||||||
|
holder.root.onClick(onSpaceSelectedListener)
|
||||||
|
holder.root.setOnLongClickListener {
|
||||||
|
onLongClickListener?.invoke(holder.root)
|
||||||
|
true
|
||||||
|
}
|
||||||
holder.name.text = matrixItem.displayName
|
holder.name.text = matrixItem.displayName
|
||||||
holder.rootView.isChecked = selected
|
holder.root.isChecked = selected
|
||||||
|
|
||||||
|
holder.chevron.setOnClickListener(onToggleExpandListener)
|
||||||
|
holder.chevron.isVisible = hasChildren
|
||||||
|
holder.chevron.setImageResource(if (expanded) R.drawable.ic_expand_more else R.drawable.ic_arrow_right)
|
||||||
|
holder.chevron.contentDescription = context.getString(if (expanded) R.string.a11y_collapse_space_children else R.string.a11y_expand_space_children)
|
||||||
|
|
||||||
avatarRenderer.render(matrixItem, holder.avatar)
|
avatarRenderer.render(matrixItem, holder.avatar)
|
||||||
holder.unreadCounter.render(countState)
|
holder.unreadCounter.render(countState)
|
||||||
@ -55,9 +70,10 @@ abstract class NewSpaceSummaryItem : VectorEpoxyModel<NewSpaceSummaryItem.Holder
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
val rootView by bind<CheckableConstraintLayout>(R.id.root)
|
val root by bind<CheckableConstraintLayout>(R.id.root)
|
||||||
val avatar by bind<ImageView>(R.id.avatar)
|
val avatar by bind<ImageView>(R.id.avatar)
|
||||||
val name by bind<TextView>(R.id.name)
|
val name by bind<TextView>(R.id.name)
|
||||||
val unreadCounter by bind<UnreadCounterBadgeView>(R.id.unread_counter)
|
val unreadCounter by bind<UnreadCounterBadgeView>(R.id.unread_counter)
|
||||||
|
val chevron by bind<ImageView>(R.id.chevron)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.spaces
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Space
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.app.core.epoxy.onClick
|
||||||
|
import im.vector.app.core.platform.CheckableConstraintLayout
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
|
@EpoxyModelClass
|
||||||
|
abstract class NewSubSpaceSummaryItem : VectorEpoxyModel<NewSubSpaceSummaryItem.Holder>(R.layout.item_new_sub_space) {
|
||||||
|
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
@EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
|
||||||
|
@EpoxyAttribute var expanded: Boolean = false
|
||||||
|
@EpoxyAttribute var hasChildren: Boolean = false
|
||||||
|
@EpoxyAttribute var indent: Int = 0
|
||||||
|
@EpoxyAttribute lateinit var matrixItem: MatrixItem
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onLongClickListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onSubSpaceSelectedListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onToggleExpandListener: ClickListener? = null
|
||||||
|
@EpoxyAttribute var selected: Boolean = false
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.root.onClick(onSubSpaceSelectedListener)
|
||||||
|
holder.name.text = matrixItem.displayName
|
||||||
|
holder.root.isChecked = selected
|
||||||
|
holder.root.setOnLongClickListener { onLongClickListener?.invoke(holder.root).let { true } }
|
||||||
|
|
||||||
|
holder.chevron.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
holder.view.context,
|
||||||
|
if (expanded) R.drawable.ic_expand_more else R.drawable.ic_arrow_right
|
||||||
|
)
|
||||||
|
)
|
||||||
|
holder.chevron.onClick(onToggleExpandListener)
|
||||||
|
holder.chevron.isVisible = hasChildren
|
||||||
|
|
||||||
|
holder.indent.isVisible = indent > 0
|
||||||
|
holder.indent.updateLayoutParams {
|
||||||
|
width = indent * 30
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarRenderer.render(matrixItem, holder.avatar)
|
||||||
|
holder.notificationBadge.render(countState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unbind(holder: Holder) {
|
||||||
|
avatarRenderer.clear(holder.avatar)
|
||||||
|
super.unbind(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val avatar by bind<ImageView>(R.id.avatar)
|
||||||
|
val name by bind<TextView>(R.id.name)
|
||||||
|
val root by bind<CheckableConstraintLayout>(R.id.root)
|
||||||
|
val chevron by bind<ImageView>(R.id.chevron)
|
||||||
|
val indent by bind<Space>(R.id.indent)
|
||||||
|
val notificationBadge by bind<UnreadCounterBadgeView>(R.id.notification_badge)
|
||||||
|
}
|
||||||
|
}
|
@ -77,7 +77,6 @@ class SpaceListFragment :
|
|||||||
|
|
||||||
private fun setupSpaceController() {
|
private fun setupSpaceController() {
|
||||||
if (vectorFeatures.isNewAppLayoutEnabled()) {
|
if (vectorFeatures.isNewAppLayoutEnabled()) {
|
||||||
enableDragAndDropForNewSpaceController()
|
|
||||||
newSpaceController.callback = this
|
newSpaceController.callback = this
|
||||||
views.groupListView.configureWith(newSpaceController)
|
views.groupListView.configureWith(newSpaceController)
|
||||||
} else {
|
} else {
|
||||||
@ -87,49 +86,6 @@ class SpaceListFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableDragAndDropForNewSpaceController() {
|
|
||||||
EpoxyTouchHelper.initDragging(newSpaceController)
|
|
||||||
.withRecyclerView(views.groupListView)
|
|
||||||
.forVerticalList()
|
|
||||||
.withTarget(NewSpaceSummaryItem::class.java)
|
|
||||||
.andCallbacks(object : EpoxyTouchHelper.DragCallbacks<NewSpaceSummaryItem>() {
|
|
||||||
var toPositionM: Int? = null
|
|
||||||
var fromPositionM: Int? = null
|
|
||||||
var initialElevation: Float? = null
|
|
||||||
|
|
||||||
override fun onDragStarted(model: NewSpaceSummaryItem?, itemView: View?, adapterPosition: Int) {
|
|
||||||
toPositionM = null
|
|
||||||
fromPositionM = null
|
|
||||||
model?.matrixItem?.id?.let {
|
|
||||||
viewModel.handle(SpaceListAction.OnStartDragging(it, false))
|
|
||||||
}
|
|
||||||
itemView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
initialElevation = itemView?.elevation
|
|
||||||
itemView?.elevation = 6f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDragReleased(model: NewSpaceSummaryItem?, itemView: View?) {
|
|
||||||
if (toPositionM == null || fromPositionM == null) return
|
|
||||||
val movedSpaceId = model?.matrixItem?.id ?: return
|
|
||||||
viewModel.handle(SpaceListAction.MoveSpace(movedSpaceId, toPositionM!! - fromPositionM!!))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearView(model: NewSpaceSummaryItem?, itemView: View?) {
|
|
||||||
itemView?.elevation = initialElevation ?: 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onModelMoved(fromPosition: Int, toPosition: Int, modelBeingMoved: NewSpaceSummaryItem?, itemView: View?) {
|
|
||||||
if (fromPositionM == null) {
|
|
||||||
fromPositionM = fromPosition
|
|
||||||
}
|
|
||||||
if (toPositionM != toPosition) {
|
|
||||||
toPositionM = toPosition
|
|
||||||
itemView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableDragAndDropForSpaceController() {
|
private fun enableDragAndDropForSpaceController() {
|
||||||
EpoxyTouchHelper.initDragging(spaceController)
|
EpoxyTouchHelper.initDragging(spaceController)
|
||||||
.withRecyclerView(views.groupListView)
|
.withRecyclerView(views.groupListView)
|
||||||
|
36
vector/src/main/res/layout/fragment_invites.xml
Normal file
36
vector/src/main/res/layout/fragment_invites.xml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/invites_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
app:title="@string/invites_title" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/invites_recycler"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
android:overScrollMode="always"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
48
vector/src/main/res/layout/item_invites_count.xml
Normal file
48
vector/src/main/res/layout/item_invites_count.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?vctr_toolbar_background"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
|
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
android:id="@+id/invites_count_badge"
|
||||||
|
style="@style/Widget.Vector.TextView.Micro"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:minHeight="16dp"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingEnd="4dp"
|
||||||
|
android:textColor="?colorOnError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:background="@drawable/bg_unread_highlight"
|
||||||
|
tools:text="4"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/invites_count_title"
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/invites_title"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="?colorSecondary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/invites_count_badge"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -34,9 +34,9 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/unread_counter"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/unread_counter"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="Element Corp" />
|
tools:text="Element Corp" />
|
||||||
|
|
||||||
@ -53,25 +53,28 @@
|
|||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:textColor="?colorOnError"
|
android:textColor="?colorOnError"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/chevron"
|
app:layout_constraintEnd_toStartOf="@id/chevron"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
tools:background="@drawable/bg_unread_highlight"
|
tools:background="@drawable/bg_unread_highlight"
|
||||||
tools:text="147"
|
tools:text="147"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/chevron"
|
android:id="@+id/chevron"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="32dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="21dp"
|
android:layout_marginEnd="21dp"
|
||||||
android:importantForAccessibility="no"
|
android:background="?selectableItemBackground"
|
||||||
android:src="@drawable/ic_arrow_right"
|
android:contentDescription="@string/a11y_expand_space_children"
|
||||||
android:visibility="visible"
|
android:scaleType="centerInside"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:tint="?vctr_content_secondary"
|
app:tint="?vctr_content_secondary"
|
||||||
tools:ignore="MissingPrefix" />
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@drawable/ic_arrow_right"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</im.vector.app.core.platform.CheckableConstraintLayout>
|
</im.vector.app.core.platform.CheckableConstraintLayout>
|
||||||
|
93
vector/src/main/res/layout/item_new_sub_space.xml
Normal file
93
vector/src/main/res/layout/item_new_sub_space.xml
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<im.vector.app.core.platform.CheckableConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/bg_space_item"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/indent"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar"
|
||||||
|
android:layout_width="26dp"
|
||||||
|
android:layout_height="26dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
android:duplicateParentState="true"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/indent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@sample/space_avatars" />
|
||||||
|
|
||||||
|
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
android:id="@+id/notification_badge"
|
||||||
|
style="@style/Widget.Vector.TextView.Micro"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:minHeight="16dp"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingEnd="4dp"
|
||||||
|
android:textColor="?colorOnError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintCircle="@id/avatar"
|
||||||
|
app:layout_constraintCircleAngle="45"
|
||||||
|
app:layout_constraintCircleRadius="14dp"
|
||||||
|
tools:background="@drawable/bg_unread_highlight"
|
||||||
|
tools:text="147"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/chevron"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/chevron"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:contentDescription="@string/a11y_expand_space_children"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?vctr_content_secondary"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@drawable/ic_arrow_right"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</im.vector.app.core.platform.CheckableConstraintLayout>
|
@ -140,6 +140,8 @@
|
|||||||
<string name="start_chat">Start Chat</string>
|
<string name="start_chat">Start Chat</string>
|
||||||
<string name="create_room">Create Room</string>
|
<string name="create_room">Create Room</string>
|
||||||
<string name="explore_rooms">Explore Rooms</string>
|
<string name="explore_rooms">Explore Rooms</string>
|
||||||
|
<string name="a11y_expand_space_children">Expand space children</string>
|
||||||
|
<string name="a11y_collapse_space_children">Collapse space children</string>
|
||||||
|
|
||||||
<!-- Last seen time -->
|
<!-- Last seen time -->
|
||||||
|
|
||||||
@ -440,6 +442,9 @@
|
|||||||
<string name="system_alerts_header">"System Alerts"</string>
|
<string name="system_alerts_header">"System Alerts"</string>
|
||||||
<string name="suggested_header">Suggested Rooms</string>
|
<string name="suggested_header">Suggested Rooms</string>
|
||||||
|
|
||||||
|
<!-- Invites fragment -->
|
||||||
|
<string name="invites_title">Invites</string>
|
||||||
|
|
||||||
<!-- People fragment -->
|
<!-- People fragment -->
|
||||||
<string name="direct_chats_header">Conversations</string>
|
<string name="direct_chats_header">Conversations</string>
|
||||||
<string name="matrix_only_filter">Matrix contacts only</string>
|
<string name="matrix_only_filter">Matrix contacts only</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user