Merge pull request #3105 from vector-im/feature/bma/displayname_fallback

Displayname and avatar fallback for DM, especially when other has left
This commit is contained in:
Benoit Marty 2021-04-06 14:31:41 +02:00 committed by GitHub
commit 0ede779ee5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 103 additions and 49 deletions

View File

@ -14,6 +14,7 @@ Improvements 🙌:
- Update reactions to Unicode 13.1 (#2998) - Update reactions to Unicode 13.1 (#2998)
- Be more robust when parsing some enums - Be more robust when parsing some enums
- Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior) - Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior)
- Add better support for empty room name fallback (#3106)
- Room list improvements (paging) - Room list improvements (paging)
Bugfix 🐛: Bugfix 🐛:

View File

@ -18,12 +18,12 @@ package org.matrix.android.sdk.common
import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider
class TestRoomDisplayNameFallbackProvider() : RoomDisplayNameFallbackProvider { class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {
override fun getNameForRoomInvite() = override fun getNameForRoomInvite() =
"Room invite" "Room invite"
override fun getNameForEmptyRoom() = override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>) =
"Empty room" "Empty room"
override fun getNameFor2members(name1: String?, name2: String?) = override fun getNameFor2members(name1: String?, name2: String?) =

View File

@ -18,7 +18,7 @@ package org.matrix.android.sdk.api
interface RoomDisplayNameFallbackProvider { interface RoomDisplayNameFallbackProvider {
fun getNameForRoomInvite(): String fun getNameForRoomInvite(): String
fun getNameForEmptyRoom(): String fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>): String
fun getNameFor2members(name1: String?, name2: String?): String fun getNameFor2members(name1: String?, name2: String?): String
fun getNameFor3members(name1: String?, name2: String?, name3: String?): String fun getNameFor3members(name1: String?, name2: String?, name3: String?): String
fun getNameFor4members(name1: String?, name2: String?, name3: String?, name4: String?): String fun getNameFor4members(name1: String?, name2: String?, name3: String?, name4: String?): String

View File

@ -39,5 +39,7 @@ internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String =
membershipStr = value.name membershipStr = value.name
} }
fun getBestName() = displayName?.takeIf { it.isNotBlank() } ?: userId
companion object companion object
} }

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2021 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.events
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
internal fun Event.getFixedRoomMemberContent(): RoomMemberContent? {
val content = content.toModel<RoomMemberContent>()
// if user is leaving, we should grab his last name and avatar from prevContent
return if (content?.membership?.isLeft() == true) {
val prevContent = resolvedPrevContent().toModel<RoomMemberContent>()
content.copy(
displayName = prevContent?.displayName,
avatarUrl = prevContent?.avatarUrl
)
} else {
content
}
}

View File

