Support Functional members #3736

This commit is contained in:
Benoit Marty 2023-11-28 17:23:40 +01:00 committed by Benoit Marty
parent bb9d1fc8d8
commit 8e0c503b45
8 changed files with 82 additions and 8 deletions

1
changelog.d/3736.feature Normal file
View File

@ -0,0 +1 @@
Support Functional members

View File

@ -20,6 +20,8 @@ import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {
override fun excludedUserIds(roomId: String) = emptyList<String>()
override fun getNameForRoomInvite() = override fun getNameForRoomInvite() =
"Room invite" "Room invite"

View File

@ -25,6 +25,10 @@ package org.matrix.android.sdk.api.provider
* *Limitation*: if the locale of the device changes, the methods will not be called again. * *Limitation*: if the locale of the device changes, the methods will not be called again.
*/ */
interface RoomDisplayNameFallbackProvider { interface RoomDisplayNameFallbackProvider {
/**
* Return the list of user ids to ignore when computing the room display name.
*/
fun excludedUserIds(roomId: String): List<String>
fun getNameForRoomInvite(): String fun getNameForRoomInvite(): String
fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>): String fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>): String
fun getNameFor1member(name: String): String fun getNameFor1member(name: String): String

View File

@ -17,6 +17,7 @@
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.MatrixConfiguration
import org.matrix.android.sdk.api.extensions.orFalse 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
@ -31,7 +32,12 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import javax.inject.Inject import javax.inject.Inject
internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { internal class RoomAvatarResolver @Inject constructor(
matrixConfiguration: MatrixConfiguration,
@UserId private val userId: String
) {
private val roomDisplayNameFallbackProvider = matrixConfiguration.roomDisplayNameFallbackProvider
/** /**
* Compute the room avatar url. * Compute the room avatar url.
@ -40,21 +46,26 @@ 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? {
val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "") val roomAvatarUrl = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")
?.root ?.root
?.asDomain() ?.asDomain()
?.content ?.content
?.toModel<RoomAvatarContent>() ?.toModel<RoomAvatarContent>()
?.avatarUrl ?.avatarUrl
if (!roomName.isNullOrEmpty()) { if (!roomAvatarUrl.isNullOrEmpty()) {
return roomName return roomAvatarUrl
} }
val roomMembers = RoomMemberHelper(realm, roomId)
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.orFalse() val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect.orFalse()
if (isDirectRoom) { if (isDirectRoom) {
val excludedUserIds = roomDisplayNameFallbackProvider.excludedUserIds(roomId)
val roomMembers = RoomMemberHelper(realm, roomId)
val members = roomMembers
.queryActiveRoomMembersEvent()
.not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray())
.findAll()
if (members.size == 1) { if (members.size == 1) {
// Use avatar of a left user // Use avatar of a left user
val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent() val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent()

View File

@ -92,18 +92,20 @@ internal class RoomDisplayNameResolver @Inject constructor(
} }
?: roomDisplayNameFallbackProvider.getNameForRoomInvite() ?: roomDisplayNameFallbackProvider.getNameForRoomInvite()
} else if (roomEntity?.membership == Membership.JOIN) { } else if (roomEntity?.membership == Membership.JOIN) {
val excludedUserIds = roomDisplayNameFallbackProvider.excludedUserIds(roomId)
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val invitedCount = roomSummary?.invitedMembersCount ?: 0 val invitedCount = roomSummary?.invitedMembersCount ?: 0
val joinedCount = roomSummary?.joinedMembersCount ?: 0 val joinedCount = roomSummary?.joinedMembersCount ?: 0
val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) { val otherMembersSubset: List<RoomMemberSummaryEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { userId -> roomSummary.heroes.mapNotNull { userId ->
roomMembers.getLastRoomMember(userId)?.takeIf { roomMembers.getLastRoomMember(userId)?.takeIf {
it.membership == Membership.INVITE || it.membership == Membership.JOIN (it.membership == Membership.INVITE || it.membership == Membership.JOIN) && !excludedUserIds.contains(it.userId)
} }
} }
} else { } else {
activeMembers.where() activeMembers.where()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray())
.limit(5) .limit(5)
.findAll() .findAll()
.createSnapshot() .createSnapshot()
@ -113,6 +115,7 @@ internal class RoomDisplayNameResolver @Inject constructor(
0 -> { 0 -> {
// Get left members if any // Get left members if any
val leftMembersNames = roomMembers.queryLeftRoomMembersEvent() val leftMembersNames = roomMembers.queryLeftRoomMembersEvent()
.not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray())
.findAll() .findAll()
.map { displayNameResolver.getBestName(it.toMatrixItem()) } .map { displayNameResolver.getBestName(it.toMatrixItem()) }
val directUserId = roomSummary?.directUserId val directUserId = roomSummary?.directUserId

View File

@ -42,6 +42,10 @@ object Config {
const val ENABLE_LOCATION_SHARING = true const val ENABLE_LOCATION_SHARING = true
const val LOCATION_MAP_TILER_KEY = "fU3vlMsMn4Jb6dnEIFsx" const val LOCATION_MAP_TILER_KEY = "fU3vlMsMn4Jb6dnEIFsx"
/// Whether to read the `io.element.functional_members` state event
// and exclude any service members when computing a room's name and avatar.
const val SUPPORT_FUNCTIONAL_MEMBERS = true
/** /**
* The maximum length of voice messages in milliseconds. * The maximum length of voice messages in milliseconds.
*/ */

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.room
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.state.StateService
private const val FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE = "io.element.functional_members"
@JsonClass(generateAdapter = true)
data class FunctionalMembersContent(
@Json(name = "service_members") val userIds: List<String>
)
fun StateService.getFunctionalMembers(): List<String> {
return getStateEvent(FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE, QueryStringValue.IsEmpty)?.let {
it.content.toModel<FunctionalMembersContent>()?.userIds
}.orEmpty()
}

View File

@ -18,13 +18,26 @@ package im.vector.app.features.room
import android.content.Context import android.content.Context
import im.vector.app.R import im.vector.app.R
import im.vector.app.config.Config
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
import org.matrix.android.sdk.api.session.getRoom
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
class VectorRoomDisplayNameFallbackProvider @Inject constructor( class VectorRoomDisplayNameFallbackProvider @Inject constructor(
private val context: Context private val context: Context,
private val activeSessionHolder: Provider<ActiveSessionHolder>,
) : RoomDisplayNameFallbackProvider { ) : RoomDisplayNameFallbackProvider {
override fun excludedUserIds(roomId: String): List<String> {
if (!Config.SUPPORT_FUNCTIONAL_MEMBERS) return emptyList()
return activeSessionHolder.get().getSafeActiveSession()
?.getRoom(roomId)?.let { room ->
room.stateService().getFunctionalMembers()
}.orEmpty()
}
override fun getNameForRoomInvite(): String { override fun getNameForRoomInvite(): String {
return context.getString(R.string.room_displayname_room_invite) return context.getString(R.string.room_displayname_room_invite)
} }