Be more lenient with the power levels as they can still be missing at the time the various screens are created e.g. after accepting an invite.

The correct solution is to subscribe to update and update the UI accordingly when receiving them.
This commit is contained in:
Stefan Ceriu
2025-06-26 16:34:34 +03:00
committed by Stefan Ceriu
parent f8d1ca54dd
commit af5b670bf3
10 changed files with 108 additions and 103 deletions

View File

@@ -26,7 +26,7 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
self.userIndicatorController = userIndicatorController
super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider)
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
updateRoomInfo(roomProxy.infoPublisher.value)
setupSubscriptions()
}
@@ -185,7 +185,7 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo: roomInfo)
self?.updateRoomInfo(roomInfo)
}
.store(in: &cancellables)
@@ -210,7 +210,7 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
.store(in: &cancellables)
}
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
switch roomInfo.joinRule {
case .knock, .knockRestricted:
state.isKnockableRoom = true
@@ -218,10 +218,11 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
state.isKnockableRoom = false
}
guard let powerLevels = roomProxy.infoPublisher.value.powerLevels else { fatalError("Missing room power levels") }
state.canAccept = powerLevels.canOwnUserInvite()
state.canDecline = powerLevels.canOwnUserKick()
state.canBan = powerLevels.canOwnUserBan()
if let powerLevels = roomProxy.infoPublisher.value.powerLevels {
state.canAccept = powerLevels.canOwnUserInvite()
state.canDecline = powerLevels.canOwnUserKick()
state.canBan = powerLevels.canOwnUserBan()
}
}
private static let loadingIndicatorIdentifier = "\(KnockRequestsListScreenViewModel.self)-Loading"

View File

@@ -39,6 +39,13 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
avatarURL: roomAvatar,
bindings: .init(name: roomName ?? "", topic: roomTopic ?? "")), mediaProvider: mediaProvider)
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo: roomInfo)
}
.store(in: &cancellables)
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
}
@@ -87,10 +94,11 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
// MARK: - Private
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
guard let powerLevels = roomInfo.powerLevels else { fatalError("Missing room power levels") }
state.canEditAvatar = powerLevels.canOwnUser(sendStateEvent: .roomAvatar)
state.canEditName = powerLevels.canOwnUser(sendStateEvent: .roomName)
state.canEditTopic = powerLevels.canOwnUser(sendStateEvent: .roomTopic)
if let powerLevels = roomInfo.powerLevels {
state.canEditAvatar = powerLevels.canOwnUser(sendStateEvent: .roomAvatar)
state.canEditName = powerLevels.canOwnUser(sendStateEvent: .roomName)
state.canEditTopic = powerLevels.canOwnUser(sendStateEvent: .roomTopic)
}
}
private func saveRoomDetails() {

View File

@@ -179,7 +179,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private func setupRoomSubscription() {
roomProxy.infoPublisher
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo)
}
@@ -233,8 +233,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.canBanUsers = powerLevels.canOwnUserBan()
state.canJoinCall = powerLevels.canOwnUserJoinCall()
state.canEditRolesOrPermissions = powerLevels.suggestedRole(forUser: roomProxy.ownUserID) == .administrator
} else {
fatalError("Missing room power levels")
}
}

View File

@@ -103,10 +103,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
bannedMembers: roomMembersDetails.bannedMembers,
bindings: state.bindings)
guard let powerLevels = roomProxy.infoPublisher.value.powerLevels else { fatalError("Missing room power levels") }
self.state.canInviteUsers = powerLevels.canOwnUserInvite()
self.state.canKickUsers = powerLevels.canOwnUserKick()
self.state.canBanUsers = powerLevels.canOwnUserBan()
if let powerLevels = roomProxy.infoPublisher.value.powerLevels {
self.state.canInviteUsers = powerLevels.canOwnUserInvite()
self.state.canKickUsers = powerLevels.canOwnUserKick()
self.state.canBanUsers = powerLevels.canOwnUserBan()
}
}
}

View File