@ -17,10 +17,11 @@
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.asDomain
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.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -39,24 +40,35 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId
* @return the room avatar url, can be a fallback to a room member avatar or null * @return the room avatar url, can be a fallback to a room member avatar or null
*/ */
fun resolve(realm: Realm, roomId: String): String? { fun resolve(realm: Realm, roomId: String): String? {
var res: String? val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")
val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")?.root ?.root
res = ContentMapper.map(roomName?.content).toModel<RoomAvatarContent>()?.avatarUrl ?.asDomain()
if (!res.isNullOrEmpty()) { ?.content
return res ?.toModel<RoomAvatarContent>()
?.avatarUrl
if (!roomName.isNullOrEmpty()) {
return roomName
} }
val roomMembers = RoomMemberHelper(realm, roomId) val roomMembers = RoomMemberHelper(realm, roomId)
val members = roomMembers.queryActiveRoomMembersEvent().findAll() val members = roomMembers.queryActiveRoomMembersEvent().findAll()
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect.orFalse()
if (isDirectRoom) { if (isDirectRoom) {
if (members.size == 1) { if (members.size == 1) {
res = members.firstOrNull()?.avatarUrl // Use avatar of a left user
val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent()
.findAll()
.firstOrNull { !it.avatarUrl.isNullOrEmpty() }
?.avatarUrl
return firstLeftAvatarUrl ?: members.firstOrNull()?.avatarUrl
} else if (members.size == 2) { } else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.avatarUrl return firstOtherMember?.avatarUrl
} }
} }
return res
return null
} }
} }

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.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
@ -51,14 +52,14 @@ internal class RoomDisplayNameResolver @Inject constructor(
* @param roomId: the roomId to resolve the name of. * @param roomId: the roomId to resolve the name of.
* @return the room display name * @return the room display name
*/ */
fun resolve(realm: Realm, roomId: String): CharSequence { fun resolve(realm: Realm, roomId: String): String {
// this algorithm is the one defined in // this algorithm is the one defined in
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617 // https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
// calculateRoomName(room, userId) // calculateRoomName(room, userId)
// For Lazy Loaded room, see algorithm here: // For Lazy Loaded room, see algorithm here:
// https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn // https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs/edit#heading=h.qif6pkqyjgzn
var name: CharSequence? var name: String?
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root
name = ContentMapper.map(roomName?.content).toModel<RoomNameContent>()?.name name = ContentMapper.map(roomName?.content).toModel<RoomNameContent>()?.name
@ -77,14 +78,14 @@ internal class RoomDisplayNameResolver @Inject constructor(
if (roomEntity?.membership == Membership.INVITE) { if (roomEntity?.membership == Membership.INVITE) {
val inviteMeEvent = roomMembers.getLastStateEvent(userId) val inviteMeEvent = roomMembers.getLastStateEvent(userId)
val inviterId = inviteMeEvent?.sender val inviterId = inviteMeEvent?.sender
name = if (inviterId != null) { name = inviterId
activeMembers.where() ?.let {
.equalTo(RoomMemberSummaryEntityFields.USER_ID, inviterId) activeMembers.where()
.findFirst() .equalTo(RoomMemberSummaryEntityFields.USER_ID, it)
?.displayName .findFirst()
} else { ?.getBestName()
roomDisplayNameFallbackProvider.getNameForRoomInvite() }
} ?: roomDisplayNameFallbackProvider.getNameForRoomInvite()
} else if (roomEntity?.membership == Membership.JOIN) { } else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val invitedCount = roomSummary?.invitedMembersCount ?: 0 val invitedCount = roomSummary?.invitedMembersCount ?: 0
@ -105,8 +106,11 @@ internal class RoomDisplayNameResolver @Inject constructor(
val otherMembersCount = otherMembersSubset.count() val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) { name = when (otherMembersCount) {
0 -> { 0 -> {
roomDisplayNameFallbackProvider.getNameForEmptyRoom() // Get left members if any
// TODO (was xx and yyy) ... val leftMembersNames = roomMembers.queryLeftRoomMembersEvent()
.findAll()
.map { it.getBestName() }
roomDisplayNameFallbackProvider.getNameForEmptyRoom(roomSummary?.isDirect.orFalse(), leftMembersNames)
} }
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> { 2 -> {
@ -150,7 +154,7 @@ internal class RoomDisplayNameResolver @Inject constructor(
if (roomMemberSummary == null) return null if (roomMemberSummary == null) return null
val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName)
return if (isUnique) { return if (isUnique) {
roomMemberSummary.displayName roomMemberSummary.getBestName()
} else { } else {
"${roomMemberSummary.displayName} (${roomMemberSummary.userId})" "${roomMemberSummary.displayName} (${roomMemberSummary.userId})"
} }

View File

@ -16,12 +16,12 @@
package org.matrix.android.sdk.internal.session.room.membership package org.matrix.android.sdk.internal.session.room.membership
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
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.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.user.UserEntityFactory import org.matrix.android.sdk.internal.session.user.UserEntityFactory
import io.realm.Realm
import javax.inject.Inject import javax.inject.Inject
internal class RoomMemberEventHandler @Inject constructor() { internal class RoomMemberEventHandler @Inject constructor() {
@ -31,7 +31,7 @@ internal class RoomMemberEventHandler @Inject constructor() {
return false return false
} }
val userId = event.stateKey ?: return false val userId = event.stateKey ?: return false
val roomMember = event.content.toModel<RoomMemberContent>() val roomMember = event.getFixedRoomMemberContent()
return handle(realm, roomId, userId, roomMember) return handle(realm, roomId, userId, roomMember)
} }

View File

@ -75,6 +75,11 @@ internal class RoomMemberHelper(private val realm: Realm,
.equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name)
} }
fun queryLeftRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> {
return queryRoomMembersEvent()
.equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.LEAVE.name)
}
fun queryActiveRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> { fun queryActiveRoomMembersEvent(): RealmQuery<RoomMemberSummaryEntity> {
return queryRoomMembersEvent() return queryRoomMembersEvent()
.beginGroup() .beginGroup()

View File

@ -107,7 +107,7 @@ internal class RoomSummaryUpdater @Inject constructor(
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
|| !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId)
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel<RoomNameContent>()?.name roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel<RoomNameContent>()?.name
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic

View File

@ -49,6 +49,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.extensions.clearWith
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.initsync.reportSubtask
@ -464,18 +465,4 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
} }
} }
private fun Event.getFixedRoomMemberContent(): RoomMemberContent? {
val content = content.toModel<RoomMemberContent>()
// if user is leaving, we should grab his last name and avatar from prevContent
return if (content?.membership?.isLeft() == true) {
val prevContent = resolvedPrevContent().toModel<RoomMemberContent>()
content.copy(
displayName = prevContent?.displayName,
avatarUrl = prevContent?.avatarUrl
)
} else {
content
}
}
} }

View File

@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
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.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent import org.matrix.android.sdk.internal.session.sync.model.accountdata.BreadcrumbsContent
@ -62,7 +63,8 @@ internal class UserAccountDataSyncHandler @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val directChatsHelper: DirectChatsHelper, private val directChatsHelper: DirectChatsHelper,
private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val roomAvatarResolver: RoomAvatarResolver private val roomAvatarResolver: RoomAvatarResolver,
private val roomDisplayNameResolver: RoomDisplayNameResolver
) { ) {
fun handle(realm: Realm, accountData: UserAccountDataSync?) { fun handle(realm: Realm, accountData: UserAccountDataSync?) {
@ -161,8 +163,9 @@ internal class UserAccountDataSyncHandler @Inject constructor(
if (roomSummaryEntity != null) { if (roomSummaryEntity != null) {
roomSummaryEntity.isDirect = true roomSummaryEntity.isDirect = true
roomSummaryEntity.directUserId = userId roomSummaryEntity.directUserId = userId
// Also update the avatar, there is a specific treatment for DMs // Also update the avatar and displayname, there is a specific treatment for DMs
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId)
} }
} }
} }
@ -172,8 +175,9 @@ internal class UserAccountDataSyncHandler @Inject constructor(
.forEach { .forEach {
it.isDirect = false it.isDirect = false
it.directUserId = null it.directUserId = null
// Also update the avatar, there was a specific treatment for DMs // Also update the avatar and displayname, there was a specific treatment for DMs
it.avatarUrl = roomAvatarResolver.resolve(realm, it.roomId) it.avatarUrl = roomAvatarResolver.resolve(realm, it.roomId)
it.displayName = roomDisplayNameResolver.resolve(realm, it.roomId)
} }
} }

View File

@ -28,8 +28,12 @@ class VectorRoomDisplayNameFallbackProvider(
return context.getString(R.string.room_displayname_room_invite) return context.getString(R.string.room_displayname_room_invite)
} }
override fun getNameForEmptyRoom(): String { override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>): String {
return context.getString(R.string.room_displayname_empty_room) return if (leftMemberNames.isEmpty()) {
context.getString(R.string.room_displayname_empty_room)
} else {
context.getString(R.string.room_displayname_empty_room_was, leftMemberNames.joinToString())
}
} }
override fun getNameFor2members(name1: String?, name2: String?): String { override fun getNameFor2members(name1: String?, name2: String?): String {