Display the notification mode for each room in the the room list (#1595)

Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
This commit is contained in:
Nicolas Mauri 2023-08-31 13:15:38 +02:00 committed by GitHub
parent f0fad25d09
commit 3ab7b1b1c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 145 additions and 63 deletions

View File

@ -1,5 +1,7 @@
"Notification" = "Notification";
"a11y_hide_password" = "Hide password";
"a11y_notifications_mentions_only" = "Mentions only";
"a11y_notifications_muted" = "Muted";
"a11y_send_files" = "Send files";
"a11y_show_password" = "Show password";
"a11y_user_menu" = "User menu";

View File

@ -231,7 +231,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
break
case (.roomDetails, .presentNotificationSettingsScreen, .notificationSettingsScreen):
asyncPresentNotificationSettingsScreen(animated: animated)
presentNotificationSettingsScreen(animated: animated)
case (.notificationSettingsScreen, .dismissNotificationSettingsScreen, .roomDetails):
break
@ -397,13 +397,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return
}
let params = await RoomDetailsScreenCoordinatorParameters(accountUserID: userSession.userID,
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
userIndicatorController: userIndicatorController,
notificationSettings: userSession.clientProxy.notificationSettings())
let params = RoomDetailsScreenCoordinatorParameters(accountUserID: userSession.userID,
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
userIndicatorController: userIndicatorController,
notificationSettings: userSession.clientProxy.notificationSettings)
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in
switch action {
@ -691,19 +691,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentRoom(roomID: roomID), userInfo: EventUserInfo(animated: true, destinationRoomProxy: targetRoomProxy))
}
private func asyncPresentNotificationSettingsScreen(animated: Bool) {
Task {
await presentNotificationSettingsScreen(animated: animated)
}
}
private func presentNotificationSettingsScreen(animated: Bool) async {
private func presentNotificationSettingsScreen(animated: Bool) {
let navigationCoordinator = NavigationStackCoordinator()
let parameters = await NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationCoordinator,
userSession: userSession,
userNotificationCenter: UNUserNotificationCenter.current(),
notificationSettings: userSession.clientProxy.notificationSettings(),
isModallyPresented: true)
let parameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationCoordinator,
userSession: userSession,
userNotificationCenter: UNUserNotificationCenter.current(),
notificationSettings: userSession.clientProxy.notificationSettings,
isModallyPresented: true)
let coordinator = NotificationSettingsScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
switch action {

View File

@ -297,22 +297,16 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: Settings
private func presentSettingsScreen(animated: Bool) {
Task {
await asyncPresentSettingsScreen(animated: animated)
}
}
private func asyncPresentSettingsScreen(animated: Bool) async {
let settingsNavigationStackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: settingsNavigationStackCoordinator)
let parameters = await SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userIndicatorController: userIndicatorController,
userSession: userSession,
bugReportService: bugReportService,
notificationSettings: userSession.clientProxy.notificationSettings(),
appSettings: appSettings)
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userIndicatorController: userIndicatorController,
userSession: userSession,
bugReportService: bugReportService,
notificationSettings: userSession.clientProxy.notificationSettings,
appSettings: appSettings)
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
settingsScreenCoordinator.callback = { [weak self] action in
guard let self else { return }

View File

@ -12,6 +12,10 @@ import Foundation
public enum L10n {
/// Hide password
public static var a11yHidePassword: String { return L10n.tr("Localizable", "a11y_hide_password") }
/// Mentions only
public static var a11yNotificationsMentionsOnly: String { return L10n.tr("Localizable", "a11y_notifications_mentions_only") }
/// Muted
public static var a11yNotificationsMuted: String { return L10n.tr("Localizable", "a11y_notifications_muted") }
/// Send files
public static var a11ySendFiles: String { return L10n.tr("Localizable", "a11y_send_files") }
/// Show password

View File

@ -139,6 +139,14 @@ struct HomeScreenRoom: Identifiable, Equatable {
var avatarURL: URL?
var notificationMode: RoomNotificationModeProxy?
var hasDecoration: Bool {
// notification setting is displayed only for .mentionsAndKeywords and .mute
let showNotificationSettings = notificationMode != nil
return hasUnreads || showNotificationSettings
}
var isPlaceholder = false
static func placeholder() -> HomeScreenRoom {

View File

@ -266,13 +266,16 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
private func buildRoom(with details: RoomSummaryDetails, invalidated: Bool) -> HomeScreenRoom {
let identifier = invalidated ? "invalidated-" + details.id : details.id
let notificationMode = details.notificationMode == .allMessages || appSettings.notificationSettingsEnabled == false ? nil : details.notificationMode
return HomeScreenRoom(id: identifier,
roomId: details.id,
name: details.name,
hasUnreads: details.unreadNotificationCount > 0,
timestamp: details.lastMessageFormattedTimestamp,
lastMessage: .init(attributedString: details.lastMessage, isLoading: false),
avatarURL: details.avatarURL)
avatarURL: details.avatarURL,
notificationMode: notificationMode)
}
private func updateVisibleRange(_ range: Range<Int>) {

View File

@ -15,6 +15,7 @@
//
import Combine
import Compound
import SwiftUI
struct HomeScreenRoomCell: View {
@ -115,17 +116,42 @@ struct HomeScreenRoomCell: View {
Spacer()
if room.hasUnreads {
Circle()
.frame(width: 12, height: 12)
.foregroundColor(.compound.iconAccentTertiary)
.padding(.leading, 12)
} else {
// Force extra padding between last message text and the right border of the screen if there is no unread dot
Circle()
.frame(width: 12, height: 12)
.hidden()
HStack(spacing: 8) {
if let notificationMode = room.notificationMode {
notificationModeIcon
.foregroundColor(room.hasUnreads ? .compound.iconAccentTertiary : .compound.iconQuaternary)
}
if room.hasUnreads {
Circle()
.frame(width: 12, height: 12)
.foregroundColor(.compound.iconAccentTertiary)
}
if !room.hasDecoration {
// Force extra padding between last message text and the right border of the screen if there is no unread dot
Circle()
.frame(width: 12, height: 12)
.hidden()
}
}
.padding(.leading, room.hasDecoration ? 12 : 0)
}
}
@ViewBuilder
var notificationModeIcon: some View {
switch room.notificationMode {
case .none, .allMessages:
EmptyView()
case .mentionsAndKeywordsOnly:
CompoundIcon(\.mention)
.font(.system(size: 15))
.accessibilityLabel(L10n.a11yNotificationsMentionsOnly)
case .mute:
CompoundIcon(\.notificationsSolidOff)
.font(.system(size: 15))
.accessibilityLabel(L10n.a11yNotificationsMuted)
}
}
@ -191,7 +217,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider {
hasUnreads: details.unreadNotificationCount > 0,
timestamp: Date.now.formattedMinimal(),
lastMessage: .init(attributedString: details.lastMessage,
isLoading: false))
isLoading: false),
notificationMode: details.notificationMode)
}
}

View File

@ -189,6 +189,7 @@ private extension InvitesScreenRoomDetails {
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
unreadNotificationCount: 0,
notificationMode: nil,
canonicalAlias: "#footest:somewhere.org",
inviter: inviter)
return .init(roomDetails: dmRoom, isUnread: false)
@ -207,6 +208,7 @@ private extension InvitesScreenRoomDetails {
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
unreadNotificationCount: 0,
notificationMode: nil,
canonicalAlias: alias,
inviter: inviter)
return .init(roomDetails: dmRoom, isUnread: isUnread)

View File

@ -50,6 +50,7 @@ struct DeveloperOptionsScreen: View {
Section("Notifications") {
Toggle(isOn: $context.notificationSettingsEnabled) {
Text("Show notification settings")
Text("Requires app reboot")
}
}

View File

@ -36,6 +36,8 @@ class ClientProxy: ClientProxyProtocol {
var roomSummaryProvider: RoomSummaryProviderProtocol?
var inviteSummaryProvider: RoomSummaryProviderProtocol?
var notificationSettings: NotificationSettingsProxyProtocol
private let roomListRecencyOrderingAllowedEventTypes = ["m.room.message", "m.room.encrypted", "m.sticker"]
@ -66,6 +68,9 @@ class ClientProxy: ClientProxyProtocol {
clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent)
mediaLoader = MediaLoader(client: client, clientQueue: clientQueue)
notificationSettings = NotificationSettingsProxy(notificationSettings: client.getNotificationSettings(),
backgroundTaskService: backgroundTaskService)
client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
self?.callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
@ -348,13 +353,6 @@ class ClientProxy: ClientProxyProtocol {
}
}
func notificationSettings() async -> NotificationSettingsProxyProtocol {
await Task.dispatch(on: clientQueue) {
NotificationSettingsProxy(notificationSettings: self.client.getNotificationSettings(),
backgroundTaskService: self.backgroundTaskService)
}
}
// MARK: Private
private func restartSync(delay: Duration = .zero) {
@ -405,12 +403,14 @@ class ClientProxy: ClientProxyProtocol {
eventStringBuilder: eventStringBuilder,
name: "AllRooms",
appSettings: appSettings,
notificationSettings: notificationSettings,
backgroundTaskService: backgroundTaskService)
try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
eventStringBuilder: eventStringBuilder,
name: "Invites",
appSettings: appSettings,
notificationSettings: notificationSettings,
backgroundTaskService: backgroundTaskService)
self.syncService = syncService

View File

@ -83,6 +83,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var inviteSummaryProvider: RoomSummaryProviderProtocol? { get }
var notificationSettings: NotificationSettingsProxyProtocol { get }
func startSync()
func stopSync()
@ -114,6 +116,4 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func searchUsers(searchTerm: String, limit: UInt) async -> Result<SearchUsersResultsProxy, ClientProxyError>
func profile(for userID: String) async -> Result<UserProfileProxy, ClientProxyError>
func notificationSettings() async -> NotificationSettingsProxyProtocol
}

View File

@ -32,6 +32,8 @@ class MockClientProxy: ClientProxyProtocol {
var inviteSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
var avatarURLPublisher: AnyPublisher<URL?, Never> { Empty().eraseToAnyPublisher() }
var notificationSettings: NotificationSettingsProxyProtocol = NotificationSettingsProxyMock(with: .init())
init(userID: String, deviceID: String? = nil, accountURL: URL? = nil, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) {
self.userID = userID
@ -141,13 +143,4 @@ class MockClientProxy: ClientProxyProtocol {
getProfileCalled = true
return getProfileResult
}
var notificationSettingsResult: NotificationSettingsProxyProtocol?
func notificationSettings() -> NotificationSettingsProxyProtocol {
if let notificationSettingsResult {
return notificationSettingsResult
} else {
return NotificationSettingsProxyMock(with: .init())
}
}
}

View File

@ -37,7 +37,7 @@ private final class WeakNotificationSettingsProxy: NotificationSettingsDelegate
final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
private(set) var notificationSettings: MatrixRustSDK.NotificationSettingsProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol?
let callbacks = PassthroughSubject<NotificationSettingsProxyCallback, Never>()
init(notificationSettings: MatrixRustSDK.NotificationSettingsProtocol, backgroundTaskService: BackgroundTaskServiceProtocol?) {

View File

@ -57,6 +57,7 @@ extension Array where Element == RoomSummary {
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
lastMessageFormattedTimestamp: "Now",
unreadNotificationCount: 4,
notificationMode: .allMessages,
canonicalAlias: nil,
inviter: RoomMemberProxyMock.mockCharlie)),
.filled(details: RoomSummaryDetails(id: "2",
@ -66,6 +67,7 @@ extension Array where Element == RoomSummary {
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
unreadNotificationCount: 1,
notificationMode: .mentionsAndKeywordsOnly,
canonicalAlias: nil,
inviter: RoomMemberProxyMock.mockCharlie)),
.filled(details: RoomSummaryDetails(id: "3",
@ -75,6 +77,7 @@ extension Array where Element == RoomSummary {
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
lastMessageFormattedTimestamp: "Later",
unreadNotificationCount: 0,
notificationMode: .mute,
canonicalAlias: nil,
inviter: RoomMemberProxyMock.mockCharlie)),
.empty
@ -87,6 +90,7 @@ extension Array where Element == RoomSummary {
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
unreadNotificationCount: 0,
notificationMode: nil,
canonicalAlias: "#footest:somewhere.org",
inviter: RoomMemberProxyMock.mockCharlie)),
.filled(details: RoomSummaryDetails(id: "someAwesomeRoomId2",
@ -96,6 +100,7 @@ extension Array where Element == RoomSummary {
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
unreadNotificationCount: 0,
notificationMode: nil,
canonicalAlias: nil,
inviter: RoomMemberProxyMock.mockCharlie))
]

View File

@ -25,6 +25,7 @@ struct RoomSummaryDetails {
let lastMessage: AttributedString?
let lastMessageFormattedTimestamp: String?
let unreadNotificationCount: UInt
let notificationMode: RoomNotificationModeProxy?
let canonicalAlias: String?
let inviter: RoomMemberProxyProtocol?
}

View File

@ -23,6 +23,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
private let eventStringBuilder: RoomEventStringBuilder
private let name: String
private var appSettings: AppSettings
private let notificationSettings: NotificationSettingsProxyProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol
private let serialDispatchQueue: DispatchQueue
@ -58,18 +59,24 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
eventStringBuilder: RoomEventStringBuilder,
name: String,
appSettings: AppSettings,
notificationSettings: NotificationSettingsProxyProtocol,
backgroundTaskService: BackgroundTaskServiceProtocol) {
self.roomListService = roomListService
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .default)
self.eventStringBuilder = eventStringBuilder
self.name = name
self.appSettings = appSettings
self.notificationSettings = notificationSettings
self.backgroundTaskService = backgroundTaskService
diffsPublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] in self?.updateRoomsWithDiffs($0) }
.store(in: &cancellables)
if appSettings.notificationSettingsEnabled {
setupNotificationSettingsSubscription()
}
}
func setRoomList(_ roomList: RoomList) {
@ -227,6 +234,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
inviterProxy = RoomMemberProxy(member: inviter, backgroundTaskService: backgroundTaskService)
}
let notificationMode = roomInfo.notificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
let details = RoomSummaryDetails(id: roomInfo.id,
name: roomInfo.name ?? roomInfo.id,
isDirect: roomInfo.isDirect,
@ -234,6 +243,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
lastMessage: attributedLastMessage,
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
unreadNotificationCount: UInt(roomInfo.notificationCount),
notificationMode: notificationMode,
canonicalAlias: roomInfo.canonicalAlias,
inviter: inviterProxy)
@ -324,6 +334,43 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
return CollectionDifference(changes)
}
private func setupNotificationSettingsSubscription() {
notificationSettings.callbacks
.receive(on: serialDispatchQueue)
.dropFirst() // drop the first one to avoid rebuilding the summaries during the first synchronization
.sink { [weak self] callback in
guard let self else { return }
switch callback {
case .settingsDidChange:
self.rebuildRoomSummaries()
}
}
.store(in: &cancellables)
}
private func rebuildRoomSummaries() {
let span = MXLog.createSpan("\(name).rebuild_room_summaries")
span.enter()
defer {
span.exit()
}
MXLog.info("\(name): Rebuilding room summaries for \(rooms.count) rooms")
rooms = rooms.map {
switch $0 {
case .empty:
return $0
case .filled(let details):
return self.buildRoomSummaryForIdentifier(details.id, invalidated: false)
case .invalidated(let details):
return self.buildRoomSummaryForIdentifier(details.id, invalidated: true)
}
}
MXLog.info("\(name): Finished rebuilding room summaries (\(rooms.count) rooms)")
}
}
extension RoomSummaryProviderState {

View File

@ -227,6 +227,7 @@ class LoggingTests: XCTestCase {
lastMessage: AttributedString(lastMessage),
lastMessageFormattedTimestamp: "Now",
unreadNotificationCount: 0,
notificationMode: nil,
canonicalAlias: nil,
inviter: nil)