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 68b95764f9
commit 57ce46e1bf
15 changed files with 150 additions and 19 deletions

View File

@@ -8593,6 +8593,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
}
//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 canUserUserIDSendStateEventCallsCount: Int {
get {

View File

@@ -36,6 +36,7 @@ struct JoinedRoomProxyMockConfiguration {
var ownUserID = RoomMemberProxyMock.mockMe.userID
var inviter: RoomMemberProxyProtocol?
var canUserSendMessage = true
var canUserInvite = true
var canUserTriggerRoomNotification = false
var canUserJoinCall = true
@@ -91,6 +92,7 @@ extension JoinedRoomProxyMock {
return .success(member.role)
}
updatePowerLevelsForUsersReturnValue = .success(())
canUserUserIDSendMessageReturnValue = .success(configuration.canUserSendMessage)
canUserUserIDSendStateEventClosure = { [weak self] userID, _ in
.success(self?.membersPublisher.value.first { $0.userID == userID }?.role ?? .user != .user)
}

View File

@@ -40,6 +40,7 @@ struct RoomScreenViewState: BindableState {
!pinnedEventsBannerState.isEmpty && lastScrollDirection != .top
}
var canSendMessage = true
var canJoinCall = false
var hasOngoingCall: Bool
var shouldShowCallButton = true

View File

@@ -340,6 +340,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
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.canAcceptKnocks = await (try? roomProxy.canUserInvite(userID: ownUserID).get()) == true
state.canDeclineKnocks = await (try? roomProxy.canUserKick(userID: ownUserID).get()) == true

View File

@@ -42,7 +42,7 @@ struct RoomScreen: View {
roomContext.send(viewAction: .footerViewAction(action))
}
composerToolbar
composer
.padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12)
.background {
if composerToolbarContext.composerFormattingEnabled {
@@ -188,6 +188,19 @@ struct RoomScreen: View {
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
private var loadingIndicator: some View {
if timelineContext.viewState.showLoading {
@@ -257,28 +270,50 @@ struct RoomScreen: View {
// MARK: - Previews
struct RoomScreen_Previews: PreviewProvider, TestablePreview {
static let roomProxyMock = JoinedRoomProxyMock(.init(id: "stable_id",
name: "Preview room",
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 let viewModels = makeViewModels()
static let readOnlyViewModels = makeViewModels(canSendMessage: false)
static var previews: some View {
NavigationStack {
RoomScreen(roomViewModel: roomViewModel,
timelineViewModel: timelineViewModel,
RoomScreen(roomViewModel: viewModels.room,
timelineViewModel: viewModels.timeline,
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
}
}

View File

@@ -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> {
do {
return try await .success(room.canUserSendState(userId: userID, stateEvent: event))

View File

@@ -150,6 +150,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
func resetPowerLevels() async -> Result<RoomPowerLevels, RoomProxyError>
func suggestedRole(for userID: String) async -> Result<RoomMemberRole, 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 canUserInvite(userID: String) async -> Result<Bool, RoomProxyError>
func canUserRedactOther(userID: String) async -> Result<Bool, RoomProxyError>

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30073fdf3797dd70a1a9a39301e355e46d78ce4ed064215e2222ea1e3ac4eff8
size 296108

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c8f9d9ad5dfe6a949e392eb6a2014a26fa1c99dcd1609eebacf9fa3129f1ced
size 306552

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bcc07c762ebeab3cb9de5c0e516fb47704a7309e28c6bbbd335e07692e55ccfa
size 182334

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc29ca8433a626ddd150d7852483179592bfe060f6290e6b94820b4b59544eb0
size 184921