Add a "Mute" shortcut and a "Notifications" section in the room details screen #1318

This commit is contained in:
Nicolas Mauri
2023-07-14 11:43:19 +02:00
committed by GitHub
parent a7256bb486
commit dfdaee0563
22 changed files with 1118 additions and 43 deletions
@@ -24,6 +24,7 @@ struct RoomDetailsScreenCoordinatorParameters {
let mediaProvider: MediaProviderProtocol
let userDiscoveryService: UserDiscoveryServiceProtocol
let userIndicatorController: UserIndicatorControllerProtocol
let notificationSettings: NotificationSettingsProxyProtocol
}
enum RoomDetailsScreenCoordinatorAction {
@@ -47,7 +48,9 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
viewModel = RoomDetailsScreenViewModel(accountUserID: parameters.accountUserID,
roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController: parameters.userIndicatorController,
notificationSettingsProxy: parameters.notificationSettings,
appSettings: ServiceLocator.shared.settings)
}
// MARK: - Public
@@ -65,6 +68,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
self.callback?(.leftRoom)
case .requestEditDetailsPresentation(let accountOwner):
self.presentRoomDetailsEditScreen(accountOwner: accountOwner)
case .requestNotificationSettingsPresentation:
self.presentNotificationSettings()
}
}
}
@@ -182,6 +187,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
message: L10n.commonUnableToInviteMessage)
}
}
private func presentNotificationSettings() { }
}
private extension Result {
@@ -15,6 +15,7 @@
//
import Foundation
import SwiftUI
import UIKit
// MARK: - Coordinator
@@ -22,6 +23,7 @@ import UIKit
// MARK: View model
enum RoomDetailsScreenViewModelAction {
case requestNotificationSettingsPresentation
case requestMemberDetailsPresentation
case requestInvitePeoplePresentation
case leftRoom
@@ -46,6 +48,8 @@ struct RoomDetailsScreenViewState: BindableState {
var canEditRoomName = false
var canEditRoomTopic = false
var canEditRoomAvatar = false
let showNotificationSettings: Bool
var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading
var canEdit: Bool {
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
@@ -58,6 +62,36 @@ struct RoomDetailsScreenViewState: BindableState {
var bindings: RoomDetailsScreenViewStateBindings
var dmRecipient: RoomMemberDetails?
var shortcuts: [RoomDetailsScreenViewShortcut] {
var shortcuts: [RoomDetailsScreenViewShortcut] = []
if showNotificationSettings {
shortcuts.append(.mute)
}
if let permalink = dmRecipient?.permalink {
shortcuts.append(.share(link: permalink))
} else if let permalink {
shortcuts.append(.share(link: permalink))
}
return shortcuts
}
var isProcessingMuteToggleAction = false
var areNotificationsMuted: Bool {
if case .loaded(let settings) = notificationSettingsState {
return settings.mode == .mute
}
return false
}
var notificationShortcutButtonTitle: String {
areNotificationsMuted ? L10n.commonUnmute : L10n.commonMute
}
var notificationShortcutButtonImage: Image {
areNotificationsMuted ? Image(systemName: "bell.slash.fill") : Image(systemName: "bell")
}
}
struct RoomDetailsScreenViewStateBindings {
@@ -138,11 +172,64 @@ enum RoomDetailsScreenViewAction {
case confirmLeave
case ignoreConfirmed
case unignoreConfirmed
case processTapNotifications
case processToogleMuteNotifications
}
enum RoomDetailsScreenViewShortcut {
case share(link: URL)
case mute
}
extension RoomDetailsScreenViewShortcut: Hashable { }
enum RoomDetailsNotificationSettingsState {
case loading
case loaded(settings: RoomNotificationSettingsProxyProtocol)
case error
}
extension RoomDetailsNotificationSettingsState {
var label: String {
switch self {
case .loading:
return L10n.commonLoading
case .loaded(let settings):
if settings.isDefault {
return L10n.screenRoomDetailsNotificationModeDefault
} else {
return L10n.screenRoomDetailsNotificationModeCustom
}
case .error:
return L10n.commonError
}
}
var isLoading: Bool {
if case .loading = self {
return true
}
return false
}
var isLoaded: Bool {
if case .loaded = self {
return true
}
return false
}
var isError: Bool {
if case .error = self {
return true
}
return false
}
}
enum RoomDetailsScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
case alert
/// Leaving room has failed..
case unknown
}
@@ -23,6 +23,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private let accountUserID: String
private let roomProxy: RoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
private var accountOwner: RoomMemberProxyProtocol? {
didSet { updatePowerLevelPermissions() }
@@ -35,10 +36,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
init(accountUserID: String,
roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
notificationSettingsProxy: NotificationSettingsProxyProtocol,
appSettings: AppSettings) {
self.accountUserID = accountUserID
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.notificationSettingsProxy = notificationSettingsProxy
super.init(initialViewState: .init(roomId: roomProxy.id,
canonicalAlias: roomProxy.canonicalAlias,
@@ -49,11 +53,16 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
topic: roomProxy.topic,
avatarURL: roomProxy.avatarURL,
joinedMembersCount: roomProxy.joinedMembersCount,
showNotificationSettings: appSettings.notificationSettingsEnabled,
notificationSettingsState: .loading,
bindings: .init()),
imageProvider: mediaProvider)
setupRoomSubscription()
fetchMembers()
setupNotificationSettingsSubscription()
fetchNotificationSettings()
}
// MARK: - Public
@@ -87,6 +96,14 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
Task { await ignore() }
case .unignoreConfirmed:
Task { await unignore() }
case .processTapNotifications:
if state.notificationSettingsState.isError {
fetchNotificationSettings()
} else {
callback?(.requestNotificationSettingsPresentation)
}
case .processToogleMuteNotifications:
Task { await toggleMuteNotifications() }
}
}
@@ -146,6 +163,66 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.canEditRoomAvatar = accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
}
private func setupNotificationSettingsSubscription() {
notificationSettingsProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
guard let self else { return }
switch callback {
case .settingsDidChange:
self.fetchNotificationSettings()
}
}
.store(in: &cancellables)
}
private func fetchNotificationSettings() {
Task {
await fetchRoomNotificationSettings()
}
}
private func fetchRoomNotificationSettings() async {
do {
let notificationMode = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted,
activeMembersCount: UInt64(roomProxy.activeMembersCount))
state.notificationSettingsState = .loaded(settings: notificationMode)
} catch {
state.notificationSettingsState = .error
state.bindings.alertInfo = AlertInfo(id: .alert,
title: L10n.commonError,
message: L10n.screenRoomDetailsErrorLoadingNotificationSettings)
}
}
private func toggleMuteNotifications() async {
guard case .loaded(let notificationMode) = state.notificationSettingsState else { return }
state.isProcessingMuteToggleAction = true
switch notificationMode.mode {
case .mute:
do {
try await notificationSettingsProxy.unmuteRoom(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted,
activeMembersCount: UInt64(roomProxy.activeMembersCount))
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert,
title: L10n.commonError,
message: L10n.screenRoomDetailsErrorUnmuting)
}
default:
do {
try await notificationSettingsProxy.setNotificationMode(roomId: roomProxy.id, mode: .mute)
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert,
title: L10n.commonError,
message: L10n.screenRoomDetailsErrorMuting)
}
}
state.isProcessingMuteToggleAction = false
}
private static let leaveRoomLoadingID = "LeaveRoomLoading"
private func leaveRoom() async {
@@ -30,6 +30,10 @@ struct RoomDetailsScreen: View {
}
topicSection
if context.viewState.showNotificationSettings {
notificationSection
}
if context.viewState.dmRecipient == nil {
aboutSection
@@ -73,14 +77,8 @@ struct RoomDetailsScreen: View {
avatarSize: .room(on: .details),
imageProvider: context.imageProvider,
subtitle: context.viewState.canonicalAlias) {
if let permalink = context.viewState.permalink {
HStack(spacing: 32) {
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
}
.padding(.top, 32)
if !context.viewState.shortcuts.isEmpty {
headerSectionShortcuts
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.avatar)
@@ -94,17 +92,29 @@ struct RoomDetailsScreen: View {
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider,
subtitle: recipient.id) {
if let permalink = recipient.permalink {
HStack(spacing: 32) {
if !context.viewState.shortcuts.isEmpty {
headerSectionShortcuts
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.dmAvatar)
}
@ViewBuilder
private var headerSectionShortcuts: some View {
HStack(spacing: 32) {
ForEach(context.viewState.shortcuts, id: \.self) { shortcut in
switch shortcut {
case .mute:
toggleMuteButton
case .share(let permalink):
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
}
.padding(.top, 32)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.dmAvatar)
.padding(.top, 32)
}
@ViewBuilder
@@ -163,6 +173,49 @@ struct RoomDetailsScreen: View {
.compoundFormSection()
.foregroundColor(.compound.textPrimary)
}
@ViewBuilder
private var notificationSection: some View {
Section {
Button {
context.send(viewAction: .processTapNotifications)
} label: {
LabeledContent {
if context.viewState.notificationSettingsState.isLoading {
ProgressView()
} else if context.viewState.notificationSettingsState.isError {
Image(systemName: "exclamationmark.circle")
} else {
Text(context.viewState.notificationSettingsState.label)
.foregroundColor(.compound.textSecondary)
.font(.compound.bodyLG)
}
} label: {
Label(L10n.screenRoomDetailsNotificationTitle, systemImage: "bell")
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
}
.listRowSeparatorTint(.compound.borderDisabled)
.buttonStyle(FormButtonStyle(accessory: context.viewState.notificationSettingsState.isLoaded ? .navigationLink : nil))
.foregroundColor(.compound.textPrimary)
.disabled(context.viewState.notificationSettingsState.isLoading)
}
@ViewBuilder
private var toggleMuteButton: some View {
Button {
context.send(viewAction: .processToogleMuteNotifications)
} label: {
if context.viewState.isProcessingMuteToggleAction {
ProgressView()
} else {
context.viewState.notificationShortcutButtonImage
}
}
.buttonStyle(FormActionButtonStyle(title: context.viewState.notificationShortcutButtonTitle))
.disabled(context.viewState.isProcessingMuteToggleAction)
}
@ViewBuilder
private var securitySection: some View {
@@ -256,10 +309,17 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
canonicalAlias: "#alias:domain.com",
members: members))
var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration()
notificationSettingsProxyMockConfiguration.roomMode.isDefault = false
let notificationSettingsProxy = NotificationSettingsProxyMock(with: notificationSettingsProxyMockConfiguration)
let appSettings = AppSettings()
return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com",
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy,
appSettings: appSettings)
}()
static let dmRoomViewModel = {
@@ -274,11 +334,15 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
members: members))
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
let appSettings = AppSettings()
return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com",
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy,
appSettings: appSettings)
}()
static var previews: some View {