From 0da5cc77c85d5f07ffa9c22cbaea3819be1d1b5b Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 May 2024 17:08:55 +0100 Subject: [PATCH 1/2] Add a flag to allow forks to show ignored user profiles. --- .../Sources/Application/AppSettings.swift | 25 +++++---- .../SettingsFlowCoordinator.swift | 4 +- .../BlockedUsersScreenCoordinator.swift | 6 ++- .../BlockedUsersScreenModels.swift | 4 +- .../BlockedUsersScreenViewModel.swift | 52 ++++++++++++++----- .../View/BlockedUsersScreen.swift | 22 ++++---- .../BlockedUsersScreenViewModelTests.swift | 25 ++++++++- 7 files changed, 98 insertions(+), 40 deletions(-) diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index a376719eb5..d710290d3e 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -181,6 +181,16 @@ final class AppSettings { } let pushGatewayBaseURL: URL = "https://matrix.org/_matrix/push/v1/notify" + + @UserPreference(key: UserDefaultsKeys.enableNotifications, defaultValue: true, storageType: .userDefaults(store)) + var enableNotifications + + @UserPreference(key: UserDefaultsKeys.enableInAppNotifications, defaultValue: true, storageType: .userDefaults(store)) + var enableInAppNotifications + + /// Tag describing which set of device specific rules a pusher executes. + @UserPreference(key: UserDefaultsKeys.pusherProfileTag, storageType: .userDefaults(store)) + var pusherProfileTag: String? // MARK: - Bug report @@ -240,17 +250,10 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.elementCallBaseURL, defaultValue: "https://call.element.io", storageType: .userDefaults(store)) var elementCallBaseURL: URL - // MARK: - Notifications - - @UserPreference(key: UserDefaultsKeys.enableNotifications, defaultValue: true, storageType: .userDefaults(store)) - var enableNotifications - - @UserPreference(key: UserDefaultsKeys.enableInAppNotifications, defaultValue: true, storageType: .userDefaults(store)) - var enableInAppNotifications - - /// Tag describing which set of device specific rules a pusher executes. - @UserPreference(key: UserDefaultsKeys.pusherProfileTag, storageType: .userDefaults(store)) - var pusherProfileTag: String? + // MARK: - Users + + /// Whether to hide the display name and avatar of ignored users as these may contain objectionable content. + let hideIgnoredUserProfiles = true // MARK: - Maps diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 6e962f6068..3caa84fa82 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -201,7 +201,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { } private func presentBlockedUsersScreen() { - let coordinator = BlockedUsersScreenCoordinator(parameters: .init(clientProxy: parameters.userSession.clientProxy, + let coordinator = BlockedUsersScreenCoordinator(parameters: .init(hideProfiles: parameters.appSettings.hideIgnoredUserProfiles, + clientProxy: parameters.userSession.clientProxy, + mediaProvider: parameters.userSession.mediaProvider, userIndicatorController: parameters.userIndicatorController)) navigationStackCoordinator.push(coordinator) } diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift index eb4c25c84e..a5abd9b16a 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift @@ -18,7 +18,9 @@ import Combine import SwiftUI struct BlockedUsersScreenCoordinatorParameters { + let hideProfiles: Bool let clientProxy: ClientProxyProtocol + let mediaProvider: MediaProviderProtocol let userIndicatorController: UserIndicatorControllerProtocol } @@ -26,7 +28,9 @@ final class BlockedUsersScreenCoordinator: CoordinatorProtocol { private let viewModel: BlockedUsersScreenViewModelProtocol init(parameters: BlockedUsersScreenCoordinatorParameters) { - viewModel = BlockedUsersScreenViewModel(clientProxy: parameters.clientProxy, + viewModel = BlockedUsersScreenViewModel(hideProfiles: parameters.hideProfiles, + clientProxy: parameters.clientProxy, + mediaProvider: parameters.mediaProvider, userIndicatorController: parameters.userIndicatorController) } diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift index 40919760d8..38387c6842 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift @@ -17,7 +17,7 @@ import Foundation struct BlockedUsersScreenViewState: BindableState { - var blockedUsers: [String] + var blockedUsers: [UserProfileProxy] var processingUserID: String? var bindings = BlockedUsersScreenViewStateBindings() @@ -28,7 +28,7 @@ struct BlockedUsersScreenViewStateBindings { } enum BlockedUsersScreenViewAction { - case unblockUser(userID: String) + case unblockUser(UserProfileProxy) } enum BlockedUsersScreenViewStateAlertType: Hashable { diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift index ae6e256744..e1724d6d93 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift @@ -20,27 +20,30 @@ import SwiftUI typealias BlockedUsersScreenViewModelType = StateStoreViewModel class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsersScreenViewModelProtocol { + let hideProfiles: Bool let clientProxy: ClientProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol - init(clientProxy: ClientProxyProtocol, + init(hideProfiles: Bool, + clientProxy: ClientProxyProtocol, + mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) { + self.hideProfiles = hideProfiles self.clientProxy = clientProxy self.userIndicatorController = userIndicatorController - super.init(initialViewState: BlockedUsersScreenViewState(blockedUsers: clientProxy.ignoredUsersPublisher.value ?? [])) + let ignoredUsers = clientProxy.ignoredUsersPublisher.value?.map { UserProfileProxy(userID: $0) } + + super.init(initialViewState: BlockedUsersScreenViewState(blockedUsers: ignoredUsers ?? []), + imageProvider: mediaProvider) showLoadingIndicator() clientProxy.ignoredUsersPublisher .receive(on: DispatchQueue.main) .sink { [weak self] blockedUsers in - guard let self else { return } - - if let blockedUsers { - hideLoadingIndicator() - state.blockedUsers = blockedUsers - } + guard let blockedUsers else { return } + Task { await self?.updateUsers(blockedUsers) } } .store(in: &cancellables) } @@ -49,12 +52,12 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers override func process(viewAction: BlockedUsersScreenViewAction) { switch viewAction { - case .unblockUser(let userID): + case .unblockUser(let user): state.bindings.alertInfo = .init(id: .unblock, title: L10n.screenBlockedUsersUnblockAlertTitle, message: L10n.screenBlockedUsersUnblockAlertDescription, primaryButton: .init(title: L10n.screenBlockedUsersUnblockAlertAction, role: .destructive) { [weak self] in - self?.unblockUser(userID) + self?.unblockUser(user) }, secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) } @@ -66,12 +69,35 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers // MARK: - Private - private func unblockUser(_ userID: String) { + private func updateUsers(_ blockedUsers: [String]) async { + defer { hideLoadingIndicator() } + + if hideProfiles { + state.blockedUsers = blockedUsers.map { UserProfileProxy(userID: $0) } + } else { + state.blockedUsers = await withTaskGroup(of: UserProfileProxy.self) { group in + for userID in blockedUsers { + group.addTask { + switch await self.clientProxy.profile(for: userID) { + case .success(let profile): profile + case .failure: UserProfileProxy(userID: userID) + } + } + } + + return await group.reduce(into: []) { partialResult, profile in + partialResult.append(profile) + } + } + } + } + + private func unblockUser(_ user: UserProfileProxy) { showLoadingIndicator() - state.processingUserID = userID + state.processingUserID = user.userID Task { - if case .failure = await clientProxy.unignoreUser(userID) { + if case .failure = await clientProxy.unignoreUser(user.userID) { state.bindings.alertInfo = .init(id: .error) } diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift index f444c0a1a3..ce456d8b84 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift @@ -40,21 +40,21 @@ struct BlockedUsersScreen: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } else { Form { - ForEach(context.viewState.blockedUsers, id: \.self) { userID in - ListRow(label: .avatar(title: userID, icon: avatar(for: userID)), - details: .isWaiting(context.viewState.processingUserID == userID), - kind: .button(action: { context.send(viewAction: .unblockUser(userID: userID)) })) + ForEach(context.viewState.blockedUsers, id: \.self) { user in + ListRow(label: .avatar(title: user.displayName ?? user.userID, icon: avatar(for: user)), + details: .isWaiting(context.viewState.processingUserID == user.userID), + kind: .button(action: { context.send(viewAction: .unblockUser(user)) })) } } } } - private func avatar(for userID: String) -> some View { - LoadableAvatarImage(url: nil, - name: String(userID.dropFirst()), - contentID: userID, + private func avatar(for user: UserProfileProxy) -> some View { + LoadableAvatarImage(url: user.avatarURL, + name: user.displayName, + contentID: user.userID, avatarSize: .user(on: .blockedUsers), - imageProvider: nil) + imageProvider: context.imageProvider) .accessibilityHidden(true) } } @@ -62,7 +62,9 @@ struct BlockedUsersScreen: View { // MARK: - Previews struct BlockedUsersScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = BlockedUsersScreenViewModel(clientProxy: ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)), + static let viewModel = BlockedUsersScreenViewModel(hideProfiles: true, + clientProxy: ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)), + mediaProvider: MockMediaProvider(), userIndicatorController: UserIndicatorControllerMock()) static var previews: some View { diff --git a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift index f9925a8a58..9fe8d82034 100644 --- a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift +++ b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift @@ -21,12 +21,33 @@ import XCTest @MainActor class BlockedUsersScreenViewModelTests: XCTestCase { - func testInitialState() { + func testInitialState() async throws { let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)) - let viewModel = BlockedUsersScreenViewModel(clientProxy: clientProxy, + let viewModel = BlockedUsersScreenViewModel(hideProfiles: true, + clientProxy: clientProxy, + mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) + let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains(where: { $0.displayName != nil }) } + try await deferred.fulfill() + + XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) + XCTAssertFalse(clientProxy.profileForCalled) + } + + func testProfiles() async throws { + let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)) + + let viewModel = BlockedUsersScreenViewModel(hideProfiles: false, + clientProxy: clientProxy, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) + + let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains(where: { $0.displayName != nil }) } + try await deferred.fulfill() + XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) + XCTAssertTrue(clientProxy.profileForCalled) } } From 701a232cfc2110b7568feedafc67360ea717f698 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 30 May 2024 17:26:49 +0100 Subject: [PATCH 2/2] Changelog. --- changelog.d/pr-2892.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-2892.misc diff --git a/changelog.d/pr-2892.misc b/changelog.d/pr-2892.misc new file mode 100644 index 0000000000..8c0f2842db --- /dev/null +++ b/changelog.d/pr-2892.misc @@ -0,0 +1 @@ +Add a flag for Forks to disable hidden profiles for ignored users \ No newline at end of file