Disable the composer when you don't have the power to post. (#4076)

This commit is contained in:
Doug
2025-04-29 10:12:20 +01:00
committed by GitHub
parent 41b18cfd52
commit 58b982a8f8
15 changed files with 150 additions and 19 deletions
@@ -8593,6 +8593,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
} }
//MARK: - canUser //MARK: - canUser
var canUserUserIDSendMessageUnderlyingCallsCount = 0
var canUserUserIDSendMessageCallsCount: Int {
get {
if Thread.isMainThread {
return canUserUserIDSendMessageUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = canUserUserIDSendMessageUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canUserUserIDSendMessageUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
canUserUserIDSendMessageUnderlyingCallsCount = newValue
}
}
}
}
var canUserUserIDSendMessageCalled: Bool {
return canUserUserIDSendMessageCallsCount > 0
}
var canUserUserIDSendMessageReceivedArguments: (userID: String, messageType: MessageLikeEventType)?
var canUserUserIDSendMessageReceivedInvocations: [(userID: String, messageType: MessageLikeEventType)] = []
var canUserUserIDSendMessageUnderlyingReturnValue: Result<Bool, RoomProxyError>!
var canUserUserIDSendMessageReturnValue: Result<Bool, RoomProxyError>! {
get {
if Thread.isMainThread {
return canUserUserIDSendMessageUnderlyingReturnValue
} else {
var returnValue: Result<Bool, RoomProxyError>? = nil
DispatchQueue.main.sync {
returnValue = canUserUserIDSendMessageUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
canUserUserIDSendMessageUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
canUserUserIDSendMessageUnderlyingReturnValue = newValue
}
}
}
}
var canUserUserIDSendMessageClosure: ((String, MessageLikeEventType) async -> Result<Bool, RoomProxyError>)?
func canUser(userID: String, sendMessage messageType: MessageLikeEventType) async -> Result<Bool, RoomProxyError> {
canUserUserIDSendMessageCallsCount += 1
canUserUserIDSendMessageReceivedArguments = (userID: userID, messageType: messageType)
DispatchQueue.main.async {
self.canUserUserIDSendMessageReceivedInvocations.append((userID: userID, messageType: messageType))
}
if let canUserUserIDSendMessageClosure = canUserUserIDSendMessageClosure {
return await canUserUserIDSendMessageClosure(userID, messageType)
} else {
return canUserUserIDSendMessageReturnValue
}
}
//MARK: - canUser
var canUserUserIDSendStateEventUnderlyingCallsCount = 0 var canUserUserIDSendStateEventUnderlyingCallsCount = 0
var canUserUserIDSendStateEventCallsCount: Int { var canUserUserIDSendStateEventCallsCount: Int {
get { get {
@@ -36,6 +36,7 @@ struct JoinedRoomProxyMockConfiguration {
var ownUserID = RoomMemberProxyMock.mockMe.userID var ownUserID = RoomMemberProxyMock.mockMe.userID
var inviter: RoomMemberProxyProtocol? var inviter: RoomMemberProxyProtocol?
var canUserSendMessage = true
var canUserInvite = true var canUserInvite = true
var canUserTriggerRoomNotification = false var canUserTriggerRoomNotification = false
var canUserJoinCall = true var canUserJoinCall = true
@@ -91,6 +92,7 @@ extension JoinedRoomProxyMock {
return .success(member.role) return .success(member.role)
} }
updatePowerLevelsForUsersReturnValue = .success(()) updatePowerLevelsForUsersReturnValue = .success(())
canUserUserIDSendMessageReturnValue = .success(configuration.canUserSendMessage)
canUserUserIDSendStateEventClosure = { [weak self] userID, _ in canUserUserIDSendStateEventClosure = { [weak self] userID, _ in
.success(self?.membersPublisher.value.first { $0.userID == userID }?.role ?? .user != .user) .success(self?.membersPublisher.value.first { $0.userID == userID }?.role ?? .user != .user)
} }
@@ -40,6 +40,7 @@ struct RoomScreenViewState: BindableState {
!pinnedEventsBannerState.isEmpty && lastScrollDirection != .top !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top
} }
var canSendMessage = true
var canJoinCall = false var canJoinCall = false
var hasOngoingCall: Bool var hasOngoingCall: Bool
var shouldShowCallButton = true var shouldShowCallButton = true
@@ -340,6 +340,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
let ownUserID = roomProxy.ownUserID let ownUserID = roomProxy.ownUserID
state.canSendMessage = await (try? roomProxy.canUser(userID: ownUserID, sendMessage: .roomMessage).get()) == true
state.canJoinCall = await (try? roomProxy.canUserJoinCall(userID: ownUserID).get()) == true state.canJoinCall = await (try? roomProxy.canUserJoinCall(userID: ownUserID).get()) == true
state.canAcceptKnocks = await (try? roomProxy.canUserInvite(userID: ownUserID).get()) == true state.canAcceptKnocks = await (try? roomProxy.canUserInvite(userID: ownUserID).get()) == true
state.canDeclineKnocks = await (try? roomProxy.canUserKick(userID: ownUserID).get()) == true state.canDeclineKnocks = await (try? roomProxy.canUserKick(userID: ownUserID).get()) == true
@@ -42,7 +42,7 @@ struct RoomScreen: View {
roomContext.send(viewAction: .footerViewAction(action)) roomContext.send(viewAction: .footerViewAction(action))
} }
composerToolbar composer
.padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12) .padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12)
.background { .background {
if composerToolbarContext.composerFormattingEnabled { if composerToolbarContext.composerFormattingEnabled {
@@ -188,6 +188,19 @@ struct RoomScreen: View {
timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive
} }
@ViewBuilder
private var composer: some View {
if roomContext.viewState.canSendMessage {
composerToolbar
} else {
Text(L10n.screenRoomTimelineNoPermissionToPost)
.font(.compound.bodyLG)
.foregroundStyle(.compound.textDisabled)
.multilineTextAlignment(.center)
.padding(.vertical, 10) // Matches the MessageComposerStyleModifier
}
}
@ViewBuilder @ViewBuilder
private var loadingIndicator: some View { private var loadingIndicator: some View {
if timelineContext.viewState.showLoading { if timelineContext.viewState.showLoading {
@@ -257,28 +270,50 @@ struct RoomScreen: View {
// MARK: - Previews // MARK: - Previews
struct RoomScreen_Previews: PreviewProvider, TestablePreview { struct RoomScreen_Previews: PreviewProvider, TestablePreview {
static let roomProxyMock = JoinedRoomProxyMock(.init(id: "stable_id", static let viewModels = makeViewModels()
name: "Preview room", static let readOnlyViewModels = makeViewModels(canSendMessage: false)
hasOngoingCall: true))
static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {
RoomScreen(roomViewModel: roomViewModel, RoomScreen(roomViewModel: viewModels.room,
timelineViewModel: timelineViewModel, timelineViewModel: viewModels.timeline,
composerToolbar: ComposerToolbar.mock()) composerToolbar: ComposerToolbar.mock())
} }
.previewDisplayName("Normal")
NavigationStack {
RoomScreen(roomViewModel: readOnlyViewModels.room,
timelineViewModel: readOnlyViewModels.timeline,
composerToolbar: ComposerToolbar.mock())
}
.previewDisplayName("Read-only")
.snapshotPreferences(expect: readOnlyViewModels.room.context.$viewState.map { !$0.canSendMessage })
}
static func makeViewModels(canSendMessage: Bool = true) -> ViewModels {
let roomProxyMock = JoinedRoomProxyMock(.init(id: "stable_id",
name: "Preview room",
hasOngoingCall: true,
canUserSendMessage: canSendMessage))
let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock)
let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
return .init(room: roomViewModel, timeline: timelineViewModel)
}
struct ViewModels {
let room: RoomScreenViewModelProtocol
let timeline: TimelineViewModelProtocol
} }
} }
@@ -559,6 +559,15 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
} }
} }
func canUser(userID: String, sendMessage messageType: MessageLikeEventType) async -> Result<Bool, RoomProxyError> {
do {
return try await .success(room.canUserSendMessage(userId: userID, message: messageType))
} catch {
MXLog.error("Failed checking if the user can send message with error: \(error)")
return .failure(.sdkError(error))
}
}
func canUser(userID: String, sendStateEvent event: StateEventType) async -> Result<Bool, RoomProxyError> { func canUser(userID: String, sendStateEvent event: StateEventType) async -> Result<Bool, RoomProxyError> {
do { do {
return try await .success(room.canUserSendState(userId: userID, stateEvent: event)) return try await .success(room.canUserSendState(userId: userID, stateEvent: event))
@@ -150,6 +150,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
func resetPowerLevels() async -> Result<RoomPowerLevels, RoomProxyError> func resetPowerLevels() async -> Result<RoomPowerLevels, RoomProxyError>
func suggestedRole(for userID: String) async -> Result<RoomMemberRole, RoomProxyError> func suggestedRole(for userID: String) async -> Result<RoomMemberRole, RoomProxyError>
func updatePowerLevelsForUsers(_ updates: [(userID: String, powerLevel: Int64)]) async -> Result<Void, RoomProxyError> func updatePowerLevelsForUsers(_ updates: [(userID: String, powerLevel: Int64)]) async -> Result<Void, RoomProxyError>
func canUser(userID: String, sendMessage messageType: MessageLikeEventType) async -> Result<Bool, RoomProxyError>
func canUser(userID: String, sendStateEvent event: StateEventType) async -> Result<Bool, RoomProxyError> func canUser(userID: String, sendStateEvent event: StateEventType) async -> Result<Bool, RoomProxyError>
func canUserInvite(userID: String) async -> Result<Bool, RoomProxyError> func canUserInvite(userID: String) async -> Result<Bool, RoomProxyError>
func canUserRedactOther(userID: String) async -> Result<Bool, RoomProxyError> func canUserRedactOther(userID: String) async -> Result<Bool, RoomProxyError>
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30073fdf3797dd70a1a9a39301e355e46d78ce4ed064215e2222ea1e3ac4eff8
size 296108
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c8f9d9ad5dfe6a949e392eb6a2014a26fa1c99dcd1609eebacf9fa3129f1ced
size 306552
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bcc07c762ebeab3cb9de5c0e516fb47704a7309e28c6bbbd335e07692e55ccfa
size 182334
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc29ca8433a626ddd150d7852483179592bfe060f6290e6b94820b4b59544eb0
size 184921