// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Combine import SwiftUI typealias NotificationSettingsEditScreenViewModelType = StateStoreViewModelV2 class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenViewModelType, NotificationSettingsEditScreenViewModelProtocol { private var actionsSubject: PassthroughSubject = .init() private let chatType: NotificationSettingsChatType private let notificationSettingsProxy: NotificationSettingsProxyProtocol private let userSession: UserSessionProtocol private let roomSummaryProvider: RoomSummaryProviderProtocol? // periphery:ignore - cancellable tasks get cancelled when reassigned @CancellableTask private var fetchDefaultRoomNotificationModesTask: Task? // periphery:ignore - cancellable tasks get cancelled when reassigned @CancellableTask private var updateRoomsWithUserDefinedModeTask: Task? var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } init(chatType: NotificationSettingsChatType, userSession: UserSessionProtocol) { let bindings = NotificationSettingsEditScreenViewStateBindings() self.chatType = chatType self.userSession = userSession notificationSettingsProxy = userSession.clientProxy.notificationSettings roomSummaryProvider = userSession.clientProxy.roomSummaryProvider super.init(initialViewState: NotificationSettingsEditScreenViewState(bindings: bindings, strings: NotificationSettingsEditScreenStrings(chatType: chatType)), mediaProvider: userSession.mediaProvider) setupNotificationSettingsSubscription() setupRoomSummaryProviderSubscription() } func fetchInitialContent() { fetchDefaultRoomNotificationModes() updateRoomsWithUserDefinedMode() } // MARK: - Public override func process(viewAction: NotificationSettingsEditScreenViewAction) { switch viewAction { case .setMode(let mode): setMode(mode) case .selectRoom(let roomIdentifier): actionsSubject.send(.requestRoomNotificationSettingsPresentation(roomID: roomIdentifier)) } } // MARK: - Private private func setupNotificationSettingsSubscription() { notificationSettingsProxy.callbacks .receive(on: DispatchQueue.main) .sink { [weak self] callback in guard let self else { return } switch callback { case .settingsDidChange: self.fetchDefaultRoomNotificationModes() self.updateRoomsWithUserDefinedMode() } } .store(in: &cancellables) } private func fetchDefaultRoomNotificationModes() { fetchDefaultRoomNotificationModesTask = Task { var mode: RoomNotificationModeProxy? let isOneToOne = chatType == .oneToOneChat let encrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isOneToOne) let unencrypted_mode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isOneToOne) if encrypted_mode == unencrypted_mode { mode = encrypted_mode } let canPushEncryptedEvents = await notificationSettingsProxy.canPushEncryptedEventsToDevice() guard !Task.isCancelled else { return } switch mode { case .allMessages: state.defaultMode = .allMessages case .mentionsAndKeywordsOnly: state.defaultMode = .mentionsAndKeywordsOnly default: state.defaultMode = nil } state.canPushEncryptedEvents = canPushEncryptedEvents } } private func setupRoomSummaryProviderSubscription() { guard let roomSummaryProvider else { MXLog.error("Room summary provider unavailable") return } roomSummaryProvider.roomListPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.updateRoomsWithUserDefinedMode() } .store(in: &cancellables) } private func updateRoomsWithUserDefinedMode() { guard let roomSummaryProvider else { MXLog.error("Room summary provider unavailable") return } updateRoomsWithUserDefinedModeTask = Task { let roomsWithUserDefinedRules = try await notificationSettingsProxy.getRoomsWithUserDefinedRules() guard !Task.isCancelled else { return } let filteredRoomsSummary = roomSummaryProvider.roomListPublisher.value.filter { summary in roomsWithUserDefinedRules.contains { summary.id == $0 } } var roomsWithUserDefinedMode: [NotificationSettingsEditScreenRoom] = [] for roomSummary in filteredRoomsSummary { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomSummary.id) else { continue } // `isOneToOneRoom` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. let isOneToOneRoom = roomProxy.infoPublisher.value.activeMembersCount == 2 // display only the rooms we're interested in switch chatType { case .oneToOneChat where isOneToOneRoom, .groupChat where !isOneToOneRoom: await roomsWithUserDefinedMode.append(buildRoom(with: roomSummary)) default: break } } // Sort the room list roomsWithUserDefinedMode.sort { $0.name.localizedCompare($1.name) == .orderedAscending } state.roomsWithUserDefinedMode = roomsWithUserDefinedMode } } private func buildRoom(with summary: RoomSummary) async -> NotificationSettingsEditScreenRoom { let notificationMode = try? await notificationSettingsProxy.getUserDefinedRoomNotificationMode(roomId: summary.id) return NotificationSettingsEditScreenRoom(id: summary.id, roomId: summary.id, name: summary.name, avatar: summary.avatar, notificationMode: notificationMode) } private func setMode(_ mode: NotificationSettingsEditScreenDefaultMode) { guard state.pendingMode == nil, !state.isSelected(mode: mode) else { return } let roomNotificationModeProxy: RoomNotificationModeProxy switch mode { case .allMessages: roomNotificationModeProxy = .allMessages case .mentionsAndKeywordsOnly: roomNotificationModeProxy = .mentionsAndKeywordsOnly } state.pendingMode = mode Task { do { // On modern clients, we don't have different settings for encrypted and non-encrypted rooms. let isOneToOne = chatType == .oneToOneChat try await notificationSettingsProxy.setDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: isOneToOne, mode: roomNotificationModeProxy) try await notificationSettingsProxy.setDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: isOneToOne, mode: roomNotificationModeProxy) } catch { // In case of failure, we let the user retry let retryAction: () -> Void = { [weak self] in self?.setMode(mode) } state.bindings.alertInfo = AlertInfo(id: .setModeFailed, title: L10n.commonError, message: L10n.screenNotificationSettingsEditFailedUpdatingDefaultMode, primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), secondaryButton: .init(title: L10n.actionRetry, action: retryAction)) } state.pendingMode = nil } } }