Skip to content

Commit

Permalink
Add a flag to disable hidden profiles for ignored users. (#2892)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixlwave authored May 31, 2024
1 parent ea08fad commit 6deedb5
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 40 deletions.
25 changes: 14 additions & 11 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ import Combine
import SwiftUI

struct BlockedUsersScreenCoordinatorParameters {
let hideProfiles: Bool
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}

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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import Foundation

struct BlockedUsersScreenViewState: BindableState {
var blockedUsers: [String]
var blockedUsers: [UserProfileProxy]
var processingUserID: String?

var bindings = BlockedUsersScreenViewStateBindings()
Expand All @@ -28,7 +28,7 @@ struct BlockedUsersScreenViewStateBindings {
}

enum BlockedUsersScreenViewAction {
case unblockUser(userID: String)
case unblockUser(UserProfileProxy)
}

enum BlockedUsersScreenViewStateAlertType: Hashable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,30 @@ import SwiftUI
typealias BlockedUsersScreenViewModelType = StateStoreViewModel<BlockedUsersScreenViewState, BlockedUsersScreenViewAction>

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)
}
Expand All @@ -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))
}
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,31 @@ 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)
}
}

// 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 {
Expand Down
25 changes: 23 additions & 2 deletions UnitTests/Sources/BlockedUsersScreenViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions changelog.d/pr-2892.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a flag for Forks to disable hidden profiles for ignored users

0 comments on commit 6deedb5

Please sign in to comment.