Skip to content

Commit

Permalink
Fixes #2486 - Add a blocked users section in the app settings
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanceriu committed Feb 28, 2024
1 parent 368b447 commit b3140f3
Show file tree
Hide file tree
Showing 22 changed files with 382 additions and 11 deletions.
40 changes: 40 additions & 0 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
bugReportFlowCoordinator?.start()
case .about:
presentLegalInformationScreen()
case .blockedUsers:
presentBlockedUsersScreen()
case .sessionVerification:
presentSessionVerificationScreen()
case .accountSessions:
Expand Down Expand Up @@ -203,6 +205,12 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.push(LegalInformationScreenCoordinator(appSettings: parameters.appSettings))
}

private func presentBlockedUsersScreen() {
let coordinator = BlockedUsersScreenCoordinator(parameters: .init(clientProxy: parameters.userSession.clientProxy,
userIndicatorController: parameters.userIndicatorController))
navigationStackCoordinator.push(coordinator)
}

private func presentSessionVerificationScreen() {
guard let sessionVerificationController = parameters.userSession.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point")
Expand Down
1 change: 1 addition & 0 deletions ElementX/Sources/Other/AccessibilityIdentifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ enum A11yIdentifiers {
let screenLock = "settings-screen_lock"
let reportBug = "settings-report_bug"
let about = "settings_about"
let blockedUsers = "settings_blocked-users"
let advancedSettings = "settings_advanced-settings"
let developerOptions = "settings_developer-options"
let logout = "settings-logout"
Expand Down
3 changes: 3 additions & 0 deletions ElementX/Sources/Other/AvatarSize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum UserAvatarSizeOnScreen {
case readReceiptSheet
case editUserDetails
case suggestions
case blockedUsers

var value: CGFloat {
switch self {
Expand All @@ -66,6 +67,8 @@ enum UserAvatarSizeOnScreen {
return 32
case .suggestions:
return 32
case .blockedUsers:
return 32
case .settings:
return 52
case .roomDetails:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import SwiftUI

struct BlockedUsersScreenCoordinatorParameters {
let clientProxy: ClientProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}

enum BlockedUsersScreenCoordinatorAction { }

final class BlockedUsersScreenCoordinator: CoordinatorProtocol {
private let viewModel: BlockedUsersScreenViewModelProtocol

private var cancellables = Set<AnyCancellable>()

private let actionsSubject: PassthroughSubject<BlockedUsersScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<BlockedUsersScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}

init(parameters: BlockedUsersScreenCoordinatorParameters) {
viewModel = BlockedUsersScreenViewModel(clientProxy: parameters.clientProxy,
userIndicatorController: parameters.userIndicatorController)
}

func start() { }

func toPresentable() -> AnyView {
AnyView(BlockedUsersScreen(context: viewModel.context))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

enum BlockedUsersScreenViewModelAction { }

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

var bindings = BlockedUsersScreenViewStateBindings()
}

struct BlockedUsersScreenViewStateBindings {
var alertInfo: AlertInfo<BlockedUsersScreenViewStateAlertType>?
}

enum BlockedUsersScreenViewAction {
case unblockUser(userID: String)
}

enum BlockedUsersScreenViewStateAlertType: Hashable {
case unblock
case error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import SwiftUI

typealias BlockedUsersScreenViewModelType = StateStoreViewModel<BlockedUsersScreenViewState, BlockedUsersScreenViewAction>

class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsersScreenViewModelProtocol {
let clientProxy: ClientProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol

private let actionsSubject: PassthroughSubject<BlockedUsersScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<BlockedUsersScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}

init(clientProxy: ClientProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.clientProxy = clientProxy
self.userIndicatorController = userIndicatorController

super.init(initialViewState: BlockedUsersScreenViewState(blockedUsers: clientProxy.ignoredUsersPublisher.value ?? []))

showLoadingIndicator()

clientProxy.ignoredUsersPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] blockedUsers in
guard let self else { return }

if let blockedUsers {
hideLoadingIndicator()
state.blockedUsers = blockedUsers
}
}
.store(in: &cancellables)
}

// MARK: - Public

override func process(viewAction: BlockedUsersScreenViewAction) {
switch viewAction {
case .unblockUser(let userID):
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)
},
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
}
}

// MARK: - Private

private func unblockUser(_ userID: String) {
state.processingUserID = userID

Task {
if case .failure = await clientProxy.unignoreUser(userID) {
state.bindings.alertInfo = .init(id: .error)
}

state.processingUserID = nil
}
}

// MARK: Loading indicator

private static let loadingIndicatorIdentifier = "BlockedUsersLoading"

private func showLoadingIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true),
title: L10n.commonLoading,
persistent: true),
delay: .milliseconds(100))
}

