Dismiss room invite notifications when rejecting them from the home screen. (#4074)

This commit is contained in:
Doug
2025-04-28 16:33:49 +01:00
committed by GitHub
parent 8091d9c2a6
commit e14672e1f6
11 changed files with 89 additions and 70 deletions
@@ -494,7 +494,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private func presentHomeScreen() { private func presentHomeScreen() {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession, let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
bugReportService: bugReportService, bugReportService: bugReportService,
selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher()) selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher(),
appSettings: appSettings,
analyticsService: analytics,
notificationManager: notificationManager,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
let coordinator = HomeScreenCoordinator(parameters: parameters) let coordinator = HomeScreenCoordinator(parameters: parameters)
coordinator.actions coordinator.actions
@@ -8,6 +8,13 @@
import Combine import Combine
import SwiftUI import SwiftUI
extension View {
/// Applies a shimmering effect to the view.
func shimmer() -> some View {
modifier(ShimmerModifier())
}
}
/// A view modifier that applies a shimmering effect to the view. /// A view modifier that applies a shimmering effect to the view.
struct ShimmerModifier: ViewModifier { struct ShimmerModifier: ViewModifier {
/// A boolean which is toggled to trigger the animation. /// A boolean which is toggled to trigger the animation.
@@ -59,18 +66,12 @@ struct ShimmerModifier: ViewModifier {
} }
} }
extension View {
/// Applies a shimmering effect to the view.
func shimmer() -> some View {
modifier(ShimmerModifier())
}
}
struct ShimmerOverlay_Previews: PreviewProvider, TestablePreview { struct ShimmerOverlay_Previews: PreviewProvider, TestablePreview {
static let viewModel = HomeScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "")))), static let viewModel = HomeScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "")))),
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
static var previews: some View { static var previews: some View {
@@ -12,6 +12,10 @@ struct HomeScreenCoordinatorParameters {
let userSession: UserSessionProtocol let userSession: UserSessionProtocol
let bugReportService: BugReportServiceProtocol let bugReportService: BugReportServiceProtocol
let selectedRoomPublisher: CurrentValuePublisher<String?, Never> let selectedRoomPublisher: CurrentValuePublisher<String?, Never>
let appSettings: AppSettings
let analyticsService: AnalyticsService
let notificationManager: NotificationManagerProtocol
let userIndicatorController: UserIndicatorControllerProtocol
} }
enum HomeScreenCoordinatorAction { enum HomeScreenCoordinatorAction {
@@ -45,10 +49,11 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
init(parameters: HomeScreenCoordinatorParameters) { init(parameters: HomeScreenCoordinatorParameters) {
viewModel = HomeScreenViewModel(userSession: parameters.userSession, viewModel = HomeScreenViewModel(userSession: parameters.userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: parameters.selectedRoomPublisher, selectedRoomPublisher: parameters.selectedRoomPublisher,
userIndicatorController: ServiceLocator.shared.userIndicatorController) appSettings: parameters.appSettings,
analyticsService: parameters.analyticsService,
notificationManager: parameters.notificationManager,
userIndicatorController: parameters.userIndicatorController)
bugReportService = parameters.bugReportService bugReportService = parameters.bugReportService
viewModel.actions viewModel.actions
@@ -16,6 +16,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
private let userSession: UserSessionProtocol private let userSession: UserSessionProtocol
private let analyticsService: AnalyticsService private let analyticsService: AnalyticsService
private let appSettings: AppSettings private let appSettings: AppSettings
private let notificationManager: NotificationManagerProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let roomSummaryProvider: RoomSummaryProviderProtocol? private let roomSummaryProvider: RoomSummaryProviderProtocol?
@@ -26,13 +27,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} }
init(userSession: UserSessionProtocol, init(userSession: UserSessionProtocol,
analyticsService: AnalyticsService,
appSettings: AppSettings,
selectedRoomPublisher: CurrentValuePublisher<String?, Never>, selectedRoomPublisher: CurrentValuePublisher<String?, Never>,
appSettings: AppSettings,
analyticsService: AnalyticsService,
notificationManager: NotificationManagerProtocol,
userIndicatorController: UserIndicatorControllerProtocol) { userIndicatorController: UserIndicatorControllerProtocol) {
self.userSession = userSession self.userSession = userSession
self.analyticsService = analyticsService self.analyticsService = analyticsService
self.appSettings = appSettings self.appSettings = appSettings
self.notificationManager = notificationManager
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
roomSummaryProvider = userSession.clientProxy.roomSummaryProvider roomSummaryProvider = userSession.clientProxy.roomSummaryProvider
@@ -462,6 +465,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
switch result { switch result {
case .success: case .success:
await notificationManager.removeDeliveredMessageNotifications(for: roomID) // Normally handled by the room flow, but that's never presented in this case.
appSettings.seenInvites.remove(roomID) appSettings.seenInvites.remove(roomID)
case .failure: case .failure:
displayError() displayError()
@@ -137,9 +137,10 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return HomeScreenViewModel(userSession: userSession, return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
} }
} }
@@ -144,9 +144,10 @@ struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview {
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded([]))))))) roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded([])))))))
return HomeScreenViewModel(userSession: userSession, return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
}() }()
} }
@@ -155,38 +155,39 @@ struct HomeScreenInviteCell_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HomeScreenInviteCell(room: .dmInvite, HomeScreenInviteCell(room: .dmInvite,
context: viewModel().context, hideInviteAvatars: false) context: makeViewModel().context, hideInviteAvatars: false)
HomeScreenInviteCell(room: .dmInvite, HomeScreenInviteCell(room: .dmInvite,
context: viewModel().context, hideInviteAvatars: false) context: makeViewModel().context, hideInviteAvatars: false)
HomeScreenInviteCell(room: .roomInvite(), HomeScreenInviteCell(room: .roomInvite(),
context: viewModel().context, hideInviteAvatars: false) context: makeViewModel().context, hideInviteAvatars: false)
HomeScreenInviteCell(room: .roomInvite(), HomeScreenInviteCell(room: .roomInvite(),
context: viewModel().context, hideInviteAvatars: false) context: makeViewModel().context, hideInviteAvatars: false)
HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org", avatarURL: .mockMXCAvatar), HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org", avatarURL: .mockMXCAvatar),
context: viewModel().context, hideInviteAvatars: false) context: makeViewModel().context, hideInviteAvatars: false)
HomeScreenInviteCell(room: .roomInvite(alias: "#footest-hidden-avatars:somewhere.org", avatarURL: .mockMXCAvatar), HomeScreenInviteCell(room: .roomInvite(alias: "#footest-hidden-avatars:somewhere.org", avatarURL: .mockMXCAvatar),
context: viewModel().context, hideInviteAvatars: true) context: makeViewModel().context, hideInviteAvatars: true)
HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org"), HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org"),
context: viewModel().context, hideInviteAvatars: false) context: makeViewModel().context, hideInviteAvatars: false)
.dynamicTypeSize(.accessibility1) .dynamicTypeSize(.accessibility1)
.previewDisplayName("Aliased room (AX1)") .previewDisplayName("Aliased room (AX1)")
} }
.previewLayout(.sizeThatFits) .previewLayout(.sizeThatFits)
} }
static func viewModel() -> HomeScreenViewModel { static func makeViewModel() -> HomeScreenViewModel {
let clientProxy = ClientProxyMock(.init()) let clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return HomeScreenViewModel(userSession: userSession, return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
} }
} }
@@ -101,37 +101,38 @@ struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview {
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
HomeScreenKnockedCell(room: .dmInvite, HomeScreenKnockedCell(room: .dmInvite,
context: viewModel().context) context: makeViewModel().context)
HomeScreenKnockedCell(room: .dmInvite, HomeScreenKnockedCell(room: .dmInvite,
context: viewModel().context) context: makeViewModel().context)
HomeScreenKnockedCell(room: .roomKnocked(), HomeScreenKnockedCell(room: .roomKnocked(),
context: viewModel().context) context: makeViewModel().context)
HomeScreenKnockedCell(room: .roomKnocked(), HomeScreenKnockedCell(room: .roomKnocked(),
context: viewModel().context) context: makeViewModel().context)
HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org", avatarURL: .mockMXCAvatar), HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org", avatarURL: .mockMXCAvatar),
context: viewModel().context) context: makeViewModel().context)
HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org"), HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org"),
context: viewModel().context) context: makeViewModel().context)
.dynamicTypeSize(.accessibility1) .dynamicTypeSize(.accessibility1)
.previewDisplayName("Aliased room (AX1)") .previewDisplayName("Aliased room (AX1)")
} }
} }
} }
static func viewModel() -> HomeScreenViewModel { static func makeViewModel() -> HomeScreenViewModel {
let clientProxy = ClientProxyMock(.init()) let clientProxy = ClientProxyMock(.init())
let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return HomeScreenViewModel(userSession: userSession, return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
} }
} }
@@ -103,7 +103,7 @@ struct HomeScreenRecoveryKeyConfirmationBanner: View {
} }
struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, TestablePreview { struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, TestablePreview {
static let viewModel = buildViewModel() static let viewModel = makeViewModel()
static var previews: some View { static var previews: some View {
HomeScreenRecoveryKeyConfirmationBanner(state: .setUpRecovery, HomeScreenRecoveryKeyConfirmationBanner(state: .setUpRecovery,
@@ -114,16 +114,17 @@ struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, Testab
.previewDisplayName("Out of sync") .previewDisplayName("Out of sync")
} }
static func buildViewModel() -> HomeScreenViewModel { static func makeViewModel() -> HomeScreenViewModel {
let clientProxy = ClientProxyMock(.init(userID: "@alice:example.com", let clientProxy = ClientProxyMock(.init(userID: "@alice:example.com",
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loading)))) roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loading))))
let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let userSession = UserSessionMock(.init(clientProxy: clientProxy))
return HomeScreenViewModel(userSession: userSession, return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
} }
} }
@@ -164,36 +164,14 @@ private extension View {
struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview { struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
static let summaryProviderGeneric = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) static let summaryProviderGeneric = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
static let viewModelGeneric = { static let viewModelGeneric = makeViewModel(roomSummaryProvider: summaryProviderGeneric)
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "John Doe", roomSummaryProvider: summaryProviderGeneric)))) static let genericRooms = summaryProviderGeneric.roomListPublisher.value.compactMap(mockRoom)
return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}()
static let summaryProviderForNotificationsState = RoomSummaryProviderMock(.init(state: .loaded(.mockRoomsWithNotificationsState))) static let summaryProviderForNotificationsState = RoomSummaryProviderMock(.init(state: .loaded(.mockRoomsWithNotificationsState)))
static let viewModelForNotificationsState = { static let viewModelForNotificationsState = makeViewModel(roomSummaryProvider: summaryProviderForNotificationsState)
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "John Doe", roomSummaryProvider: summaryProviderForNotificationsState)))) static let notificationsStateRooms = summaryProviderForNotificationsState.roomListPublisher.value.compactMap(mockRoom)
return HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: ServiceLocator.shared.settings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}()
static func mockRoom(summary: RoomSummary) -> HomeScreenRoom? {
HomeScreenRoom(summary: summary, hideUnreadMessagesBadge: false)
}
static var previews: some View { static var previews: some View {
let genericRooms: [HomeScreenRoom] = summaryProviderGeneric.roomListPublisher.value.compactMap(mockRoom)
let notificationsStateRooms: [HomeScreenRoom] = summaryProviderForNotificationsState.roomListPublisher.value.compactMap(mockRoom)
VStack(spacing: 0) { VStack(spacing: 0) {
ForEach(genericRooms) { room in ForEach(genericRooms) { room in
HomeScreenRoomCell(room: room, context: viewModelGeneric.context, isSelected: false) HomeScreenRoomCell(room: room, context: viewModelGeneric.context, isSelected: false)
@@ -212,4 +190,19 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
.previewLayout(.sizeThatFits) .previewLayout(.sizeThatFits)
.previewDisplayName("Notifications State") .previewDisplayName("Notifications State")
} }
static func mockRoom(summary: RoomSummary) -> HomeScreenRoom? {
HomeScreenRoom(summary: summary, hideUnreadMessagesBadge: false)
}
static func makeViewModel(roomSummaryProvider: RoomSummaryProviderProtocol) -> HomeScreenViewModel {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "John Doe", roomSummaryProvider: roomSummaryProvider))))
return HomeScreenViewModel(userSession: userSession,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
} }
@@ -18,6 +18,7 @@ class HomeScreenViewModelTests: XCTestCase {
var clientProxy: ClientProxyMock! var clientProxy: ClientProxyMock!
var roomSummaryProvider: RoomSummaryProviderMock! var roomSummaryProvider: RoomSummaryProviderMock!
var appSettings: AppSettings! var appSettings: AppSettings!
var notificationManager: NotificationManagerMock!
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
@@ -298,6 +299,7 @@ class HomeScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
} }
func testDeclineInvite() async throws { func testDeclineInvite() async throws {
@@ -325,6 +327,8 @@ class HomeScreenViewModelTests: XCTestCase {
await fulfillment(of: [rejectExpectation], timeout: 1.0) await fulfillment(of: [rejectExpectation], timeout: 1.0)
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
XCTAssertTrue(notificationManager.removeDeliveredMessageNotificationsForCalled)
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, [invitedRoomIDs[0]])
} }
// MARK: - Helpers // MARK: - Helpers
@@ -350,10 +354,13 @@ class HomeScreenViewModelTests: XCTestCase {
userSession.sessionSecurityStatePublisher = securityStatePublisher userSession.sessionSecurityStatePublisher = securityStatePublisher
} }
notificationManager = NotificationManagerMock()
viewModel = HomeScreenViewModel(userSession: userSession, viewModel = HomeScreenViewModel(userSession: userSession,
analyticsService: ServiceLocator.shared.analytics,
appSettings: appSettings,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(), selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: appSettings,
analyticsService: ServiceLocator.shared.analytics,
notificationManager: notificationManager,
userIndicatorController: ServiceLocator.shared.userIndicatorController) userIndicatorController: ServiceLocator.shared.userIndicatorController)
} }
} }