@@ -114,8 +114,9 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
// MARK: - Permissions
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
guard let powerLevels = roomInfo.powerLevels else { fatalError("Missing room power levels") }
state.permissions = .init(powerLevels: powerLevels.values)
if let powerLevels = roomInfo.powerLevels {
state.permissions = .init(powerLevels: powerLevels.values)
}
}
private func editPermissions(group: RoomRolesAndPermissionsScreenPermissionsGroup) {

View File

@@ -19,10 +19,13 @@ private enum SuggestionTriggerRegex {
final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private var canMentionAllUsers = false
private(set) var suggestionsPublisher: AnyPublisher<[SuggestionItem], Never> = Empty().eraseToAnyPublisher()
private let suggestionTriggerSubject = CurrentValueSubject<SuggestionTrigger?, Never>(nil)
private var cancellables = Set<AnyCancellable>()
init(roomProxy: JoinedRoomProxyProtocol,
roomListPublisher: AnyPublisher<[RoomSummary], Never>) {
self.roomProxy = roomProxy
@@ -47,8 +50,14 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
self?.suggestionTriggerSubject.value != nil ? .milliseconds(500) : .milliseconds(0)
}
guard let powerLevels = roomProxy.infoPublisher.value.powerLevels else { fatalError("Missing room power levels") }
canMentionAllUsers = powerLevels.canOwnUserTriggerRoomNotification()
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo)
}
.store(in: &cancellables)
updateRoomInfo(roomProxy.infoPublisher.value)
}
func processTextMessage(_ textMessage: String, selectedRange: NSRange) {
@@ -61,6 +70,12 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
// MARK: - Private
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
if let powerLevels = roomProxy.infoPublisher.value.powerLevels {
canMentionAllUsers = powerLevels.canOwnUserTriggerRoomNotification()
}
}
private func membersSuggestions(suggestionTrigger: SuggestionTrigger,
members: [RoomMemberProxyProtocol],
ownUserID: String) -> [SuggestionItem] {

View File

@@ -77,12 +77,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
super.init(initialViewState: appHooks.roomScreenHook.update(viewState),
mediaProvider: mediaProvider)
updateRoomInfo(roomProxy.infoPublisher.value)
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
Task {
await handleRoomInfoUpdate(roomProxy.infoPublisher.value)
await updateVerificationBadge()
}
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
}
override func process(viewAction: RoomScreenViewAction) {
@@ -157,31 +157,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
appSettings.$knockingEnabled
.weakAssign(to: \.state.isKnockingEnabled, on: self)
.store(in: &cancellables)
let roomInfoSubscription = roomProxy
.infoPublisher
roomInfoSubscription
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
guard let self else { return }
state.roomTitle = roomInfo.displayName ?? roomProxy.id
state.roomAvatar = roomInfo.avatar
state.hasOngoingCall = roomInfo.hasRoomCall
self?.updateRoomInfo(roomInfo)
}
.store(in: &cancellables)
Task { [weak self] in
for await roomInfo in roomInfoSubscription.receive(on: DispatchQueue.main).values {
guard !Task.isCancelled else {
return
}
await self?.handleRoomInfoUpdate(roomInfo)
}
}
.store(in: &cancellables)
let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main)
Task { [weak self] in
@@ -332,7 +315,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxyProtocol) async {
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
state.roomTitle = roomInfo.displayName ?? roomProxy.id
state.roomAvatar = roomInfo.avatar
state.hasOngoingCall = roomInfo.hasRoomCall
state.hasSuccessor = roomInfo.successor != nil
let pinnedEventIDs = roomInfo.pinnedEventIDs
@@ -348,12 +334,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.isKnockableRoom = false
}
guard let powerLevels = roomInfo.powerLevels else { fatalError("Missing room power levels") }
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
state.canJoinCall = powerLevels.canOwnUserJoinCall()
state.canAcceptKnocks = powerLevels.canOwnUserInvite()
state.canDeclineKnocks = powerLevels.canOwnUserKick()
state.canBan = powerLevels.canOwnUserBan()
if let powerLevels = roomInfo.powerLevels {
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
state.canJoinCall = powerLevels.canOwnUserJoinCall()
state.canAcceptKnocks = powerLevels.canOwnUserInvite()
state.canDeclineKnocks = powerLevels.canOwnUserKick()
state.canBan = powerLevels.canOwnUserBan()
}
}
private func setupPinnedEventsTimelineItemProviderIfNeeded() {

View File

@@ -23,18 +23,14 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
super.init(initialViewState: ThreadTimelineScreenViewState())
Task { [weak self] in
for await roomInfo in roomProxy.infoPublisher.receive(on: DispatchQueue.main).values {
guard !Task.isCancelled else {
return
}
self?.handleRoomInfoUpdate(roomInfo)
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo)
}
}
.store(in: &cancellables)
.store(in: &cancellables)
handleRoomInfoUpdate(roomProxy.infoPublisher.value)
updateRoomInfo(roomProxy.infoPublisher.value)
}
// MARK: - Public
@@ -62,8 +58,9 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
// MARK: - Private
private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxyProtocol) {
guard let powerLevels = roomInfo.powerLevels else { fatalError("Missing room power levels") }
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
if let powerLevels = roomInfo.powerLevels {
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
}
}
}

View File

