mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:06:00 +00:00
Invite people in an existing room (#915)
* Handle power levels * Add “invite” button in RoomMembersListScreen * Refactor DI in RoomDetailsScreenCoordinator * Add navigation to “select people” coordinator * Add InviteUsersScreenRoomContext * Add invite user by id api * Updare InviteUsersScreenRoomContext * Support disabled state on FormButtonStyles * Handle membership in UserProfileCell * Fix conflicts * Refactor RoomContext -> RoomType * Refactor UserProfileCell * Add invite action * Setup navigation to user flow coordinator * Add invite logic * Fix test build errors * Address comments * Fix localisations * Update required state * Handle error * Move invite logic in RoomDetailsScreenCoordinator * Add navigation from room’s members list * Fix UTs * Fix missing display name in RoomMemberDetails * Add user indicator * Add feature flag * Add RoomDetailsViewModel UTs * Add room details ui tests * Add InviteUsersViewModelTests UTs * Add UI tests * Update UI tests * Empty commit
This commit is contained in:
@@ -30,6 +30,7 @@ final class AppSettings {
|
||||
case startChatFlowEnabled
|
||||
case startChatUserSuggestionsEnabled
|
||||
case invitesFlowEnabled
|
||||
case inviteMorePeopleFlowEnabled
|
||||
case readReceiptsEnabled
|
||||
}
|
||||
|
||||
@@ -187,6 +188,9 @@ final class AppSettings {
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.invitesFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var invitesFlowEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.inviteMorePeopleFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var inviteMorePeopleFlowEnabled
|
||||
|
||||
// MARK: Receipts
|
||||
|
||||
|
||||
@@ -331,6 +331,11 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
||||
set(value) { underlyingIsIgnored = value }
|
||||
}
|
||||
var underlyingIsIgnored: Bool!
|
||||
var canInviteUsers: Bool {
|
||||
get { return underlyingCanInviteUsers }
|
||||
set(value) { underlyingCanInviteUsers = value }
|
||||
}
|
||||
var underlyingCanInviteUsers: Bool!
|
||||
|
||||
//MARK: - ignoreUser
|
||||
|
||||
@@ -869,6 +874,27 @@ class RoomProxyMock: RoomProxyProtocol {
|
||||
fetchDetailsForReceivedInvocations.append(eventID)
|
||||
fetchDetailsForClosure?(eventID)
|
||||
}
|
||||
//MARK: - invite
|
||||
|
||||
var inviteUserIDCallsCount = 0
|
||||
var inviteUserIDCalled: Bool {
|
||||
return inviteUserIDCallsCount > 0
|
||||
}
|
||||
var inviteUserIDReceivedUserID: String?
|
||||
var inviteUserIDReceivedInvocations: [String] = []
|
||||
var inviteUserIDReturnValue: Result<Void, RoomProxyError>!
|
||||
var inviteUserIDClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func invite(userID: String) async -> Result<Void, RoomProxyError> {
|
||||
inviteUserIDCallsCount += 1
|
||||
inviteUserIDReceivedUserID = userID
|
||||
inviteUserIDReceivedInvocations.append(userID)
|
||||
if let inviteUserIDClosure = inviteUserIDClosure {
|
||||
return await inviteUserIDClosure(userID)
|
||||
} else {
|
||||
return inviteUserIDReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
||||
|
||||
@@ -22,11 +22,12 @@ struct RoomMemberProxyMockConfiguration {
|
||||
var displayName: String
|
||||
var avatarURL: URL?
|
||||
var membership: MembershipState
|
||||
var isNameAmbiguous: Bool
|
||||
var isNameAmbiguous = false
|
||||
var powerLevel: Int
|
||||
var normalizedPowerLevel: Int
|
||||
var isAccountOwner: Bool
|
||||
var isIgnored: Bool
|
||||
var isAccountOwner = false
|
||||
var isIgnored = false
|
||||
var canInviteUsers = false
|
||||
}
|
||||
|
||||
extension RoomMemberProxyMock {
|
||||
@@ -41,6 +42,7 @@ extension RoomMemberProxyMock {
|
||||
normalizedPowerLevel = configuration.normalizedPowerLevel
|
||||
isAccountOwner = configuration.isAccountOwner
|
||||
isIgnored = configuration.isIgnored
|
||||
canInviteUsers = configuration.canInviteUsers
|
||||
}
|
||||
|
||||
// Mocks
|
||||
@@ -49,11 +51,8 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Alice",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
normalizedPowerLevel: 50))
|
||||
}
|
||||
|
||||
static var mockInvitedAlice: RoomMemberProxyMock {
|
||||
@@ -61,11 +60,8 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Alice",
|
||||
avatarURL: nil,
|
||||
membership: .invite,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
normalizedPowerLevel: 50))
|
||||
}
|
||||
|
||||
static var mockBob: RoomMemberProxyMock {
|
||||
@@ -73,11 +69,8 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Bob",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
normalizedPowerLevel: 50))
|
||||
}
|
||||
|
||||
static var mockCharlie: RoomMemberProxyMock {
|
||||
@@ -85,11 +78,8 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Charlie",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
normalizedPowerLevel: 50))
|
||||
}
|
||||
|
||||
static var mockDan: RoomMemberProxyMock {
|
||||
@@ -97,11 +87,8 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Dan",
|
||||
avatarURL: URL.picturesDirectory,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: false))
|
||||
normalizedPowerLevel: 50))
|
||||
}
|
||||
|
||||
static var mockMe: RoomMemberProxyMock {
|
||||
@@ -109,11 +96,10 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Me",
|
||||
avatarURL: URL.picturesDirectory,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: true,
|
||||
isIgnored: false))
|
||||
canInviteUsers: true))
|
||||
}
|
||||
|
||||
static var mockIgnored: RoomMemberProxyMock {
|
||||
@@ -121,10 +107,8 @@ extension RoomMemberProxyMock {
|
||||
displayName: "Ignored",
|
||||
avatarURL: nil,
|
||||
membership: .join,
|
||||
isNameAmbiguous: false,
|
||||
powerLevel: 50,
|
||||
normalizedPowerLevel: 50,
|
||||
isAccountOwner: false,
|
||||
isIgnored: true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ struct A11yIdentifiers {
|
||||
let avatar = "room_details-avatar"
|
||||
let dmAvatar = "room_details-dm_avatar"
|
||||
let people = "room_details-people"
|
||||
let invite = "room_details-invite"
|
||||
}
|
||||
|
||||
struct RoomMemberDetailsScreen {
|
||||
|
||||
@@ -44,7 +44,8 @@ enum FormRowAccessory: View {
|
||||
/// to change the background colour depending on whether the button is currently pressed or not.
|
||||
struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
var iconAlignment: VerticalAlignment = .firstTextBaseline
|
||||
|
||||
/// Disables the button while providing an entry point for UI customizations
|
||||
var isDisabled = false
|
||||
/// An accessory to be added on the trailing side of the row.
|
||||
var accessory: FormRowAccessory?
|
||||
|
||||
@@ -54,12 +55,14 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
.labelStyle(FormRowLabelStyle(alignment: iconAlignment, role: configuration.role))
|
||||
.frame(maxHeight: .infinity) // Make sure the label fills the cell vertically.
|
||||
}
|
||||
.buttonStyle(Style(accessory: accessory))
|
||||
.buttonStyle(Style(isDisabled: isDisabled, accessory: accessory))
|
||||
.listRowInsets(EdgeInsets()) // Remove insets so the background fills the cell.
|
||||
.disabled(isDisabled)
|
||||
}
|
||||
|
||||
/// Inner style used to set the pressed background colour.
|
||||
struct Style: ButtonStyle {
|
||||
var isDisabled: Bool
|
||||
var accessory: FormRowAccessory?
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
@@ -70,6 +73,7 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
accessory
|
||||
.opacity(isDisabled ? 0.54 : 1)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(FormRow.insets) // Re-apply the normal insets using padding.
|
||||
@@ -132,7 +136,15 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
||||
}
|
||||
.buttonStyle(FormButtonStyle())
|
||||
Button { } label: {
|
||||
Text("Selection")
|
||||
Text("Selected")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
|
||||
Button { } label: {
|
||||
Text("Selected (disabled)")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: true, accessory: .selection(isSelected: true)))
|
||||
Button { } label: {
|
||||
Text("Unselected")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct UserProfileCell: View {
|
||||
let user: UserProfile
|
||||
let membership: MembershipState?
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
|
||||
var body: some View {
|
||||
@@ -35,25 +37,92 @@ struct UserProfileCell: View {
|
||||
.font(.compound.headingSM)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
|
||||
if user.displayName != nil {
|
||||
Text(user.userID)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
subtitleContent
|
||||
|
||||
if !user.isVerified {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text(Image(systemName: "exclamationmark.circle.fill"))
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
|
||||
Text(L10n.screenStartChatUnknownProfile)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.font(.compound.bodyXS)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
warningContent
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@ViewBuilder
|
||||
private var subtitleContent: some View {
|
||||
if let membershipText = membership?.localizedDescription {
|
||||
Text(membershipText)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
} else if user.displayName != nil {
|
||||
Text(user.userID)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var warningContent: some View {
|
||||
if !user.isVerified, membership == nil {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text(Image(systemName: "exclamationmark.circle.fill"))
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
|
||||
Text(L10n.screenStartChatUnknownProfile)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.font(.compound.bodyXS)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MembershipState {
|
||||
var localizedDescription: String? {
|
||||
switch self {
|
||||
case .join:
|
||||
return L10n.screenRoomDetailsAlreadyAMember
|
||||
case .invite:
|
||||
return L10n.screenRoomDetailsAlreadyInvited
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserProfileCell_Previews: PreviewProvider {
|
||||
static let action: () -> Void = { }
|
||||
|
||||
static var previews: some View {
|
||||
Form {
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .mockAlice, membership: nil, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
|
||||
.previewDisplayName("Selected user")
|
||||
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .mockBob, membership: nil, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
|
||||
.previewDisplayName("Unselected user")
|
||||
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .mockCharlie, membership: .join, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: true, accessory: .selection(isSelected: true)))
|
||||
.previewDisplayName("Selected disabled user")
|
||||
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .init(userID: "@someone:matrix.org"), membership: .join, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: true, accessory: .selection(isSelected: true)))
|
||||
.previewDisplayName("Unverified joined user")
|
||||
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .init(userID: "@someone:matrix.org"), membership: nil, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
|
||||
.previewDisplayName("Unverified user")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ class MockUserIndicatorController: UserIndicatorControllerProtocol {
|
||||
func retractIndicatorWithId(_ id: String) { }
|
||||
|
||||
func retractAllIndicators() { }
|
||||
|
||||
var alertInfo: AlertInfo<UUID>?
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
|
||||
}
|
||||
}
|
||||
|
||||
@Published var alertInfo: AlertInfo<UUID>?
|
||||
|
||||
init(rootCoordinator: CoordinatorProtocol) {
|
||||
self.rootCoordinator = rootCoordinator
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@ protocol UserIndicatorControllerProtocol: CoordinatorProtocol {
|
||||
func submitIndicator(_ indicator: UserIndicator)
|
||||
func retractIndicatorWithId(_ id: String)
|
||||
func retractAllIndicators()
|
||||
var alertInfo: AlertInfo<UUID>? { get set }
|
||||
}
|
||||
|
||||
@@ -40,5 +40,8 @@ struct UserIndicatorPresenter: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(item: $userIndicatorController.alertInfo) {
|
||||
$0.alert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ struct DeveloperOptionsScreenViewStateBindings {
|
||||
var startChatFlowEnabled: Bool
|
||||
var startChatUserSuggestionsEnabled: Bool
|
||||
var invitesFlowEnabled: Bool
|
||||
var inviteMorePeopleFlowEnabled: Bool
|
||||
}
|
||||
|
||||
enum DeveloperOptionsScreenViewAction {
|
||||
@@ -36,5 +37,6 @@ enum DeveloperOptionsScreenViewAction {
|
||||
case changedStartChatFlowEnabled
|
||||
case changedStartChatUserSuggestionsEnabled
|
||||
case changedInvitesFlowEnabled
|
||||
case changedInviteMorePeopleFlowEnabled
|
||||
case clearCache
|
||||
}
|
||||
|
||||
+15
-9
@@ -21,16 +21,20 @@ typealias DeveloperOptionsScreenViewModelType = StateStoreViewModel<DeveloperOpt
|
||||
class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol {
|
||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
||||
|
||||
private let appSettings: AppSettings
|
||||
|
||||
init() {
|
||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
||||
startChatFlowEnabled: ServiceLocator.shared.settings.startChatFlowEnabled,
|
||||
startChatUserSuggestionsEnabled: ServiceLocator.shared.settings.startChatUserSuggestionsEnabled,
|
||||
invitesFlowEnabled: ServiceLocator.shared.settings.invitesFlowEnabled)
|
||||
appSettings = ServiceLocator.shared.settings
|
||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents,
|
||||
startChatFlowEnabled: appSettings.startChatFlowEnabled,
|
||||
startChatUserSuggestionsEnabled: appSettings.startChatUserSuggestionsEnabled,
|
||||
invitesFlowEnabled: appSettings.invitesFlowEnabled,
|
||||
inviteMorePeopleFlowEnabled: appSettings.inviteMorePeopleFlowEnabled)
|
||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||
|
||||
super.init(initialViewState: state)
|
||||
|
||||
ServiceLocator.shared.settings.$shouldCollapseRoomStateEvents
|
||||
appSettings.$shouldCollapseRoomStateEvents
|
||||
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
@@ -38,13 +42,15 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
||||
override func process(viewAction: DeveloperOptionsScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .changedShouldCollapseRoomStateEvents:
|
||||
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||
appSettings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||
case .changedStartChatFlowEnabled:
|
||||
ServiceLocator.shared.settings.startChatFlowEnabled = state.bindings.startChatFlowEnabled
|
||||
appSettings.startChatFlowEnabled = state.bindings.startChatFlowEnabled
|
||||
case .changedStartChatUserSuggestionsEnabled:
|
||||
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
||||
appSettings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
||||
case .changedInvitesFlowEnabled:
|
||||
ServiceLocator.shared.settings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
||||
appSettings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
||||
case .changedInviteMorePeopleFlowEnabled:
|
||||
appSettings.inviteMorePeopleFlowEnabled = state.bindings.inviteMorePeopleFlowEnabled
|
||||
case .clearCache:
|
||||
callback?(.clearCache)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
|
||||
Toggle(isOn: $context.startChatFlowEnabled) {
|
||||
Text("Show Start Chat flow")
|
||||
Text("Show start chat flow")
|
||||
}
|
||||
.onChange(of: context.startChatFlowEnabled) { _ in
|
||||
context.send(viewAction: .changedStartChatFlowEnabled)
|
||||
@@ -45,11 +45,18 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
|
||||
Toggle(isOn: $context.invitesFlowEnabled) {
|
||||
Text("Show Invites flow")
|
||||
Text("Show invites flow")
|
||||
}
|
||||
.onChange(of: context.invitesFlowEnabled) { _ in
|
||||
context.send(viewAction: .changedInvitesFlowEnabled)
|
||||
}
|
||||
|
||||
Toggle(isOn: $context.inviteMorePeopleFlowEnabled) {
|
||||
Text("Show invite more people flow")
|
||||
}
|
||||
.onChange(of: context.inviteMorePeopleFlowEnabled) { _ in
|
||||
context.send(viewAction: .changedInviteMorePeopleFlowEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
@@ -337,7 +337,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
defer {
|
||||
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
|
||||
}
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLeavingRoom, persistent: true))
|
||||
|
||||
let room = await userSession.clientProxy.roomForIdentifier(roomId)
|
||||
let result = await room?.leaveRoom()
|
||||
|
||||
@@ -18,14 +18,15 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct InviteUsersScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
let selectedUsers: CurrentValuePublisher<[UserProfile], Never>
|
||||
let roomType: InviteUsersScreenRoomType
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
}
|
||||
|
||||
enum InviteUsersScreenCoordinatorAction {
|
||||
case close
|
||||
case proceed
|
||||
case invite(users: [String])
|
||||
case toggleUser(UserProfile)
|
||||
}
|
||||
|
||||
@@ -42,17 +43,20 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: InviteUsersScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers, userSession: parameters.userSession, userDiscoveryService: parameters.userDiscoveryService)
|
||||
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers,
|
||||
roomType: parameters.roomType,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userDiscoveryService: parameters.userDiscoveryService)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .close:
|
||||
self.actionsSubject.send(.close)
|
||||
case .proceed:
|
||||
self.actionsSubject.send(.proceed)
|
||||
case .invite(let users):
|
||||
self.actionsSubject.send(.invite(users: users))
|
||||
case .toggleUser(let user):
|
||||
self.actionsSubject.send(.toggleUser(user))
|
||||
}
|
||||
|
||||
@@ -15,23 +15,30 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum InviteUsersScreenErrorType: Error {
|
||||
case unknown
|
||||
}
|
||||
|
||||
enum InviteUsersScreenViewModelAction {
|
||||
case close
|
||||
case proceed
|
||||
case invite(users: [String])
|
||||
case toggleUser(UserProfile)
|
||||
}
|
||||
|
||||
enum InviteUsersScreenRoomType {
|
||||
case draft
|
||||
case room(members: [RoomMemberProxyProtocol], userIndicatorController: UserIndicatorControllerProtocol)
|
||||
}
|
||||
|
||||
struct InviteUsersScreenViewState: BindableState {
|
||||
var bindings = InviteUsersScreenViewStateBindings()
|
||||
|
||||
var usersSection: UserDiscoverySection = .init(type: .suggestions, users: [])
|
||||
|
||||
var selectedUsers: [UserProfile] = []
|
||||
var membershipState: [String: MembershipState] = .init()
|
||||
|
||||
var isSearching: Bool {
|
||||
!bindings.searchQuery.isEmpty
|
||||
@@ -44,7 +51,30 @@ struct InviteUsersScreenViewState: BindableState {
|
||||
var scrollToLastID: String?
|
||||
|
||||
func isUserSelected(_ user: UserProfile) -> Bool {
|
||||
selectedUsers.contains { $0.userID == user.userID }
|
||||
isUserDisabled(user) || selectedUsers.contains { $0.userID == user.userID }
|
||||
}
|
||||
|
||||
func isUserDisabled(_ user: UserProfile) -> Bool {
|
||||
let membershipState = membershipState(user)
|
||||
return membershipState == .invite || membershipState == .join
|
||||
}
|
||||
|
||||
func membershipState(_ user: UserProfile) -> MembershipState? {
|
||||
membershipState[user.userID]
|
||||
}
|
||||
|
||||
let isCreatingRoom: Bool
|
||||
|
||||
var actionText: String {
|
||||
if isCreatingRoom {
|
||||
return selectedUsers.isEmpty ? L10n.actionSkip : L10n.actionNext
|
||||
} else {
|
||||
return L10n.actionInvite
|
||||
}
|
||||
}
|
||||
|
||||
var isActionDisabled: Bool {
|
||||
isCreatingRoom ? false : selectedUsers.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +86,6 @@ struct InviteUsersScreenViewStateBindings {
|
||||
}
|
||||
|
||||
enum InviteUsersScreenViewAction {
|
||||
case close
|
||||
case proceed
|
||||
case toggleUser(UserProfile)
|
||||
}
|
||||
|
||||
@@ -15,49 +15,77 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
||||
|
||||
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
private let roomType: InviteUsersScreenRoomType
|
||||
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(selectedUsers: CurrentValuePublisher<[UserProfile], Never>, userSession: UserSessionProtocol, userDiscoveryService: UserDiscoveryServiceProtocol) {
|
||||
self.userSession = userSession
|
||||
init(selectedUsers: CurrentValuePublisher<[UserProfile], Never>,
|
||||
roomType: InviteUsersScreenRoomType,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userDiscoveryService: UserDiscoveryServiceProtocol) {
|
||||
self.roomType = roomType
|
||||
self.mediaProvider = mediaProvider
|
||||
self.userDiscoveryService = userDiscoveryService
|
||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value), imageProvider: userSession.mediaProvider)
|
||||
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
setupSubscriptions()
|
||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
|
||||
|
||||
buildMembershipStateIfNeeded()
|
||||
setupSubscriptions(selectedUsers: selectedUsers)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: InviteUsersScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .close:
|
||||
actionsSubject.send(.close)
|
||||
case .proceed:
|
||||
actionsSubject.send(.proceed)
|
||||
switch roomType {
|
||||
case .draft:
|
||||
actionsSubject.send(.proceed)
|
||||
case .room:
|
||||
actionsSubject.send(.invite(users: state.selectedUsers.map(\.userID)))
|
||||
}
|
||||
case .toggleUser(let user):
|
||||
actionsSubject.send(.toggleUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
private func buildMembershipStateIfNeeded() {
|
||||
guard case let .room(members, userIndicatorController) = roomType else {
|
||||
return
|
||||
}
|
||||
let indicatorID = UUID().uuidString
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
|
||||
Task.detached { [members] in
|
||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||
let membershipState = members
|
||||
.reduce(into: [String: MembershipState]()) { partialResult, member in
|
||||
partialResult[member.userID] = member.membership
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
self.state.membershipState = membershipState
|
||||
userIndicatorController.retractIndicatorWithId(indicatorID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupSubscriptions() {
|
||||
@CancellableTask
|
||||
private var fetchUsersTask: Task<Void, Never>?
|
||||
|
||||
private func setupSubscriptions(selectedUsers: CurrentValuePublisher<[UserProfile], Never>) {
|
||||
context.$viewState
|
||||
.map(\.bindings.searchQuery)
|
||||
.debounceAndRemoveDuplicates()
|
||||
@@ -65,11 +93,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
self?.fetchUsers()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@CancellableTask
|
||||
private var fetchUsersTask: Task<Void, Never>?
|
||||
|
||||
private func fetchUsers() {
|
||||
guard searchQuery.count >= 3 else {
|
||||
fetchSuggestions()
|
||||
@@ -107,3 +138,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
context.searchQuery
|
||||
}
|
||||
}
|
||||
|
||||
private extension InviteUsersScreenRoomType {
|
||||
var isCreatingRoom: Bool {
|
||||
switch self {
|
||||
case .draft:
|
||||
return true
|
||||
case .room:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,11 @@ struct InviteUsersScreen: View {
|
||||
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
||||
Button { context.send(viewAction: .toggleUser(user)) } label: {
|
||||
UserProfileCell(user: user,
|
||||
membership: context.viewState.membershipState(user),
|
||||
imageProvider: context.imageProvider)
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: context.viewState.isUserSelected(user))))
|
||||
.buttonStyle(FormButtonStyle(isDisabled: context.viewState.isUserDisabled(user),
|
||||
accessory: .selection(isSelected: context.viewState.isUserSelected(user))))
|
||||
}
|
||||
} header: {
|
||||
if let title = context.viewState.usersSection.title {
|
||||
@@ -107,8 +109,9 @@ struct InviteUsersScreen: View {
|
||||
|
||||
private var nextButton: some View {
|
||||
Button { context.send(viewAction: .proceed) } label: {
|
||||
Text(context.viewState.selectedUsers.isEmpty ? L10n.actionSkip : L10n.actionNext)
|
||||
Text(context.viewState.actionText)
|
||||
}
|
||||
.disabled(context.viewState.isActionDisabled)
|
||||
}
|
||||
|
||||
private func deselect(_ user: UserProfile) {
|
||||
@@ -120,12 +123,10 @@ struct InviteUsersScreen: View {
|
||||
|
||||
struct InviteUsersScreen_Previews: PreviewProvider {
|
||||
static let viewModel = {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
let userDiscoveryService = UserDiscoveryServiceMock()
|
||||
userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice])
|
||||
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
|
||||
return InviteUsersScreenViewModel(selectedUsers: .init([]), userSession: userSession, userDiscoveryService: userDiscoveryService)
|
||||
return InviteUsersScreenViewModel(selectedUsers: .init([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDetailsScreenCoordinatorParameters {
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let roomProxy: RoomProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
}
|
||||
|
||||
enum RoomDetailsScreenCoordinatorAction {
|
||||
@@ -30,7 +32,11 @@ enum RoomDetailsScreenCoordinatorAction {
|
||||
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: RoomDetailsScreenCoordinatorParameters
|
||||
private var viewModel: RoomDetailsScreenViewModelProtocol
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
private let selectedUsers: CurrentValueSubject<[UserProfile], Never> = .init([])
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator {
|
||||
parameters.navigationStackCoordinator
|
||||
}
|
||||
|
||||
var callback: ((RoomDetailsScreenCoordinatorAction) -> Void)?
|
||||
|
||||
@@ -50,6 +56,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .requestMemberDetailsPresentation(let members):
|
||||
self.presentRoomMembersList(members)
|
||||
case .requestInvitePeoplePresentation(let members):
|
||||
self.presentInviteUsersScreen(members: members)
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
case .leftRoom:
|
||||
@@ -62,12 +70,96 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
AnyView(RoomDetailsScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
|
||||
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
members: members)
|
||||
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case .invite:
|
||||
self?.presentInviteUsersScreen(members: members)
|
||||
}
|
||||
}
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentInviteUsersScreen(members: [RoomMemberProxyProtocol]) {
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator)
|
||||
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers),
|
||||
roomType: .room(members: members, userIndicatorController: userIndicatorController),
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userDiscoveryService: parameters.userDiscoveryService)
|
||||
|
||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
coordinator.actions.sink { [weak self] result in
|
||||
guard let self else { return }
|
||||
|
||||
switch result {
|
||||
case .proceed:
|
||||
break
|
||||
case .invite(let users):
|
||||
self.inviteUsers(users, in: parameters.roomProxy)
|
||||
case .toggleUser(let user):
|
||||
self.toggleUser(user)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
||||
}
|
||||
|
||||
private func toggleUser(_ user: UserProfile) {
|
||||
var selectedUsers = selectedUsers.value
|
||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
||||
selectedUsers.remove(at: index)
|
||||
} else {
|
||||
selectedUsers.append(user)
|
||||
}
|
||||
self.selectedUsers.send(selectedUsers)
|
||||
}
|
||||
|
||||
private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) {
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
Task {
|
||||
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
|
||||
for user in users {
|
||||
group.addTask {
|
||||
await room.invite(userID: user)
|
||||
}
|
||||
}
|
||||
|
||||
return await group.first { inviteResult in
|
||||
inviteResult.isFailure
|
||||
} ?? .success(())
|
||||
}
|
||||
|
||||
guard case .failure = result else {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
|
||||
title: L10n.commonUnableToInviteTitle,
|
||||
message: L10n.commonUnableToInviteMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Result {
|
||||
var isFailure: Bool {
|
||||
switch self {
|
||||
case .success:
|
||||
return false
|
||||
case .failure:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import UIKit
|
||||
|
||||
enum RoomDetailsScreenViewModelAction {
|
||||
case requestMemberDetailsPresentation([RoomMemberProxyProtocol])
|
||||
case requestInvitePeoplePresentation([RoomMemberProxyProtocol])
|
||||
case leftRoom
|
||||
case cancel
|
||||
}
|
||||
@@ -41,6 +42,7 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
var members: [RoomMemberDetails] = []
|
||||
var joinedMembersCount = 0
|
||||
var isProcessingIgnoreRequest = false
|
||||
var canInviteUsers = false
|
||||
|
||||
var isLoadingMembers: Bool {
|
||||
members.isEmpty
|
||||
@@ -124,6 +126,7 @@ struct LeaveRoomAlertItem: AlertItem {
|
||||
|
||||
enum RoomDetailsScreenViewAction {
|
||||
case processTapPeople
|
||||
case processTapInvite
|
||||
case processTapLeave
|
||||
case processTapIgnore
|
||||
case processTapUnignore
|
||||
|
||||
@@ -56,6 +56,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
switch viewAction {
|
||||
case .processTapPeople:
|
||||
callback?(.requestMemberDetailsPresentation(members))
|
||||
case .processTapInvite:
|
||||
callback?(.requestInvitePeoplePresentation(members))
|
||||
case .processTapLeave:
|
||||
let joinedMembers = members.filter { $0.membership == .join }
|
||||
guard joinedMembers.count > 1 else {
|
||||
@@ -84,27 +86,29 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
guard let self else { return }
|
||||
|
||||
buildMembersDetailsTask = Task {
|
||||
let (membersDetails, joinedMembersCount) = await self.buildMembersDetails(members: members)
|
||||
let roomMembersDetails = await self.buildMembersDetails(members: members)
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 {
|
||||
self.dmRecipient = members.first(where: { !$0.isAccountOwner })
|
||||
}
|
||||
|
||||
self.state.members = membersDetails
|
||||
self.state.joinedMembersCount = joinedMembersCount
|
||||
|
||||
self.state.members = roomMembersDetails.members
|
||||
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
|
||||
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
|
||||
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
|
||||
self.members = members
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> (memberDetails: [RoomMemberDetails], joinedMembersCount: Int) {
|
||||
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
|
||||
await Task.detached {
|
||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||
var roomMembersDetails: [RoomMemberDetails] = []
|
||||
var accountOwner: RoomMemberProxyProtocol?
|
||||
var joinedMembersCount = 0
|
||||
roomMembersDetails.reserveCapacity(members.count)
|
||||
|
||||
@@ -114,9 +118,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
if member.membership == .join {
|
||||
joinedMembersCount += 1
|
||||
}
|
||||
|
||||
if accountOwner == nil, member.isAccountOwner {
|
||||
accountOwner = member
|
||||
}
|
||||
}
|
||||
|
||||
return (roomMembersDetails, joinedMembersCount)
|
||||
return .init(members: roomMembersDetails, joinedMembersCount: joinedMembersCount, accountOwner: accountOwner)
|
||||
}
|
||||
.value
|
||||
}
|
||||
@@ -124,7 +132,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
private static let leaveRoomLoadingID = "LeaveRoomLoading"
|
||||
|
||||
private func leaveRoom() async {
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLeavingRoom, persistent: true))
|
||||
let result = await roomProxy.leaveRoom()
|
||||
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
|
||||
switch result {
|
||||
@@ -159,3 +167,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct RoomMembersDetails {
|
||||
let members: [RoomMemberDetails]
|
||||
let joinedMembersCount: Int
|
||||
let accountOwner: RoomMemberProxyProtocol?
|
||||
}
|
||||
|
||||
@@ -128,12 +128,22 @@ struct RoomDetailsScreen: View {
|
||||
Label(L10n.commonPeople, systemImage: "person")
|
||||
}
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
|
||||
.disabled(context.viewState.isLoadingMembers)
|
||||
|
||||
if context.viewState.canInviteUsers, ServiceLocator.shared.settings.inviteMorePeopleFlowEnabled {
|
||||
Button {
|
||||
context.send(viewAction: .processTapInvite)
|
||||
} label: {
|
||||
Label(L10n.screenRoomDetailsInvitePeopleTitle, systemImage: "person.badge.plus")
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
|
||||
}
|
||||
}
|
||||
.formSectionStyle()
|
||||
.listRowSeparatorTint(.element.quinaryContent)
|
||||
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
|
||||
.foregroundColor(.element.primaryContent)
|
||||
|
||||
.disabled(context.viewState.isLoadingMembers)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
+5
-1
@@ -22,7 +22,9 @@ struct RoomMembersListScreenCoordinatorParameters {
|
||||
let members: [RoomMemberProxyProtocol]
|
||||
}
|
||||
|
||||
enum RoomMembersListScreenCoordinatorAction { }
|
||||
enum RoomMembersListScreenCoordinatorAction {
|
||||
case invite
|
||||
}
|
||||
|
||||
final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: RoomMembersListScreenCoordinatorParameters
|
||||
@@ -45,6 +47,8 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case let .selectMember(member):
|
||||
self.selectMember(member)
|
||||
case .invite:
|
||||
callback?(.invite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ import Foundation
|
||||
|
||||
enum RoomMembersListScreenViewModelAction {
|
||||
case selectMember(_ member: RoomMemberProxyProtocol)
|
||||
case invite
|
||||
}
|
||||
|
||||
struct RoomMembersListScreenViewState: BindableState {
|
||||
private var joinedMembers: [RoomMemberDetails]
|
||||
private var invitedMembers: [RoomMemberDetails]
|
||||
var canInviteUsers = false
|
||||
var bindings: RoomMembersListScreenViewStateBindings
|
||||
|
||||
init(joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings = .init()) {
|
||||
@@ -59,6 +61,7 @@ struct RoomMembersListScreenViewStateBindings {
|
||||
|
||||
enum RoomMembersListScreenViewAction {
|
||||
case selectMember(id: String)
|
||||
case invite
|
||||
}
|
||||
|
||||
private extension RoomMemberDetails {
|
||||
|
||||
+32
-12
@@ -29,7 +29,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
self.members = members
|
||||
super.init(initialViewState: .init(), imageProvider: mediaProvider)
|
||||
|
||||
buildMembersDetails(members: members)
|
||||
setupState(members: members)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -42,12 +42,14 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
return
|
||||
}
|
||||
callback?(.selectMember(member))
|
||||
case .invite:
|
||||
callback?(.invite)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
func buildMembersDetails(members: [RoomMemberProxyProtocol]) {
|
||||
private func setupState(members: [RoomMemberProxyProtocol]) {
|
||||
Task {
|
||||
let indicatorId = UUID().uuidString
|
||||
defer {
|
||||
@@ -55,24 +57,42 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
}
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
|
||||
let (invitedMembers, joinedMembers) = await split(members: members)
|
||||
self.state = .init(joinedMembers: joinedMembers, invitedMembers: invitedMembers)
|
||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||
self.state = .init(joinedMembers: roomMembersDetails.joinedMembers, invitedMembers: roomMembersDetails.invitedMembers)
|
||||
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func split(members: [RoomMemberProxyProtocol]) async -> (invited: [RoomMemberDetails], joined: [RoomMemberDetails]) {
|
||||
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
|
||||
await Task.detached {
|
||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||
let invitedMembers = members
|
||||
.filter { $0.membership == .invite }
|
||||
.map(RoomMemberDetails.init(withProxy:))
|
||||
var invitedMembers: [RoomMemberDetails] = .init()
|
||||
var joinedMembers: [RoomMemberDetails] = .init()
|
||||
var accountOwner: RoomMemberProxyProtocol?
|
||||
|
||||
let joinedMembers = members
|
||||
.filter { $0.membership == .join }
|
||||
.map(RoomMemberDetails.init(withProxy:))
|
||||
for member in members {
|
||||
if accountOwner == nil, member.isAccountOwner {
|
||||
accountOwner = member
|
||||
}
|
||||
|
||||
switch member.membership {
|
||||
case .invite:
|
||||
invitedMembers.append(.init(withProxy: member))
|
||||
case .join:
|
||||
joinedMembers.append(.init(withProxy: member))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return (invitedMembers, joinedMembers)
|
||||
return .init(invitedMembers: invitedMembers, joinedMembers: joinedMembers, accountOwner: accountOwner)
|
||||
}
|
||||
.value
|
||||
}
|
||||
}
|
||||
|
||||
private struct RoomMembersDetails {
|
||||
var invitedMembers: [RoomMemberDetails]
|
||||
var joinedMembers: [RoomMemberDetails]
|
||||
var accountOwner: RoomMemberProxyProtocol?
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ struct RoomMembersListScreen: View {
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
.navigationTitle(L10n.commonPeople)
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
inviteButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -53,6 +58,17 @@ struct RoomMembersListScreen: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var inviteButton: some View {
|
||||
if context.viewState.canInviteUsers, ServiceLocator.shared.settings.inviteMorePeopleFlowEnabled {
|
||||
Button {
|
||||
context.send(viewAction: .invite)
|
||||
} label: {
|
||||
Text(L10n.actionInvite)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@@ -22,6 +22,7 @@ struct RoomScreenCoordinatorParameters {
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
}
|
||||
|
||||
enum RoomScreenCoordinatorAction {
|
||||
@@ -163,7 +164,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
private func displayRoomDetails() {
|
||||
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
roomProxy: parameters.roomProxy,
|
||||
mediaProvider: parameters.mediaProvider)
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userDiscoveryService: parameters.userDiscoveryService)
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
|
||||
@@ -80,18 +80,19 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
private func presentInviteUsersScreen() {
|
||||
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: parameters.userSession,
|
||||
userDiscoveryService: parameters.userDiscoveryService,
|
||||
selectedUsers: selectedUsersPublisher)
|
||||
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: selectedUsersPublisher,
|
||||
roomType: .draft,
|
||||
mediaProvider: parameters.userSession.mediaProvider,
|
||||
userDiscoveryService: parameters.userDiscoveryService)
|
||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||
coordinator.actions.sink { [weak self] result in
|
||||
guard let self else { return }
|
||||
|
||||
switch result {
|
||||
case .close:
|
||||
parameters.navigationStackCoordinator?.pop()
|
||||
case .proceed:
|
||||
openCreateRoomScreen()
|
||||
case .invite:
|
||||
break
|
||||
case .toggleUser(let user):
|
||||
toggleUser(user)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ struct StartChatScreen: View {
|
||||
Section {
|
||||
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
||||
Button { context.send(viewAction: .selectUser(user)) } label: {
|
||||
UserProfileCell(user: user, imageProvider: context.imageProvider)
|
||||
UserProfileCell(user: user, membership: nil, imageProvider: context.imageProvider)
|
||||
}
|
||||
.buttonStyle(FormButtonStyle())
|
||||
}
|
||||
|
||||
@@ -493,7 +493,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
|
||||
private lazy var slidingSyncRequiredState = [RequiredState(key: "m.room.avatar", value: ""),
|
||||
RequiredState(key: "m.room.encryption", value: "")]
|
||||
RequiredState(key: "m.room.encryption", value: ""),
|
||||
RequiredState(key: "m.room.power_levels", value: "")]
|
||||
|
||||
private lazy var slidingSyncNotificationsRequiredState = [RequiredState(key: "m.room.member", value: "$ME"),
|
||||
RequiredState(key: "m.room.power_levels", value: ""),
|
||||
|
||||
@@ -66,6 +66,10 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
|
||||
var isIgnored: Bool {
|
||||
member.isIgnored()
|
||||
}
|
||||
|
||||
var canInviteUsers: Bool {
|
||||
member.canInvite()
|
||||
}
|
||||
|
||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
||||
sendAccountDataEventBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true)
|
||||
|
||||
@@ -33,6 +33,7 @@ protocol RoomMemberProxyProtocol {
|
||||
var normalizedPowerLevel: Int { get }
|
||||
var isAccountOwner: Bool { get }
|
||||
var isIgnored: Bool { get }
|
||||
var canInviteUsers: Bool { get }
|
||||
|
||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||
func unignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||
|
||||
@@ -449,6 +449,18 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func invite(userID: String) async -> Result<Void, RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
do {
|
||||
MXLog.info("Inviting user \(userID)")
|
||||
return try .success(self.room.inviteUserById(userId: userID))
|
||||
} catch {
|
||||
MXLog.error("Failed inviting user \(userID) with error: \(error)")
|
||||
return .failure(.failedInvitingUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invitedMembersCount: UInt {
|
||||
UInt(room.invitedMembersCount())
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ enum RoomProxyError: Error {
|
||||
case failedLeavingRoom
|
||||
case failedAcceptingInvite
|
||||
case failedRejectingInvite
|
||||
case failedInvitingUser
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -105,6 +106,8 @@ protocol RoomProxyProtocol {
|
||||
|
||||
func fetchDetails(for eventID: String)
|
||||
|
||||
func invite(userID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
var invitedMembersCount: UInt { get }
|
||||
|
||||
var joinedMembersCount: UInt { get }
|
||||
|
||||
@@ -26,7 +26,7 @@ struct RoomMemberDetails: Identifiable, Equatable {
|
||||
|
||||
init(withProxy proxy: RoomMemberProxyProtocol) {
|
||||
id = proxy.userID
|
||||
name = proxy.displayName
|
||||
name = proxy.displayName ?? proxy.userID
|
||||
avatarURL = proxy.avatarURL
|
||||
permalink = proxy.permalink
|
||||
isAccountOwner = proxy.isAccountOwner
|
||||
|
||||
@@ -203,7 +203,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
roomProxy: roomProxy,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
emojiProvider: emojiProvider)
|
||||
emojiProvider: emojiProvider,
|
||||
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
@@ -262,7 +263,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
|
||||
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
|
||||
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
||||
|
||||
|
||||
@@ -162,7 +162,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: nil)),
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -172,7 +173,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -184,7 +186,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "New room", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
@@ -199,7 +202,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -214,7 +218,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline, paginating", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -229,7 +234,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -245,7 +251,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -260,7 +267,8 @@ class MockScreen: Identifiable {
|
||||
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||
timelineController: timelineController,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
emojiProvider: EmojiProvider())
|
||||
emojiProvider: EmojiProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock())
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
@@ -293,7 +301,8 @@ class MockScreen: Identifiable {
|
||||
members: members))
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: MockMediaProvider()))
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomDetailsScreenWithRoomAvatar:
|
||||
@@ -308,7 +317,22 @@ class MockScreen: Identifiable {
|
||||
members: members))
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: MockMediaProvider()))
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomDetailsScreenWithInvite:
|
||||
ServiceLocator.shared.settings.inviteMorePeopleFlowEnabled = true
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let members: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockCharlie]
|
||||
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||
displayName: "Room",
|
||||
isEncrypted: true,
|
||||
members: members))
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomMembersListScreen:
|
||||
@@ -381,7 +405,8 @@ class MockScreen: Identifiable {
|
||||
members: members))
|
||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: MockMediaProvider()))
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .invitesWithBadges:
|
||||
@@ -414,15 +439,17 @@ class MockScreen: Identifiable {
|
||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .inviteUsers:
|
||||
case .inviteUsers, .inviteUsersInRoom, .inviteUsersInRoomExistingMembers:
|
||||
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = true
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let userDiscoveryMock = UserDiscoveryServiceMock()
|
||||
userDiscoveryMock.fetchSuggestionsReturnValue = .success([.mockAlice, .mockBob, .mockCharlie])
|
||||
userDiscoveryMock.searchProfilesWithReturnValue = .success([])
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider())
|
||||
let mediaProvider = MockMediaProvider()
|
||||
let usersSubject = CurrentValueSubject<[UserProfile], Never>([])
|
||||
let coordinator = InviteUsersScreenCoordinator(parameters: .init(userSession: userSession, userDiscoveryService: userDiscoveryMock, selectedUsers: usersSubject.asCurrentValuePublisher()))
|
||||
let members: [RoomMemberProxyMock] = id == .inviteUsersInRoomExistingMembers ? [.mockInvitedAlice, .mockBob] : []
|
||||
let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(members: members, userIndicatorController: MockUserIndicatorController())
|
||||
let coordinator = InviteUsersScreenCoordinator(parameters: .init(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: mediaProvider, userDiscoveryService: userDiscoveryMock))
|
||||
coordinator.actions.sink { action in
|
||||
switch action {
|
||||
case .toggleUser(let user):
|
||||
|
||||
@@ -43,6 +43,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case userSessionScreen
|
||||
case roomDetailsScreen
|
||||
case roomDetailsScreenWithRoomAvatar
|
||||
case roomDetailsScreenWithInvite
|
||||
case roomDetailsScreenDmDetails
|
||||
case roomMembersListScreen
|
||||
case roomMembersListScreenPendingInvites
|
||||
@@ -56,6 +57,8 @@ enum UITestsScreenIdentifier: String {
|
||||
case invitesWithBadges
|
||||
case invitesNoInvites
|
||||
case inviteUsers
|
||||
case inviteUsersInRoom
|
||||
case inviteUsersInRoomExistingMembers
|
||||
case createRoom
|
||||
case createRoomNoUsers
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user