RoomMemberDetailsScreen (#727)
* 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
* some unit tests
* improved the tests
* removed unused comment
* Update ElementX/Sources/Services/Room/RoomProxy.swift
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
* optional displayName
* removing toggle
* removed cancel title
* Update UnitTests/Sources/RoomMemberDetailsViewModelTests.swift
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
* removing Group
* pr suggestion
* better naming
* removed capitalizingFirstLetter
* Update ElementX/Sources/Other/Extensions/Alert.swift
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
* trailing closure
* removed useless catch clause
* naming conformed to ignore
---------
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
pull/749/head
parent
9cd38d1a4c
commit
8daa23b27a
|
@ -163,7 +163,7 @@
|
|||
{
|
||||
"identity" : "swift-snapshot-testing",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
|
||||
"state" : {
|
||||
"revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334",
|
||||
"version" : "1.11.0"
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
"ios_no" = "No";
|
||||
"action_confirm" = "Confirm";
|
||||
"action_match" = "Match";
|
||||
"action_copy_link" = "Copy Link";
|
||||
"action_share_link" = "Share Link";
|
||||
|
||||
"message" = "Message";
|
||||
|
||||
|
@ -72,12 +74,19 @@
|
|||
// Room Details
|
||||
"room_details_title" = "Info";
|
||||
"room_details_about_section_title" = "About";
|
||||
"room_details_copy_link" = "Copy Link";
|
||||
"room_details_leave_room_alert_subtitle" = "Are you sure that you want to leave the room?";
|
||||
"room_details_leave_private_room_alert_subtitle" = "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite.";
|
||||
"room_details_leave_empty_room_alert_subtitle" = "Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you.";
|
||||
"room_details_room_left_toast" = "Room left";
|
||||
|
||||
// Room Member Details
|
||||
"room_member_details_block_user" = "Block user";
|
||||
"room_member_details_unblock_user" = "Unblock user";
|
||||
"room_member_details_block_alert_action" = "Block";
|
||||
"room_member_details_unblock_alert_action" = "Unblock";
|
||||
"room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
|
||||
"room_member_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
|
||||
|
||||
// Onboarding
|
||||
"ftue_auth_carousel_welcome_title" = "Be in your Element";
|
||||
"ftue_auth_carousel_welcome_body" = "Welcome to the %@ Beta. Supercharged, for speed and simplicity.";
|
||||
|
|
|
@ -14,8 +14,12 @@ extension ElementL10n {
|
|||
public static let a11yAllChatsUserAvatarMenu = ElementL10n.tr("Untranslated", "a11y_all_chats_user_avatar_menu")
|
||||
/// Confirm
|
||||
public static let actionConfirm = ElementL10n.tr("Untranslated", "action_confirm")
|
||||
/// Copy Link
|
||||
public static let actionCopyLink = ElementL10n.tr("Untranslated", "action_copy_link")
|
||||
/// Match
|
||||
public static let actionMatch = ElementL10n.tr("Untranslated", "action_match")
|
||||
/// Share Link
|
||||
public static let actionShareLink = ElementL10n.tr("Untranslated", "action_share_link")
|
||||
/// Attach Screenshot
|
||||
public static let bugReportScreenAttachScreenshot = ElementL10n.tr("Untranslated", "bug_report_screen_attach_screenshot")
|
||||
/// Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can.
|
||||
|
@ -104,8 +108,6 @@ extension ElementL10n {
|
|||
public static let retrievingDirectRoomError = ElementL10n.tr("Untranslated", "retrieving_direct_room_error")
|
||||
/// About
|
||||
public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title")
|
||||
/// Copy Link
|
||||
public static let roomDetailsCopyLink = ElementL10n.tr("Untranslated", "room_details_copy_link")
|
||||
/// Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you.
|
||||
public static let roomDetailsLeaveEmptyRoomAlertSubtitle = ElementL10n.tr("Untranslated", "room_details_leave_empty_room_alert_subtitle")
|
||||
/// Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite.
|
||||
|
@ -116,6 +118,18 @@ extension ElementL10n {
|
|||
public static let roomDetailsRoomLeftToast = ElementL10n.tr("Untranslated", "room_details_room_left_toast")
|
||||
/// Info
|
||||
public static let roomDetailsTitle = ElementL10n.tr("Untranslated", "room_details_title")
|
||||
/// Block
|
||||
public static let roomMemberDetailsBlockAlertAction = ElementL10n.tr("Untranslated", "room_member_details_block_alert_action")
|
||||
/// Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.
|
||||
public static let roomMemberDetailsBlockAlertDescription = ElementL10n.tr("Untranslated", "room_member_details_block_alert_description")
|
||||
/// Block user
|
||||
public static let roomMemberDetailsBlockUser = ElementL10n.tr("Untranslated", "room_member_details_block_user")
|
||||
/// Unblock
|
||||
public static let roomMemberDetailsUnblockAlertAction = ElementL10n.tr("Untranslated", "room_member_details_unblock_alert_action")
|
||||
/// On unblocking the user, you will be able to see all messages by them again.
|
||||
public static let roomMemberDetailsUnblockAlertDescription = ElementL10n.tr("Untranslated", "room_member_details_unblock_alert_description")
|
||||
/// Unblock user
|
||||
public static let roomMemberDetailsUnblockUser = ElementL10n.tr("Untranslated", "room_member_details_unblock_user")
|
||||
/// Failed loading messages
|
||||
public static let roomTimelineBackpaginationFailure = ElementL10n.tr("Untranslated", "room_timeline_backpagination_failure")
|
||||
/// Retry decryption
|
||||
|
|
|
@ -78,7 +78,51 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
|||
set(value) { underlyingNormalizedPowerLevel = value }
|
||||
}
|
||||
var underlyingNormalizedPowerLevel: Int!
|
||||
var isAccountOwner: Bool {
|
||||
get { return underlyingIsAccountOwner }
|
||||
set(value) { underlyingIsAccountOwner = value }
|
||||
}
|
||||
var underlyingIsAccountOwner: Bool!
|
||||
var isIgnored: Bool {
|
||||
get { return underlyingIsIgnored }
|
||||
set(value) { underlyingIsIgnored = value }
|
||||
}
|
||||
var underlyingIsIgnored: Bool!
|
||||
|
||||
//MARK: - ignoreUser
|
||||
|
||||
var ignoreUserCallsCount = 0
|
||||
var ignoreUserCalled: Bool {
|
||||
return ignoreUserCallsCount > 0
|
||||
}
|
||||
var ignoreUserReturnValue: Result<Void, RoomMemberProxyError>!
|
||||
var ignoreUserClosure: (() async -> Result<Void, RoomMemberProxyError>)?
|
||||
|
||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
||||
ignoreUserCallsCount += 1
|
||||
if let ignoreUserClosure = ignoreUserClosure {
|
||||
return await ignoreUserClosure()
|
||||
} else {
|
||||
return ignoreUserReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - unignoreUser
|
||||
|
||||
var unignoreUserCallsCount = 0
|
||||
var unignoreUserCalled: Bool {
|
||||
return unignoreUserCallsCount > 0
|
||||
}
|
||||
var unignoreUserReturnValue: Result<Void, RoomMemberProxyError>!
|
||||
var unignoreUserClosure: (() async -> Result<Void, RoomMemberProxyError>)?
|
||||
|
||||
func unignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
||||
unignoreUserCallsCount += 1
|
||||
if let unignoreUserClosure = unignoreUserClosure {
|
||||
return await unignoreUserClosure()
|
||||
} else {
|
||||
return unignoreUserReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class RoomProxyMock: RoomProxyProtocol {
|
||||
var id: String {
|
||||
|
|
|
@ -20,11 +20,13 @@ import MatrixRustSDK
|
|||
struct RoomMemberProxyMockConfiguration {
|
||||
var userID: String
|
||||
var displayName: String
|
||||
var avatarURL: String?
|
||||
var avatarURL: URL?
|
||||
var membership: MembershipState
|
||||
var isNameAmbiguous: Bool
|
||||
var powerLevel: Int
|
||||
var normalizedPowerLevel: Int
|
||||
var isAccountOwner: Bool
|
||||
var isIgnored: Bool
|
||||
}
|
||||
|
||||
extension RoomMemberProxyMock {
|
||||
|
@ -32,43 +34,85 @@ extension RoomMemberProxyMock {
|
|||
self.init()
|
||||
userID = configuration.userID
|
||||
displayName = configuration.displayName
|
||||
if let avatarURL = configuration.avatarURL {
|
||||
self.avatarURL = URL(string: avatarURL)
|
||||
}
|
||||
avatarURL = configuration.avatarURL
|
||||
membership = configuration.membership
|
||||
isNameAmbiguous = configuration.isNameAmbiguous
|
||||
powerLevel = configuration.powerLevel
|
||||
normalizedPowerLevel = configuration.normalizedPowerLevel
|
||||
isAccountOwner = configuration.isAccountOwner
|
||||
isIgnored = configuration.isIgnored
|
||||
}
|
||||
|
||||
// Mocks
|
||||
static var mockAlice: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "alice@matrix.org",
|
||||
RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org",
|
||||
displayName: "Alice",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50))
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
}
|
||||
|
||||
static var mockBob: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "bob@matrix.org",
|
||||
RoomMemberProxyMock(with: .init(userID: "@bob:matrix.org",
|
||||
displayName: "Bob",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50))
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
}
|
||||
|
||||
static var mockCharlie: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "charlie@matrix.org",
|
||||
RoomMemberProxyMock(with: .init(userID: "@charlie:matrix.org",
|
||||
displayName: "Charlie",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50))
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
}
|
||||
|
||||
static var mockDan: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "@dan:matrix.org",
|
||||
displayName: "Dan",
|
||||
avatarURL: URL.picturesDirectory,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
}
|
||||
|
||||
static var mockMe: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "@me:matrix.org",
|
||||
displayName: "Me",
|
||||
avatarURL: URL.picturesDirectory,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: true,
|
||||
isIgnored: false))
|
||||
}
|
||||
|
||||
static var mockIgnored: RoomMemberProxyMock {
|
||||
RoomMemberProxyMock(with: .init(userID: "@ignored:matrix.org",
|
||||
displayName: "Ignored",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: true))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ enum UserAvatarSizeOnScreen {
|
|||
case settings
|
||||
case roomDetails
|
||||
case startChat
|
||||
case memberDetails
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
|
@ -60,6 +61,8 @@ enum UserAvatarSizeOnScreen {
|
|||
return 44
|
||||
case .startChat:
|
||||
return 36
|
||||
case .memberDetails:
|
||||
return 70
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ protocol AlertItem {
|
|||
}
|
||||
|
||||
extension View {
|
||||
func alert<Item, Actions, Message>(item: Binding<Item?>, actions: (Item) -> Actions, message: (Item) -> Message) -> some View where Item: AlertItem, Actions: View, Message: View {
|
||||
func alert<Item, Actions, Message>(item: Binding<Item?>, @ViewBuilder actions: (Item) -> Actions, @ViewBuilder message: (Item) -> Message) -> some View where Item: AlertItem, Actions: View, Message: View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
item.wrappedValue != nil
|
||||
}, set: { newValue in
|
||||
|
@ -32,7 +32,7 @@ extension View {
|
|||
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message)
|
||||
}
|
||||
|
||||
func alert<Item, Actions>(item: Binding<Item?>, actions: (Item) -> Actions) -> some View where Item: AlertItem, Actions: View {
|
||||
func alert<Item, Actions>(item: Binding<Item?>, @ViewBuilder actions: (Item) -> Actions) -> some View where Item: AlertItem, Actions: View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
item.wrappedValue != nil
|
||||
}, set: { newValue in
|
||||
|
@ -43,3 +43,29 @@ extension View {
|
|||
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions)
|
||||
}
|
||||
}
|
||||
|
||||
// Only for Alerts that display a simple error message with a message and one or two buttons
|
||||
struct ErrorAlertItem: AlertItem {
|
||||
struct Action {
|
||||
var title: String
|
||||
var action: () -> Void
|
||||
}
|
||||
|
||||
var title = ElementL10n.dialogTitleError
|
||||
var message = ElementL10n.unknownError
|
||||
var cancelAction = Action(title: ElementL10n.ok, action: { })
|
||||
var primaryAction: Action?
|
||||
}
|
||||
|
||||
extension View {
|
||||
func errorAlert(item: Binding<ErrorAlertItem?>) -> some View {
|
||||
alert(item: item) { item in
|
||||
Button(item.cancelAction.title) { item.cancelAction.action() }
|
||||
if let primaryAction = item.primaryAction {
|
||||
Button(primaryAction.title) { primaryAction.action() }
|
||||
}
|
||||
} message: { item in
|
||||
Text(item.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ final class RoomDetailsCoordinator: CoordinatorProtocol {
|
|||
|
||||
switch action {
|
||||
case .requestMemberDetailsPresentation(let members):
|
||||
self.presentRoomMemberDetails(members)
|
||||
self.presentRoomMembersList(members)
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
case .leftRoom:
|
||||
|
@ -62,13 +62,11 @@ final class RoomDetailsCoordinator: CoordinatorProtocol {
|
|||
AnyView(RoomDetailsScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
private func presentRoomMemberDetails(_ members: [RoomMemberProxyProtocol]) {
|
||||
let params = RoomMemberDetailsCoordinatorParameters(mediaProvider: parameters.mediaProvider,
|
||||
members: members)
|
||||
let coordinator = RoomMemberDetailsCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self] _ in
|
||||
self?.navigationStackCoordinator.pop()
|
||||
}
|
||||
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
|
||||
let params = RoomMembersListCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
members: members)
|
||||
let coordinator = RoomMembersListCoordinator(parameters: params)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ struct RoomDetailsMember: Identifiable, Equatable {
|
|||
let name: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
@MainActor
|
||||
init(withProxy proxy: RoomMemberProxyProtocol) {
|
||||
id = proxy.userID
|
||||
name = proxy.displayName
|
||||
|
|
|
@ -69,7 +69,7 @@ struct RoomDetailsScreen: View {
|
|||
Button { context.send(viewAction: .copyRoomLink) } label: {
|
||||
Image(systemName: "link")
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: ElementL10n.roomDetailsCopyLink))
|
||||
.buttonStyle(FormActionButtonStyle(title: ElementL10n.actionCopyLink))
|
||||
|
||||
ShareLink(item: permalink) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
|
|
|
@ -17,35 +17,26 @@
|
|||
import SwiftUI
|
||||
|
||||
struct RoomMemberDetailsCoordinatorParameters {
|
||||
let roomMemberProxy: RoomMemberProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let members: [RoomMemberProxyProtocol]
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
enum RoomMemberDetailsCoordinatorAction { }
|
||||
|
||||
final class RoomMemberDetailsCoordinator: CoordinatorProtocol {
|
||||
private let parameters: RoomMemberDetailsCoordinatorParameters
|
||||
private var viewModel: RoomMemberDetailsViewModelProtocol
|
||||
|
||||
|
||||
var callback: ((RoomMemberDetailsCoordinatorAction) -> Void)?
|
||||
|
||||
|
||||
init(parameters: RoomMemberDetailsCoordinatorParameters) {
|
||||
viewModel = RoomMemberDetailsViewModel(mediaProvider: parameters.mediaProvider,
|
||||
members: parameters.members)
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: parameters.roomMemberProxy, mediaProvider: parameters.mediaProvider)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func start() { }
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(RoomMemberDetailsScreen(context: viewModel.context))
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// 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 RoomMemberDetailsViewModelAction { }
|
||||
|
||||
struct RoomMemberDetailsViewState: BindableState {
|
||||
let userID: String
|
||||
let name: String?
|
||||
let avatarURL: URL?
|
||||
let isAccountOwner: Bool
|
||||
let permalink: URL?
|
||||
var isIgnored: Bool
|
||||
|
||||
var bindings: RoomMemberDetailsViewStateBindings
|
||||
}
|
||||
|
||||
struct RoomMemberDetailsViewStateBindings {
|
||||
var ignoreUserAlert: IgnoreUserAlertItem?
|
||||
var errorAlert: ErrorAlertItem?
|
||||
}
|
||||
|
||||
struct IgnoreUserAlertItem: AlertItem {
|
||||
enum Action {
|
||||
case ignore
|
||||
case unignore
|
||||
}
|
||||
|
||||
let action: Action
|
||||
let cancelTitle = ElementL10n.actionCancel
|
||||
|
||||
var title: String {
|
||||
switch action {
|
||||
case .ignore: return ElementL10n.roomMemberDetailsBlockUser
|
||||
case .unignore: return ElementL10n.roomMemberDetailsUnblockUser
|
||||
}
|
||||
}
|
||||
|
||||
var confirmationTitle: String {
|
||||
switch action {
|
||||
case .ignore: return ElementL10n.roomMemberDetailsBlockAlertAction
|
||||
case .unignore: return ElementL10n.roomMemberDetailsUnblockAlertAction
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch action {
|
||||
case .ignore: return ElementL10n.roomMemberDetailsBlockAlertDescription
|
||||
case .unignore: return ElementL10n.roomMemberDetailsUnblockAlertDescription
|
||||
}
|
||||
}
|
||||
|
||||
var viewAction: RoomMemberDetailsViewAction {
|
||||
switch action {
|
||||
case .ignore: return .ignoreConfirmed
|
||||
case .unignore: return .unignoreConfirmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsViewAction {
|
||||
case showUnblockAlert
|
||||
case showBlockAlert
|
||||
case ignoreConfirmed
|
||||
case unignoreConfirmed
|
||||
case copyUserLink
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
typealias RoomMemberDetailsViewModelType = StateStoreViewModel<RoomMemberDetailsViewState, RoomMemberDetailsViewAction>
|
||||
|
||||
class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDetailsViewModelProtocol {
|
||||
let roomMemberProxy: RoomMemberProxyProtocol
|
||||
|
||||
var callback: ((RoomMemberDetailsViewModelAction) -> Void)?
|
||||
|
||||
init(roomMemberProxy: RoomMemberProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||
self.roomMemberProxy = roomMemberProxy
|
||||
let initialViewState = RoomMemberDetailsViewState(userID: roomMemberProxy.userID,
|
||||
name: roomMemberProxy.displayName,
|
||||
avatarURL: roomMemberProxy.avatarURL,
|
||||
isAccountOwner: roomMemberProxy.isAccountOwner,
|
||||
permalink: roomMemberProxy.permalink,
|
||||
isIgnored: roomMemberProxy.isIgnored,
|
||||
bindings: .init())
|
||||
super.init(initialViewState: initialViewState, imageProvider: mediaProvider)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: RoomMemberDetailsViewAction) async {
|
||||
switch viewAction {
|
||||
case .showUnblockAlert:
|
||||
state.bindings.ignoreUserAlert = .init(action: .unignore)
|
||||
case .showBlockAlert:
|
||||
state.bindings.ignoreUserAlert = .init(action: .ignore)
|
||||
case .copyUserLink:
|
||||
copyUserLink()
|
||||
case .ignoreConfirmed:
|
||||
await ignoreUser()
|
||||
case .unignoreConfirmed:
|
||||
await unignoreUser()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func copyUserLink() {
|
||||
if let userLink = state.permalink {
|
||||
UIPasteboard.general.url = userLink
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: ElementL10n.linkCopiedToClipboard))
|
||||
} else {
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
|
||||
private func ignoreUser() async {
|
||||
switch await roomMemberProxy.ignoreUser() {
|
||||
case .success:
|
||||
state.isIgnored = true
|
||||
case .failure:
|
||||
state.bindings.errorAlert = .init()
|
||||
}
|
||||
}
|
||||
|
||||
private func unignoreUser() async {
|
||||
switch await roomMemberProxy.unignoreUser() {
|
||||
case .success:
|
||||
state.isIgnored = false
|
||||
case .failure:
|
||||
state.bindings.errorAlert = .init()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct RoomMemberDetailsScreen: View {
|
||||
@ObservedObject var context: RoomMemberDetailsViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
headerSection
|
||||
|
||||
// TODO: Uncomment when the feature is ready
|
||||
// if !context.viewState.isAccountOwner {
|
||||
// blockUserSection
|
||||
// }
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.element.formBackground.ignoresSafeArea())
|
||||
.alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage)
|
||||
.errorAlert(item: $context.errorAlert)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var headerSection: some View {
|
||||
VStack(spacing: 8.0) {
|
||||
LoadableAvatarImage(url: context.viewState.avatarURL,
|
||||
name: context.viewState.name,
|
||||
contentID: context.viewState.userID,
|
||||
avatarSize: .user(on: .memberDetails),
|
||||
imageProvider: context.imageProvider)
|
||||
if let name = context.viewState.name {
|
||||
Text(name)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.element.title1Bold)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
Text(context.viewState.userID)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if let permalink = context.viewState.permalink {
|
||||
HStack(spacing: 32) {
|
||||
Button { context.send(viewAction: .copyUserLink) } label: {
|
||||
Image(systemName: "link")
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: ElementL10n.actionCopyLink))
|
||||
|
||||
ShareLink(item: permalink) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: ElementL10n.actionShareLink))
|
||||
}
|
||||
.padding(.top, 32)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
private var blockUserSection: some View {
|
||||
Section {
|
||||
if context.viewState.isIgnored {
|
||||
Button {
|
||||
context.send(viewAction: .showUnblockAlert)
|
||||
} label: {
|
||||
Label(ElementL10n.roomMemberDetailsUnblockUser, systemImage: "slash.circle")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: nil))
|
||||
} else {
|
||||
Button(role: .destructive) {
|
||||
context.send(viewAction: .showBlockAlert)
|
||||
} label: {
|
||||
Label(ElementL10n.roomMemberDetailsBlockUser, systemImage: "slash.circle")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: nil))
|
||||
}
|
||||
}
|
||||
.formSectionStyle()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func blockUserAlertActions(_ item: IgnoreUserAlertItem) -> some View {
|
||||
Button(item.cancelTitle, role: .cancel) { }
|
||||
Button(item.confirmationTitle,
|
||||
role: item.action == .ignore ? .destructive : nil) {
|
||||
context.send(viewAction: item.viewAction)
|
||||
}
|
||||
}
|
||||
|
||||
private func blockUserAlertMessage(_ item: IgnoreUserAlertItem) -> some View {
|
||||
Text(item.description)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomMemberDetails_Previews: PreviewProvider {
|
||||
static let otherUserViewModel = {
|
||||
let member = RoomMemberProxyMock.mockDan
|
||||
return RoomMemberDetailsViewModel(roomMemberProxy: member, mediaProvider: MockMediaProvider())
|
||||
}()
|
||||
|
||||
static let accountOwnerViewModel = {
|
||||
let member = RoomMemberProxyMock.mockMe
|
||||
return RoomMemberDetailsViewModel(roomMemberProxy: member, mediaProvider: MockMediaProvider())
|
||||
}()
|
||||
|
||||
static let ignoredUserViewModel = {
|
||||
let member = RoomMemberProxyMock.mockIgnored
|
||||
return RoomMemberDetailsViewModel(roomMemberProxy: member, mediaProvider: MockMediaProvider())
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
RoomMemberDetailsScreen(context: otherUserViewModel.context)
|
||||
.previewDisplayName("Other User")
|
||||
RoomMemberDetailsScreen(context: accountOwnerViewModel.context)
|
||||
.previewDisplayName("Account Owner")
|
||||
RoomMemberDetailsScreen(context: ignoredUserViewModel.context)
|
||||
.previewDisplayName("Ignored User")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct RoomMembersListCoordinatorParameters {
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let members: [RoomMemberProxyProtocol]
|
||||
}
|
||||
|
||||
enum RoomMembersListCoordinatorAction { }
|
||||
|
||||
final class RoomMembersListCoordinator: CoordinatorProtocol {
|
||||
private let parameters: RoomMembersListCoordinatorParameters
|
||||
private var viewModel: RoomMembersListViewModelProtocol
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
|
||||
|
||||
var callback: ((RoomMembersListCoordinatorAction) -> Void)?
|
||||
|
||||
init(parameters: RoomMembersListCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = RoomMembersListViewModel(mediaProvider: parameters.mediaProvider,
|
||||
members: parameters.members)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case let .selectMember(member):
|
||||
self.selectMember(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(RoomMembersListScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func selectMember(_ member: RoomMemberProxyProtocol) {
|
||||
let parameters = RoomMemberDetailsCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider)
|
||||
let coordinator = RoomMemberDetailsCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
|
@ -16,14 +16,14 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
enum RoomMemberDetailsViewModelAction {
|
||||
case cancel
|
||||
enum RoomMembersListViewModelAction {
|
||||
case selectMember(_ member: RoomMemberProxyProtocol)
|
||||
}
|
||||
|
||||
struct RoomMemberDetailsViewState: BindableState {
|
||||
struct RoomMembersListViewState: BindableState {
|
||||
var members: [RoomDetailsMember]
|
||||
|
||||
var bindings: RoomMemberDetailsViewStateBindings
|
||||
var bindings: RoomMembersListViewStateBindings
|
||||
|
||||
var visibleMembers: [RoomDetailsMember] {
|
||||
if bindings.searchQuery.isEmpty {
|
||||
|
@ -37,13 +37,13 @@ struct RoomMemberDetailsViewState: BindableState {
|
|||
}
|
||||
}
|
||||
|
||||
struct RoomMemberDetailsViewStateBindings {
|
||||
struct RoomMembersListViewStateBindings {
|
||||
var searchQuery = ""
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<RoomDetailsErrorType>?
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsViewAction {
|
||||
enum RoomMembersListViewAction {
|
||||
case selectMember(id: String)
|
||||
}
|
|
@ -16,16 +16,18 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
typealias RoomMemberDetailsViewModelType = StateStoreViewModel<RoomMemberDetailsViewState, RoomMemberDetailsViewAction>
|
||||
typealias RoomMembersListViewModelType = StateStoreViewModel<RoomMembersListViewState, RoomMembersListViewAction>
|
||||
|
||||
class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDetailsViewModelProtocol {
|
||||
class RoomMembersListViewModel: RoomMembersListViewModelType, RoomMembersListViewModelProtocol {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let members: [RoomMemberProxyProtocol]
|
||||
|
||||
var callback: ((RoomMemberDetailsViewModelAction) -> Void)?
|
||||
var callback: ((RoomMembersListViewModelAction) -> Void)?
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol,
|
||||
members: [RoomMemberProxyProtocol]) {
|
||||
self.mediaProvider = mediaProvider
|
||||
self.members = members
|
||||
super.init(initialViewState: .init(members: members.map { RoomDetailsMember(withProxy: $0) },
|
||||
bindings: .init()),
|
||||
imageProvider: mediaProvider)
|
||||
|
@ -33,10 +35,14 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
|
|||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: RoomMemberDetailsViewAction) async {
|
||||
override func process(viewAction: RoomMembersListViewAction) async {
|
||||
switch viewAction {
|
||||
case .selectMember(let id):
|
||||
MXLog.debug("Member selected: \(id)")
|
||||
guard let member = members.first(where: { $0.userID == id }) else {
|
||||
MXLog.error("Selected member \(id) not found")
|
||||
return
|
||||
}
|
||||
callback?(.selectMember(member))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomMembersListViewModelProtocol {
|
||||
var callback: ((RoomMembersListViewModelAction) -> Void)? { get set }
|
||||
var context: RoomMembersListViewModelType.Context { get }
|
||||
}
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMemberDetailsMemberCell: View {
|
||||
struct RoomMembersListMemberCell: View {
|
||||
@ScaledMetric private var avatarSize = AvatarSize.user(on: .roomDetails).value
|
||||
|
||||
let member: RoomDetailsMember
|
||||
let context: RoomMemberDetailsViewModel.Context
|
||||
let context: RoomMembersListViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
|
@ -46,19 +46,19 @@ struct RoomMemberDetailsMemberCell: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct RoomMemberDetailsMemberCell_Previews: PreviewProvider {
|
||||
struct RoomMembersListMemberCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let members: [RoomMemberProxyMock] = [
|
||||
.mockAlice,
|
||||
.mockBob,
|
||||
.mockCharlie
|
||||
]
|
||||
let viewModel = RoomMemberDetailsViewModel(mediaProvider: MockMediaProvider(),
|
||||
members: members)
|
||||
let viewModel = RoomMembersListViewModel(mediaProvider: MockMediaProvider(),
|
||||
members: members)
|
||||
|
||||
return VStack {
|
||||
ForEach(members, id: \.userID) { member in
|
||||
RoomMemberDetailsMemberCell(member: .init(withProxy: member), context: viewModel.context)
|
||||
RoomMembersListMemberCell(member: .init(withProxy: member), context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,17 +16,17 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMemberDetailsScreen: View {
|
||||
struct RoomMembersListScreen: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@ObservedObject var context: RoomMemberDetailsViewModel.Context
|
||||
@ObservedObject var context: RoomMembersListViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Section {
|
||||
ForEach(context.viewState.visibleMembers) { member in
|
||||
RoomMemberDetailsMemberCell(member: member, context: context)
|
||||
RoomMembersListMemberCell(member: member, context: context)
|
||||
.id(member.id)
|
||||
}
|
||||
} header: {
|
||||
|
@ -48,20 +48,20 @@ struct RoomMemberDetailsScreen: View {
|
|||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomMemberDetails_Previews: PreviewProvider {
|
||||
struct RoomMembersList_Previews: PreviewProvider {
|
||||
static let viewModel = {
|
||||
let members: [RoomMemberProxyMock] = [
|
||||
.mockAlice,
|
||||
.mockBob,
|
||||
.mockCharlie
|
||||
]
|
||||
return RoomMemberDetailsViewModel(mediaProvider: MockMediaProvider(),
|
||||
members: members)
|
||||
return RoomMembersListViewModel(mediaProvider: MockMediaProvider(),
|
||||
members: members)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
RoomMemberDetailsScreen(context: viewModel.context)
|
||||
RoomMembersListScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,9 +18,16 @@ import Foundation
|
|||
import MatrixRustSDK
|
||||
|
||||
final class RoomMemberProxy: RoomMemberProxyProtocol {
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private let member: RoomMemberProtocol
|
||||
|
||||
init(member: RoomMemberProtocol) {
|
||||
private let backgroundAccountDataTaskName = "SendAccountDataEvent"
|
||||
private var sendAccountDataEventBackgroundTask: BackgroundTaskProtocol?
|
||||
|
||||
private let userInitiatedDispatchQueue = DispatchQueue(label: "io.element.elementx.roommemberproxy.userinitiated", qos: .userInitiated)
|
||||
|
||||
init(member: RoomMemberProtocol, backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
self.member = member
|
||||
}
|
||||
|
||||
|
@ -51,4 +58,44 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
|
|||
var normalizedPowerLevel: Int {
|
||||
Int(member.normalizedPowerLevel())
|
||||
}
|
||||
|
||||
var isAccountOwner: Bool {
|
||||
member.isAccountUser()
|
||||
}
|
||||
|
||||
var isIgnored: Bool {
|
||||
member.isIgnored()
|
||||
}
|
||||
|
||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
||||
sendAccountDataEventBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true)
|
||||
defer {
|
||||
sendAccountDataEventBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||
do {
|
||||
try self.member.ignore()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.ignoreUserFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
||||
sendAccountDataEventBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true)
|
||||
defer {
|
||||
sendAccountDataEventBackgroundTask?.stop()
|
||||
}
|
||||
|
||||
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||
do {
|
||||
try self.member.unignore()
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.unignoreUserFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,12 @@
|
|||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomMemberProxyError: Error {
|
||||
case ignoreUserFailed
|
||||
case unignoreUserFailed
|
||||
}
|
||||
|
||||
@MainActor
|
||||
// sourcery: AutoMockable
|
||||
protocol RoomMemberProxyProtocol {
|
||||
var userID: String { get }
|
||||
|
@ -26,4 +32,15 @@ protocol RoomMemberProxyProtocol {
|
|||
var isNameAmbiguous: Bool { get }
|
||||
var powerLevel: Int { get }
|
||||
var normalizedPowerLevel: Int { get }
|
||||
var isAccountOwner: Bool { get }
|
||||
var isIgnored: Bool { get }
|
||||
|
||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||
func unignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||
}
|
||||
|
||||
extension RoomMemberProxyProtocol {
|
||||
var permalink: URL? {
|
||||
try? PermalinkBuilder.permalinkTo(userIdentifier: userID)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,13 +272,16 @@ class RoomProxy: RoomProxyProtocol {
|
|||
}
|
||||
|
||||
func members() async -> Result<[RoomMemberProxyProtocol], RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
do {
|
||||
let members = try await Task.dispatch(on: .global()) {
|
||||
let members = try self.room.members()
|
||||
return .success(members.map { RoomMemberProxy(member: $0) })
|
||||
} catch {
|
||||
return .failure(.failedRetrievingMembers)
|
||||
return members
|
||||
}
|
||||
|
||||
let proxiedMembers = await members.asyncMap { RoomMemberProxy(member: $0, backgroundTaskService: self.backgroundTaskService) }
|
||||
return .success(proxiedMembers)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingMembers)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -295,11 +295,12 @@ class MockScreen: Identifiable {
|
|||
mediaProvider: MockMediaProvider()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomMemberDetailsScreen:
|
||||
case .roomMembersListScreen:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
let coordinator = RoomMemberDetailsCoordinator(parameters: .init(mediaProvider: MockMediaProvider(),
|
||||
members: members))
|
||||
let coordinator = RoomMembersListCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
members: members))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .reportContent:
|
||||
|
@ -312,6 +313,11 @@ class MockScreen: Identifiable {
|
|||
let coordinator = StartChatCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider())))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomMemberDetailsAccountOwner:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = RoomMemberDetailsCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockMe, mediaProvider: MockMediaProvider()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@ enum UITestsScreenIdentifier: String {
|
|||
case userSessionScreen
|
||||
case roomDetailsScreen
|
||||
case roomDetailsScreenWithRoomAvatar
|
||||
case roomMemberDetailsScreen
|
||||
case roomMembersListScreen
|
||||
case roomMemberDetailsAccountOwner
|
||||
case reportContent
|
||||
case startChat
|
||||
}
|
||||
|
|
|
@ -18,9 +18,8 @@ import ElementX
|
|||
import XCTest
|
||||
|
||||
class RoomMemberDetailsScreenUITests: XCTestCase {
|
||||
func testInitialStateComponents() {
|
||||
let app = Application.launch(.roomMemberDetailsScreen)
|
||||
|
||||
app.assertScreenshot(.roomMemberDetailsScreen)
|
||||
func testInitialStateComponentsForAccountOwner() {
|
||||
let app = Application.launch(.roomMemberDetailsAccountOwner)
|
||||
app.assertScreens |