private func hideLoadingIndicator() {
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine

@MainActor
protocol BlockedUsersScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<BlockedUsersScreenViewModelAction, Never> { get }
var context: BlockedUsersScreenViewModelType.Context { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Compound
import SwiftUI

struct BlockedUsersScreen: View {
@ObservedObject var context: BlockedUsersScreenViewModel.Context

var body: some View {
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)) }))
}
}
.compoundList()
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(L10n.commonBlockedUsers)
.alert(item: $context.alertInfo)
}

// MARK: - Private

private func avatar(for userID: String) -> some View {
LoadableAvatarImage(url: nil,
name: String(userID.dropFirst()),
contentID: userID,
avatarSize: .user(on: .blockedUsers),
imageProvider: nil)
.accessibilityHidden(true)
}
}

// MARK: - Previews

struct BlockedUsersScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = BlockedUsersScreenViewModel(clientProxy: MockClientProxy(userID: RoomMemberProxyMock.mockMe.userID),
userIndicatorController: UserIndicatorControllerMock())

static var previews: some View {
NavigationStack {
BlockedUsersScreen(context: viewModel.context)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum SettingsScreenCoordinatorAction {
case appLock
case bugReport
case about
case blockedUsers
case sessionVerification
case accountSessions
case notifications
Expand Down Expand Up @@ -74,6 +75,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.bugReport)
case .about:
actionsSubject.send(.about)
case .blockedUsers:
actionsSubject.send(.blockedUsers)
case .sessionVerification:
actionsSubject.send(.sessionVerification)
case .secureBackup:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum SettingsScreenViewModelAction {
case appLock
case reportBug
case about
case blockedUsers
case sessionVerification
case secureBackup
case accountSessionsList
Expand Down Expand Up @@ -61,6 +62,7 @@ enum SettingsScreenViewAction {
case appLock
case reportBug
case about
case blockedUsers
case sessionVerification
case secureBackup
case accountSessionsList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
actionsSubject.send(.reportBug)
case .about:
actionsSubject.send(.about)
case .blockedUsers:
actionsSubject.send(.blockedUsers)
case .logout:
actionsSubject.send(.logout)
case .sessionVerification:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ struct SettingsScreen: View {
context.send(viewAction: .about)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.about)

ListRow(label: .default(title: L10n.commonBlockedUsers,
icon: \.block),
kind: .navigationLink {
context.send(viewAction: .blockedUsers)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.blockedUsers)
}
}

Expand Down
5 changes: 4 additions & 1 deletion ElementX/Sources/Services/Client/MockClientProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ class MockClientProxy: ClientProxyProtocol {

var userDisplayNamePublisher: CurrentValuePublisher<String?, Never> { CurrentValueSubject<String?, Never>("User display name").asCurrentValuePublisher() }

var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { CurrentValueSubject<[String]?, Never>([]).asCurrentValuePublisher() }
var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> {
let ignoredUsers = [RoomMemberProxyMock].allMembers.map(\.userID)
return CurrentValueSubject<[String]?, Never>(ignoredUsers).asCurrentValuePublisher()
}

var notificationSettings: NotificationSettingsProxyProtocol = NotificationSettingsProxyMock(with: .init())

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b3140f3

Please sign in to comment.