mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Ignore User UI (#737)
* generated files * Revert "generated files" This reverts commit f62c1dbcd9e505083ad4ff17532e2c054e253187. * 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 499fbe129f73a75e903d9f4952fe2ad672930f04. * replaced the asyncMap with a @MainActor builder function * pr comments * added localazy to setup * sdk bump to 1.0.49
This commit is contained in:
parent
2b753b1135
commit
e1df5310b7
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ enum UITestsScreenIdentifier: String {
|
||||
case roomDetailsScreenWithRoomAvatar
|
||||
case roomMembersListScreen
|
||||
case roomMemberDetailsAccountOwner
|
||||
case roomMemberDetails
|
||||
case roomMemberDetailsIgnoredUser
|
||||
case reportContent
|
||||
case startChat
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMemberDetails.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMemberDetails.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMemberDetails.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMemberDetails.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetails.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetails.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetails.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetails.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMemberDetailsIgnoredUser.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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())
|
||||
|
1
changelog.d/733.feature
Normal file
1
changelog.d/733.feature
Normal file
@ -0,0 +1 @@
|
||||
Ignore User functionality added in the Room Member Details View.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user