Adopt room info power levels (#4245)

* Use the newly RoomInfo published PowerLevels; update permissions at the same time as the room info and avoid unnecessary async calls.

* Introduce a RoomInfoProxyProtocol and associated mock and move away from mocked SDK RoomInfos

* Bump the SDK to v25.06.23

* Expose new non-throwing methods for checking one's own user permissions

* Fix the unit and preview tests (except the TimelineMediaPreviewDetails for which simplified snapshots were generated)

* Converge on one single implementation for computing a room's avatar.

* Fix more unit tests

* Fix the TimelineMediaPreviewDetailsView snapshot tests

This reverts commit 9b6c819e611ad2fa3de2c34a4a7aa556fc9faadd.

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Stefan Ceriu
2025-06-24 19:48:00 +03:00
committed by GitHub
parent 0f9d700c99
commit 5ac6ec28d5
31 changed files with 1274 additions and 362 deletions
@@ -46,8 +46,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
let timeline: TimelineProxyProtocol
private let infoSubject: CurrentValueSubject<RoomInfoProxy, Never>
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
private let infoSubject: CurrentValueSubject<RoomInfoProxyProtocol, Never>
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> {
infoSubject.asCurrentValuePublisher()
}
@@ -137,7 +137,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: SDKListener { [weak self] roomInfo in
MXLog.info("Received room info update")
self?.infoSubject.send(.init(roomInfo: roomInfo))
self?.infoSubject.send(RoomInfoProxy(roomInfo: roomInfo))
})
}
@@ -8,20 +8,7 @@
import Foundation
import MatrixRustSDK
protocol BaseRoomInfoProxyProtocol {
var id: String { get }
var displayName: String? { get }
var avatar: RoomAvatar { get }
var topic: String? { get }
var canonicalAlias: String? { get }
var avatarURL: URL? { get }
var activeMembersCount: Int { get }
var joinedMembersCount: Int { get }
var isDirect: Bool { get }
var isSpace: Bool { get }
}
struct RoomInfoProxy: BaseRoomInfoProxyProtocol {
struct RoomInfoProxy: RoomInfoProxyProtocol {
let roomInfo: RoomInfo
var id: String { roomInfo.id }
@@ -31,18 +18,6 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol {
var topic: String? { roomInfo.topic }
/// The room's avatar URL. Use this for editing and favour ``avatar`` for display.
var avatarURL: URL? { roomInfo.avatarUrl.flatMap(URL.init) }
/// The room's avatar info for use in a ``RoomAvatarImage``.
var avatar: RoomAvatar {
guard successor == nil else {
return .tombstoned
}
if isDirect, avatarURL == nil, heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
return .room(id: id, name: displayName, avatarURL: avatarURL)
}
// Here we're assuming unknown rooms are unencrypted.
// Fortunately https://github.com/matrix-org/matrix-rust-sdk/pull/4778 makes that very much of an edge case and we
@@ -54,28 +29,17 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol {
var isDirect: Bool { roomInfo.isDirect }
var isPublic: Bool { roomInfo.isPublic }
// A room might be non public but also not private given the fact that the join rule might be missing or unsupported.
var isPrivate: Bool {
switch roomInfo.joinRule {
case .invite, .knock, .restricted, .knockRestricted:
true
default:
false
}
}
var isSpace: Bool { roomInfo.isSpace }
var successor: SuccessorRoom? { roomInfo.successorRoom }
var isFavourite: Bool { roomInfo.isFavourite }
var canonicalAlias: String? { roomInfo.canonicalAlias }
var alternativeAliases: [String] { roomInfo.alternativeAliases }
var membership: Membership { roomInfo.membership }
var inviter: RoomMemberProxy? { roomInfo.inviter.map(RoomMemberProxy.init) }
var inviter: RoomMemberProxyProtocol? { roomInfo.inviter.map(RoomMemberProxy.init) }
var heroes: [RoomHero] { roomInfo.heroes }
var activeMembersCount: Int { Int(roomInfo.activeMembersCount) }
var invitedMembersCount: Int { Int(roomInfo.invitedMembersCount) }
var joinedMembersCount: Int { Int(roomInfo.joinedMembersCount) }
var userPowerLevels: [String: Int] { roomInfo.userPowerLevels.mapValues(Int.init) }
var highlightCount: Int { Int(roomInfo.highlightCount) }
var notificationCount: Int { Int(roomInfo.notificationCount) }
var cachedUserDefinedNotificationMode: RoomNotificationMode? { roomInfo.cachedUserDefinedNotificationMode }
@@ -89,41 +53,14 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol {
var joinRule: JoinRule? { roomInfo.joinRule }
var historyVisibility: RoomHistoryVisibility { roomInfo.historyVisibility }
/// Find the first alias that matches the given homeserver
/// - Parameters:
/// - serverName: the homserver in question
/// - useFallback: whether to return any alias if none match
func firstAliasMatching(serverName: String?, useFallback: Bool) -> String? {
guard let serverName else { return nil }
// Check if the canonical alias matches the homeserver
if let canonicalAlias = roomInfo.canonicalAlias,
canonicalAlias.range(of: serverName) != nil {
return canonicalAlias
}
// Otherwise check the alternative aliases and return the first one that matches
if let matchingAlternativeAlias = roomInfo.alternativeAliases.filter({ $0.range(of: serverName) != nil }).first {
return matchingAlternativeAlias
}
guard useFallback else {
return nil
}
// Or just return the canonical alias if any
if let canonicalAlias = roomInfo.canonicalAlias {
return canonicalAlias
}
// And finally return whatever the first alternative alias is
return roomInfo.alternativeAliases.first
}
var powerLevels: RoomPowerLevelsProxyProtocol { RoomPowerLevelsProxy(roomInfo.powerLevels) }
}
struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
let roomPreviewInfo: RoomPreviewInfo
let successor: SuccessorRoom? = nil
var id: String { roomPreviewInfo.roomId }
var displayName: String? { roomPreviewInfo.name }
var heroes: [RoomHero] { roomPreviewInfo.heroes ?? [] }
@@ -137,12 +74,4 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
var joinRule: JoinRule { roomPreviewInfo.joinRule }
var membership: Membership? { roomPreviewInfo.membership }
/// The room's avatar info for use in a ``RoomAvatarImage``.
var avatar: RoomAvatar {
if isDirect, avatarURL == nil, heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
return .room(id: id, name: displayName, avatarURL: avatarURL)
}
}
@@ -0,0 +1,130 @@
//
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
protocol BaseRoomInfoProxyProtocol {
var id: String { get }
var displayName: String? { get }
var topic: String? { get }
var canonicalAlias: String? { get }
var avatarURL: URL? { get }
var activeMembersCount: Int { get }
var joinedMembersCount: Int { get }
var isDirect: Bool { get }
var isSpace: Bool { get }
var successor: SuccessorRoom? { get }
var heroes: [RoomHero] { get }
}
// sourcery: AutoMockable
protocol RoomInfoProxyProtocol: BaseRoomInfoProxyProtocol {
var id: String { get }
var creator: String? { get }
var displayName: String? { get }
var rawName: String? { get }
var topic: String? { get }
/// The room's avatar URL. Use this for editing and favour ``avatar`` for display.
var avatarURL: URL? { get }
var isEncrypted: Bool { get }
var isDirect: Bool { get }
var isPublic: Bool { get }
var isPrivate: Bool { get }
var isSpace: Bool { get }
var isFavourite: Bool { get }
var canonicalAlias: String? { get }
var alternativeAliases: [String] { get }
var membership: Membership { get }
var inviter: RoomMemberProxyProtocol? { get }
var activeMembersCount: Int { get }
var invitedMembersCount: Int { get }
var joinedMembersCount: Int { get }
var highlightCount: Int { get }
var notificationCount: Int { get }
var cachedUserDefinedNotificationMode: RoomNotificationMode? { get }
var hasRoomCall: Bool { get }
var activeRoomCallParticipants: [String] { get }
var isMarkedUnread: Bool { get }
var unreadMessagesCount: UInt { get }
var unreadNotificationsCount: UInt { get }
var unreadMentionsCount: UInt { get }
var pinnedEventIDs: Set<String> { get }
var joinRule: JoinRule? { get }
var historyVisibility: RoomHistoryVisibility { get }
var powerLevels: RoomPowerLevelsProxyProtocol { get }
}
extension BaseRoomInfoProxyProtocol {
/// The room's avatar info for use in a ``RoomAvatarImage``.
var avatar: RoomAvatar {
guard successor == nil else {
return .tombstoned
}
if isDirect, avatarURL == nil, heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
return .room(id: id, name: displayName, avatarURL: avatarURL)
}
}
extension RoomInfoProxyProtocol {
/// A room might be non public but also not private given the fact that the join rule might be missing or unsupported.
var isPrivate: Bool {
switch joinRule {
case .invite, .knock, .restricted, .knockRestricted:
true
default:
false
}
}
/// Checks if the other person left the room in a direct chat
var isUserAloneInDirectRoom: Bool {
isDirect && activeMembersCount == 1
}
/// Find the first alias that matches the given homeserver
/// - Parameters:
/// - serverName: the homserver in question
/// - useFallback: whether to return any alias if none match
func firstAliasMatching(serverName: String?, useFallback: Bool) -> String? {
guard let serverName else { return nil }
// Check if the canonical alias matches the homeserver
if let canonicalAlias,
canonicalAlias.range(of: serverName) != nil {
return canonicalAlias
}
// Otherwise check the alternative aliases and return the first one that matches
if let matchingAlternativeAlias = alternativeAliases.filter({ $0.range(of: serverName) != nil }).first {
return matchingAlternativeAlias
}
guard useFallback else {
return nil
}
// Or just return the canonical alias if any
if let canonicalAlias {
return canonicalAlias
}
// And finally return whatever the first alternative alias is
return alternativeAliases.first
}
}
@@ -11,6 +11,17 @@ import MatrixRustSDK
protocol RoomPowerLevelsProxyProtocol {
var values: RoomPowerLevelsValues { get }
func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool
func canOwnUser(sendStateEvent event: StateEventType) -> Bool
func canOwnUserInvite() -> Bool
func canOwnUserRedactOther() -> Bool
func canOwnUserRedactOwn() -> Bool
func canOwnUserKick() -> Bool
func canOwnUserBan() -> Bool
func canOwnUserTriggerRoomNotification() -> Bool
func canOwnUserPinOrUnpin() -> Bool
func canOwnUserJoinCall() -> Bool
func canUser(userID: String, sendMessage messageType: MessageLikeEventType) -> Result<Bool, RoomProxyError>
func canUser(userID: String, sendStateEvent event: StateEventType) -> Result<Bool, RoomProxyError>
func canUserInvite(userID: String) -> Result<Bool, RoomProxyError>
@@ -18,6 +18,46 @@ struct RoomPowerLevelsProxy: RoomPowerLevelsProxyProtocol {
powerLevels.values()
}
func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool {
powerLevels.canOwnUserSendMessage(message: messageType)
}
func canOwnUser(sendStateEvent stateEventType: StateEventType) -> Bool {
powerLevels.canOwnUserSendState(stateEvent: stateEventType)
}
func canOwnUserInvite() -> Bool {
powerLevels.canOwnUserInvite()
}
func canOwnUserRedactOther() -> Bool {
powerLevels.canOwnUserRedactOther()
}
func canOwnUserRedactOwn() -> Bool {
powerLevels.canOwnUserRedactOwn()
}
func canOwnUserKick() -> Bool {
powerLevels.canOwnUserKick()
}
func canOwnUserBan() -> Bool {
powerLevels.canOwnUserBan()
}
func canOwnUserTriggerRoomNotification() -> Bool {
powerLevels.canOwnUserTriggerRoomNotification()
}
func canOwnUserPinOrUnpin() -> Bool {
powerLevels.canOwnUserPinUnpin()
}
func canOwnUserJoinCall() -> Bool {
powerLevels.canOwnUserSendState(stateEvent: .callMember)
}
func canUser(userID: String, sendMessage messageType: MessageLikeEventType) -> Result<Bool, RoomProxyError> {
do {
return try .success(powerLevels.canUserSendMessage(userId: userID, message: messageType))
@@ -64,7 +64,7 @@ enum KnockRequestsState {
// sourcery: AutoMockable
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> { get }
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> { get }
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get }