mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Add a flag to disable hidden profiles for ignored users. (#2892)
This commit is contained in:
@@ -182,6 +182,16 @@ final class AppSettings {
|
|||||||
|
|
||||||
let pushGatewayBaseURL: URL = "https://matrix.org/_matrix/push/v1/notify"
|
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
|
// MARK: - Bug report
|
||||||
|
|
||||||
let bugReportServiceBaseURL: URL = "https://riot.im/bugreports"
|
let bugReportServiceBaseURL: URL = "https://riot.im/bugreports"
|
||||||
@@ -240,17 +250,10 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.elementCallBaseURL, defaultValue: "https://call.element.io", storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.elementCallBaseURL, defaultValue: "https://call.element.io", storageType: .userDefaults(store))
|
||||||
var elementCallBaseURL: URL
|
var elementCallBaseURL: URL
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Users
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.enableNotifications, defaultValue: true, storageType: .userDefaults(store))
|
/// Whether to hide the display name and avatar of ignored users as these may contain objectionable content.
|
||||||
var enableNotifications
|
let hideIgnoredUserProfiles = true
|
||||||
|
|
||||||
@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: - Maps
|
// MARK: - Maps
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func presentBlockedUsersScreen() {
|
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))
|
userIndicatorController: parameters.userIndicatorController))
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BlockedUsersScreenCoordinatorParameters {
|
struct BlockedUsersScreenCoordinatorParameters {
|
||||||
|
let hideProfiles: Bool
|
||||||
let clientProxy: ClientProxyProtocol
|
let clientProxy: ClientProxyProtocol
|
||||||
|
let mediaProvider: MediaProviderProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +28,9 @@ final class BlockedUsersScreenCoordinator: CoordinatorProtocol {
|
|||||||
private let viewModel: BlockedUsersScreenViewModelProtocol
|
private let viewModel: BlockedUsersScreenViewModelProtocol
|
||||||
|
|
||||||
init(parameters: BlockedUsersScreenCoordinatorParameters) {
|
init(parameters: BlockedUsersScreenCoordinatorParameters) {
|
||||||
viewModel = BlockedUsersScreenViewModel(clientProxy: parameters.clientProxy,
|
viewModel = BlockedUsersScreenViewModel(hideProfiles: parameters.hideProfiles,
|
||||||
|
clientProxy: parameters.clientProxy,
|
||||||
|
mediaProvider: parameters.mediaProvider,
|
||||||
userIndicatorController: parameters.userIndicatorController)
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct BlockedUsersScreenViewState: BindableState {
|
struct BlockedUsersScreenViewState: BindableState {
|
||||||
var blockedUsers: [String]
|
var blockedUsers: [UserProfileProxy]
|
||||||
var processingUserID: String?
|
var processingUserID: String?
|
||||||
|
|
||||||
var bindings = BlockedUsersScreenViewStateBindings()
|
var bindings = BlockedUsersScreenViewStateBindings()
|
||||||
@@ -28,7 +28,7 @@ struct BlockedUsersScreenViewStateBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum BlockedUsersScreenViewAction {
|
enum BlockedUsersScreenViewAction {
|
||||||
case unblockUser(userID: String)
|
case unblockUser(UserProfileProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BlockedUsersScreenViewStateAlertType: Hashable {
|
enum BlockedUsersScreenViewStateAlertType: Hashable {
|
||||||
|
|||||||
@@ -20,27 +20,30 @@ import SwiftUI
|
|||||||
typealias BlockedUsersScreenViewModelType = StateStoreViewModel<BlockedUsersScreenViewState, BlockedUsersScreenViewAction>
|
typealias BlockedUsersScreenViewModelType = StateStoreViewModel<BlockedUsersScreenViewState, BlockedUsersScreenViewAction>
|
||||||
|
|
||||||
class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsersScreenViewModelProtocol {
|
class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsersScreenViewModelProtocol {
|
||||||
|
let hideProfiles: Bool
|
||||||
let clientProxy: ClientProxyProtocol
|
let clientProxy: ClientProxyProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
init(clientProxy: ClientProxyProtocol,
|
init(hideProfiles: Bool,
|
||||||
|
clientProxy: ClientProxyProtocol,
|
||||||
|
mediaProvider: MediaProviderProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
|
self.hideProfiles = hideProfiles
|
||||||
self.clientProxy = clientProxy
|
self.clientProxy = clientProxy
|
||||||
self.userIndicatorController = userIndicatorController
|
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()
|
showLoadingIndicator()
|
||||||
|
|
||||||
clientProxy.ignoredUsersPublisher
|
clientProxy.ignoredUsersPublisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] blockedUsers in
|
.sink { [weak self] blockedUsers in
|
||||||
guard let self else { return }
|
guard let blockedUsers else { return }
|
||||||
|
Task { await self?.updateUsers(blockedUsers) }
|
||||||
if let blockedUsers {
|
|
||||||
hideLoadingIndicator()
|
|
||||||
state.blockedUsers = blockedUsers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
@@ -49,12 +52,12 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers
|
|||||||
|
|
||||||
override func process(viewAction: BlockedUsersScreenViewAction) {
|
override func process(viewAction: BlockedUsersScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .unblockUser(let userID):
|
case .unblockUser(let user):
|
||||||
state.bindings.alertInfo = .init(id: .unblock,
|
state.bindings.alertInfo = .init(id: .unblock,
|
||||||
title: L10n.screenBlockedUsersUnblockAlertTitle,
|
title: L10n.screenBlockedUsersUnblockAlertTitle,
|
||||||
message: L10n.screenBlockedUsersUnblockAlertDescription,
|
message: L10n.screenBlockedUsersUnblockAlertDescription,
|
||||||
primaryButton: .init(title: L10n.screenBlockedUsersUnblockAlertAction, role: .destructive) { [weak self] in
|
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))
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
}
|
}
|
||||||
@@ -66,12 +69,35 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers
|
|||||||
|
|
||||||
// MARK: - Private
|
// 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()
|
showLoadingIndicator()
|
||||||
state.processingUserID = userID
|
state.processingUserID = user.userID
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if case .failure = await clientProxy.unignoreUser(userID) {
|
if case .failure = await clientProxy.unignoreUser(user.userID) {
|
||||||
state.bindings.alertInfo = .init(id: .error)
|
state.bindings.alertInfo = .init(id: .error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,21 +40,21 @@ struct BlockedUsersScreen: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
} else {
|
} else {
|
||||||
Form {
|
Form {
|
||||||
ForEach(context.viewState.blockedUsers, id: \.self) { userID in
|
ForEach(context.viewState.blockedUsers, id: \.self) { user in
|
||||||
ListRow(label: .avatar(title: userID, icon: avatar(for: userID)),
|
ListRow(label: .avatar(title: user.displayName ?? user.userID, icon: avatar(for: user)),
|
||||||
details: .isWaiting(context.viewState.processingUserID == userID),
|
details: .isWaiting(context.viewState.processingUserID == user.userID),
|
||||||
kind: .button(action: { context.send(viewAction: .unblockUser(userID: userID)) }))
|
kind: .button(action: { context.send(viewAction: .unblockUser(user)) }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func avatar(for userID: String) -> some View {
|
private func avatar(for user: UserProfileProxy) -> some View {
|
||||||
LoadableAvatarImage(url: nil,
|
LoadableAvatarImage(url: user.avatarURL,
|
||||||
name: String(userID.dropFirst()),
|
name: user.displayName,
|
||||||
contentID: userID,
|
contentID: user.userID,
|
||||||
avatarSize: .user(on: .blockedUsers),
|
avatarSize: .user(on: .blockedUsers),
|
||||||
imageProvider: nil)
|
imageProvider: context.imageProvider)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,9 @@ struct BlockedUsersScreen: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct BlockedUsersScreen_Previews: PreviewProvider, TestablePreview {
|
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())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
@@ -21,12 +21,33 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class BlockedUsersScreenViewModelTests: XCTestCase {
|
class BlockedUsersScreenViewModelTests: XCTestCase {
|
||||||
func testInitialState() {
|
func testInitialState() async throws {
|
||||||
let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID))
|
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)
|
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(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Add a flag for Forks to disable hidden profiles for ignored users
|
||||||
Reference in New Issue
Block a user