diff --git a/changelog.d/3736.feature b/changelog.d/3736.feature new file mode 100644 index 0000000000..c7c7d151f0 --- /dev/null +++ b/changelog.d/3736.feature @@ -0,0 +1 @@ +Support Functional members (https://github.com/vector-im/element-meta/blob/develop/spec/functional_members.md) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt index a74f5010c2..773f480b5d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt @@ -20,6 +20,8 @@ import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { + override fun excludedUserIds(roomId: String) = emptyList() + override fun getNameForRoomInvite() = "Room invite" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt index 37d9b46b0b..6dc9f315a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt @@ -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 fun getNameForRoomInvite(): String fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String fun getNameFor1member(name: String): String diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index c3d55b267a..742e4b8ec7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -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() ?.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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 7497ecf21b..7a5b91a0ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -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 = 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 diff --git a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt index d8873a71a4..44ea65244b 100644 --- a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt +++ b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt @@ -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) diff --git a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt new file mode 100644 index 0000000000..3d473d15d9 --- /dev/null +++ b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt @@ -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() + + override fun getNameForRoomInvite() = + "Room invite" + + override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List) = + "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" +} diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index e51d09e13b..758d4e2a43 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -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. */ diff --git a/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt b/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt new file mode 100644 index 0000000000..821acf948c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt @@ -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? = null +) + +fun StateService.getFunctionalMembers(): List { + return getStateEvent(FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.userIds + .orEmpty() +} diff --git a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt index cfbc2748ad..57967d7a05 100644 --- a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt +++ b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt @@ -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, ) : RoomDisplayNameFallbackProvider { + override fun excludedUserIds(roomId: String): List { + 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) }