@@ -119,8 +119,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
setupSubscriptions()
setupDirectRoomSubscriptionsIfNeeded()
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in
guard let self else {
return nil
@@ -144,6 +142,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
state.timelineState.paginationState = timelineController.paginationState
buildTimelineViews(timelineItems: timelineController.timelineItems)
updateRoomInfo(roomProxy.infoPublisher.value)
updateMembers(roomProxy.membersPublisher.value)
// Note: beware if we get to e.g. restore a reply / edit,
@@ -402,16 +401,17 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
}
private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) {
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
state.pinnedEventIDs = roomInfo.pinnedEventIDs
guard let powerLevels = roomInfo.powerLevels else { fatalError("Missing room power levels") }
state.canCurrentUserSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
state.canCurrentUserRedactOthers = powerLevels.canOwnUserRedactOther()
state.canCurrentUserRedactSelf = powerLevels.canOwnUserRedactOwn()
state.canCurrentUserPin = powerLevels.canOwnUserPinOrUnpin()
state.canCurrentUserKick = powerLevels.canOwnUserKick()
state.canCurrentUserBan = powerLevels.canOwnUserBan()
if let powerLevels = roomInfo.powerLevels {
state.canCurrentUserSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
state.canCurrentUserRedactOthers = powerLevels.canOwnUserRedactOther()
state.canCurrentUserRedactSelf = powerLevels.canOwnUserRedactOwn()
state.canCurrentUserPin = powerLevels.canOwnUserPinOrUnpin()
state.canCurrentUserKick = powerLevels.canOwnUserKick()
state.canCurrentUserBan = powerLevels.canOwnUserBan()
}
}
private func setupSubscriptions() {
@@ -440,17 +440,12 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
.store(in: &cancellables)
let roomInfoSubscription = roomProxy.infoPublisher
Task { [weak self] in
for await roomInfo in roomInfoSubscription.receive(on: DispatchQueue.main).values {
guard !Task.isCancelled else {
return
}
self?.updateRoomInfo(roomInfo: roomInfo)
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo)
}
}
.store(in: &cancellables)
.store(in: &cancellables)
setupAppSettingsSubscriptions()

View File

@@ -165,15 +165,19 @@ class RoomScreenViewModelTests: XCTestCase {
func testRoomInfoUpdate() async throws {
var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)
let infoSubject = CurrentValueSubject<RoomInfoProxyProtocol, Never>(RoomInfoProxyMock(configuration))
let roomProxyMock = JoinedRoomProxyMock(configuration)
let powerLevelsMock = RoomPowerLevelsProxyMock(configuration: .init())
powerLevelsMock.canUserJoinCallUserIDReturnValue = .success(false)
powerLevelsMock.canOwnUserJoinCallReturnValue = false
roomProxyMock.powerLevelsReturnValue = .success(powerLevelsMock)
// setup the room proxy actions publisher
powerLevelsMock.canUserJoinCallUserIDReturnValue = .success(false)
let roomInfoProxyMock = RoomInfoProxyMock(configuration)
roomInfoProxyMock.powerLevels = powerLevelsMock
let infoSubject = CurrentValueSubject<RoomInfoProxyProtocol, Never>(roomInfoProxyMock)
roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil,
@@ -186,27 +190,25 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController)
self.viewModel = viewModel
var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.roomTitle == "StartingName" &&
viewState.roomAvatar == .room(id: "TestID", name: "StartingName", avatarURL: nil) &&
!viewState.canJoinCall &&
!viewState.hasOngoingCall
}
try await deferred.fulfill()
configuration.name = "NewName"
configuration.avatarURL = .mockMXCAvatar
configuration.hasOngoingCall = true
powerLevelsMock.canUserJoinCallUserIDReturnValue = .success(true)
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
XCTAssertEqual(viewModel.state.roomTitle, "StartingName")
XCTAssertEqual(viewModel.state.roomAvatar, .room(id: "TestID", name: "StartingName", avatarURL: nil))
XCTAssertFalse(viewModel.state.canJoinCall)
XCTAssertFalse(viewModel.state.hasOngoingCall)
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.roomTitle == "NewName" &&
viewState.roomAvatar == .room(id: "TestID", name: "NewName", avatarURL: .mockMXCAvatar) &&
viewState.canJoinCall &&
viewState.hasOngoingCall
}
configuration.name = "NewName"
configuration.avatarURL = .mockMXCAvatar
configuration.hasOngoingCall = true
powerLevelsMock.canUserJoinCallUserIDReturnValue = .success(true)
infoSubject.send(RoomInfoProxyMock(configuration))
try await deferred.fulfill()
}