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
@@ -19,47 +19,52 @@ struct BannedRoomProxyMockConfiguration {
extension BannedRoomProxyMock {
@MainActor
convenience init(_ configuration: KnockedRoomProxyMockConfiguration) {
convenience init(_ configuration: BannedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
info = RoomInfoProxy(roomInfo: .init(configuration))
info = RoomInfoProxyMock(configuration)
}
}
extension RoomInfo {
@MainActor init(_ configuration: BannedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
encryptionState: .notEncrypted,
creator: nil,
displayName: configuration.name,
rawName: nil,
topic: nil,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: false,
isPublic: false,
isSpace: false,
successorRoom: nil,
isFavourite: false,
canonicalAlias: nil,
alternativeAliases: [],
membership: .knocked,
inviter: nil,
heroes: [],
activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count),
invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count),
joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count),
userPowerLevels: [:],
highlightCount: 0,
notificationCount: 0,
cachedUserDefinedNotificationMode: nil,
hasRoomCall: false,
activeRoomCallParticipants: [],
isMarkedUnread: false,
numUnreadMessages: 0,
numUnreadNotifications: 0,
numUnreadMentions: 0,
pinnedEventIds: [],
joinRule: .knock,
historyVisibility: .shared)
extension RoomInfoProxyMock {
@MainActor convenience init(_ configuration: BannedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
isEncrypted = false
creator = nil
displayName = configuration.name
rawName = nil
topic = nil
avatarURL = configuration.avatarURL
isDirect = false
isPublic = false
isSpace = false
successor = nil
isFavourite = false
canonicalAlias = nil
alternativeAliases = []
membership = .knocked
inviter = nil
heroes = []
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
highlightCount = 0
notificationCount = 0
cachedUserDefinedNotificationMode = nil
hasRoomCall = false
activeRoomCallParticipants = []
isMarkedUnread = false
unreadMessagesCount = 0
unreadNotificationsCount = 0
unreadMentionsCount = 0
pinnedEventIDs = []
joinRule = .knock
historyVisibility = .shared
powerLevels = RoomPowerLevelsProxyMock(configuration: .init())
}
}
@@ -6472,11 +6472,11 @@ class InvitedRoomProxyMock: InvitedRoomProxyProtocol, @unchecked Sendable {
}
}
class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> {
get { return underlyingInfoPublisher }
set(value) { underlyingInfoPublisher = value }
}
var underlyingInfoPublisher: CurrentValuePublisher<RoomInfoProxy, Never>!
var underlyingInfoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never>!
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
get { return underlyingMembersPublisher }
set(value) { underlyingMembersPublisher = value }
@@ -13406,6 +13406,127 @@ class RoomDirectorySearchProxyMock: RoomDirectorySearchProxyProtocol, @unchecked
}
}
}
class RoomInfoProxyMock: RoomInfoProxyProtocol, @unchecked Sendable {
var id: String {
get { return underlyingId }
set(value) { underlyingId = value }
}
var underlyingId: String!
var creator: String?
var displayName: String?
var rawName: String?
var topic: String?
var avatarURL: URL?
var isEncrypted: Bool {
get { return underlyingIsEncrypted }
set(value) { underlyingIsEncrypted = value }
}
var underlyingIsEncrypted: Bool!
var isDirect: Bool {
get { return underlyingIsDirect }
set(value) { underlyingIsDirect = value }
}
var underlyingIsDirect: Bool!
var isPublic: Bool {
get { return underlyingIsPublic }
set(value) { underlyingIsPublic = value }
}
var underlyingIsPublic: Bool!
var isPrivate: Bool {
get { return underlyingIsPrivate }
set(value) { underlyingIsPrivate = value }
}
var underlyingIsPrivate: Bool!
var isSpace: Bool {
get { return underlyingIsSpace }
set(value) { underlyingIsSpace = value }
}
var underlyingIsSpace: Bool!
var isFavourite: Bool {
get { return underlyingIsFavourite }
set(value) { underlyingIsFavourite = value }
}
var underlyingIsFavourite: Bool!
var canonicalAlias: String?
var alternativeAliases: [String] = []
var membership: Membership {
get { return underlyingMembership }
set(value) { underlyingMembership = value }
}
var underlyingMembership: Membership!
var inviter: RoomMemberProxyProtocol?
var activeMembersCount: Int {
get { return underlyingActiveMembersCount }
set(value) { underlyingActiveMembersCount = value }
}
var underlyingActiveMembersCount: Int!
var invitedMembersCount: Int {
get { return underlyingInvitedMembersCount }
set(value) { underlyingInvitedMembersCount = value }
}
var underlyingInvitedMembersCount: Int!
var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value }
}
var underlyingJoinedMembersCount: Int!
var highlightCount: Int {
get { return underlyingHighlightCount }
set(value) { underlyingHighlightCount = value }
}
var underlyingHighlightCount: Int!
var notificationCount: Int {
get { return underlyingNotificationCount }
set(value) { underlyingNotificationCount = value }
}
var underlyingNotificationCount: Int!
var cachedUserDefinedNotificationMode: RoomNotificationMode?
var hasRoomCall: Bool {
get { return underlyingHasRoomCall }
set(value) { underlyingHasRoomCall = value }
}
var underlyingHasRoomCall: Bool!
var activeRoomCallParticipants: [String] = []
var isMarkedUnread: Bool {
get { return underlyingIsMarkedUnread }
set(value) { underlyingIsMarkedUnread = value }
}
var underlyingIsMarkedUnread: Bool!
var unreadMessagesCount: UInt {
get { return underlyingUnreadMessagesCount }
set(value) { underlyingUnreadMessagesCount = value }
}
var underlyingUnreadMessagesCount: UInt!
var unreadNotificationsCount: UInt {
get { return underlyingUnreadNotificationsCount }
set(value) { underlyingUnreadNotificationsCount = value }
}
var underlyingUnreadNotificationsCount: UInt!
var unreadMentionsCount: UInt {
get { return underlyingUnreadMentionsCount }
set(value) { underlyingUnreadMentionsCount = value }
}
var underlyingUnreadMentionsCount: UInt!
var pinnedEventIDs: Set<String> {
get { return underlyingPinnedEventIDs }
set(value) { underlyingPinnedEventIDs = value }
}
var underlyingPinnedEventIDs: Set<String>!
var joinRule: JoinRule?
var historyVisibility: RoomHistoryVisibility {
get { return underlyingHistoryVisibility }
set(value) { underlyingHistoryVisibility = value }
}
var underlyingHistoryVisibility: RoomHistoryVisibility!
var powerLevels: RoomPowerLevelsProxyProtocol {
get { return underlyingPowerLevels }
set(value) { underlyingPowerLevels = value }
}
var underlyingPowerLevels: RoomPowerLevelsProxyProtocol!
var successor: SuccessorRoom?
var heroes: [RoomHero] = []
}
class RoomMemberProxyMock: RoomMemberProxyProtocol, @unchecked Sendable {
var userID: String {
get { return underlyingUserID }
@@ -13467,6 +13588,658 @@ class RoomPowerLevelsProxyMock: RoomPowerLevelsProxyProtocol, @unchecked Sendabl
}
var underlyingValues: RoomPowerLevelsValues!
//MARK: - canOwnUser
var canOwnUserSendMessageUnderlyingCallsCount = 0
var canOwnUserSendMessageCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserSendMessageUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserSendMessageUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserSendMessageUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserSendMessageUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserSendMessageCalled: Bool {
return canOwnUserSendMessageCallsCount > 0
}
var canOwnUserSendMessageReceivedMessageType: MessageLikeEventType?
var canOwnUserSendMessageReceivedInvocations: [MessageLikeEventType] = []
var canOwnUserSendMessageUnderlyingReturnValue: Bool!
var canOwnUserSendMessageReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserSendMessageUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserSendMessageUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserSendMessageUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserSendMessageUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserSendMessageClosure: ((MessageLikeEventType) -> Bool)?
func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool {
canOwnUserSendMessageCallsCount += 1
canOwnUserSendMessageReceivedMessageType = messageType
DispatchQueue.main.async {
self.canOwnUserSendMessageReceivedInvocations.append(messageType)
}
if let canOwnUserSendMessageClosure = canOwnUserSendMessageClosure {
return canOwnUserSendMessageClosure(messageType)
} else {
return canOwnUserSendMessageReturnValue
}
}
//MARK: - canOwnUser
var canOwnUserSendStateEventUnderlyingCallsCount = 0
var canOwnUserSendStateEventCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserSendStateEventUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserSendStateEventUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserSendStateEventUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserSendStateEventUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserSendStateEventCalled: Bool {
return canOwnUserSendStateEventCallsCount > 0
}
var canOwnUserSendStateEventReceivedEvent: StateEventType?
var canOwnUserSendStateEventReceivedInvocations: [StateEventType] = []
var canOwnUserSendStateEventUnderlyingReturnValue: Bool!
var canOwnUserSendStateEventReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserSendStateEventUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserSendStateEventUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserSendStateEventUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserSendStateEventUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserSendStateEventClosure: ((StateEventType) -> Bool)?
func canOwnUser(sendStateEvent event: StateEventType) -> Bool {
canOwnUserSendStateEventCallsCount += 1
canOwnUserSendStateEventReceivedEvent = event
DispatchQueue.main.async {
self.canOwnUserSendStateEventReceivedInvocations.append(event)
}
if let canOwnUserSendStateEventClosure = canOwnUserSendStateEventClosure {
return canOwnUserSendStateEventClosure(event)
} else {
return canOwnUserSendStateEventReturnValue
}
}
//MARK: - canOwnUserInvite
var canOwnUserInviteUnderlyingCallsCount = 0
var canOwnUserInviteCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserInviteUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserInviteUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserInviteUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserInviteUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserInviteCalled: Bool {
return canOwnUserInviteCallsCount > 0
}
var canOwnUserInviteUnderlyingReturnValue: Bool!
var canOwnUserInviteReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserInviteUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserInviteUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserInviteUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserInviteUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserInviteClosure: (() -> Bool)?
func canOwnUserInvite() -> Bool {
canOwnUserInviteCallsCount += 1
if let canOwnUserInviteClosure = canOwnUserInviteClosure {
return canOwnUserInviteClosure()
} else {
return canOwnUserInviteReturnValue
}
}
//MARK: - canOwnUserRedactOther
var canOwnUserRedactOtherUnderlyingCallsCount = 0
var canOwnUserRedactOtherCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserRedactOtherUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserRedactOtherUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserRedactOtherUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserRedactOtherUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserRedactOtherCalled: Bool {
return canOwnUserRedactOtherCallsCount > 0
}
var canOwnUserRedactOtherUnderlyingReturnValue: Bool!
var canOwnUserRedactOtherReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserRedactOtherUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserRedactOtherUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserRedactOtherUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserRedactOtherUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserRedactOtherClosure: (() -> Bool)?
func canOwnUserRedactOther() -> Bool {
canOwnUserRedactOtherCallsCount += 1
if let canOwnUserRedactOtherClosure = canOwnUserRedactOtherClosure {
return canOwnUserRedactOtherClosure()
} else {
return canOwnUserRedactOtherReturnValue
}
}
//MARK: - canOwnUserRedactOwn
var canOwnUserRedactOwnUnderlyingCallsCount = 0
var canOwnUserRedactOwnCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserRedactOwnUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserRedactOwnUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserRedactOwnUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserRedactOwnUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserRedactOwnCalled: Bool {
return canOwnUserRedactOwnCallsCount > 0
}
var canOwnUserRedactOwnUnderlyingReturnValue: Bool!
var canOwnUserRedactOwnReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserRedactOwnUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserRedactOwnUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserRedactOwnUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserRedactOwnUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserRedactOwnClosure: (() -> Bool)?
func canOwnUserRedactOwn() -> Bool {
canOwnUserRedactOwnCallsCount += 1
if let canOwnUserRedactOwnClosure = canOwnUserRedactOwnClosure {
return canOwnUserRedactOwnClosure()
} else {
return canOwnUserRedactOwnReturnValue
}
}
//MARK: - canOwnUserKick
var canOwnUserKickUnderlyingCallsCount = 0
var canOwnUserKickCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserKickUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserKickUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserKickUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserKickUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserKickCalled: Bool {
return canOwnUserKickCallsCount > 0
}
var canOwnUserKickUnderlyingReturnValue: Bool!
var canOwnUserKickReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserKickUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserKickUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserKickUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserKickUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserKickClosure: (() -> Bool)?
func canOwnUserKick() -> Bool {
canOwnUserKickCallsCount += 1
if let canOwnUserKickClosure = canOwnUserKickClosure {
return canOwnUserKickClosure()
} else {
return canOwnUserKickReturnValue
}
}
//MARK: - canOwnUserBan
var canOwnUserBanUnderlyingCallsCount = 0
var canOwnUserBanCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserBanUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserBanUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserBanUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserBanUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserBanCalled: Bool {
return canOwnUserBanCallsCount > 0
}
var canOwnUserBanUnderlyingReturnValue: Bool!
var canOwnUserBanReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserBanUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserBanUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserBanUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserBanUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserBanClosure: (() -> Bool)?
func canOwnUserBan() -> Bool {
canOwnUserBanCallsCount += 1
if let canOwnUserBanClosure = canOwnUserBanClosure {
return canOwnUserBanClosure()
} else {
return canOwnUserBanReturnValue
}
}
//MARK: - canOwnUserTriggerRoomNotification
var canOwnUserTriggerRoomNotificationUnderlyingCallsCount = 0
var canOwnUserTriggerRoomNotificationCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserTriggerRoomNotificationUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserTriggerRoomNotificationUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserTriggerRoomNotificationUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserTriggerRoomNotificationUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserTriggerRoomNotificationCalled: Bool {
return canOwnUserTriggerRoomNotificationCallsCount > 0
}
var canOwnUserTriggerRoomNotificationUnderlyingReturnValue: Bool!
var canOwnUserTriggerRoomNotificationReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserTriggerRoomNotificationUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserTriggerRoomNotificationUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserTriggerRoomNotificationUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserTriggerRoomNotificationUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserTriggerRoomNotificationClosure: (() -> Bool)?
func canOwnUserTriggerRoomNotification() -> Bool {
canOwnUserTriggerRoomNotificationCallsCount += 1
if let canOwnUserTriggerRoomNotificationClosure = canOwnUserTriggerRoomNotificationClosure {
return canOwnUserTriggerRoomNotificationClosure()
} else {
return canOwnUserTriggerRoomNotificationReturnValue
}
}
//MARK: - canOwnUserPinOrUnpin
var canOwnUserPinOrUnpinUnderlyingCallsCount = 0
var canOwnUserPinOrUnpinCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserPinOrUnpinUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserPinOrUnpinUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserPinOrUnpinUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserPinOrUnpinUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserPinOrUnpinCalled: Bool {
return canOwnUserPinOrUnpinCallsCount > 0
}
var canOwnUserPinOrUnpinUnderlyingReturnValue: Bool!
var canOwnUserPinOrUnpinReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserPinOrUnpinUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserPinOrUnpinUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserPinOrUnpinUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserPinOrUnpinUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserPinOrUnpinClosure: (() -> Bool)?
func canOwnUserPinOrUnpin() -> Bool {
canOwnUserPinOrUnpinCallsCount += 1
if let canOwnUserPinOrUnpinClosure = canOwnUserPinOrUnpinClosure {
return canOwnUserPinOrUnpinClosure()
} else {
return canOwnUserPinOrUnpinReturnValue
}
}
//MARK: - canOwnUserJoinCall
var canOwnUserJoinCallUnderlyingCallsCount = 0
var canOwnUserJoinCallCallsCount: Int {
get {
if Thread.isMainThread {
return canOwnUserJoinCallUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserJoinCallUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserJoinCallUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canOwnUserJoinCallUnderlyingCallsCount = newValue
}
}
}
}
var canOwnUserJoinCallCalled: Bool {
return canOwnUserJoinCallCallsCount > 0
}
var canOwnUserJoinCallUnderlyingReturnValue: Bool!
var canOwnUserJoinCallReturnValue: Bool! {
get {
if Thread.isMainThread {
return canOwnUserJoinCallUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = canOwnUserJoinCallUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canOwnUserJoinCallUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canOwnUserJoinCallUnderlyingReturnValue = newValue
}
}
}
}
var canOwnUserJoinCallClosure: (() -> Bool)?
func canOwnUserJoinCall() -> Bool {
canOwnUserJoinCallCallsCount += 1
if let canOwnUserJoinCallClosure = canOwnUserJoinCallClosure {
return canOwnUserJoinCallClosure()
} else {
return canOwnUserJoinCallReturnValue
}
}
//MARK: - canUser
var canUserUserIDSendMessageUnderlyingCallsCount = 0
@@ -24,47 +24,52 @@ extension InvitedRoomProxyMock {
self.init()
id = configuration.id
inviter = configuration.inviter
info = RoomInfoProxy(roomInfo: .init(configuration))
info = RoomInfoProxyMock(configuration)
rejectInvitationReturnValue = .success(())
}
}
extension RoomInfo {
@MainActor init(_ configuration: InvitedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
encryptionState: .notEncrypted,
creator: nil,
displayName: configuration.name,
rawName: nil,
topic: nil,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: false,
isPublic: false,
isSpace: false,
successorRoom: nil,
isFavourite: false,
canonicalAlias: nil,
alternativeAliases: [],
membership: .knocked,
inviter: .init(configuration.inviter),
heroes: [],
activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count),
invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count),
joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count),
userPowerLevels: [:],
highlightCount: 0,
notificationCount: 0,
cachedUserDefinedNotificationMode: nil,
hasRoomCall: false,
activeRoomCallParticipants: [],
isMarkedUnread: false,
numUnreadMessages: 0,
numUnreadNotifications: 0,
numUnreadMentions: 0,
pinnedEventIds: [],
joinRule: .invite,
historyVisibility: .shared)
extension RoomInfoProxyMock {
@MainActor convenience init(_ configuration: InvitedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
isEncrypted = false
creator = nil
displayName = configuration.name
rawName = nil
topic = nil
avatarURL = configuration.avatarURL
isDirect = false
isPublic = false
isSpace = false
successor = nil
isFavourite = false
canonicalAlias = nil
alternativeAliases = []
membership = .knocked
inviter = .init(configuration.inviter)
heroes = []
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
highlightCount = 0
notificationCount = 0
cachedUserDefinedNotificationMode = nil
hasRoomCall = false
activeRoomCallParticipants = []
isMarkedUnread = false
unreadMessagesCount = 0
unreadNotificationsCount = 0
unreadMentionsCount = 0
pinnedEventIDs = []
joinRule = .invite
historyVisibility = .shared
powerLevels = RoomPowerLevelsProxyMock(configuration: .init())
}
}
@@ -62,7 +62,6 @@ extension JoinedRoomProxyMock {
ownUserID = configuration.ownUserID
infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher()
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
knockRequestsStatePublisher = CurrentValueSubject(configuration.knockRequestsState).asCurrentValuePublisher()
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
@@ -134,53 +133,52 @@ extension JoinedRoomProxyMock {
isVisibleInRoomDirectoryReturnValue = .success(configuration.isVisibleInPublicDirectory)
predecessorRoom = configuration.predecessor
let roomInfoProxyMock = RoomInfoProxyMock(configuration)
roomInfoProxyMock.powerLevels = powerLevelsProxyMock
infoPublisher = CurrentValueSubject(roomInfoProxyMock).asCurrentValuePublisher()
}
}
extension RoomInfo {
@MainActor init(_ configuration: JoinedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
encryptionState: configuration.isEncrypted ? .encrypted : .notEncrypted,
creator: nil,
displayName: configuration.name,
rawName: configuration.name,
topic: configuration.topic,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: configuration.isDirect,
isPublic: configuration.isPublic,
isSpace: configuration.isSpace,
successorRoom: configuration.successor,
isFavourite: false,
canonicalAlias: configuration.canonicalAlias,
alternativeAliases: configuration.alternativeAliases,
membership: configuration.membership,
inviter: configuration.inviter.map { RoomMember(userId: $0.userID,
displayName: $0.displayName,
avatarUrl: $0.avatarURL?.absoluteString,
membership: $0.membership,
isNameAmbiguous: false,
powerLevel: Int64($0.powerLevel),
normalizedPowerLevel: Int64($0.powerLevel),
isIgnored: $0.isIgnored,
suggestedRoleForPowerLevel: $0.role,
membershipChangeReason: $0.membershipChangeReason) },
heroes: configuration.heroes.map(RoomHero.init),
activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count),
invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count),
joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count),
userPowerLevels: [:],
highlightCount: 0,
notificationCount: 0,
cachedUserDefinedNotificationMode: .allMessages,
hasRoomCall: configuration.hasOngoingCall,
activeRoomCallParticipants: [],
isMarkedUnread: false,
numUnreadMessages: 0,
numUnreadNotifications: 0,
numUnreadMentions: 0,
pinnedEventIds: Array(configuration.pinnedEventIDs),
joinRule: configuration.joinRule,
historyVisibility: .shared)
extension RoomInfoProxyMock {
@MainActor convenience init(_ configuration: JoinedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
isEncrypted = configuration.isEncrypted
creator = nil
displayName = configuration.name
rawName = configuration.name
topic = configuration.topic
avatarURL = configuration.avatarURL
isDirect = configuration.isDirect
isPublic = configuration.isPublic
isSpace = configuration.isSpace
successor = configuration.successor
isFavourite = false
canonicalAlias = configuration.canonicalAlias
alternativeAliases = configuration.alternativeAliases
membership = configuration.membership
inviter = configuration.inviter
heroes = configuration.heroes.map(RoomHero.init)
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
highlightCount = 0
notificationCount = 0
cachedUserDefinedNotificationMode = .allMessages
hasRoomCall = configuration.hasOngoingCall
activeRoomCallParticipants = []
isMarkedUnread = false
unreadMessagesCount = 0
unreadNotificationsCount = 0
unreadMentionsCount = 0
pinnedEventIDs = configuration.pinnedEventIDs
joinRule = configuration.joinRule
historyVisibility = .shared
powerLevels = RoomPowerLevelsProxyMock(configuration: configuration.powerLevelsConfiguration)
}
}
@@ -22,44 +22,49 @@ extension KnockedRoomProxyMock {
convenience init(_ configuration: KnockedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
info = RoomInfoProxy(roomInfo: .init(configuration))
info = RoomInfoProxyMock(configuration)
}
}
extension RoomInfo {
@MainActor init(_ configuration: KnockedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
encryptionState: .notEncrypted,
creator: nil,
displayName: configuration.name,
rawName: nil,
topic: nil,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: false,
isPublic: false,
isSpace: false,
successorRoom: nil,
isFavourite: false,
canonicalAlias: nil,
alternativeAliases: [],
membership: .knocked,
inviter: nil,
heroes: [],
activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count),
invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count),
joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count),
userPowerLevels: [:],
highlightCount: 0,
notificationCount: 0,
cachedUserDefinedNotificationMode: nil,
hasRoomCall: false,
activeRoomCallParticipants: [],
isMarkedUnread: false,
numUnreadMessages: 0,
numUnreadNotifications: 0,
numUnreadMentions: 0,
pinnedEventIds: [],
joinRule: .knock,
historyVisibility: .shared)
extension RoomInfoProxyMock {
@MainActor convenience init(_ configuration: KnockedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
isEncrypted = false
creator = nil
displayName = configuration.name
rawName = nil
topic = nil
avatarURL = configuration.avatarURL
isDirect = false
isPublic = false
isSpace = false
successor = nil
isFavourite = false
canonicalAlias = nil
alternativeAliases = []
membership = .knocked
inviter = nil
heroes = []
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
highlightCount = 0
notificationCount = 0
cachedUserDefinedNotificationMode = nil
hasRoomCall = false
activeRoomCallParticipants = []
isMarkedUnread = false
unreadMessagesCount = 0
unreadNotificationsCount = 0
unreadMentionsCount = 0
pinnedEventIDs = []
joinRule = .knock
historyVisibility = .shared
powerLevels = RoomPowerLevelsProxyMock(configuration: .init())
}
}
@@ -26,6 +26,17 @@ extension RoomPowerLevelsProxyMock {
underlyingValues = RoomPowerLevelsValues.mock
canOwnUserSendMessageReturnValue = configuration.canUserSendMessage
canOwnUserSendStateEventReturnValue = configuration.canUserSendState
canOwnUserInviteReturnValue = configuration.canUserInvite
canOwnUserRedactOtherReturnValue = configuration.canUserRedactOther
canOwnUserRedactOwnReturnValue = configuration.canUserRedactOwn
canOwnUserKickReturnValue = configuration.canUserKick
canOwnUserBanReturnValue = configuration.canUserBan
canOwnUserTriggerRoomNotificationReturnValue = configuration.canUserTriggerRoomNotification
canOwnUserPinOrUnpinReturnValue = configuration.canUserPin
canOwnUserJoinCallReturnValue = configuration.canUserJoinCall
canUserUserIDSendMessageReturnValue = .success(configuration.canUserSendMessage)
canUserUserIDSendStateEventReturnValue = .success(configuration.canUserSendState)
canUserInviteUserIDReturnValue = .success(configuration.canUserInvite)
@@ -140,7 +140,7 @@ enum TimelineMediaPreviewItem: Equatable {
case loading(Loading)
/// Wraps a media file and title to be previewed with QuickLook.
class Media: NSObject, QLPreviewItem, Identifiable {
@Observable class Media: NSObject, QLPreviewItem, Identifiable {
fileprivate(set) var timelineItem: EventBasedMessageTimelineItemProtocol
var fileHandle: MediaFileHandleProxy?
var downloadError: Error?
@@ -180,27 +180,24 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
if case let .media(mediaItem) = viewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: viewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Image")
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
})
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream())
}
if case let .media(mediaItem) = loadingViewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: loadingViewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Loading")
.snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
})
}
if case let .media(mediaItem) = unknownTypeViewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: unknownTypeViewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Unknown type")
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream())
}
if case let .media(mediaItem) = presentedOnRoomViewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: presentedOnRoomViewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Incoming on Room")
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream())
}
}
@@ -27,9 +27,6 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider)
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
Task {
await updatePermissions()
}
setupSubscriptions()
}
@@ -189,7 +186,6 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo: roomInfo)
Task { await self?.updatePermissions() }
}
.store(in: &cancellables)
@@ -214,20 +210,17 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
.store(in: &cancellables)
}
private func updateRoomInfo(roomInfo: RoomInfoProxy) {
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
switch roomInfo.joinRule {
case .knock, .knockRestricted:
state.isKnockableRoom = true
default:
state.isKnockableRoom = false
}
}
private func updatePermissions() async {
let powerLevels = try? await roomProxy.powerLevels().get()
state.canAccept = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canDecline = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canBan = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true
state.canAccept = (try? roomInfo.powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canDecline = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canBan = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true
}
private static let loadingIndicatorIdentifier = "\(KnockRequestsListScreenViewModel.self)-Loading"
@@ -39,12 +39,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
avatarURL: roomAvatar,
bindings: .init(name: roomName ?? "", topic: roomTopic ?? "")), mediaProvider: mediaProvider)
Task {
let powerLevels = try? await roomProxy.powerLevels().get()
state.canEditAvatar = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == .some(true)
state.canEditName = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == .some(true)
state.canEditTopic = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == .some(true)
}
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
}
// MARK: - Public
@@ -91,6 +86,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
// MARK: - Private
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
state.canEditAvatar = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == .some(true)
state.canEditName = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == .some(true)
state.canEditTopic = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == .some(true)
}
private func saveRoomDetails() {
Task {
let userIndicatorID = UUID().uuidString
@@ -97,7 +97,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
updateRoomInfo(roomProxy.infoPublisher.value)
Task { await updatePowerLevelPermissions() }
setupRoomSubscription()
Task { await fetchMembersIfNeeded() }
@@ -183,7 +182,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo)
Task { await self?.updatePowerLevelPermissions() }
}
.store(in: &cancellables)
@@ -206,7 +204,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.store(in: &cancellables)
}
private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
state.isEncrypted = roomInfo.isEncrypted
state.isDirect = roomInfo.isDirect
state.bindings.isFavourite = roomInfo.isFavourite
@@ -225,6 +223,18 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
default:
state.isKnockableRoom = false
}
state.canEditRoomName = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == true
state.canEditRoomTopic = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == true
state.canEditRoomAvatar = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == true
state.canInviteUsers = (try? roomInfo.powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canKickUsers = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canBanUsers = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true
state.canJoinCall = (try? roomInfo.powerLevels.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true
Task {
state.canEditRolesOrPermissions = await (try? roomProxy.suggestedRole(for: roomProxy.ownUserID).get()) == .administrator
}
}
private func fetchMembersIfNeeded() async {
@@ -280,19 +290,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}
private func updatePowerLevelPermissions() async {
state.canEditRolesOrPermissions = await (try? roomProxy.suggestedRole(for: roomProxy.ownUserID).get()) == .administrator
let powerLevels = try? await roomProxy.powerLevels().get()
state.canEditRoomName = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == true
state.canEditRoomTopic = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == true
state.canEditRoomAvatar = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == true
state.canInviteUsers = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canKickUsers = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canBanUsers = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true
state.canJoinCall = (try? powerLevels?.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true
}
private func setupNotificationSettingsSubscription() {
notificationSettingsProxy.callbacks
.receive(on: DispatchQueue.main)
@@ -338,13 +338,13 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
RoomDetailsScreen(context: genericRoomViewModel.context)
.snapshotPreferences(expect: genericRoomViewModel.context.$viewState.map { state in
state.shortcuts.contains(.invite)
state.canSeeSecurityAndPrivacy == true
})
.previewDisplayName("Generic Room")
RoomDetailsScreen(context: simpleRoomViewModel.context)
.snapshotPreferences(expect: simpleRoomViewModel.context.$viewState.map { state in
state.shortcuts.contains(.invite)
state.canSeeSecurityAndPrivacy == true
})
.previewDisplayName("Simple Room")
@@ -99,10 +99,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
bannedMembers: roomMembersDetails.bannedMembers,
bindings: state.bindings)
let powerLevels = try? await roomProxy.powerLevels().get()
self.state.canInviteUsers = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true
self.state.canKickUsers = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true
self.state.canBanUsers = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true
let powerLevels = roomProxy.infoPublisher.value.powerLevels
self.state.canInviteUsers = (try? powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true
self.state.canKickUsers = (try? powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true
self.state.canBanUsers = (try? powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true
hideLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
}
@@ -39,12 +39,12 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
// Automatically update the room permissions
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
Task { await self?.updatePermissions() }
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo: roomInfo)
}
.store(in: &cancellables)
Task { await updatePermissions() }
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
}
// MARK: - Public
@@ -113,13 +113,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
// MARK: - Permissions
private func updatePermissions() async {
switch await roomProxy.powerLevels() {
case .success(let powerLevels):
state.permissions = .init(powerLevels: powerLevels.values)
case .failure:
break
}
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
state.permissions = .init(powerLevels: roomInfo.powerLevels.values)
}
private func editPermissions(group: RoomRolesAndPermissionsScreenPermissionsGroup) {
@@ -47,9 +47,7 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
self?.suggestionTriggerSubject.value != nil ? .milliseconds(500) : .milliseconds(0)
}
Task {
canMentionAllUsers = await (try? roomProxy.powerLevels().get().canUserTriggerRoomNotification(userID: roomProxy.ownUserID).get()) == true
}
canMentionAllUsers = (try? roomProxy.infoPublisher.value.powerLevels.canUserTriggerRoomNotification(userID: roomProxy.ownUserID).get()) == true
}
func processTextMessage(_ textMessage: String, selectedRange: NSRange) {
@@ -332,7 +332,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxy) async {
private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxyProtocol) async {
state.hasSuccessor = roomInfo.successor != nil
let pinnedEventIDs = roomInfo.pinnedEventIDs
@@ -348,12 +348,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.isKnockableRoom = false
}
let powerLevels = try? await roomProxy.powerLevels().get()
state.canSendMessage = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true
state.canJoinCall = (try? powerLevels?.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true
state.canAcceptKnocks = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canDeclineKnocks = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canBan = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true
state.canSendMessage = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true
state.canJoinCall = (try? roomInfo.powerLevels.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true
state.canAcceptKnocks = (try? roomInfo.powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true
state.canDeclineKnocks = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canBan = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true
}
private func setupPinnedEventsTimelineItemProviderIfNeeded() {
@@ -29,14 +29,12 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
return
}
await self?.handleRoomInfoUpdate(roomInfo)
self?.handleRoomInfoUpdate(roomInfo)
}
}
.store(in: &cancellables)
Task {
await handleRoomInfoUpdate(roomProxy.infoPublisher.value)
}
handleRoomInfoUpdate(roomProxy.infoPublisher.value)
}
// MARK: - Public
@@ -64,7 +62,7 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
// MARK: - Private
private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxy) async {
state.canSendMessage = await (try? roomProxy.powerLevels().get().canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true
private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxyProtocol) {
state.canSendMessage = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true
}
}
@@ -119,9 +119,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
setupSubscriptions()
setupDirectRoomSubscriptionsIfNeeded()
// Set initial values for redacting from the macOS context menu.
Task { await updatePermissions() }
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in
guard let self else {
return nil
@@ -403,14 +402,15 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
}
private func updatePermissions() async {
let powerLevels = try? await roomProxy.powerLevels().get()
state.canCurrentUserSendMessage = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true
state.canCurrentUserRedactOthers = (try? powerLevels?.canUserRedactOther(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserRedactSelf = (try? powerLevels?.canUserRedactOwn(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserPin = (try? powerLevels?.canUserPinOrUnpin(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserKick = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserBan = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
state.pinnedEventIDs = roomInfo.pinnedEventIDs
state.canCurrentUserSendMessage = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true
state.canCurrentUserRedactOthers = (try? roomInfo.powerLevels.canUserRedactOther(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserRedactSelf = (try? roomInfo.powerLevels.canUserRedactOwn(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserPin = (try? roomInfo.powerLevels.canUserPinOrUnpin(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserKick = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true
state.canCurrentUserBan = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true
}
private func setupSubscriptions() {
@@ -445,8 +445,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
guard !Task.isCancelled else {
return
}
self?.state.pinnedEventIDs = roomInfo.pinnedEventIDs
await self?.updatePermissions()
self?.updateRoomInfo(roomInfo: roomInfo)
}
}
.store(in: &cancellables)
@@ -487,10 +487,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.displayMediaUploadPreviewScreen(url: url))
case .showActionMenu(let actionMenuInfo):
Task {
await self.updatePermissions()
self.state.bindings.actionMenuInfo = actionMenuInfo
}
self.state.bindings.actionMenuInfo = actionMenuInfo
case .showDebugInfo(let debugInfo):
state.bindings.debugInfo = debugInfo
case .viewInRoomTimeline(let eventID):
@@ -996,13 +993,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
}
private extension RoomInfoProxy {
/// Checks if the other person left the room in a direct chat
var isUserAloneInDirectRoom: Bool {
isDirect && activeMembersCount == 1
}
}
// MARK: - Mocks
extension TimelineViewModel {
@@ -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 }