mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 18:05:36 +03:00
Merge pull request #8700 from vector-im/feature/fga/handle_functional_members
Support Functional members #3736
This commit is contained in:
commit
5e4b8ed536
10 changed files with 134 additions and 10 deletions
1
changelog.d/3736.feature
Normal file
1
changelog.d/3736.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Support Functional members (https://github.com/vector-im/element-meta/blob/develop/spec/functional_members.md)
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
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.Matrix
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
import org.matrix.android.sdk.api.SyncConfig
|
import org.matrix.android.sdk.api.SyncConfig
|
||||||
|
@ -25,7 +24,7 @@ import org.matrix.android.sdk.api.SyncConfig
|
||||||
fun getMatrixInstance(): Matrix {
|
fun getMatrixInstance(): Matrix {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val configuration = MatrixConfiguration(
|
val configuration = MatrixConfiguration(
|
||||||
roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context),
|
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
|
||||||
syncConfig = SyncConfig(longPollTimeout = 5_000L),
|
syncConfig = SyncConfig(longPollTimeout = 5_000L),
|
||||||
)
|
)
|
||||||
return Matrix(context, configuration)
|
return Matrix(context, configuration)
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -42,6 +42,12 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -18,13 +18,28 @@ 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)
|
||||||
|
?.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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue