Merge pull request #6051 from vector-im/feature/fre/start_dm_on_first_msg
Start DM on first message (UI)
This commit is contained in:
commit
909ce290c8
|
@ -0,0 +1 @@
|
||||||
|
Create DM room only on first message - Design implementation & debug feature flag
|
|
@ -40,6 +40,18 @@ interface RoomService {
|
||||||
*/
|
*/
|
||||||
suspend fun createRoom(createRoomParams: CreateRoomParams): String
|
suspend fun createRoom(createRoomParams: CreateRoomParams): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a room locally.
|
||||||
|
* This room will not be synchronized with the server and will not come back from the sync, so all the events related to this room will be generated
|
||||||
|
* locally.
|
||||||
|
*/
|
||||||
|
suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a local room with all its related events.
|
||||||
|
*/
|
||||||
|
suspend fun deleteLocalRoom(roomId: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters.
|
* Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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.api.session.room.model.localecho
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object RoomLocalEcho {
|
||||||
|
|
||||||
|
private const val PREFIX = "!local."
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell whether the provider room id is a local id.
|
||||||
|
*/
|
||||||
|
fun isLocalEchoId(roomId: String) = roomId.startsWith(PREFIX)
|
||||||
|
|
||||||
|
internal fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}"
|
||||||
|
}
|
|
@ -23,13 +23,20 @@ import io.realm.kotlin.createObject
|
||||||
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.CurrentStateEventEntityFields
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||||
|
|
||||||
|
internal fun CurrentStateEventEntity.Companion.whereRoomId(
|
||||||
|
realm: Realm,
|
||||||
|
roomId: String
|
||||||
|
): RealmQuery<CurrentStateEventEntity> {
|
||||||
|
return realm.where(CurrentStateEventEntity::class.java)
|
||||||
|
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun CurrentStateEventEntity.Companion.whereType(
|
internal fun CurrentStateEventEntity.Companion.whereType(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
type: String
|
type: String
|
||||||
): RealmQuery<CurrentStateEventEntity> {
|
): RealmQuery<CurrentStateEventEntity> {
|
||||||
return realm.where(CurrentStateEventEntity::class.java)
|
return whereRoomId(realm = realm, roomId = roomId)
|
||||||
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
|
|
||||||
.equalTo(CurrentStateEventEntityFields.TYPE, type)
|
.equalTo(CurrentStateEventEntityFields.TYPE, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,9 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
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.create.CreateLocalRoomTask
|
||||||
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.delete.DeleteLocalRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
|
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
|
||||||
|
@ -60,6 +62,8 @@ import javax.inject.Inject
|
||||||
internal class DefaultRoomService @Inject constructor(
|
internal class DefaultRoomService @Inject constructor(
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
private val createRoomTask: CreateRoomTask,
|
private val createRoomTask: CreateRoomTask,
|
||||||
|
private val createLocalRoomTask: CreateLocalRoomTask,
|
||||||
|
private val deleteLocalRoomTask: DeleteLocalRoomTask,
|
||||||
private val joinRoomTask: JoinRoomTask,
|
private val joinRoomTask: JoinRoomTask,
|
||||||
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
||||||
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
|
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
|
||||||
|
@ -78,6 +82,14 @@ internal class DefaultRoomService @Inject constructor(
|
||||||
return createRoomTask.executeRetry(createRoomParams, 3)
|
return createRoomTask.executeRetry(createRoomParams, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String {
|
||||||
|
return createLocalRoomTask.execute(createRoomParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteLocalRoom(roomId: String) {
|
||||||
|
deleteLocalRoomTask.execute(DeleteLocalRoomTask.Params(roomId))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getRoom(roomId: String): Room? {
|
override fun getRoom(roomId: String): Room? {
|
||||||
return roomGetter.getRoom(roomId)
|
return roomGetter.getRoom(roomId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,8 +43,12 @@ 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.CreateLocalRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
|
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
|
||||||
|
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.create.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.DeleteLocalRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
|
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
|
||||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
|
import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
|
||||||
|
@ -204,6 +208,12 @@ internal abstract class RoomModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
|
abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,267 @@
|
||||||
|
/*
|
||||||
|
* 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.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
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.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.model.GuestAccess
|
||||||
|
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.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.send.SendState
|
||||||
|
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
|
||||||
|
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.database.awaitNotEmptyResult
|
||||||
|
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.toEntity
|
||||||
|
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.EventInsertType
|
||||||
|
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.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.getOrNull
|
||||||
|
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.room.membership.RoomMemberEventHandler
|
||||||
|
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||||
|
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||||
|
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.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
|
||||||
|
|
||||||
|
internal class DefaultCreateLocalRoomTask @Inject constructor(
|
||||||
|
@UserId private val userId: String,
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
private val roomMemberEventHandler: RoomMemberEventHandler,
|
||||||
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
|
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||||
|
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
|
||||||
|
private val userService: UserService,
|
||||||
|
private val clock: Clock,
|
||||||
|
) : CreateLocalRoomTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: CreateRoomParams): String {
|
||||||
|
val createRoomBody = createRoomBodyBuilder.build(params.withDefault())
|
||||||
|
val roomId = RoomLocalEcho.createLocalEchoId()
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
createLocalRoomEntity(realm, roomId, createRoomBody)
|
||||||
|
createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for room to be created in DB
|
||||||
|
try {
|
||||||
|
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
|
||||||
|
realm.where(RoomSummaryEntity::class.java)
|
||||||
|
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
|
||||||
|
}
|
||||||
|
} catch (exception: TimeoutCancellationException) {
|
||||||
|
throw CreateRoomFailure.CreatedWithTimeout(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a local room entity from the given room creation params.
|
||||||
|
* This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room.
|
||||||
|
*/
|
||||||
|
private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
|
||||||
|
RoomEntity.getOrCreate(realm, roomId).apply {
|
||||||
|
membership = Membership.JOIN
|
||||||
|
chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
|
||||||
|
membersLoadStatus = RoomMembersLoadStatusType.LOADED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
|
||||||
|
val otherUserId = createRoomBody.getDirectUserId()
|
||||||
|
if (otherUserId != null) {
|
||||||
|
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
||||||
|
isDirect = true
|
||||||
|
directUserId = otherUserId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomSummaryUpdater.update(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
membership = Membership.JOIN,
|
||||||
|
roomSummary = RoomSyncSummary(
|
||||||
|
heroes = createRoomBody.invitedUserIds.orEmpty().take(5),
|
||||||
|
joinedMembersCount = 1,
|
||||||
|
invitedMembersCount = createRoomBody.invitedUserIds?.size ?: 0
|
||||||
|
),
|
||||||
|
updateMembers = !createRoomBody.invitedUserIds.isNullOrEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single chunk containing the necessary events to display the local room.
|
||||||
|
*
|
||||||
|
* @param realm the current instance of realm
|
||||||
|
* @param roomId the id of the local room
|
||||||
|
* @param createRoomBody the room creation params
|
||||||
|
*
|
||||||
|
* @return a chunk entity
|
||||||
|
*/
|
||||||
|
private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
|
||||||
|
val chunkEntity = realm.createObject<ChunkEntity>().apply {
|
||||||
|
isLastBackward = true
|
||||||
|
isLastForward = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventList = createLocalRoomEvents(createRoomBody)
|
||||||
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
|
||||||
|
for (event in eventList) {
|
||||||
|
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = clock.epochMillis()
|
||||||
|
val eventEntity = event.toEntity(roomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
|
||||||
|
if (event.stateKey != null) {
|
||||||
|
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||||
|
eventId = event.eventId
|
||||||
|
root = eventEntity
|
||||||
|
}
|
||||||
|
if (event.type == EventType.STATE_ROOM_MEMBER) {
|
||||||
|
roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
|
||||||
|
roomMemberEventHandler.handle(realm, roomId, event, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roomMemberContentsByUser.getOrPut(event.senderId) {
|
||||||
|
// If we don't have any new state on this user, get it from db
|
||||||
|
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
|
||||||
|
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkEntity.addTimelineEvent(
|
||||||
|
roomId = roomId,
|
||||||
|
eventEntity = eventEntity,
|
||||||
|
direction = PaginationDirection.FORWARDS,
|
||||||
|
roomMemberContentsByUser = roomMemberContentsByUser
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,3 +120,20 @@ internal data class CreateRoomBody(
|
||||||
@Json(name = "room_version")
|
@Json(name = "room_version")
|
||||||
val roomVersion: String?
|
val roomVersion: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if the created room can be a direct chat one.
|
||||||
|
*
|
||||||
|
* @return true if it is a direct chat
|
||||||
|
*/
|
||||||
|
private fun CreateRoomBody.isDirect(): Boolean {
|
||||||
|
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && isDirect == true
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CreateRoomBody.getDirectUserId(): String? {
|
||||||
|
return if (isDirect()) {
|
||||||
|
invitedUserIds?.firstOrNull()
|
||||||
|
?: invite3pids?.firstOrNull()?.address
|
||||||
|
?: throw IllegalStateException("You can't create a direct room without an invitedUser")
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
|
@ -62,11 +62,6 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
||||||
) : CreateRoomTask {
|
) : CreateRoomTask {
|
||||||
|
|
||||||
override suspend fun execute(params: CreateRoomParams): String {
|
override suspend fun execute(params: CreateRoomParams): String {
|
||||||
val otherUserId = if (params.isDirect()) {
|
|
||||||
params.getFirstInvitedUserId()
|
|
||||||
?: throw IllegalStateException("You can't create a direct room without an invitedUser")
|
|
||||||
} else null
|
|
||||||
|
|
||||||
if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) {
|
if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) {
|
||||||
try {
|
try {
|
||||||
aliasAvailabilityChecker.check(params.roomAliasName)
|
aliasAvailabilityChecker.check(params.roomAliasName)
|
||||||
|
@ -111,14 +106,13 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
||||||
RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis()
|
RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherUserId != null) {
|
handleDirectChatCreation(roomId, createRoomBody.getDirectUserId())
|
||||||
handleDirectChatCreation(roomId, otherUserId)
|
|
||||||
}
|
|
||||||
setReadMarkers(roomId)
|
setReadMarkers(roomId)
|
||||||
return roomId
|
return roomId
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String) {
|
private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String?) {
|
||||||
|
otherUserId ?: return // This is not a direct room
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||||
this.directUserId = otherUserId
|
this.directUserId = otherUserId
|
||||||
|
@ -133,21 +127,4 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
||||||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
|
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
|
||||||
return readMarkersTask.execute(setReadMarkerParams)
|
return readMarkersTask.execute(setReadMarkerParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells if the created room can be a direct chat one.
|
|
||||||
*
|
|
||||||
* @return true if it is a direct chat
|
|
||||||
*/
|
|
||||||
private fun CreateRoomParams.isDirect(): Boolean {
|
|
||||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT &&
|
|
||||||
isDirect == true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the first invited user id
|
|
||||||
*/
|
|
||||||
private fun CreateRoomParams.getFirstInvitedUserId(): String? {
|
|
||||||
return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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.delete
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
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.CurrentStateEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
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.RoomSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
||||||
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface DeleteLocalRoomTask : Task<Params, Unit> {
|
||||||
|
data class Params(val roomId: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultDeleteLocalRoomTask @Inject constructor(
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
) : DeleteLocalRoomTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: Params) {
|
||||||
|
val roomId = params.roomId
|
||||||
|
|
||||||
|
if (RoomLocalEcho.isLocalEchoId(roomId)) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
Timber.i("## DeleteLocalRoomTask - delete local room id $roomId")
|
||||||
|
RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") }
|
||||||
|
?.deleteAllFromRealm()
|
||||||
|
CurrentStateEventEntity.whereRoomId(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - CurrentStateEventEntity - delete ${it.size} entries") }
|
||||||
|
?.deleteAllFromRealm()
|
||||||
|
EventEntity.whereRoomId(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - EventEntity - delete ${it.size} entries") }
|
||||||
|
?.deleteAllFromRealm()
|
||||||
|
TimelineEventEntity.whereRoomId(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - TimelineEventEntity - delete ${it.size} entries") }
|
||||||
|
?.forEach { it.deleteOnCascade(true) }
|
||||||
|
ChunkEntity.where(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - ChunkEntity - delete ${it.size} entries") }
|
||||||
|
?.forEach { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
|
||||||
|
RoomSummaryEntity.where(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - RoomSummaryEntity - delete ${it.size} entries") }
|
||||||
|
?.deleteAllFromRealm()
|
||||||
|
RoomEntity.where(realm, roomId = roomId).findAll()
|
||||||
|
?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") }
|
||||||
|
?.deleteAllFromRealm()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,11 @@ class DebugFeaturesStateFactory @Inject constructor(
|
||||||
key = DebugFeatureKeys.forceUsageOfOpusEncoder,
|
key = DebugFeatureKeys.forceUsageOfOpusEncoder,
|
||||||
factory = VectorFeatures::forceUsageOfOpusEncoder
|
factory = VectorFeatures::forceUsageOfOpusEncoder
|
||||||
),
|
),
|
||||||
|
createBooleanFeature(
|
||||||
|
label = "Start DM on first message",
|
||||||
|
key = DebugFeatureKeys.startDmOnFirstMsg,
|
||||||
|
factory = VectorFeatures::shouldStartDmOnFirstMessage
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,9 @@ class DebugVectorFeatures(
|
||||||
override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder)
|
override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder)
|
||||||
?: vectorFeatures.forceUsageOfOpusEncoder()
|
?: vectorFeatures.forceUsageOfOpusEncoder()
|
||||||
|
|
||||||
|
override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg)
|
||||||
|
?: vectorFeatures.shouldStartDmOnFirstMessage()
|
||||||
|
|
||||||
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
it.remove(key)
|
it.remove(key)
|
||||||
|
@ -127,4 +130,5 @@ object DebugFeatureKeys {
|
||||||
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
||||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||||
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
|
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
|
||||||
|
val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg")
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ interface VectorFeatures {
|
||||||
fun allowExternalUnifiedPushDistributors(): Boolean
|
fun allowExternalUnifiedPushDistributors(): Boolean
|
||||||
fun isScreenSharingEnabled(): Boolean
|
fun isScreenSharingEnabled(): Boolean
|
||||||
fun forceUsageOfOpusEncoder(): Boolean
|
fun forceUsageOfOpusEncoder(): Boolean
|
||||||
|
fun shouldStartDmOnFirstMessage(): Boolean
|
||||||
|
|
||||||
enum class OnboardingVariant {
|
enum class OnboardingVariant {
|
||||||
LEGACY,
|
LEGACY,
|
||||||
|
@ -50,4 +51,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
||||||
override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
|
override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
|
||||||
override fun isScreenSharingEnabled(): Boolean = true
|
override fun isScreenSharingEnabled(): Boolean = true
|
||||||
override fun forceUsageOfOpusEncoder(): Boolean = false
|
override fun forceUsageOfOpusEncoder(): Boolean = false
|
||||||
|
override fun shouldStartDmOnFirstMessage(): Boolean = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.userdirectory.PendingSelection
|
import im.vector.app.features.userdirectory.PendingSelection
|
||||||
|
|
||||||
sealed class CreateDirectRoomAction : VectorViewModelAction {
|
sealed class CreateDirectRoomAction : VectorViewModelAction {
|
||||||
data class CreateRoomAndInviteSelectedUsers(
|
data class PrepareRoomWithSelectedUsers(
|
||||||
val selections: Set<PendingSelection>
|
val selections: Set<PendingSelection>
|
||||||
) : CreateDirectRoomAction()
|
) : CreateDirectRoomAction()
|
||||||
|
|
||||||
|
object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction()
|
||||||
|
|
||||||
data class QrScannedAction(
|
data class QrScannedAction(
|
||||||
val result: String
|
val result: String
|
||||||
) : CreateDirectRoomAction()
|
) : CreateDirectRoomAction()
|
||||||
|
|
|
@ -161,7 +161,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnMenuItemSubmitClick(action: UserListSharedAction.OnMenuItemSubmitClick) {
|
private fun handleOnMenuItemSubmitClick(action: UserListSharedAction.OnMenuItemSubmitClick) {
|
||||||
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selections))
|
viewModel.handle(CreateDirectRoomAction.PrepareRoomWithSelectedUsers(action.selections))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderCreateAndInviteState(state: Async<String>) {
|
private fun renderCreateAndInviteState(state: Async<String>) {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.mvrx.runCatchingToAsync
|
import im.vector.app.core.mvrx.runCatchingToAsync
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.VectorFeatures
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||||
|
@ -46,7 +47,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: CreateDirectRoomViewState,
|
@Assisted initialState: CreateDirectRoomViewState,
|
||||||
private val rawService: RawService,
|
private val rawService: RawService,
|
||||||
val session: Session,
|
val session: Session,
|
||||||
val analyticsTracker: AnalyticsTracker
|
val analyticsTracker: AnalyticsTracker,
|
||||||
|
val vectorFeatures: VectorFeatures
|
||||||
) :
|
) :
|
||||||
VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
|
VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@ -59,7 +61,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun handle(action: CreateDirectRoomAction) {
|
override fun handle(action: CreateDirectRoomAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections)
|
is CreateDirectRoomAction.PrepareRoomWithSelectedUsers -> onSubmitInvitees(action.selections)
|
||||||
|
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onCreateRoomWithInvitees()
|
||||||
is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action)
|
is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,16 +97,18 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
if (existingRoomId != null) {
|
if (existingRoomId != null) {
|
||||||
// Do not create a new DM, just tell that the creation is successful by passing the existing roomId
|
// Do not create a new DM, just tell that the creation is successful by passing the existing roomId
|
||||||
setState {
|
setState { copy(createAndInviteState = Success(existingRoomId)) }
|
||||||
copy(createAndInviteState = Success(existingRoomId))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Create the DM
|
createLocalRoomWithSelectedUsers(selections)
|
||||||
createRoomAndInviteSelectedUsers(selections)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRoomAndInviteSelectedUsers(selections: Set<PendingSelection>) {
|
private fun onCreateRoomWithInvitees() {
|
||||||
|
// Create the DM
|
||||||
|
withState { createLocalRoomWithSelectedUsers(it.pendingSelections) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLocalRoomWithSelectedUsers(selections: Set<PendingSelection>) {
|
||||||
setState { copy(createAndInviteState = Loading()) }
|
setState { copy(createAndInviteState = Loading()) }
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
@ -124,8 +129,12 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = runCatchingToAsync {
|
val result = runCatchingToAsync {
|
||||||
|
if (vectorFeatures.shouldStartDmOnFirstMessage()) {
|
||||||
|
session.roomService().createLocalRoom(roomParams)
|
||||||
|
} else {
|
||||||
session.roomService().createRoom(roomParams)
|
session.roomService().createRoom(roomParams)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse()))
|
analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse()))
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
|
|
|
@ -19,7 +19,9 @@ package im.vector.app.features.createdirect
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.app.features.userdirectory.PendingSelection
|
||||||
|
|
||||||
data class CreateDirectRoomViewState(
|
data class CreateDirectRoomViewState(
|
||||||
|
val pendingSelections: Set<PendingSelection> = emptySet(),
|
||||||
val createAndInviteState: Async<String> = Uninitialized
|
val createAndInviteState: Async<String> = Uninitialized
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
||||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
import org.matrix.android.sdk.api.session.sync.SyncRequestState
|
import org.matrix.android.sdk.api.session.sync.SyncRequestState
|
||||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||||
|
@ -103,4 +104,6 @@ data class RoomDetailViewState(
|
||||||
fun isDm() = asyncRoomSummary()?.isDirect == true
|
fun isDm() = asyncRoomSummary()?.isDirect == true
|
||||||
|
|
||||||
fun isThreadTimeline() = rootThreadEventId != null
|
fun isThreadTimeline() = rootThreadEventId != null
|
||||||
|
|
||||||
|
fun isLocalRoom() = RoomLocalEcho.isLocalEchoId(roomId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1430,6 +1430,9 @@ class TimelineFragment @Inject constructor(
|
||||||
updateJumpToReadMarkerViewVisibility()
|
updateJumpToReadMarkerViewVisibility()
|
||||||
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
|
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
|
||||||
}
|
}
|
||||||
|
}.apply {
|
||||||
|
// For local rooms, pin the view's content to the top edge (the layout is reversed)
|
||||||
|
stackFromEnd = isLocalRoom()
|
||||||
}
|
}
|
||||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
|
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
|
||||||
|
@ -1701,7 +1704,26 @@ class TimelineFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderToolbar(roomSummary: RoomSummary?) {
|
private fun renderToolbar(roomSummary: RoomSummary?) {
|
||||||
if (!isThreadTimeLine()) {
|
when {
|
||||||
|
isLocalRoom() -> {
|
||||||
|
views.includeRoomToolbar.roomToolbarContentView.isVisible = false
|
||||||
|
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
||||||
|
setupToolbar(views.roomToolbar)
|
||||||
|
.setTitle(R.string.room_member_open_or_create_dm)
|
||||||
|
.allowBack(useCross = true)
|
||||||
|
}
|
||||||
|
isThreadTimeLine() -> {
|
||||||
|
views.includeRoomToolbar.roomToolbarContentView.isVisible = false
|
||||||
|
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true
|
||||||
|
timelineArgs.threadTimelineArgs?.let {
|
||||||
|
val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl)
|
||||||
|
avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView)
|
||||||
|
views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel)
|
||||||
|
views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName
|
||||||
|
}
|
||||||
|
views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
views.includeRoomToolbar.roomToolbarContentView.isVisible = true
|
views.includeRoomToolbar.roomToolbarContentView.isVisible = true
|
||||||
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
||||||
if (roomSummary == null) {
|
if (roomSummary == null) {
|
||||||
|
@ -1716,16 +1738,7 @@ class TimelineFragment @Inject constructor(
|
||||||
shieldView.render(roomSummary.roomEncryptionTrustLevel)
|
shieldView.render(roomSummary.roomEncryptionTrustLevel)
|
||||||
views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
|
views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
views.includeRoomToolbar.roomToolbarContentView.isVisible = false
|
|
||||||
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true
|
|
||||||
timelineArgs.threadTimelineArgs?.let {
|
|
||||||
val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl)
|
|
||||||
avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView)
|
|
||||||
views.includeThreadToolbar.roomToolbarThreadShieldImageView.render(it.roomEncryptionTrustLevel)
|
|
||||||
views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName
|
|
||||||
}
|
}
|
||||||
views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2660,6 +2673,11 @@ class TimelineFragment @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun isThreadTimeLine(): Boolean = timelineArgs.threadTimelineArgs?.rootThreadEventId != null
|
private fun isThreadTimeLine(): Boolean = timelineArgs.threadTimelineArgs?.rootThreadEventId != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the current room is a local room, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isLocalRoom(): Boolean = withState(timelineViewModel) { it.isLocalRoom() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the root thread event if we are in a thread room, otherwise returns null.
|
* Returns the root thread event if we are in a thread room, otherwise returns null.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -735,14 +735,17 @@ class TimelineViewModel @AssistedInject constructor(
|
||||||
return@withState false
|
return@withState false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialState.isThreadTimeline()) {
|
when {
|
||||||
|
initialState.isLocalRoom() -> false
|
||||||
|
initialState.isThreadTimeline() -> {
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.menu_thread_timeline_view_in_room,
|
R.id.menu_thread_timeline_view_in_room,
|
||||||
R.id.menu_thread_timeline_copy_link,
|
R.id.menu_thread_timeline_copy_link,
|
||||||
R.id.menu_thread_timeline_share -> true
|
R.id.menu_thread_timeline_share -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.timeline_setting -> true
|
R.id.timeline_setting -> true
|
||||||
R.id.invite -> state.canInvite
|
R.id.invite -> state.canInvite
|
||||||
|
@ -758,6 +761,7 @@ class TimelineViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
|
|
|
@ -603,14 +603,15 @@ class TimelineEventController @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
|
private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
|
||||||
return if (hasReachedInvite && hasUTD) {
|
return when {
|
||||||
true
|
hasReachedInvite && hasUTD -> true
|
||||||
} else {
|
else -> {
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
date.toLocalDate() != nextDate?.toLocalDate()
|
date.toLocalDate() != nextDate?.toLocalDate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if added.
|
* Return true if added.
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EncryptionItemFactory @Inject constructor(
|
class EncryptionItemFactory @Inject constructor(
|
||||||
|
@ -55,13 +56,20 @@ class EncryptionItemFactory @Inject constructor(
|
||||||
val description: String
|
val description: String
|
||||||
val shield: StatusTileTimelineItem.ShieldUIState
|
val shield: StatusTileTimelineItem.ShieldUIState
|
||||||
if (isSafeAlgorithm) {
|
if (isSafeAlgorithm) {
|
||||||
|
val isDirect = session.getRoomSummary(event.root.roomId.orEmpty())?.isDirect.orFalse()
|
||||||
title = stringProvider.getString(R.string.encryption_enabled)
|
title = stringProvider.getString(R.string.encryption_enabled)
|
||||||
description = stringProvider.getString(
|
description = stringProvider.getString(
|
||||||
if (session.getRoomSummary(event.root.roomId ?: "")?.isDirect.orFalse()) {
|
when {
|
||||||
|
isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> {
|
||||||
|
R.string.direct_room_encryption_enabled_tile_description_future
|
||||||
|
}
|
||||||
|
isDirect -> {
|
||||||
R.string.direct_room_encryption_enabled_tile_description
|
R.string.direct_room_encryption_enabled_tile_description
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
R.string.encryption_enabled_tile_description
|
R.string.encryption_enabled_tile_description
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
shield = StatusTileTimelineItem.ShieldUIState.BLACK
|
shield = StatusTileTimelineItem.ShieldUIState.BLACK
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -117,6 +117,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||||
highlighted = true
|
highlighted = true
|
||||||
}
|
}
|
||||||
val data = BasedMergedItem.Data(
|
val data = BasedMergedItem.Data(
|
||||||
|
roomId = mergedEvent.root.roomId,
|
||||||
userId = mergedEvent.root.senderId ?: "",
|
userId = mergedEvent.root.senderId ?: "",
|
||||||
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
||||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||||
|
@ -199,6 +200,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||||
highlighted = true
|
highlighted = true
|
||||||
}
|
}
|
||||||
val data = BasedMergedItem.Data(
|
val data = BasedMergedItem.Data(
|
||||||
|
roomId = mergedEvent.root.roomId,
|
||||||
userId = mergedEvent.root.senderId ?: "",
|
userId = mergedEvent.root.senderId ?: "",
|
||||||
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
||||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
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.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -176,6 +177,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide fake events for local rooms
|
||||||
|
if (RoomLocalEcho.isLocalEchoId(roomId) &&
|
||||||
|
root.getClearType() == EventType.STATE_ROOM_MEMBER ||
|
||||||
|
root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Allow only the the threads within the rootThreadEventId along with the root event
|
// Allow only the the threads within the rootThreadEventId along with the root event
|
||||||
if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) {
|
if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) {
|
||||||
return if (root.getRootThreadEventId() == rootThreadEventId) {
|
return if (root.getRootThreadEventId() == rootThreadEventId) {
|
||||||
|
|
|
@ -55,6 +55,7 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder>(@LayoutRes layoutId:
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
|
val roomId: String?,
|
||||||
val localId: Long,
|
val localId: Long,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val userId: String,
|
val userId: String,
|
||||||
|
|
|
@ -25,9 +25,9 @@ import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
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
|
||||||
|
@ -38,8 +38,10 @@ import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||||
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
|
@ -51,68 +53,104 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
var movementMethod: MovementMethod? = null
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
|
private val roomSummary
|
||||||
|
get() = attributes.roomSummary
|
||||||
|
|
||||||
|
private val isDirectRoom
|
||||||
|
get() = distinctMergeData.lastOrNull()?.isDirectRoom
|
||||||
|
?: roomSummary?.isDirect
|
||||||
|
?: false
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
|
|
||||||
bindCreationSummaryTile(holder)
|
bindCreationSummaryTile(holder)
|
||||||
|
bindMergedViews(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindMergedViews(holder: Holder) {
|
||||||
|
holder.mergedView.isVisible = !attributes.isLocalRoom
|
||||||
if (attributes.isCollapsed) {
|
if (attributes.isCollapsed) {
|
||||||
// Take the oldest data
|
// Take the oldest data
|
||||||
val data = distinctMergeData.lastOrNull()
|
val data = distinctMergeData.lastOrNull()
|
||||||
|
renderSummaryText(holder, data)
|
||||||
val createdFromCurrentUser = data?.userId == attributes.currentUserId
|
|
||||||
val summary = if (createdFromCurrentUser) {
|
|
||||||
if (data?.isDirectRoom == true) {
|
|
||||||
holder.expandView.resources.getString(R.string.direct_room_created_summary_item_by_you)
|
|
||||||
} else {
|
|
||||||
holder.expandView.resources.getString(R.string.room_created_summary_item_by_you)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (data?.isDirectRoom == true) {
|
|
||||||
holder.expandView.resources.getString(R.string.direct_room_created_summary_item, data.memberName)
|
|
||||||
} else {
|
|
||||||
holder.expandView.resources.getString(R.string.room_created_summary_item, data?.memberName ?: data?.userId ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.summaryView.text = summary
|
|
||||||
holder.summaryView.visibility = View.VISIBLE
|
holder.summaryView.visibility = View.VISIBLE
|
||||||
holder.avatarView.visibility = View.VISIBLE
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
holder.avatarView.visibility = View.VISIBLE
|
holder.avatarView.visibility = View.VISIBLE
|
||||||
attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
||||||
} else {
|
} else {
|
||||||
holder.avatarView.visibility = View.GONE
|
holder.avatarView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
bindEncryptionTile(holder)
|
||||||
bindEncryptionTile(holder, data)
|
|
||||||
} else {
|
} else {
|
||||||
holder.avatarView.visibility = View.INVISIBLE
|
holder.avatarView.visibility = View.INVISIBLE
|
||||||
holder.summaryView.visibility = View.GONE
|
holder.summaryView.visibility = View.GONE
|
||||||
holder.encryptionTile.isGone = true
|
holder.encryptionTile.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindEncryptionTile(holder: Holder, data: Data?) {
|
private fun renderSummaryText(holder: Holder, data: Data?) {
|
||||||
|
val resources = holder.expandView.resources
|
||||||
|
val createdFromCurrentUser = data?.userId == attributes.currentUserId
|
||||||
|
val summary = if (createdFromCurrentUser) {
|
||||||
|
if (isDirectRoom) {
|
||||||
|
resources.getString(R.string.direct_room_created_summary_item_by_you)
|
||||||
|
} else {
|
||||||
|
resources.getString(R.string.room_created_summary_item_by_you)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isDirectRoom) {
|
||||||
|
resources.getString(R.string.direct_room_created_summary_item, data?.memberName.orEmpty())
|
||||||
|
} else {
|
||||||
|
resources.getString(R.string.room_created_summary_item, data?.memberName.orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.summaryView.text = summary
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindEncryptionTile(holder: Holder) {
|
||||||
if (attributes.hasEncryptionEvent) {
|
if (attributes.hasEncryptionEvent) {
|
||||||
holder.encryptionTile.isVisible = true
|
holder.encryptionTile.isVisible = true
|
||||||
holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
this.marginEnd = leftGuideline
|
this.marginEnd = leftGuideline
|
||||||
}
|
}
|
||||||
if (attributes.isEncryptionAlgorithmSecure) {
|
if (attributes.isEncryptionAlgorithmSecure) {
|
||||||
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
renderE2ESecureTile(holder)
|
||||||
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
|
|
||||||
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
|
|
||||||
} else {
|
} else {
|
||||||
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
renderE2EUnsecureTile(holder)
|
||||||
}
|
}
|
||||||
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
} else {
|
||||||
|
holder.encryptionTile.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderE2ESecureTile(holder: Holder) {
|
||||||
|
val resources = holder.expandView.resources
|
||||||
|
val description = when {
|
||||||
|
isDirectRoom -> {
|
||||||
|
if (attributes.isLocalRoom) {
|
||||||
|
resources.getString(R.string.direct_room_encryption_enabled_tile_description_future)
|
||||||
|
} else {
|
||||||
|
resources.getString(R.string.direct_room_encryption_enabled_tile_description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
resources.getString(R.string.encryption_enabled_tile_description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||||
null, null, null
|
null, null, null
|
||||||
)
|
)
|
||||||
} else {
|
holder.e2eTitleDescriptionView.text = description
|
||||||
|
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderE2EUnsecureTile(holder: Holder) {
|
||||||
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
|
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
|
||||||
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
@ -120,57 +158,18 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||||
null, null, null
|
null, null, null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
holder.encryptionTile.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindCreationSummaryTile(holder: Holder) {
|
private fun bindCreationSummaryTile(holder: Holder) {
|
||||||
val roomSummary = attributes.roomSummary
|
|
||||||
val roomDisplayName = roomSummary?.displayName
|
val roomDisplayName = roomSummary?.displayName
|
||||||
holder.roomNameText.setTextOrHide(roomDisplayName)
|
|
||||||
val isDirect = roomSummary?.isDirect == true
|
|
||||||
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
|
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
|
||||||
|
|
||||||
if (isDirect) {
|
holder.roomNameText.setTextOrHide(roomDisplayName)
|
||||||
holder.roomDescriptionText.text = holder.view.resources.getString(
|
renderRoomDescription(holder)
|
||||||
R.string.this_is_the_beginning_of_dm,
|
renderRoomTopic(holder)
|
||||||
roomSummary?.displayName ?: ""
|
|
||||||
)
|
|
||||||
} else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) {
|
|
||||||
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
|
|
||||||
} else {
|
|
||||||
holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val topic = roomSummary?.topic
|
|
||||||
if (topic.isNullOrBlank()) {
|
|
||||||
// do not show hint for DMs or group DMs
|
|
||||||
val canSetTopic = attributes.canChangeTopic && !isDirect
|
|
||||||
if (canSetTopic) {
|
|
||||||
val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
|
|
||||||
val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
|
|
||||||
holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holder.roomTopicText.setTextOrHide(
|
|
||||||
span {
|
|
||||||
span(holder.view.resources.getString(R.string.topic_prefix)) {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+topic.linkify(attributes.callback)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
holder.roomTopicText.movementMethod = movementMethod
|
|
||||||
|
|
||||||
val roomItem = roomSummary?.toMatrixItem()
|
val roomItem = roomSummary?.toMatrixItem()
|
||||||
val shouldSetAvatar = attributes.canChangeAvatar &&
|
val shouldSetAvatar = attributes.canChangeAvatar &&
|
||||||
(roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) &&
|
(roomSummary?.isDirect == false || (isDirectRoom && membersCount >= 2)) &&
|
||||||
roomItem?.avatarUrl.isNullOrBlank()
|
roomItem?.avatarUrl.isNullOrBlank()
|
||||||
|
|
||||||
holder.roomAvatarImageView.isVisible = roomItem != null
|
holder.roomAvatarImageView.isVisible = roomItem != null
|
||||||
|
@ -193,7 +192,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val canInvite = attributes.canInvite && !isDirect
|
val canInvite = attributes.canInvite && !isDirectRoom
|
||||||
holder.addPeopleButton.isVisible = canInvite
|
holder.addPeopleButton.isVisible = canInvite
|
||||||
if (canInvite) {
|
if (canInvite) {
|
||||||
holder.addPeopleButton.onClick {
|
holder.addPeopleButton.onClick {
|
||||||
|
@ -202,7 +201,60 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun renderRoomDescription(holder: Holder) {
|
||||||
|
val roomDisplayName = roomSummary?.displayName
|
||||||
|
val resources = holder.roomDescriptionText.resources
|
||||||
|
val description = when {
|
||||||
|
isDirectRoom -> {
|
||||||
|
if (attributes.isLocalRoom) {
|
||||||
|
resources.getString(R.string.send_your_first_msg_to_invite, roomSummary?.displayName.orEmpty())
|
||||||
|
} else {
|
||||||
|
resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName.orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomDisplayName.isNullOrBlank() || roomSummary?.name.isNullOrBlank() -> {
|
||||||
|
holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.roomDescriptionText.text = description
|
||||||
|
if (isDirectRoom && attributes.isLocalRoom) {
|
||||||
|
TextViewCompat.setTextAppearance(holder.roomDescriptionText, R.style.TextAppearance_Vector_Subtitle)
|
||||||
|
holder.roomDescriptionText.setTextColor(ThemeUtils.getColor(holder.roomDescriptionText.context, R.attr.vctr_content_primary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRoomTopic(holder: Holder) {
|
||||||
|
val topic = roomSummary?.topic
|
||||||
|
if (topic.isNullOrBlank()) {
|
||||||
|
// do not show hint for DMs or group DMs
|
||||||
|
val canSetTopic = attributes.canChangeTopic && !isDirectRoom
|
||||||
|
if (canSetTopic) {
|
||||||
|
val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text)
|
||||||
|
val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink))
|
||||||
|
holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.roomTopicText.setTextOrHide(
|
||||||
|
span {
|
||||||
|
span(holder.view.resources.getString(R.string.topic_prefix)) {
|
||||||
|
textStyle = "bold"
|
||||||
|
}
|
||||||
|
+topic.linkify(attributes.callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
holder.roomTopicText.movementMethod = movementMethod
|
||||||
|
}
|
||||||
|
|
||||||
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||||
|
val mergedView by bind<View>(R.id.mergedSumContainer)
|
||||||
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
||||||
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)
|
val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)
|
||||||
|
@ -236,5 +288,8 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
||||||
val canChangeAvatar: Boolean = false,
|
val canChangeAvatar: Boolean = false,
|
||||||
val canChangeName: Boolean = false,
|
val canChangeName: Boolean = false,
|
||||||
val canChangeTopic: Boolean = false
|
val canChangeTopic: Boolean = false
|
||||||
) : BasedMergedItem.Attributes
|
) : BasedMergedItem.Attributes {
|
||||||
|
|
||||||
|
val isLocalRoom = RoomLocalEcho.isLocalEchoId(roomSummary?.roomId.orEmpty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,10 @@ class RoomListFragment @Inject constructor(
|
||||||
(it.contentEpoxyController as? RoomSummaryPagedController)?.roomChangeMembershipStates = ms
|
(it.contentEpoxyController as? RoomSummaryPagedController)?.roomChangeMembershipStates = ms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
roomListViewModel.onEach(RoomListViewState::localRoomIds) {
|
||||||
|
// Local rooms should not exist anymore when the room list is shown
|
||||||
|
roomListViewModel.deleteLocalRooms(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshCollapseStates() {
|
private fun refreshCollapseStates() {
|
||||||
|
|
|
@ -48,7 +48,10 @@ import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
|
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
|
||||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
||||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||||
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.state.isPublic
|
import org.matrix.android.sdk.api.session.room.state.isPublic
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.flow.flow
|
import org.matrix.android.sdk.flow.flow
|
||||||
|
@ -96,6 +99,7 @@ class RoomListViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observeMembershipChanges()
|
observeMembershipChanges()
|
||||||
|
observeLocalRooms()
|
||||||
|
|
||||||
appStateHandler.selectedRoomGroupingFlow
|
appStateHandler.selectedRoomGroupingFlow
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
@ -123,6 +127,23 @@ class RoomListViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeLocalRooms() {
|
||||||
|
val queryParams = roomSummaryQueryParams {
|
||||||
|
memberships = listOf(Membership.JOIN)
|
||||||
|
}
|
||||||
|
session
|
||||||
|
.flow()
|
||||||
|
.liveRoomSummaries(queryParams)
|
||||||
|
.map { roomSummaries ->
|
||||||
|
roomSummaries.mapNotNull { summary ->
|
||||||
|
summary.roomId.takeIf { RoomLocalEcho.isLocalEchoId(it) }
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
.setOnEach { roomIds ->
|
||||||
|
copy(localRoomIds = roomIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
|
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
|
private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
|
||||||
|
@ -173,6 +194,14 @@ class RoomListViewModel @AssistedInject constructor(
|
||||||
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
|
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteLocalRooms(roomsIds: Set<String>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
roomsIds.forEach {
|
||||||
|
session.roomService().deleteLocalRoom(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
||||||
|
|
|
@ -30,7 +30,8 @@ data class RoomListViewState(
|
||||||
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
|
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
|
||||||
val asyncSuggestedRooms: Async<List<SpaceChildInfo>> = Uninitialized,
|
val asyncSuggestedRooms: Async<List<SpaceChildInfo>> = Uninitialized,
|
||||||
val currentUserName: String? = null,
|
val currentUserName: String? = null,
|
||||||
val currentRoomGrouping: Async<RoomGroupingMethod> = Uninitialized
|
val currentRoomGrouping: Async<RoomGroupingMethod> = Uninitialized,
|
||||||
|
val localRoomIds: Set<String> = emptySet()
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
|
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
android:id="@+id/userListToolbar"
|
android:id="@+id/userListToolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?actionBarSize"
|
android:layout_height="?actionBarSize"
|
||||||
app:title="@string/fab_menu_create_chat"/>
|
android:paddingEnd="@dimen/layout_horizontal_margin"
|
||||||
|
app:title="@string/fab_menu_create_chat"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_create_direct_room"
|
android:id="@+id/action_create_direct_room"
|
||||||
android:title="@string/create_room_action_create"
|
android:title="@string/create_room_action_go"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -1624,6 +1624,7 @@
|
||||||
|
|
||||||
<!-- Create room screen -->
|
<!-- Create room screen -->
|
||||||
<string name="create_room_action_create">"CREATE"</string>
|
<string name="create_room_action_create">"CREATE"</string>
|
||||||
|
<string name="create_room_action_go">Go</string>
|
||||||
<string name="create_room_name_section">"Room name"</string>
|
<string name="create_room_name_section">"Room name"</string>
|
||||||
<string name="create_room_name_hint">"Name"</string>
|
<string name="create_room_name_hint">"Name"</string>
|
||||||
<string name="create_room_topic_section">"Room topic (optional)"</string>
|
<string name="create_room_topic_section">"Room topic (optional)"</string>
|
||||||
|
@ -2402,7 +2403,8 @@
|
||||||
|
|
||||||
<string name="encryption_enabled">Encryption enabled</string>
|
<string name="encryption_enabled">Encryption enabled</string>
|
||||||
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more & verify users in their profile.</string>
|
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more & verify users in their profile.</string>
|
||||||
<string name="direct_room_encryption_enabled_tile_description">Messages in this room are end-to-end encrypted.</string>
|
<string name="direct_room_encryption_enabled_tile_description">Messages in this chat are end-to-end encrypted.</string>
|
||||||
|
<string name="direct_room_encryption_enabled_tile_description_future">Messages in this chat will be end-to-end encrypted.</string>
|
||||||
<string name="encryption_not_enabled">Encryption not enabled</string>
|
<string name="encryption_not_enabled">Encryption not enabled</string>
|
||||||
<string name="encryption_misconfigured">Encryption is misconfigured</string>
|
<string name="encryption_misconfigured">Encryption is misconfigured</string>
|
||||||
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
|
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
|
||||||
|
@ -2414,6 +2416,7 @@
|
||||||
<string name="this_is_the_beginning_of_room">This is the beginning of %s.</string>
|
<string name="this_is_the_beginning_of_room">This is the beginning of %s.</string>
|
||||||
<string name="this_is_the_beginning_of_room_no_name">This is the beginning of this conversation.</string>
|
<string name="this_is_the_beginning_of_room_no_name">This is the beginning of this conversation.</string>
|
||||||
<string name="this_is_the_beginning_of_dm">This is the beginning of your direct message history with %s.</string>
|
<string name="this_is_the_beginning_of_dm">This is the beginning of your direct message history with %s.</string>
|
||||||
|
<string name="send_your_first_msg_to_invite">Send your first message to invite %s to chat</string>
|
||||||
<!-- First param will be replaced by the value of add_a_topic_link_text, that will be clickable-->
|
<!-- First param will be replaced by the value of add_a_topic_link_text, that will be clickable-->
|
||||||
<string name="room_created_summary_no_topic_creation_text">%s to let people know what this room is about.</string>
|
<string name="room_created_summary_no_topic_creation_text">%s to let people know what this room is about.</string>
|
||||||
<string name="add_a_topic_link_text">Add a topic</string>
|
<string name="add_a_topic_link_text">Add a topic</string>
|
||||||
|
|
Loading…
Reference in New Issue