Fixes #2320 - Implement support for (un)marking rooms as favourite from the room list and room detail screens

This commit is contained in:
Stefan Ceriu
2024-02-19 15:13:15 +02:00
parent 3887403150
commit 6d9271ce59
60 changed files with 372 additions and 179 deletions
@@ -53,8 +53,9 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
init(parameters: HomeScreenCoordinatorParameters) {
viewModel = HomeScreenViewModel(userSession: parameters.userSession,
selectedRoomPublisher: parameters.selectedRoomPublisher,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: parameters.selectedRoomPublisher,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
bugReportService = parameters.bugReportService
@@ -53,6 +53,7 @@ enum HomeScreenViewAction {
case globalSearch
case markRoomAsUnread(roomIdentifier: String)
case markRoomAsRead(roomIdentifier: String)
case markRoomAsFavourite(roomIdentifier: String, isFavourite: Bool)
}
enum HomeScreenRoomListMode: CustomStringConvertible {
@@ -95,6 +96,7 @@ struct HomeScreenViewState: BindableState {
var shouldShowFilters = false
var markAsUnreadEnabled = false
var markAsFavouriteEnabled = false
var hasPendingInvitations = false
var hasUnreadPendingInvitations = false
@@ -154,6 +156,8 @@ struct HomeScreenRoom: Identifiable, Equatable {
let isHighlighted: Bool
let isFavourite: Bool
var timestamp: String?
var lastMessage: AttributedString?
@@ -168,6 +172,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
name: "Placeholder room name",
badges: .init(isDotShown: false, isMentionShown: false, isMuteShown: false, isCallShown: false),
isHighlighted: false,
isFavourite: false,
timestamp: "Now",
lastMessage: placeholderLastMessage,
isPlaceholder: true)
@@ -194,6 +199,7 @@ extension HomeScreenRoom {
isMuteShown: isMuteShown,
isCallShown: isCallShown),
isHighlighted: isHighlighted,
isFavourite: details.isFavourite,
timestamp: details.lastMessageFormattedTimestamp,
lastMessage: details.lastMessage,
avatarURL: details.avatarURL)
@@ -21,6 +21,7 @@ typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState, Hom
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
private let userSession: UserSessionProtocol
private let analyticsService: AnalyticsService
private let appSettings: AppSettings
private let userIndicatorController: UserIndicatorControllerProtocol
@@ -38,10 +39,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
}
init(userSession: UserSessionProtocol,
selectedRoomPublisher: CurrentValuePublisher<String?, Never>,
analyticsService: AnalyticsService,
appSettings: AppSettings,
selectedRoomPublisher: CurrentValuePublisher<String?, Never>,
userIndicatorController: UserIndicatorControllerProtocol) {
self.userSession = userSession
self.analyticsService = analyticsService
self.appSettings = appSettings
self.userIndicatorController = userIndicatorController
@@ -115,6 +118,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
.weakAssign(to: \.state.markAsUnreadEnabled, on: self)
.store(in: &cancellables)
appSettings.$markAsFavouriteEnabled
.weakAssign(to: \.state.markAsFavouriteEnabled, on: self)
.store(in: &cancellables)
appSettings.$hideUnreadMessagesBadge
.sink { [weak self] _ in self?.updateRooms() }
.store(in: &cancellables)
@@ -182,9 +189,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return
}
switch await roomProxy.flagAsUnread() {
switch await roomProxy.flagAsUnread(true) {
case .success:
ServiceLocator.shared.analytics.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle)
analyticsService.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle)
case .failure(let error):
MXLog.error("Failed marking room \(roomIdentifier) as unread with error: \(error)")
}
@@ -196,9 +203,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return
}
switch await roomProxy.flagAsRead() {
switch await roomProxy.flagAsUnread(false) {
case .success:
ServiceLocator.shared.analytics.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle)
analyticsService.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle)
if case .failure(let error) = await roomProxy.markAsRead(receiptType: appSettings.sharePresence ? .read : .readPrivate) {
MXLog.error("Failed marking room \(roomIdentifier) as read with error: \(error)")
@@ -207,6 +214,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
MXLog.error("Failed flagging room \(roomIdentifier) as read with error: \(error)")
}
}
case .markRoomAsFavourite(let roomIdentifier, let isFavourite):
Task {
await markRoomAsFavourite(roomIdentifier, isFavourite: isFavourite)
}
}
}
@@ -241,7 +252,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return
}
ServiceLocator.shared.analytics.signpost.beginFirstRooms()
analyticsService.signpost.beginFirstRooms()
let hasUserBeenMigrated = appSettings.migratedAccounts[userSession.userID] == true
@@ -322,7 +333,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
}
if roomListMode == .rooms, state.roomListMode == .skeletons {
ServiceLocator.shared.analytics.signpost.endFirstRooms()
analyticsService.signpost.endFirstRooms()
}
state.roomListMode = roomListMode
@@ -399,6 +410,20 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
roomSummaryProvider.updateVisibleRange(range)
}
private func markRoomAsFavourite(_ roomID: String, isFavourite: Bool) async {
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room for identifier: \(roomID)")
return
}
switch await roomProxy.flagAsFavourite(isFavourite) {
case .success:
analyticsService.trackInteraction(name: .MobileRoomListRoomContextMenuFavouriteToggle)
case .failure(let error):
MXLog.error("Failed marking room \(roomID) as favourite: \(isFavourite) with error: \(error)")
}
}
private static let leaveRoomLoadingID = "LeaveRoomLoading"
private func startLeaveRoomProcess(roomId: String) {
@@ -238,8 +238,9 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
analyticsService: ServiceLocator.shared.analytics,
appSettings: appSettings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@@ -155,8 +155,9 @@ struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview {
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}()
}
@@ -73,8 +73,9 @@ struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, Testab
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@@ -181,8 +181,9 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}()
@@ -193,8 +194,9 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}()
@@ -63,6 +63,22 @@ struct HomeScreenRoomList: View {
}
}
if context.viewState.markAsFavouriteEnabled {
if room.isFavourite {
Button {
context.send(viewAction: .markRoomAsFavourite(roomIdentifier: room.id, isFavourite: false))
} label: {
Label(L10n.commonFavourited, icon: \.favouriteSolid)
}
} else {
Button {
context.send(viewAction: .markRoomAsFavourite(roomIdentifier: room.id, isFavourite: true))
} label: {
Label(L10n.commonFavourite, icon: \.favourite)
}
}
}
Button {
context.send(viewAction: .showRoomDetails(roomIdentifier: room.id))
} label: {
@@ -73,8 +73,9 @@ struct HomeScreenSessionVerificationBanner_Previews: PreviewProvider, TestablePr
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
}
@@ -186,14 +186,15 @@ private extension InvitesScreenRoomDetails {
avatarURL: nil,
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
isMarkedUnread: false,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: "#footest:somewhere.org",
inviter: inviter,
hasOngoingCall: false)
hasOngoingCall: false,
isMarkedUnread: false,
isFavourite: false)
return .init(roomDetails: dmRoom, isUnread: false)
}
@@ -209,14 +210,15 @@ private extension InvitesScreenRoomDetails {
avatarURL: avatarURL,
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
isMarkedUnread: false,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: alias,
inviter: inviter,
hasOngoingCall: false)
hasOngoingCall: false,
isMarkedUnread: false,
isFavourite: false)
return .init(roomDetails: dmRoom, isUnread: isUnread)
}
}
@@ -21,6 +21,7 @@ struct RoomDetailsScreenCoordinatorParameters {
let accountUserID: String
let roomProxy: RoomProxyProtocol
let mediaProvider: MediaProviderProtocol
let analyticsService: AnalyticsService
let userIndicatorController: UserIndicatorControllerProtocol
let notificationSettings: NotificationSettingsProxyProtocol
let attributedStringBuilder: AttributedStringBuilderProtocol
@@ -49,6 +50,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
viewModel = RoomDetailsScreenViewModel(accountUserID: parameters.accountUserID,
roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider,
analyticsService: parameters.analyticsService,
userIndicatorController: parameters.userIndicatorController,
notificationSettingsProxy: parameters.notificationSettings,
attributedStringBuilder: parameters.attributedStringBuilder)
@@ -129,6 +129,8 @@ struct RoomDetailsScreenViewStateBindings {
}
}
}
var isFavourite = false
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomDetailsScreenErrorType>?
@@ -180,6 +182,7 @@ enum RoomDetailsScreenViewAction {
case processToogleMuteNotifications
case displayAvatar
case processTapPolls
case toggleFavourite(isFavourite: Bool)
}
enum RoomDetailsScreenViewShortcut {
@@ -22,6 +22,7 @@ typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreen
class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol {
private let accountUserID: String
private let roomProxy: RoomProxyProtocol
private let analyticsService: AnalyticsService
private let mediaProvider: MediaProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
@@ -42,12 +43,14 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
init(accountUserID: String,
roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol,
analyticsService: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol,
notificationSettingsProxy: NotificationSettingsProxyProtocol,
attributedStringBuilder: AttributedStringBuilderProtocol) {
self.accountUserID = accountUserID
self.roomProxy = roomProxy
self.mediaProvider = mediaProvider
self.analyticsService = analyticsService
self.userIndicatorController = userIndicatorController
self.notificationSettingsProxy = notificationSettingsProxy
self.attributedStringBuilder = attributedStringBuilder
@@ -67,6 +70,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
notificationSettingsState: .loading,
bindings: .init()),
imageProvider: mediaProvider)
updateRoomInfo()
setupRoomSubscription()
fetchMembers()
@@ -122,6 +127,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
displayFullScreenAvatar()
case .processTapPolls:
actionsSubject.send(.requestPollsHistoryPresentation)
case .toggleFavourite(let isFavourite):
Task { await toggleFavourite(isFavourite) }
}
}
@@ -129,21 +136,28 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private func setupRoomSubscription() {
roomProxy.actions
.filter { $0 == .stateUpdate }
.filter { $0 == .roomInfoUpdate }
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
guard let self else { return }
self.state.title = self.roomProxy.roomTitle
let topic = attributedStringBuilder.fromPlain(self.roomProxy.topic)
self.state.topic = topic
self.state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
self.state.avatarURL = self.roomProxy.avatarURL
self.state.joinedMembersCount = self.roomProxy.joinedMembersCount
self?.updateRoomInfo()
}
.store(in: &cancellables)
}
private func updateRoomInfo() {
state.title = roomProxy.roomTitle
let topic = attributedStringBuilder.fromPlain(roomProxy.topic)
state.topic = topic
state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
state.avatarURL = roomProxy.avatarURL
state.joinedMembersCount = roomProxy.joinedMembersCount
Task {
state.bindings.isFavourite = await roomProxy.isFavourite
}
}
private func fetchMembers() {
Task {
await fetchMembersIfNeeded()
@@ -245,6 +259,15 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
state.isProcessingMuteToggleAction = false
}
private func toggleFavourite(_ isFavourite: Bool) async {
if case let .failure(error) = await roomProxy.flagAsFavourite(isFavourite) {
MXLog.error("Failed flagging room as favourite with error: \(error)")
state.bindings.isFavourite = !isFavourite
} else {
analyticsService.trackInteraction(name: .MobileRoomFavouriteToggle)
}
}
private static let leaveRoomLoadingID = "LeaveRoomLoading"
@@ -187,6 +187,13 @@ struct RoomDetailsScreen: View {
})
.disabled(context.viewState.notificationSettingsState.isLoading)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
ListRow(label: .default(title: L10n.commonFavourite, icon: \.favourite),
kind: .toggle($context.isFavourite))
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.favourite)
.onChange(of: context.isFavourite) { newValue in
context.send(viewAction: .toggleFavourite(isFavourite: newValue))
}
}
.disabled(context.viewState.notificationSettingsState.isLoading)
}
@@ -306,6 +313,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
return RoomDetailsScreenViewModel(accountUserID: RoomMemberProxyMock.mockMe.userID,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .userDirectory,
@@ -331,6 +339,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
return RoomDetailsScreenViewModel(accountUserID: RoomMemberProxyMock.mockMe.userID,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .userDirectory,
@@ -355,6 +364,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
return RoomDetailsScreenViewModel(accountUserID: RoomMemberProxyMock.mockMe.userID,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
analyticsService: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .userDirectory,
@@ -293,7 +293,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
roomProxy
.actions
.filter { $0 == .stateUpdate }
.filter { $0 == .roomInfoUpdate }
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
guard let self else { return }
@@ -50,9 +50,11 @@ protocol DeveloperOptionsProtocol: AnyObject {
var userSuggestionsEnabled: Bool { get set }
var mentionsBadgeEnabled: Bool { get set }
var roomListFiltersEnabled: Bool { get set }
var markAsUnreadEnabled: Bool { get set }
var hideUnreadMessagesBadge: Bool { get set }
var markAsUnreadEnabled: Bool { get set }
var markAsFavouriteEnabled: Bool { get set }
var elementCallBaseURL: URL { get set }
}
@@ -60,6 +60,10 @@ struct DeveloperOptionsScreen: View {
Text("Mark as unread")
}
Toggle(isOn: $context.markAsFavouriteEnabled) {
Text("Mark as favourite")
}
Toggle(isOn: $context.hideUnreadMessagesBadge) {
Text("Hide grey dots")
}