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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
|
@ -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.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(
|
||||
realm: Realm,
|
||||
roomId: String,
|
||||
type: String
|
||||
): RealmQuery<CurrentStateEventEntity> {
|
||||
return realm.where(CurrentStateEventEntity::class.java)
|
||||
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
|
||||
return whereRoomId(realm = realm, roomId = roomId)
|
||||
.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.session.room.alias.DeleteRoomAliasTask
|
||||
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.delete.DeleteLocalRoomTask
|
||||
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.joining.JoinRoomTask
|
||||
|
@ -60,6 +62,8 @@ import javax.inject.Inject
|
|||
internal class DefaultRoomService @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val createRoomTask: CreateRoomTask,
|
||||
private val createLocalRoomTask: CreateLocalRoomTask,
|
||||
private val deleteLocalRoomTask: DeleteLocalRoomTask,
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
||||
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
|
||||
|
@ -78,6 +82,14 @@ internal class DefaultRoomService @Inject constructor(
|
|||
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? {
|
||||
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.GetRoomIdByAliasTask
|
||||
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.DefaultCreateLocalRoomTask
|
||||
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.DefaultGetRoomDirectoryVisibilityTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
|
||||
|
@ -204,6 +208,12 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
|
||||
|
||||
@Binds
|
||||
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")
|
||||
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 {
|
||||
|
||||
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) {
|
||||
try {
|
||||
aliasAvailabilityChecker.check(params.roomAliasName)
|
||||
|
@ -111,14 +106,13 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||
RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis()
|
||||
}
|
||||
|
||||
if (otherUserId != null) {
|
||||
handleDirectChatCreation(roomId, otherUserId)
|
||||
}
|
||||
handleDirectChatCreation(roomId, createRoomBody.getDirectUserId())
|
||||
setReadMarkers(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 ->
|
||||
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||
this.directUserId = otherUserId
|
||||
|
@ -133,21 +127,4 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
|
||||
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,
|
||||
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)
|
||||
?: vectorFeatures.forceUsageOfOpusEncoder()
|
||||
|
||||
override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg)
|
||||
?: vectorFeatures.shouldStartDmOnFirstMessage()
|
||||
|
||||
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
||||
if (value == null) {
|
||||
it.remove(key)
|
||||
|
@ -127,4 +130,5 @@ object DebugFeatureKeys {
|
|||
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||
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 isScreenSharingEnabled(): Boolean
|
||||
fun forceUsageOfOpusEncoder(): Boolean
|
||||
fun shouldStartDmOnFirstMessage(): Boolean
|
||||
|
||||
enum class OnboardingVariant {
|
||||
LEGACY,
|
||||
|
@ -50,4 +51,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
|||
override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
|
||||
override fun isScreenSharingEnabled(): Boolean = true
|
||||
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
|
||||
|
||||
sealed class CreateDirectRoomAction : VectorViewModelAction {
|
||||
data class CreateRoomAndInviteSelectedUsers(
|
||||
data class PrepareRoomWithSelectedUsers(
|
||||
val selections: Set<PendingSelection>
|
||||
) : CreateDirectRoomAction()
|
||||
|
||||
object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction()
|
||||
|
||||
data class QrScannedAction(
|
||||
val result: String
|
||||
) : CreateDirectRoomAction()
|
||||
|
|
|
@ -161,7 +161,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
|||
}
|
||||
|
||||
private fun handleOnMenuItemSubmitClick(action: UserListSharedAction.OnMenuItemSubmitClick) {
|
||||
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selections))
|
||||
viewModel.handle(CreateDirectRoomAction.PrepareRoomWithSelectedUsers(action.selections))
|
||||
}
|
||||
|
||||
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.mvrx.runCatchingToAsync
|
||||
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.plan.CreatedRoom
|
||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||
|
@ -46,7 +47,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
|||
@Assisted initialState: CreateDirectRoomViewState,
|
||||
private val rawService: RawService,
|
||||
val session: Session,
|
||||
val analyticsTracker: AnalyticsTracker
|
||||
val analyticsTracker: AnalyticsTracker,
|
||||
val vectorFeatures: VectorFeatures
|
||||
) :
|
||||
VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
|
||||
|
||||
|
@ -59,7 +61,8 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
|||
|
||||
override fun handle(action: CreateDirectRoomAction) {
|
||||
when (action) {
|
||||
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections)
|
||||
is CreateDirectRoomAction.PrepareRoomWithSelectedUsers -> onSubmitInvitees(action.selections)
|
||||
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onCreateRoomWithInvitees()
|
||||
is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action)
|
||||
}
|
||||
}
|
||||
|
@ -94,16 +97,18 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
|||
}
|
||||
if (existingRoomId != null) {
|
||||
// Do not create a new DM, just tell that the creation is successful by passing the existing roomId
|
||||
setState {
|
||||
copy(createAndInviteState = Success(existingRoomId))
|
||||
}
|
||||
setState { copy(createAndInviteState = Success(existingRoomId)) }
|
||||
} else {
|
||||
// Create the DM
|
||||
createRoomAndInviteSelectedUsers(selections)
|
||||
createLocalRoomWithSelectedUsers(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()) }
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
@ -124,7 +129,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
val result = runCatchingToAsync {
|
||||
session.roomService().createRoom(roomParams)
|
||||
if (vectorFeatures.shouldStartDmOnFirstMessage()) {
|
||||
session.roomService().createLocalRoom(roomParams)
|
||||
} else {
|
||||
session.roomService().createRoom(roomParams)
|
||||
}
|
||||
}
|
||||
analyticsTracker.capture(CreatedRoom(isDM = roomParams.isDirect.orFalse()))
|
||||
|
||||
|
|
|
@ -19,7 +19,9 @@ package im.vector.app.features.createdirect
|
|||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
|
||||
data class CreateDirectRoomViewState(
|
||||
val pendingSelections: Set<PendingSelection> = emptySet(),
|
||||
val createAndInviteState: Async<String> = Uninitialized
|
||||
) : 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.model.RoomMemberSummary
|
||||
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.sync.SyncRequestState
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
|
@ -103,4 +104,6 @@ data class RoomDetailViewState(
|
|||
fun isDm() = asyncRoomSummary()?.isDirect == true
|
||||
|
||||
fun isThreadTimeline() = rootThreadEventId != null
|
||||
|
||||
fun isLocalRoom() = RoomLocalEcho.isLocalEchoId(roomId)
|
||||
}
|
||||
|
|
|
@ -1430,6 +1430,9 @@ class TimelineFragment @Inject constructor(
|
|||
updateJumpToReadMarkerViewVisibility()
|
||||
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()
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
|
||||
|
@ -1701,31 +1704,41 @@ class TimelineFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun renderToolbar(roomSummary: RoomSummary?) {
|
||||
if (!isThreadTimeLine()) {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isVisible = true
|
||||
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
||||
if (roomSummary == null) {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isClickable = false
|
||||
} else {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
|
||||
views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
|
||||
avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
|
||||
val showPresence = roomSummary.isDirect
|
||||
views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence)
|
||||
val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield
|
||||
shieldView.render(roomSummary.roomEncryptionTrustLevel)
|
||||
views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
|
||||
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)
|
||||
}
|
||||
} 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
|
||||
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.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
||||
if (roomSummary == null) {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isClickable = false
|
||||
} else {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
|
||||
views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName
|
||||
avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView)
|
||||
val showPresence = roomSummary.isDirect
|
||||
views.includeRoomToolbar.roomToolbarPresenceImageView.render(showPresence, roomSummary.directUserPresence)
|
||||
val shieldView = if (showPresence) views.includeRoomToolbar.roomToolbarTitleShield else views.includeRoomToolbar.roomToolbarAvatarShield
|
||||
shieldView.render(roomSummary.roomEncryptionTrustLevel)
|
||||
views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
|
@ -735,26 +735,30 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
return@withState false
|
||||
}
|
||||
|
||||
if (initialState.isThreadTimeline()) {
|
||||
when (itemId) {
|
||||
R.id.menu_thread_timeline_view_in_room,
|
||||
R.id.menu_thread_timeline_copy_link,
|
||||
R.id.menu_thread_timeline_share -> true
|
||||
else -> false
|
||||
when {
|
||||
initialState.isLocalRoom() -> false
|
||||
initialState.isThreadTimeline() -> {
|
||||
when (itemId) {
|
||||
R.id.menu_thread_timeline_view_in_room,
|
||||
R.id.menu_thread_timeline_copy_link,
|
||||
R.id.menu_thread_timeline_share -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (itemId) {
|
||||
R.id.timeline_setting -> true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call -> state.isCallOptionAvailable()
|
||||
R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
|
||||
// Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
|
||||
R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
|
||||
R.id.search -> state.isSearchAvailable()
|
||||
R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
|
||||
R.id.dev_tools -> vectorPreferences.developerMode()
|
||||
else -> false
|
||||
else -> {
|
||||
when (itemId) {
|
||||
R.id.timeline_setting -> true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call -> state.isCallOptionAvailable()
|
||||
R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
|
||||
// Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
|
||||
R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
|
||||
R.id.search -> state.isSearchAvailable()
|
||||
R.id.menu_timeline_thread_list -> vectorPreferences.areThreadMessagesEnabled()
|
||||
R.id.dev_tools -> vectorPreferences.developerMode()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -603,12 +603,13 @@ class TimelineEventController @Inject constructor(
|
|||
}
|
||||
|
||||
private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
|
||||
return if (hasReachedInvite && hasUTD) {
|
||||
true
|
||||
} else {
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
date.toLocalDate() != nextDate?.toLocalDate()
|
||||
return when {
|
||||
hasReachedInvite && hasUTD -> true
|
||||
else -> {
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
date.toLocalDate() != nextDate?.toLocalDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
||||
import javax.inject.Inject
|
||||
|
||||
class EncryptionItemFactory @Inject constructor(
|
||||
|
@ -55,12 +56,19 @@ class EncryptionItemFactory @Inject constructor(
|
|||
val description: String
|
||||
val shield: StatusTileTimelineItem.ShieldUIState
|
||||
if (isSafeAlgorithm) {
|
||||
val isDirect = session.getRoomSummary(event.root.roomId.orEmpty())?.isDirect.orFalse()
|
||||
title = stringProvider.getString(R.string.encryption_enabled)
|
||||
description = stringProvider.getString(
|
||||
if (session.getRoomSummary(event.root.roomId ?: "")?.isDirect.orFalse()) {
|
||||
R.string.direct_room_encryption_enabled_tile_description
|
||||
} else {
|
||||
R.string.encryption_enabled_tile_description
|
||||
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
|
||||
}
|
||||
else -> {
|
||||
R.string.encryption_enabled_tile_description
|
||||
}
|
||||
}
|
||||
)
|
||||
shield = StatusTileTimelineItem.ShieldUIState.BLACK
|
||||
|
|
|
@ -117,6 +117,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||
highlighted = true
|
||||
}
|
||||
val data = BasedMergedItem.Data(
|
||||
roomId = mergedEvent.root.roomId,
|
||||
userId = mergedEvent.root.senderId ?: "",
|
||||
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
|
@ -199,6 +200,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||
highlighted = true
|
||||
}
|
||||
val data = BasedMergedItem.Data(
|
||||
roomId = mergedEvent.root.roomId,
|
||||
userId = mergedEvent.root.senderId ?: "",
|
||||
avatarUrl = mergedEvent.senderInfo.avatarUrl,
|
||||
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.room.model.Membership
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
@ -176,6 +177,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||
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
|
||||
if (userPreferencesProvider.areThreadMessagesEnabled() && isFromThreadTimeline) {
|
||||
return if (root.getRootThreadEventId() == rootThreadEventId) {
|
||||
|
|
|
@ -55,6 +55,7 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder>(@LayoutRes layoutId:
|
|||
}
|
||||
|
||||
data class Data(
|
||||
val roomId: String?,
|
||||
val localId: Long,
|
||||
val eventId: String,
|
||||
val userId: String,
|
||||
|
|
|
@ -25,9 +25,9 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
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.timeline.TimelineEventController
|
||||
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 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
|
||||
|
||||
@EpoxyModelClass
|
||||
|
@ -51,126 +53,123 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
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 bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
bindCreationSummaryTile(holder)
|
||||
bindMergedViews(holder)
|
||||
}
|
||||
|
||||
private fun bindMergedViews(holder: Holder) {
|
||||
holder.mergedView.isVisible = !attributes.isLocalRoom
|
||||
if (attributes.isCollapsed) {
|
||||
// Take the oldest data
|
||||
val data = distinctMergeData.lastOrNull()
|
||||
|
||||
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
|
||||
renderSummaryText(holder, data)
|
||||
holder.summaryView.visibility = View.VISIBLE
|
||||
holder.avatarView.visibility = View.VISIBLE
|
||||
if (data != null) {
|
||||
holder.avatarView.visibility = View.VISIBLE
|
||||
attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
||||
} else {
|
||||
holder.avatarView.visibility = View.GONE
|
||||
}
|
||||
|
||||
bindEncryptionTile(holder, data)
|
||||
bindEncryptionTile(holder)
|
||||
} else {
|
||||
holder.avatarView.visibility = View.INVISIBLE
|
||||
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) {
|
||||
holder.encryptionTile.isVisible = true
|
||||
holder.encryptionTile.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
this.marginEnd = leftGuideline
|
||||
}
|
||||
if (attributes.isEncryptionAlgorithmSecure) {
|
||||
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||
holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) {
|
||||
holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description)
|
||||
} else {
|
||||
holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||
}
|
||||
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||
null, null, null
|
||||
)
|
||||
renderE2ESecureTile(holder)
|
||||
} else {
|
||||
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.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
|
||||
null, null, null
|
||||
)
|
||||
renderE2EUnsecureTile(holder)
|
||||
}
|
||||
} 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(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||
null, null, null
|
||||
)
|
||||
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.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
|
||||
null, null, null
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindCreationSummaryTile(holder: Holder) {
|
||||
val roomSummary = attributes.roomSummary
|
||||
val roomDisplayName = roomSummary?.displayName
|
||||
holder.roomNameText.setTextOrHide(roomDisplayName)
|
||||
val isDirect = roomSummary?.isDirect == true
|
||||
val membersCount = roomSummary?.otherMemberIds?.size ?: 0
|
||||
|
||||
if (isDirect) {
|
||||
holder.roomDescriptionText.text = holder.view.resources.getString(
|
||||
R.string.this_is_the_beginning_of_dm,
|
||||
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
|
||||
holder.roomNameText.setTextOrHide(roomDisplayName)
|
||||
renderRoomDescription(holder)
|
||||
renderRoomTopic(holder)
|
||||
|
||||
val roomItem = roomSummary?.toMatrixItem()
|
||||
val shouldSetAvatar = attributes.canChangeAvatar &&
|
||||
(roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) &&
|
||||
(roomSummary?.isDirect == false || (isDirectRoom && membersCount >= 2)) &&
|
||||
roomItem?.avatarUrl.isNullOrBlank()
|
||||
|
||||
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
|
||||
if (canInvite) {
|
||||
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) {
|
||||
val mergedView by bind<View>(R.id.mergedSumContainer)
|
||||
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)
|
||||
|
@ -236,5 +288,8 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||
val canChangeAvatar: Boolean = false,
|
||||
val canChangeName: 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
|
||||
}
|
||||
}
|
||||
roomListViewModel.onEach(RoomListViewState::localRoomIds) {
|
||||
// Local rooms should not exist anymore when the room list is shown
|
||||
roomListViewModel.deleteLocalRooms(it)
|
||||
}
|
||||
}
|
||||
|
||||
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.room.UpdatableLivePageResult
|
||||
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.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.state.isPublic
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
|
@ -96,6 +99,7 @@ class RoomListViewModel @AssistedInject constructor(
|
|||
|
||||
init {
|
||||
observeMembershipChanges()
|
||||
observeLocalRooms()
|
||||
|
||||
appStateHandler.selectedRoomGroupingFlow
|
||||
.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()
|
||||
|
||||
private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
|
||||
|
@ -173,6 +194,14 @@ class RoomListViewModel @AssistedInject constructor(
|
|||
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
|
||||
}
|
||||
|
||||
fun deleteLocalRooms(roomsIds: Set<String>) {
|
||||
viewModelScope.launch {
|
||||
roomsIds.forEach {
|
||||
session.roomService().deleteLocalRoom(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
||||
|
|
|
@ -30,7 +30,8 @@ data class RoomListViewState(
|
|||
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
|
||||
val asyncSuggestedRooms: Async<List<SpaceChildInfo>> = Uninitialized,
|
||||
val currentUserName: String? = null,
|
||||
val currentRoomGrouping: Async<RoomGroupingMethod> = Uninitialized
|
||||
val currentRoomGrouping: Async<RoomGroupingMethod> = Uninitialized,
|
||||
val localRoomIds: Set<String> = emptySet()
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: RoomListParams) : this(displayMode = args.displayMode)
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
android:id="@+id/userListToolbar"
|
||||
android:layout_width="match_parent"
|
||||
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>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/action_create_direct_room"
|
||||
android:title="@string/create_room_action_create"
|
||||
android:title="@string/create_room_action_go"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -1624,6 +1624,7 @@
|
|||
|
||||
<!-- Create room screen -->
|
||||
<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_hint">"Name"</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_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_misconfigured">Encryption is misconfigured</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_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="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-->
|
||||
<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>
|
||||
|
|
Loading…
Reference in New Issue