Ignore User UI (#737)

* generated files

* Revert "generated files"

This reverts commit f62c1dbcd9.

* renaming files to RoomMembersList

* completed the renaming of the list files

* added generated files

* basic setup of the view and the mock

* added a new mock with a avatar

* share/copy link

* copyUserLink implemented

* removed unimplemented tests

* block user UI

* navigation to room member details added

* implemented but we require a sync from the Rust side

* adjusted some UI test screens

* alert for unblocking

* completed

* some tests

* changelog

* ignore user ui enabled

* loader inside the button when the request is fetching

* removed unused code

* blocking the button while loading

* improved the code

* changelog

* UI tests

* unit tests

* added collection concurrency kit

* Revert "added collection concurrency kit"

This reverts commit 499fbe129f.

* replaced the asyncMap with a @MainActor builder function

* pr comments

* added localazy to setup

* sdk bump to 1.0.49
pull/749/head
Mauro 2023-03-28 11:00:40 +02:00 committed by GitHub
parent 2b753b1135
commit e1df5310b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 187 additions and 58 deletions

View File

@ -111,8 +111,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
"state" : {
"revision" : "9eee9ba2f14eaa4981759295790bcbdbdebca747",
"version" : "1.0.48-alpha"
"revision" : "9650501c92a1ed802a757f2c6807a36d59619f43",
"version" : "1.0.49-alpha"
}
},
{

View File

@ -27,6 +27,7 @@ struct A11yIdentifiers {
static let sessionVerificationScreen = SessionVerificationScreen()
static let softLogoutScreen = SoftLogoutScreen()
static let startChatScreen = StartChatScreen()
static let roomMemberDetailsScreen = RoomMemberDetailsScreen()
struct BugReportScreen {
let report = "bug_report-report"
@ -98,4 +99,9 @@ struct A11yIdentifiers {
let closeStartChat = "start_chat-close"
let inviteFriends = "start_chat-invite_friends"
}
struct RoomMemberDetailsScreen {
let ignore = "room_member_details-ignore"
let unignore = "room_member_details-unignore"
}
}

View File

@ -1,37 +0,0 @@
//
// Copyright 2023 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
extension Sequence {
func asyncMap<T>(_ transform: @escaping (Element) async -> T) async -> [T] {
await withTaskGroup(of: T.self) { group in
var transformedElements = [T]()
for element in self {
group.addTask {
await transform(element)
}
}
for await transformedElement in group {
transformedElements.append(transformedElement)
}
return transformedElements
}
}
}

View File

@ -19,13 +19,16 @@ import SwiftUI
/// A view to be added on the trailing edge of a form row.
enum FormRowAccessory: View {
case navigationLink
case progressView
var body: some View {
switch self {
case .navigationLink:
return Image(systemName: "chevron.forward")
Image(systemName: "chevron.forward")
.font(.element.subheadlineBold)
.foregroundColor(.element.quaternaryContent)
case .progressView:
ProgressView()
}
}
}

View File

@ -25,6 +25,7 @@ struct RoomMemberDetailsViewState: BindableState {
let isAccountOwner: Bool
let permalink: URL?
var isIgnored: Bool
var isProcessingIgnoreRequest = false
var bindings: RoomMemberDetailsViewStateBindings
}
@ -34,7 +35,7 @@ struct RoomMemberDetailsViewStateBindings {
var errorAlert: ErrorAlertItem?
}
struct IgnoreUserAlertItem: AlertItem {
struct IgnoreUserAlertItem: AlertItem, Equatable {
enum Action {
case ignore
case unignore
@ -73,8 +74,8 @@ struct IgnoreUserAlertItem: AlertItem {
}
enum RoomMemberDetailsViewAction {
case showUnblockAlert
case showBlockAlert
case showUnignoreAlert
case showIgnoreAlert
case ignoreConfirmed
case unignoreConfirmed
case copyUserLink

View File

@ -39,9 +39,9 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
override func process(viewAction: RoomMemberDetailsViewAction) async {
switch viewAction {
case .showUnblockAlert:
case .showUnignoreAlert:
state.bindings.ignoreUserAlert = .init(action: .unignore)
case .showBlockAlert:
case .showIgnoreAlert:
state.bindings.ignoreUserAlert = .init(action: .ignore)
case .copyUserLink:
copyUserLink()
@ -64,7 +64,10 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
}
private func ignoreUser() async {
switch await roomMemberProxy.ignoreUser() {
state.isProcessingIgnoreRequest = true
let result = await roomMemberProxy.ignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.isIgnored = true
case .failure:
@ -73,7 +76,10 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
}
private func unignoreUser() async {
switch await roomMemberProxy.unignoreUser() {
state.isProcessingIgnoreRequest = true
let result = await roomMemberProxy.unignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.isIgnored = false
case .failure:

View File

@ -23,10 +23,9 @@ struct RoomMemberDetailsScreen: View {
Form {
headerSection
// TODO: Uncomment when the feature is ready
// if !context.viewState.isAccountOwner {
// blockUserSection
// }
if !context.viewState.isAccountOwner {
blockUserSection
}
}
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
@ -77,18 +76,22 @@ struct RoomMemberDetailsScreen: View {
Section {
if context.viewState.isIgnored {
Button {
context.send(viewAction: .showUnblockAlert)
context.send(viewAction: .showUnignoreAlert)
} label: {
Label(L10n.screenRoomMemberDetailsUnblockUser, systemImage: "slash.circle")
}
.buttonStyle(FormButtonStyle(accessory: nil))
.accessibilityIdentifier(A11yIdentifiers.roomMemberDetailsScreen.unignore)
.buttonStyle(FormButtonStyle(accessory: context.viewState.isProcessingIgnoreRequest ? .progressView : nil))
.disabled(context.viewState.isProcessingIgnoreRequest)
} else {
Button(role: .destructive) {
context.send(viewAction: .showBlockAlert)
context.send(viewAction: .showIgnoreAlert)
} label: {
Label(L10n.screenRoomMemberDetailsBlockUser, systemImage: "slash.circle")
}
.buttonStyle(FormButtonStyle(accessory: nil))
.accessibilityIdentifier(A11yIdentifiers.roomMemberDetailsScreen.ignore)
.buttonStyle(FormButtonStyle(accessory: context.viewState.isProcessingIgnoreRequest ? .progressView : nil))
.disabled(context.viewState.isProcessingIgnoreRequest)
}
}
.formSectionStyle()

View File

@ -278,12 +278,17 @@ class RoomProxy: RoomProxyProtocol {
return members
}
let proxiedMembers = await members.asyncMap { RoomMemberProxy(member: $0, backgroundTaskService: self.backgroundTaskService) }
let proxiedMembers = buildRoomMemberProxies(members: members)
return .success(proxiedMembers)
} catch {
return .failure(.failedRetrievingMembers)
}
}
@MainActor
private func buildRoomMemberProxies(members: [RoomMember]) -> [RoomMemberProxy] {
members.map { RoomMemberProxy(member: $0, backgroundTaskService: backgroundTaskService) }
}
func retryDecryption(for sessionID: String) async {
await Task.dispatch(on: .global()) { [weak self] in

View File

@ -318,6 +318,16 @@ class MockScreen: Identifiable {
let coordinator = RoomMemberDetailsCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockMe, mediaProvider: MockMediaProvider()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomMemberDetails:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = RoomMemberDetailsCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockAlice, mediaProvider: MockMediaProvider()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomMemberDetailsIgnoredUser:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = RoomMemberDetailsCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
}
}()
}

View File

@ -44,6 +44,8 @@ enum UITestsScreenIdentifier: String {
case roomDetailsScreenWithRoomAvatar
case roomMembersListScreen
case roomMemberDetailsAccountOwner
case roomMemberDetails
case roomMemberDetailsIgnoredUser
case reportContent
case startChat
}

View File

@ -15,7 +15,7 @@ struct SetupProject: ParsableCommand {
}
func brewBundleInstall() throws {
try Utilities.zsh("brew install xcodegen swiftgen swiftlint swiftformat git-lfs sourcery kiliankoe/formulae/swift-outdated")
try Utilities.zsh("brew install xcodegen swiftgen swiftlint swiftformat git-lfs sourcery kiliankoe/formulae/swift-outdated localazy/tools/localazy")
}
func xcodegen() throws {

View File

@ -20,6 +20,25 @@ import XCTest
class RoomMemberDetailsScreenUITests: XCTestCase {
func testInitialStateComponentsForAccountOwner() {
let app = Application.launch(.roomMemberDetailsAccountOwner)
XCTAssertFalse(app.buttons[A11yIdentifiers.roomMemberDetailsScreen.ignore].exists)
XCTAssertFalse(app.buttons[A11yIdentifiers.roomMemberDetailsScreen.unignore].exists)
app.assertScreenshot(.roomMemberDetailsAccountOwner)
}
func testInitialStateComponents() {
let app = Application.launch(.roomMemberDetails)
XCTAssert(app.buttons[A11yIdentifiers.roomMemberDetailsScreen.ignore].waitForExistence(timeout: 1))
XCTAssertFalse(app.buttons[A11yIdentifiers.roomMemberDetailsScreen.unignore].exists)
app.assertScreenshot(.roomMemberDetails)
}
func testInitialStateComponentsForIgnoredUser() {
let app = Application.launch(.roomMemberDetailsIgnoredUser)
XCTAssertFalse(app.buttons[A11yIdentifiers.roomMemberDetailsScreen.ignore].exists)
XCTAssert(app.buttons[A11yIdentifiers.roomMemberDetailsScreen.unignore].waitForExistence(timeout: 1))
app.assertScreenshot(.roomMemberDetailsIgnoredUser)
}
}

Binary file not shown.

Binary file not shown.

View File

@ -38,6 +38,92 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
XCTAssertNil(context.errorAlert)
}
func testIgnoreSuccess() async throws {
roomMemberProxyMock = RoomMemberProxyMock.mockAlice
roomMemberProxyMock.ignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .success(())
}
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
context.send(viewAction: .showIgnoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .ignore))
context.send(viewAction: .ignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertFalse(context.viewState.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
}
func testIgnoreFailure() async throws {
roomMemberProxyMock = RoomMemberProxyMock.mockAlice
roomMemberProxyMock.ignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .failure(.ignoreUserFailed)
}
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
context.send(viewAction: .showIgnoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .ignore))
context.send(viewAction: .ignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertFalse(context.viewState.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertNotNil(context.errorAlert)
XCTAssertFalse(context.viewState.isIgnored)
}
func testUnignoreSuccess() async throws {
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored
roomMemberProxyMock.unignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .success(())
}
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
context.send(viewAction: .showUnignoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .unignore))
context.send(viewAction: .unignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertFalse(context.viewState.isIgnored)
}
func testUnignoreFailure() async throws {
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored
roomMemberProxyMock.unignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .failure(.unignoreUserFailed)
}
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
context.send(viewAction: .showUnignoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .unignore))
context.send(viewAction: .unignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
XCTAssertNotNil(context.errorAlert)
}
func testInitialStateAccountOwner() async {
roomMemberProxyMock = RoomMemberProxyMock.mockMe
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())

View File

@ -0,0 +1 @@
Ignore User functionality added in the Room Member Details View.

View File

@ -42,7 +42,7 @@ include:
packages:
MatrixRustSDK:
url: https://github.com/matrix-org/matrix-rust-components-swift
exactVersion: 1.0.48-alpha
exactVersion: 1.0.49-alpha
# path: ../matrix-rust-sdk
DesignKit:
path: DesignKit