Merge pull request #8700 from vector-im/feature/fga/handle_functional_members

Support Functional members #3736
This commit is contained in:
Benoit Marty 2023-12-06 14:16:11 +01:00 committed by GitHub
commit 5e4b8ed536
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 10 deletions

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

@ -0,0 +1 @@
Support Functional members (https://github.com/vector-im/element-meta/blob/develop/spec/functional_members.md)

View file

@ -20,6 +20,8 @@ import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {
override fun excludedUserIds(roomId: String) = emptyList<String>()
override fun getNameForRoomInvite() =
"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.
*/
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 getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>): String
fun getNameFor1member(name: String): String

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room
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.session.events.model.EventType
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 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.
@ -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
*/
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
?.asDomain()
?.content
?.toModel<RoomAvatarContent>()
?.avatarUrl
if (!roomName.isNullOrEmpty()) {
return roomName
if (!roomAvatarUrl.isNullOrEmpty()) {
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)
val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect.orFalse()
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) {
// Use avatar of a left user
val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent()

View file

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

View file

@ -17,7 +17,6 @@
package im.vector.app.core.utils
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.SyncConfig
@ -25,7 +24,7 @@ import org.matrix.android.sdk.api.SyncConfig
fun getMatrixInstance(): Matrix {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val configuration = MatrixConfiguration(
roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context),
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
syncConfig = SyncConfig(longPollTimeout = 5_000L),
)
return Matrix(context, configuration)

View file

@ -0,0 +1,45 @@
/*
* 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 im.vector.app.core.utils
import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {
override fun excludedUserIds(roomId: String) = emptyList<String>()
override fun getNameForRoomInvite() =
"Room invite"
override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List<String>) =
"Empty room"
override fun getNameFor1member(name: String) =
name
override fun getNameFor2members(name1: String, name2: String) =
"$name1 and $name2"
override fun getNameFor3members(name1: String, name2: String, name3: String) =
"$name1, $name2 and $name3"
override fun getNameFor4members(name1: String, name2: String, name3: String, name4: String) =
"$name1, $name2, $name3 and $name4"
override fun getNameFor4membersAndMore(name1: String, name2: String, name3: String, remainingCount: Int) =
"$name1, $name2, $name3 and $remainingCount others"
}

View file

@ -42,6 +42,12 @@ object Config {
const val ENABLE_LOCATION_SHARING = true
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.
*/

View file

@ -0,0 +1,38 @@
/*
* 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>? = null
)
fun StateService.getFunctionalMembers(): List<String> {
return getStateEvent(FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE, QueryStringValue.IsEmpty)
?.content
?.toModel<FunctionalMembersContent>()
?.userIds
.orEmpty()
}

View file

@ -18,13 +18,28 @@ package im.vector.app.features.room
import android.content.Context
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.session.getRoom
import javax.inject.Inject
import javax.inject.Provider
class VectorRoomDisplayNameFallbackProvider @Inject constructor(
private val context: Context
private val context: Context,
private val activeSessionHolder: Provider<ActiveSessionHolder>,
) : RoomDisplayNameFallbackProvider {
override fun excludedUserIds(roomId: String): List<String> {
if (!Config.SUPPORT_FUNCTIONAL_MEMBERS) return emptyList()
return activeSessionHolder.get()
.getSafeActiveSession()
?.getRoom(roomId)
?.stateService()
?.getFunctionalMembers()
.orEmpty()
}
override fun getNameForRoomInvite(): String {
return context.getString(R.string.room_displayname_room_invite)
}