mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +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 startChatFlowEnabled
|
||||||
case startChatUserSuggestionsEnabled
|
case startChatUserSuggestionsEnabled
|
||||||
case invitesFlowEnabled
|
case invitesFlowEnabled
|
||||||
|
case inviteMorePeopleFlowEnabled
|
||||||
case readReceiptsEnabled
|
case readReceiptsEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +188,9 @@ final class AppSettings {
|
|||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.invitesFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.invitesFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var invitesFlowEnabled
|
var invitesFlowEnabled
|
||||||
|
|
||||||
|
@UserPreference(key: UserDefaultsKeys.inviteMorePeopleFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
|
var inviteMorePeopleFlowEnabled
|
||||||
|
|
||||||
// MARK: Receipts
|
// MARK: Receipts
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,11 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
|||||||
set(value) { underlyingIsIgnored = value }
|
set(value) { underlyingIsIgnored = value }
|
||||||
}
|
}
|
||||||
var underlyingIsIgnored: Bool!
|
var underlyingIsIgnored: Bool!
|
||||||
|
var canInviteUsers: Bool {
|
||||||
|
get { return underlyingCanInviteUsers }
|
||||||
|
set(value) { underlyingCanInviteUsers = value }
|
||||||
|
}
|
||||||
|
var underlyingCanInviteUsers: Bool!
|
||||||
|
|
||||||
//MARK: - ignoreUser
|
//MARK: - ignoreUser
|
||||||
|
|
||||||
@@ -869,6 +874,27 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
fetchDetailsForReceivedInvocations.append(eventID)
|
fetchDetailsForReceivedInvocations.append(eventID)
|
||||||
fetchDetailsForClosure?(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 {
|
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
||||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ struct RoomMemberProxyMockConfiguration {
|
|||||||
var displayName: String
|
var displayName: String
|
||||||
var avatarURL: URL?
|
var avatarURL: URL?
|
||||||
var membership: MembershipState
|
var membership: MembershipState
|
||||||
var isNameAmbiguous: Bool
|
var isNameAmbiguous = false
|
||||||
var powerLevel: Int
|
var powerLevel: Int
|
||||||
var normalizedPowerLevel: Int
|
var normalizedPowerLevel: Int
|
||||||
var isAccountOwner: Bool
|
var isAccountOwner = false
|
||||||
var isIgnored: Bool
|
var isIgnored = false
|
||||||
|
var canInviteUsers = false
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomMemberProxyMock {
|
extension RoomMemberProxyMock {
|
||||||
@@ -41,6 +42,7 @@ extension RoomMemberProxyMock {
|
|||||||
normalizedPowerLevel = configuration.normalizedPowerLevel
|
normalizedPowerLevel = configuration.normalizedPowerLevel
|
||||||
isAccountOwner = configuration.isAccountOwner
|
isAccountOwner = configuration.isAccountOwner
|
||||||
isIgnored = configuration.isIgnored
|
isIgnored = configuration.isIgnored
|
||||||
|
canInviteUsers = configuration.canInviteUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
@@ -49,11 +51,8 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50))
|
||||||
isAccountOwner: false,
|
|
||||||
isIgnored: false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockInvitedAlice: RoomMemberProxyMock {
|
static var mockInvitedAlice: RoomMemberProxyMock {
|
||||||
@@ -61,11 +60,8 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
membership: .invite,
|
membership: .invite,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50))
|
||||||
isAccountOwner: false,
|
|
||||||
isIgnored: false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockBob: RoomMemberProxyMock {
|
static var mockBob: RoomMemberProxyMock {
|
||||||
@@ -73,11 +69,8 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50))
|
||||||
isAccountOwner: false,
|
|
||||||
isIgnored: false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockCharlie: RoomMemberProxyMock {
|
static var mockCharlie: RoomMemberProxyMock {
|
||||||
@@ -85,11 +78,8 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Charlie",
|
displayName: "Charlie",
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50))
|
||||||
isAccountOwner: false,
|
|
||||||
isIgnored: false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockDan: RoomMemberProxyMock {
|
static var mockDan: RoomMemberProxyMock {
|
||||||
@@ -97,11 +87,8 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Dan",
|
displayName: "Dan",
|
||||||
avatarURL: URL.picturesDirectory,
|
avatarURL: URL.picturesDirectory,
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50))
|
||||||
isAccountOwner: false,
|
|
||||||
isIgnored: false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockMe: RoomMemberProxyMock {
|
static var mockMe: RoomMemberProxyMock {
|
||||||
@@ -109,11 +96,10 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Me",
|
displayName: "Me",
|
||||||
avatarURL: URL.picturesDirectory,
|
avatarURL: URL.picturesDirectory,
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50,
|
||||||
isAccountOwner: true,
|
isAccountOwner: true,
|
||||||
isIgnored: false))
|
canInviteUsers: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mockIgnored: RoomMemberProxyMock {
|
static var mockIgnored: RoomMemberProxyMock {
|
||||||
@@ -121,10 +107,8 @@ extension RoomMemberProxyMock {
|
|||||||
displayName: "Ignored",
|
displayName: "Ignored",
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isNameAmbiguous: false,
|
|
||||||
powerLevel: 50,
|
powerLevel: 50,
|
||||||
normalizedPowerLevel: 50,
|
normalizedPowerLevel: 50,
|
||||||
isAccountOwner: false,
|
|
||||||
isIgnored: true))
|
isIgnored: true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ struct A11yIdentifiers {
|
|||||||
let avatar = "room_details-avatar"
|
let avatar = "room_details-avatar"
|
||||||
let dmAvatar = "room_details-dm_avatar"
|
let dmAvatar = "room_details-dm_avatar"
|
||||||
let people = "room_details-people"
|
let people = "room_details-people"
|
||||||
|
let invite = "room_details-invite"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomMemberDetailsScreen {
|
struct RoomMemberDetailsScreen {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ enum FormRowAccessory: View {
|
|||||||
/// to change the background colour depending on whether the button is currently pressed or not.
|
/// to change the background colour depending on whether the button is currently pressed or not.
|
||||||
struct FormButtonStyle: PrimitiveButtonStyle {
|
struct FormButtonStyle: PrimitiveButtonStyle {
|
||||||
var iconAlignment: VerticalAlignment = .firstTextBaseline
|
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.
|
/// An accessory to be added on the trailing side of the row.
|
||||||
var accessory: FormRowAccessory?
|
var accessory: FormRowAccessory?
|
||||||
|
|
||||||
@@ -54,12 +55,14 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
|||||||
.labelStyle(FormRowLabelStyle(alignment: iconAlignment, role: configuration.role))
|
.labelStyle(FormRowLabelStyle(alignment: iconAlignment, role: configuration.role))
|
||||||
.frame(maxHeight: .infinity) // Make sure the label fills the cell vertically.
|
.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.
|
.listRowInsets(EdgeInsets()) // Remove insets so the background fills the cell.
|
||||||
|
.disabled(isDisabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inner style used to set the pressed background colour.
|
/// Inner style used to set the pressed background colour.
|
||||||
struct Style: ButtonStyle {
|
struct Style: ButtonStyle {
|
||||||
|
var isDisabled: Bool
|
||||||
var accessory: FormRowAccessory?
|
var accessory: FormRowAccessory?
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
@@ -70,6 +73,7 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
accessory
|
accessory
|
||||||
|
.opacity(isDisabled ? 0.54 : 1)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.padding(FormRow.insets) // Re-apply the normal insets using padding.
|
.padding(FormRow.insets) // Re-apply the normal insets using padding.
|
||||||
@@ -132,7 +136,15 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
.buttonStyle(FormButtonStyle())
|
.buttonStyle(FormButtonStyle())
|
||||||
Button { } label: {
|
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)))
|
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import MatrixRustSDK
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserProfileCell: View {
|
struct UserProfileCell: View {
|
||||||
let user: UserProfile
|
let user: UserProfile
|
||||||
|
let membership: MembershipState?
|
||||||
let imageProvider: ImageProviderProtocol?
|
let imageProvider: ImageProviderProtocol?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -35,25 +37,92 @@ struct UserProfileCell: View {
|
|||||||
.font(.compound.headingSM)
|
.font(.compound.headingSM)
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(.element.primaryContent)
|
||||||
|
|
||||||
if user.displayName != nil {
|
subtitleContent
|
||||||
Text(user.userID)
|
|
||||||
.font(.compound.bodyMD)
|
|
||||||
.foregroundColor(.element.tertiaryContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.isVerified {
|
warningContent
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .combine)
|
.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 retractIndicatorWithId(_ id: String) { }
|
||||||
|
|
||||||
func retractAllIndicators() { }
|
func retractAllIndicators() { }
|
||||||
|
|
||||||
|
var alertInfo: AlertInfo<UUID>?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var alertInfo: AlertInfo<UUID>?
|
||||||
|
|
||||||
init(rootCoordinator: CoordinatorProtocol) {
|
init(rootCoordinator: CoordinatorProtocol) {
|
||||||
self.rootCoordinator = rootCoordinator
|
self.rootCoordinator = rootCoordinator
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ protocol UserIndicatorControllerProtocol: CoordinatorProtocol {
|
|||||||
func submitIndicator(_ indicator: UserIndicator)
|
func submitIndicator(_ indicator: UserIndicator)
|
||||||
func retractIndicatorWithId(_ id: String)
|
func retractIndicatorWithId(_ id: String)
|
||||||
func retractAllIndicators()
|
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 startChatFlowEnabled: Bool
|
||||||
var startChatUserSuggestionsEnabled: Bool
|
var startChatUserSuggestionsEnabled: Bool
|
||||||
var invitesFlowEnabled: Bool
|
var invitesFlowEnabled: Bool
|
||||||
|
var inviteMorePeopleFlowEnabled: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DeveloperOptionsScreenViewAction {
|
enum DeveloperOptionsScreenViewAction {
|
||||||
@@ -36,5 +37,6 @@ enum DeveloperOptionsScreenViewAction {
|
|||||||
case changedStartChatFlowEnabled
|
case changedStartChatFlowEnabled
|
||||||
case changedStartChatUserSuggestionsEnabled
|
case changedStartChatUserSuggestionsEnabled
|
||||||
case changedInvitesFlowEnabled
|
case changedInvitesFlowEnabled
|
||||||
|
case changedInviteMorePeopleFlowEnabled
|
||||||
case clearCache
|
case clearCache
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-9
@@ -21,16 +21,20 @@ typealias DeveloperOptionsScreenViewModelType = StateStoreViewModel<DeveloperOpt
|
|||||||
class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol {
|
class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol {
|
||||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
appSettings = ServiceLocator.shared.settings
|
||||||
startChatFlowEnabled: ServiceLocator.shared.settings.startChatFlowEnabled,
|
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents,
|
||||||
startChatUserSuggestionsEnabled: ServiceLocator.shared.settings.startChatUserSuggestionsEnabled,
|
startChatFlowEnabled: appSettings.startChatFlowEnabled,
|
||||||
invitesFlowEnabled: ServiceLocator.shared.settings.invitesFlowEnabled)
|
startChatUserSuggestionsEnabled: appSettings.startChatUserSuggestionsEnabled,
|
||||||
|
invitesFlowEnabled: appSettings.invitesFlowEnabled,
|
||||||
|
inviteMorePeopleFlowEnabled: appSettings.inviteMorePeopleFlowEnabled)
|
||||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||||
|
|
||||||
super.init(initialViewState: state)
|
super.init(initialViewState: state)
|
||||||
|
|
||||||
ServiceLocator.shared.settings.$shouldCollapseRoomStateEvents
|
appSettings.$shouldCollapseRoomStateEvents
|
||||||
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
@@ -38,13 +42,15 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
override func process(viewAction: DeveloperOptionsScreenViewAction) {
|
override func process(viewAction: DeveloperOptionsScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .changedShouldCollapseRoomStateEvents:
|
case .changedShouldCollapseRoomStateEvents:
|
||||||
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
appSettings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||||
case .changedStartChatFlowEnabled:
|
case .changedStartChatFlowEnabled:
|
||||||
ServiceLocator.shared.settings.startChatFlowEnabled = state.bindings.startChatFlowEnabled
|
appSettings.startChatFlowEnabled = state.bindings.startChatFlowEnabled
|
||||||
case .changedStartChatUserSuggestionsEnabled:
|
case .changedStartChatUserSuggestionsEnabled:
|
||||||
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
appSettings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
||||||
case .changedInvitesFlowEnabled:
|
case .changedInvitesFlowEnabled:
|
||||||
ServiceLocator.shared.settings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
appSettings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
||||||
|
case .changedInviteMorePeopleFlowEnabled:
|
||||||
|
appSettings.inviteMorePeopleFlowEnabled = state.bindings.inviteMorePeopleFlowEnabled
|
||||||
case .clearCache:
|
case .clearCache:
|
||||||
callback?(.clearCache)
|
callback?(.clearCache)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct DeveloperOptionsScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $context.startChatFlowEnabled) {
|
Toggle(isOn: $context.startChatFlowEnabled) {
|
||||||
Text("Show Start Chat flow")
|
Text("Show start chat flow")
|
||||||
}
|
}
|
||||||
.onChange(of: context.startChatFlowEnabled) { _ in
|
.onChange(of: context.startChatFlowEnabled) { _ in
|
||||||
context.send(viewAction: .changedStartChatFlowEnabled)
|
context.send(viewAction: .changedStartChatFlowEnabled)
|
||||||
@@ -45,11 +45,18 @@ struct DeveloperOptionsScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $context.invitesFlowEnabled) {
|
Toggle(isOn: $context.invitesFlowEnabled) {
|
||||||
Text("Show Invites flow")
|
Text("Show invites flow")
|
||||||
}
|
}
|
||||||
.onChange(of: context.invitesFlowEnabled) { _ in
|
.onChange(of: context.invitesFlowEnabled) { _ in
|
||||||
context.send(viewAction: .changedInvitesFlowEnabled)
|
context.send(viewAction: .changedInvitesFlowEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $context.inviteMorePeopleFlowEnabled) {
|
||||||
|
Text("Show invite more people flow")
|
||||||
|
}
|
||||||
|
.onChange(of: context.inviteMorePeopleFlowEnabled) { _ in
|
||||||
|
context.send(viewAction: .changedInviteMorePeopleFlowEnabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
defer {
|
defer {
|
||||||
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
|
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 room = await userSession.clientProxy.roomForIdentifier(roomId)
|
||||||
let result = await room?.leaveRoom()
|
let result = await room?.leaveRoom()
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InviteUsersScreenCoordinatorParameters {
|
struct InviteUsersScreenCoordinatorParameters {
|
||||||
let userSession: UserSessionProtocol
|
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
|
||||||
let selectedUsers: CurrentValuePublisher<[UserProfile], Never>
|
let selectedUsers: CurrentValuePublisher<[UserProfile], Never>
|
||||||
|
let roomType: InviteUsersScreenRoomType
|
||||||
|
let mediaProvider: MediaProviderProtocol
|
||||||
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenCoordinatorAction {
|
enum InviteUsersScreenCoordinatorAction {
|
||||||
case close
|
|
||||||
case proceed
|
case proceed
|
||||||
|
case invite(users: [String])
|
||||||
case toggleUser(UserProfile)
|
case toggleUser(UserProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,17 +43,20 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
|||||||
init(parameters: InviteUsersScreenCoordinatorParameters) {
|
init(parameters: InviteUsersScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
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() {
|
func start() {
|
||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .close:
|
|
||||||
self.actionsSubject.send(.close)
|
|
||||||
case .proceed:
|
case .proceed:
|
||||||
self.actionsSubject.send(.proceed)
|
self.actionsSubject.send(.proceed)
|
||||||
|
case .invite(let users):
|
||||||
|
self.actionsSubject.send(.invite(users: users))
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
self.actionsSubject.send(.toggleUser(user))
|
self.actionsSubject.send(.toggleUser(user))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,23 +15,30 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
enum InviteUsersScreenErrorType: Error {
|
enum InviteUsersScreenErrorType: Error {
|
||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenViewModelAction {
|
enum InviteUsersScreenViewModelAction {
|
||||||
case close
|
|
||||||
case proceed
|
case proceed
|
||||||
|
case invite(users: [String])
|
||||||
case toggleUser(UserProfile)
|
case toggleUser(UserProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum InviteUsersScreenRoomType {
|
||||||
|
case draft
|
||||||
|
case room(members: [RoomMemberProxyProtocol], userIndicatorController: UserIndicatorControllerProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
struct InviteUsersScreenViewState: BindableState {
|
struct InviteUsersScreenViewState: BindableState {
|
||||||
var bindings = InviteUsersScreenViewStateBindings()
|
var bindings = InviteUsersScreenViewStateBindings()
|
||||||
|
|
||||||
var usersSection: UserDiscoverySection = .init(type: .suggestions, users: [])
|
var usersSection: UserDiscoverySection = .init(type: .suggestions, users: [])
|
||||||
|
|
||||||
var selectedUsers: [UserProfile] = []
|
var selectedUsers: [UserProfile] = []
|
||||||
|
var membershipState: [String: MembershipState] = .init()
|
||||||
|
|
||||||
var isSearching: Bool {
|
var isSearching: Bool {
|
||||||
!bindings.searchQuery.isEmpty
|
!bindings.searchQuery.isEmpty
|
||||||
@@ -44,7 +51,30 @@ struct InviteUsersScreenViewState: BindableState {
|
|||||||
var scrollToLastID: String?
|
var scrollToLastID: String?
|
||||||
|
|
||||||
func isUserSelected(_ user: UserProfile) -> Bool {
|
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 {
|
enum InviteUsersScreenViewAction {
|
||||||
case close
|
|
||||||
case proceed
|
case proceed
|
||||||
case toggleUser(UserProfile)
|
case toggleUser(UserProfile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,49 +15,77 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import MatrixRustSDK
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
|
||||||
|
|
||||||
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
|
||||||
private let userSession: UserSessionProtocol
|
private let mediaProvider: MediaProviderProtocol
|
||||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
|
private let roomType: InviteUsersScreenRoomType
|
||||||
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
|
||||||
|
|
||||||
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
|
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(selectedUsers: CurrentValuePublisher<[UserProfile], Never>, userSession: UserSessionProtocol, userDiscoveryService: UserDiscoveryServiceProtocol) {
|
init(selectedUsers: CurrentValuePublisher<[UserProfile], Never>,
|
||||||
self.userSession = userSession
|
roomType: InviteUsersScreenRoomType,
|
||||||
|
mediaProvider: MediaProviderProtocol,
|
||||||
|
userDiscoveryService: UserDiscoveryServiceProtocol) {
|
||||||
|
self.roomType = roomType
|
||||||
|
self.mediaProvider = mediaProvider
|
||||||
self.userDiscoveryService = userDiscoveryService
|
self.userDiscoveryService = userDiscoveryService
|
||||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value), imageProvider: userSession.mediaProvider)
|
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
|
||||||
|
|
||||||
selectedUsers
|
buildMembershipStateIfNeeded()
|
||||||
.sink { [weak self] users in
|
setupSubscriptions(selectedUsers: selectedUsers)
|
||||||
self?.state.selectedUsers = users
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
setupSubscriptions()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
override func process(viewAction: InviteUsersScreenViewAction) {
|
override func process(viewAction: InviteUsersScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .close:
|
|
||||||
actionsSubject.send(.close)
|
|
||||||
case .proceed:
|
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):
|
case .toggleUser(let user):
|
||||||
actionsSubject.send(.toggleUser(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
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupSubscriptions() {
|
@CancellableTask
|
||||||
|
private var fetchUsersTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
private func setupSubscriptions(selectedUsers: CurrentValuePublisher<[UserProfile], Never>) {
|
||||||
context.$viewState
|
context.$viewState
|
||||||
.map(\.bindings.searchQuery)
|
.map(\.bindings.searchQuery)
|
||||||
.debounceAndRemoveDuplicates()
|
.debounceAndRemoveDuplicates()
|
||||||
@@ -65,11 +93,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
self?.fetchUsers()
|
self?.fetchUsers()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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() {
|
private func fetchUsers() {
|
||||||
guard searchQuery.count >= 3 else {
|
guard searchQuery.count >= 3 else {
|
||||||
fetchSuggestions()
|
fetchSuggestions()
|
||||||
@@ -107,3 +138,14 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
context.searchQuery
|
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
|
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
||||||
Button { context.send(viewAction: .toggleUser(user)) } label: {
|
Button { context.send(viewAction: .toggleUser(user)) } label: {
|
||||||
UserProfileCell(user: user,
|
UserProfileCell(user: user,
|
||||||
|
membership: context.viewState.membershipState(user),
|
||||||
imageProvider: context.imageProvider)
|
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: {
|
} header: {
|
||||||
if let title = context.viewState.usersSection.title {
|
if let title = context.viewState.usersSection.title {
|
||||||
@@ -107,8 +109,9 @@ struct InviteUsersScreen: View {
|
|||||||
|
|
||||||
private var nextButton: some View {
|
private var nextButton: some View {
|
||||||
Button { context.send(viewAction: .proceed) } label: {
|
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) {
|
private func deselect(_ user: UserProfile) {
|
||||||
@@ -120,12 +123,10 @@ struct InviteUsersScreen: View {
|
|||||||
|
|
||||||
struct InviteUsersScreen_Previews: PreviewProvider {
|
struct InviteUsersScreen_Previews: PreviewProvider {
|
||||||
static let viewModel = {
|
static let viewModel = {
|
||||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
|
||||||
mediaProvider: MockMediaProvider())
|
|
||||||
let userDiscoveryService = UserDiscoveryServiceMock()
|
let userDiscoveryService = UserDiscoveryServiceMock()
|
||||||
userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice])
|
userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice])
|
||||||
userDiscoveryService.searchProfilesWithReturnValue = .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 {
|
static var previews: some View {
|
||||||
|
|||||||
@@ -14,12 +14,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct RoomDetailsScreenCoordinatorParameters {
|
struct RoomDetailsScreenCoordinatorParameters {
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator
|
let navigationStackCoordinator: NavigationStackCoordinator
|
||||||
let roomProxy: RoomProxyProtocol
|
let roomProxy: RoomProxyProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomDetailsScreenCoordinatorAction {
|
enum RoomDetailsScreenCoordinatorAction {
|
||||||
@@ -30,7 +32,11 @@ enum RoomDetailsScreenCoordinatorAction {
|
|||||||
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||||
private let parameters: RoomDetailsScreenCoordinatorParameters
|
private let parameters: RoomDetailsScreenCoordinatorParameters
|
||||||
private var viewModel: RoomDetailsScreenViewModelProtocol
|
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)?
|
var callback: ((RoomDetailsScreenCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
@@ -50,6 +56,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case .requestMemberDetailsPresentation(let members):
|
case .requestMemberDetailsPresentation(let members):
|
||||||
self.presentRoomMembersList(members)
|
self.presentRoomMembersList(members)
|
||||||
|
case .requestInvitePeoplePresentation(let members):
|
||||||
|
self.presentInviteUsersScreen(members: members)
|
||||||
case .cancel:
|
case .cancel:
|
||||||
self.callback?(.cancel)
|
self.callback?(.cancel)
|
||||||
case .leftRoom:
|
case .leftRoom:
|
||||||
@@ -62,12 +70,96 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
AnyView(RoomDetailsScreen(context: viewModel.context))
|
AnyView(RoomDetailsScreen(context: viewModel.context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
|
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
|
||||||
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
members: members)
|
members: members)
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
|
coordinator.callback = { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .invite:
|
||||||
|
self?.presentInviteUsersScreen(members: members)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
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 {
|
enum RoomDetailsScreenViewModelAction {
|
||||||
case requestMemberDetailsPresentation([RoomMemberProxyProtocol])
|
case requestMemberDetailsPresentation([RoomMemberProxyProtocol])
|
||||||
|
case requestInvitePeoplePresentation([RoomMemberProxyProtocol])
|
||||||
case leftRoom
|
case leftRoom
|
||||||
case cancel
|
case cancel
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ struct RoomDetailsScreenViewState: BindableState {
|
|||||||
var members: [RoomMemberDetails] = []
|
var members: [RoomMemberDetails] = []
|
||||||
var joinedMembersCount = 0
|
var joinedMembersCount = 0
|
||||||
var isProcessingIgnoreRequest = false
|
var isProcessingIgnoreRequest = false
|
||||||
|
var canInviteUsers = false
|
||||||
|
|
||||||
var isLoadingMembers: Bool {
|
var isLoadingMembers: Bool {
|
||||||
members.isEmpty
|
members.isEmpty
|
||||||
@@ -124,6 +126,7 @@ struct LeaveRoomAlertItem: AlertItem {
|
|||||||
|
|
||||||
enum RoomDetailsScreenViewAction {
|
enum RoomDetailsScreenViewAction {
|
||||||
case processTapPeople
|
case processTapPeople
|
||||||
|
case processTapInvite
|
||||||
case processTapLeave
|
case processTapLeave
|
||||||
case processTapIgnore
|
case processTapIgnore
|
||||||
case processTapUnignore
|
case processTapUnignore
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .processTapPeople:
|
case .processTapPeople:
|
||||||
callback?(.requestMemberDetailsPresentation(members))
|
callback?(.requestMemberDetailsPresentation(members))
|
||||||
|
case .processTapInvite:
|
||||||
|
callback?(.requestInvitePeoplePresentation(members))
|
||||||
case .processTapLeave:
|
case .processTapLeave:
|
||||||
let joinedMembers = members.filter { $0.membership == .join }
|
let joinedMembers = members.filter { $0.membership == .join }
|
||||||
guard joinedMembers.count > 1 else {
|
guard joinedMembers.count > 1 else {
|
||||||
@@ -84,27 +86,29 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
buildMembersDetailsTask = Task {
|
buildMembersDetailsTask = Task {
|
||||||
let (membersDetails, joinedMembersCount) = await self.buildMembersDetails(members: members)
|
let roomMembersDetails = await self.buildMembersDetails(members: members)
|
||||||
|
|
||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 {
|
if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 {
|
||||||
self.dmRecipient = members.first(where: { !$0.isAccountOwner })
|
self.dmRecipient = members.first(where: { !$0.isAccountOwner })
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state.members = membersDetails
|
self.state.members = roomMembersDetails.members
|
||||||
self.state.joinedMembersCount = joinedMembersCount
|
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
|
||||||
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
|
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
|
||||||
|
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
|
||||||
self.members = members
|
self.members = members
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> (memberDetails: [RoomMemberDetails], joinedMembersCount: Int) {
|
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
|
||||||
await Task.detached {
|
await Task.detached {
|
||||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||||
var roomMembersDetails: [RoomMemberDetails] = []
|
var roomMembersDetails: [RoomMemberDetails] = []
|
||||||
|
var accountOwner: RoomMemberProxyProtocol?
|
||||||
var joinedMembersCount = 0
|
var joinedMembersCount = 0
|
||||||
roomMembersDetails.reserveCapacity(members.count)
|
roomMembersDetails.reserveCapacity(members.count)
|
||||||
|
|
||||||
@@ -114,9 +118,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
if member.membership == .join {
|
if member.membership == .join {
|
||||||
joinedMembersCount += 1
|
joinedMembersCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if accountOwner == nil, member.isAccountOwner {
|
||||||
|
accountOwner = member
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (roomMembersDetails, joinedMembersCount)
|
return .init(members: roomMembersDetails, joinedMembersCount: joinedMembersCount, accountOwner: accountOwner)
|
||||||
}
|
}
|
||||||
.value
|
.value
|
||||||
}
|
}
|
||||||
@@ -124,7 +132,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
private static let leaveRoomLoadingID = "LeaveRoomLoading"
|
private static let leaveRoomLoadingID = "LeaveRoomLoading"
|
||||||
|
|
||||||
private func leaveRoom() async {
|
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()
|
let result = await roomProxy.leaveRoom()
|
||||||
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
|
||||||
switch result {
|
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")
|
Label(L10n.commonPeople, systemImage: "person")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
|
|
||||||
.foregroundColor(.element.primaryContent)
|
|
||||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
|
.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
|
@ViewBuilder
|
||||||
|
|||||||
+5
-1
@@ -22,7 +22,9 @@ struct RoomMembersListScreenCoordinatorParameters {
|
|||||||
let members: [RoomMemberProxyProtocol]
|
let members: [RoomMemberProxyProtocol]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomMembersListScreenCoordinatorAction { }
|
enum RoomMembersListScreenCoordinatorAction {
|
||||||
|
case invite
|
||||||
|
}
|
||||||
|
|
||||||
final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
||||||
private let parameters: RoomMembersListScreenCoordinatorParameters
|
private let parameters: RoomMembersListScreenCoordinatorParameters
|
||||||
@@ -45,6 +47,8 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
|||||||
switch action {
|
switch action {
|
||||||
case let .selectMember(member):
|
case let .selectMember(member):
|
||||||
self.selectMember(member)
|
self.selectMember(member)
|
||||||
|
case .invite:
|
||||||
|
callback?(.invite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ import Foundation
|
|||||||
|
|
||||||
enum RoomMembersListScreenViewModelAction {
|
enum RoomMembersListScreenViewModelAction {
|
||||||
case selectMember(_ member: RoomMemberProxyProtocol)
|
case selectMember(_ member: RoomMemberProxyProtocol)
|
||||||
|
case invite
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomMembersListScreenViewState: BindableState {
|
struct RoomMembersListScreenViewState: BindableState {
|
||||||
private var joinedMembers: [RoomMemberDetails]
|
private var joinedMembers: [RoomMemberDetails]
|
||||||
private var invitedMembers: [RoomMemberDetails]
|
private var invitedMembers: [RoomMemberDetails]
|
||||||
|
var canInviteUsers = false
|
||||||
var bindings: RoomMembersListScreenViewStateBindings
|
var bindings: RoomMembersListScreenViewStateBindings
|
||||||
|
|
||||||
init(joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings = .init()) {
|
init(joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings = .init()) {
|
||||||
@@ -59,6 +61,7 @@ struct RoomMembersListScreenViewStateBindings {
|
|||||||
|
|
||||||
enum RoomMembersListScreenViewAction {
|
enum RoomMembersListScreenViewAction {
|
||||||
case selectMember(id: String)
|
case selectMember(id: String)
|
||||||
|
case invite
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension RoomMemberDetails {
|
private extension RoomMemberDetails {
|
||||||
|
|||||||
+32
-12
@@ -29,7 +29,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
self.members = members
|
self.members = members
|
||||||
super.init(initialViewState: .init(), imageProvider: mediaProvider)
|
super.init(initialViewState: .init(), imageProvider: mediaProvider)
|
||||||
|
|
||||||
buildMembersDetails(members: members)
|
setupState(members: members)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -42,12 +42,14 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
callback?(.selectMember(member))
|
callback?(.selectMember(member))
|
||||||
|
case .invite:
|
||||||
|
callback?(.invite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
func buildMembersDetails(members: [RoomMemberProxyProtocol]) {
|
private func setupState(members: [RoomMemberProxyProtocol]) {
|
||||||
Task {
|
Task {
|
||||||
let indicatorId = UUID().uuidString
|
let indicatorId = UUID().uuidString
|
||||||
defer {
|
defer {
|
||||||
@@ -55,24 +57,42 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
}
|
}
|
||||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true))
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
|
|
||||||
let (invitedMembers, joinedMembers) = await split(members: members)
|
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||||
self.state = .init(joinedMembers: joinedMembers, invitedMembers: invitedMembers)
|
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 {
|
await Task.detached {
|
||||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
||||||
let invitedMembers = members
|
var invitedMembers: [RoomMemberDetails] = .init()
|
||||||
.filter { $0.membership == .invite }
|
var joinedMembers: [RoomMemberDetails] = .init()
|
||||||
.map(RoomMemberDetails.init(withProxy:))
|
var accountOwner: RoomMemberProxyProtocol?
|
||||||
|
|
||||||
let joinedMembers = members
|
for member in members {
|
||||||
.filter { $0.membership == .join }
|
if accountOwner == nil, member.isAccountOwner {
|
||||||
.map(RoomMemberDetails.init(withProxy:))
|
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
|
.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())
|
.background(Color.element.background.ignoresSafeArea())
|
||||||
.navigationTitle(L10n.commonPeople)
|
.navigationTitle(L10n.commonPeople)
|
||||||
.alert(item: $context.alertInfo) { $0.alert }
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
inviteButton
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@@ -53,6 +58,17 @@ struct RoomMembersListScreen: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.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
|
// MARK: - Previews
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct RoomScreenCoordinatorParameters {
|
|||||||
let timelineController: RoomTimelineControllerProtocol
|
let timelineController: RoomTimelineControllerProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenCoordinatorAction {
|
enum RoomScreenCoordinatorAction {
|
||||||
@@ -163,7 +164,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
private func displayRoomDetails() {
|
private func displayRoomDetails() {
|
||||||
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: parameters.roomProxy,
|
roomProxy: parameters.roomProxy,
|
||||||
mediaProvider: parameters.mediaProvider)
|
mediaProvider: parameters.mediaProvider,
|
||||||
|
userDiscoveryService: parameters.userDiscoveryService)
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
||||||
coordinator.callback = { [weak self] action in
|
coordinator.callback = { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
|
|||||||
@@ -80,18 +80,19 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func presentInviteUsersScreen() {
|
private func presentInviteUsersScreen() {
|
||||||
let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: parameters.userSession,
|
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: selectedUsersPublisher,
|
||||||
userDiscoveryService: parameters.userDiscoveryService,
|
roomType: .draft,
|
||||||
selectedUsers: selectedUsersPublisher)
|
mediaProvider: parameters.userSession.mediaProvider,
|
||||||
|
userDiscoveryService: parameters.userDiscoveryService)
|
||||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||||
coordinator.actions.sink { [weak self] result in
|
coordinator.actions.sink { [weak self] result in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .close:
|
|
||||||
parameters.navigationStackCoordinator?.pop()
|
|
||||||
case .proceed:
|
case .proceed:
|
||||||
openCreateRoomScreen()
|
openCreateRoomScreen()
|
||||||
|
case .invite:
|
||||||
|
break
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
toggleUser(user)
|
toggleUser(user)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ struct StartChatScreen: View {
|
|||||||
Section {
|
Section {
|
||||||
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
||||||
Button { context.send(viewAction: .selectUser(user)) } label: {
|
Button { context.send(viewAction: .selectUser(user)) } label: {
|
||||||
UserProfileCell(user: user, imageProvider: context.imageProvider)
|
UserProfileCell(user: user, membership: nil, imageProvider: context.imageProvider)
|
||||||
}
|
}
|
||||||
.buttonStyle(FormButtonStyle())
|
.buttonStyle(FormButtonStyle())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,7 +493,8 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private lazy var slidingSyncRequiredState = [RequiredState(key: "m.room.avatar", value: ""),
|
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"),
|
private lazy var slidingSyncNotificationsRequiredState = [RequiredState(key: "m.room.member", value: "$ME"),
|
||||||
RequiredState(key: "m.room.power_levels", value: ""),
|
RequiredState(key: "m.room.power_levels", value: ""),
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
|
|||||||
var isIgnored: Bool {
|
var isIgnored: Bool {
|
||||||
member.isIgnored()
|
member.isIgnored()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var canInviteUsers: Bool {
|
||||||
|
member.canInvite()
|
||||||
|
}
|
||||||
|
|
||||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
func ignoreUser() async -> Result<Void, RoomMemberProxyError> {
|
||||||
sendAccountDataEventBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true)
|
sendAccountDataEventBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ protocol RoomMemberProxyProtocol {
|
|||||||
var normalizedPowerLevel: Int { get }
|
var normalizedPowerLevel: Int { get }
|
||||||
var isAccountOwner: Bool { get }
|
var isAccountOwner: Bool { get }
|
||||||
var isIgnored: Bool { get }
|
var isIgnored: Bool { get }
|
||||||
|
var canInviteUsers: Bool { get }
|
||||||
|
|
||||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||||
func unignoreUser() 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 {
|
var invitedMembersCount: UInt {
|
||||||
UInt(room.invitedMembersCount())
|
UInt(room.invitedMembersCount())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ enum RoomProxyError: Error {
|
|||||||
case failedLeavingRoom
|
case failedLeavingRoom
|
||||||
case failedAcceptingInvite
|
case failedAcceptingInvite
|
||||||
case failedRejectingInvite
|
case failedRejectingInvite
|
||||||
|
case failedInvitingUser
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -105,6 +106,8 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
func fetchDetails(for eventID: String)
|
func fetchDetails(for eventID: String)
|
||||||
|
|
||||||
|
func invite(userID: String) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
var invitedMembersCount: UInt { get }
|
var invitedMembersCount: UInt { get }
|
||||||
|
|
||||||
var joinedMembersCount: UInt { get }
|
var joinedMembersCount: UInt { get }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct RoomMemberDetails: Identifiable, Equatable {
|
|||||||
|
|
||||||
init(withProxy proxy: RoomMemberProxyProtocol) {
|
init(withProxy proxy: RoomMemberProxyProtocol) {
|
||||||
id = proxy.userID
|
id = proxy.userID
|
||||||
name = proxy.displayName
|
name = proxy.displayName ?? proxy.userID
|
||||||
avatarURL = proxy.avatarURL
|
avatarURL = proxy.avatarURL
|
||||||
permalink = proxy.permalink
|
permalink = proxy.permalink
|
||||||
isAccountOwner = proxy.isAccountOwner
|
isAccountOwner = proxy.isAccountOwner
|
||||||
|
|||||||
@@ -203,7 +203,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
emojiProvider: emojiProvider)
|
emojiProvider: emojiProvider,
|
||||||
|
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
coordinator.callback = { [weak self] action in
|
coordinator.callback = { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
@@ -262,7 +263,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
|
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
|
||||||
|
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: nil)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: nil)),
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
@@ -172,7 +173,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: MockRoomTimelineController(),
|
timelineController: MockRoomTimelineController(),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
@@ -184,7 +186,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "New room", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "New room", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
@@ -199,7 +202,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
@@ -214,7 +218,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline, paginating", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Small timeline, paginating", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
@@ -229,7 +234,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
@@ -245,7 +251,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
@@ -260,7 +267,8 @@ class MockScreen: Identifiable {
|
|||||||
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
roomProxy: RoomProxyMock(with: .init(displayName: "Large timeline", avatarURL: URL.picturesDirectory)),
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
emojiProvider: EmojiProvider())
|
emojiProvider: EmojiProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
@@ -293,7 +301,8 @@ class MockScreen: Identifiable {
|
|||||||
members: members))
|
members: members))
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider()))
|
mediaProvider: MockMediaProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomDetailsScreenWithRoomAvatar:
|
case .roomDetailsScreenWithRoomAvatar:
|
||||||
@@ -308,7 +317,22 @@ class MockScreen: Identifiable {
|
|||||||
members: members))
|
members: members))
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
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)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomMembersListScreen:
|
case .roomMembersListScreen:
|
||||||
@@ -381,7 +405,8 @@ class MockScreen: Identifiable {
|
|||||||
members: members))
|
members: members))
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider()))
|
mediaProvider: MockMediaProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .invitesWithBadges:
|
case .invitesWithBadges:
|
||||||
@@ -414,15 +439,17 @@ class MockScreen: Identifiable {
|
|||||||
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
let coordinator = InvitesScreenCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .inviteUsers:
|
case .inviteUsers, .inviteUsersInRoom, .inviteUsersInRoomExistingMembers:
|
||||||
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = true
|
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = true
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let userDiscoveryMock = UserDiscoveryServiceMock()
|
let userDiscoveryMock = UserDiscoveryServiceMock()
|
||||||
userDiscoveryMock.fetchSuggestionsReturnValue = .success([.mockAlice, .mockBob, .mockCharlie])
|
userDiscoveryMock.fetchSuggestionsReturnValue = .success([.mockAlice, .mockBob, .mockCharlie])
|
||||||
userDiscoveryMock.searchProfilesWithReturnValue = .success([])
|
userDiscoveryMock.searchProfilesWithReturnValue = .success([])
|
||||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider())
|
let mediaProvider = MockMediaProvider()
|
||||||
let usersSubject = CurrentValueSubject<[UserProfile], Never>([])
|
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
|
coordinator.actions.sink { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case userSessionScreen
|
case userSessionScreen
|
||||||
case roomDetailsScreen
|
case roomDetailsScreen
|
||||||
case roomDetailsScreenWithRoomAvatar
|
case roomDetailsScreenWithRoomAvatar
|
||||||
|
case roomDetailsScreenWithInvite
|
||||||
case roomDetailsScreenDmDetails
|
case roomDetailsScreenDmDetails
|
||||||
case roomMembersListScreen
|
case roomMembersListScreen
|
||||||
case roomMembersListScreenPendingInvites
|
case roomMembersListScreenPendingInvites
|
||||||
@@ -56,6 +57,8 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case invitesWithBadges
|
case invitesWithBadges
|
||||||
case invitesNoInvites
|
case invitesNoInvites
|
||||||
case inviteUsers
|
case inviteUsers
|
||||||
|
case inviteUsersInRoom
|
||||||
|
case inviteUsersInRoomExistingMembers
|
||||||
case createRoom
|
case createRoom
|
||||||
case createRoomNoUsers
|
case createRoomNoUsers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,16 @@ class InviteUsersScreenUITests: XCTestCase {
|
|||||||
app.collectionViews.firstMatch.cells.element(boundBy: 1).tap()
|
app.collectionViews.firstMatch.cells.element(boundBy: 1).tap()
|
||||||
try await app.assertScreenshot(.inviteUsers, step: 1)
|
try await app.assertScreenshot(.inviteUsers, step: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testInviteUsers() async throws {
|
||||||
|
let app = Application.launch(.inviteUsersInRoom)
|
||||||
|
app.collectionViews.firstMatch.cells.element(boundBy: 1).tap()
|
||||||
|
try await app.assertScreenshot(.inviteUsersInRoom, step: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInviteUserExistingMembers() async throws {
|
||||||
|
let app = Application.launch(.inviteUsersInRoomExistingMembers)
|
||||||
|
app.collectionViews.firstMatch.cells.element(boundBy: 1).tap()
|
||||||
|
try await app.assertScreenshot(.inviteUsersInRoomExistingMembers, step: 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ class RoomDetailsScreenUITests: XCTestCase {
|
|||||||
XCTAssert(app.buttons[A11yIdentifiers.roomDetailsScreen.people].waitForExistence(timeout: 1))
|
XCTAssert(app.buttons[A11yIdentifiers.roomDetailsScreen.people].waitForExistence(timeout: 1))
|
||||||
try await app.assertScreenshot(.roomDetailsScreenWithRoomAvatar)
|
try await app.assertScreenshot(.roomDetailsScreenWithRoomAvatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testInitialStateComponentsWithInvite() async throws {
|
||||||
|
let app = Application.launch(.roomDetailsScreenWithInvite)
|
||||||
|
|
||||||
|
XCTAssert(app.buttons[A11yIdentifiers.roomDetailsScreen.invite].waitForExistence(timeout: 1))
|
||||||
|
try await app.assertScreenshot(.roomDetailsScreenWithInvite)
|
||||||
|
}
|
||||||
|
|
||||||
func testInitialStateComponentsDmDetails() async throws {
|
func testInitialStateComponentsDmDetails() async throws {
|
||||||
let app = Application.launch(.roomDetailsScreenDmDetails)
|
let app = Application.launch(.roomDetailsScreenDmDetails)
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5998aef45c8d50471c13c400b07f172f2c47760f1896cdc1d26d73a7bdcd6e1e
|
||||||
|
size 108653
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5998aef45c8d50471c13c400b07f172f2c47760f1896cdc1d26d73a7bdcd6e1e
|
||||||
|
size 108653
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6047011ae61868f6d4ddce792dcdc41ed98ea22f277823de9e65a7f3336481d5
|
||||||
|
size 97139
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9afaa98b21ab14e513ef00a5826891c34edf424271ea0ef736353967120d1467
|
||||||
|
size 97265
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0a2540eb953c411f815d3eea12b559fe5cc7632af644b841f1487d5379d0efc3
|
||||||
|
size 133083
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0a2540eb953c411f815d3eea12b559fe5cc7632af644b841f1487d5379d0efc3
|
||||||
|
size 133083
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e0e8a9421366b6cfc72443ab9d7ee96ffe0b81ada3cd7ed48b911fae4cccf845
|
||||||
|
size 112719
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:03ac9acfa2dd004be4389c02b6760d35fcdff0bc75a3a3d67e820ab719a2d0f0
|
||||||
|
size 120758
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:871a7cdcab72ffd00146d252eac9eb00309158af303457ca260bf194741d0fd7
|
||||||
|
size 111420
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:871a7cdcab72ffd00146d252eac9eb00309158af303457ca260bf194741d0fd7
|
||||||
|
size 111420
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6b14407efd99162bdd2ad4b859896cdf11b8bf98fc3522e85ae399e909f39c22
|
||||||
|
size 103057
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f9e75b96549882fdc8e154c6782d855e9c761439db9647e4a60f3db3475b7d89
|
||||||
|
size 111269
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8f4003754398e766a8190e9856da6905adccd84cda9b2733a80d66cb1e7b9258
|
||||||
|
size 136980
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8f4003754398e766a8190e9856da6905adccd84cda9b2733a80d66cb1e7b9258
|
||||||
|
size 136980
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fe3a4bfcb6ea5b31f8640df8fc8aa5eecc197a6e314d296f3636f26a155aaba4
|
||||||
|
size 117935
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4e95327be10fc4925a6d43045d91cda86a1898fa745a4117573cfc4add17ebc3
|
||||||
|
size 150407
|
||||||
@@ -25,26 +25,73 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
var clientProxy: MockClientProxy!
|
var clientProxy: MockClientProxy!
|
||||||
var userDiscoveryService: UserDiscoveryServiceMock!
|
var userDiscoveryService: UserDiscoveryServiceMock!
|
||||||
|
|
||||||
private let usersSubject = CurrentValueSubject<[UserProfile], Never>([])
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
var context: InviteUsersScreenViewModel.Context {
|
var context: InviteUsersScreenViewModel.Context {
|
||||||
viewModel.context
|
viewModel.context
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
func testSelectUser() {
|
||||||
|
setupWithRoomType(roomType: .draft)
|
||||||
|
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
XCTAssertTrue(context.viewState.selectedUsers.count == 1)
|
||||||
|
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfile.mockAlice.userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReselectUser() {
|
||||||
|
setupWithRoomType(roomType: .draft)
|
||||||
|
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
|
||||||
|
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfile.mockAlice.userID)
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeselectUser() {
|
||||||
|
setupWithRoomType(roomType: .draft)
|
||||||
|
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
|
||||||
|
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfile.mockAlice.userID)
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInviteButton() async {
|
||||||
|
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||||
|
setupWithRoomType(roomType: .room(members: mockedMembers, userIndicatorController: MockUserIndicatorController()))
|
||||||
|
_ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false })
|
||||||
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await Task.yield()
|
||||||
|
context.send(viewAction: .proceed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = await viewModel.actions.values.first()
|
||||||
|
|
||||||
|
guard case let .invite(members) = action else {
|
||||||
|
XCTFail("Sent action should be 'invite'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(members, [RoomMemberProxyMock.mockAlice.userID])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupWithRoomType(roomType: InviteUsersScreenRoomType) {
|
||||||
|
let usersSubject = CurrentValueSubject<[UserProfile], Never>([])
|
||||||
clientProxy = .init(userID: "")
|
clientProxy = .init(userID: "")
|
||||||
userDiscoveryService = UserDiscoveryServiceMock()
|
userDiscoveryService = UserDiscoveryServiceMock()
|
||||||
userDiscoveryService.fetchSuggestionsReturnValue = .success([])
|
userDiscoveryService.fetchSuggestionsReturnValue = .success([])
|
||||||
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
||||||
let userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
|
||||||
usersSubject.send([])
|
usersSubject.send([])
|
||||||
let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(), userSession: userSession, userDiscoveryService: userDiscoveryService)
|
let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService)
|
||||||
viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie])
|
viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie])
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { action in
|
||||||
guard let self else { return }
|
|
||||||
switch action {
|
switch action {
|
||||||
case .toggleUser(let user):
|
case .toggleUser(let user):
|
||||||
var selectedUsers = usersSubject.value
|
var selectedUsers = usersSubject.value
|
||||||
@@ -60,29 +107,4 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSelectUser() {
|
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
|
||||||
context.send(viewAction: .toggleUser(.mockAlice))
|
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.count == 1)
|
|
||||||
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfile.mockAlice.userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testReselectUser() {
|
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
|
||||||
context.send(viewAction: .toggleUser(.mockAlice))
|
|
||||||
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
|
|
||||||
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfile.mockAlice.userID)
|
|
||||||
context.send(viewAction: .toggleUser(.mockAlice))
|
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDeselectUser() {
|
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
|
||||||
context.send(viewAction: .toggleUser(.mockAlice))
|
|
||||||
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
|
|
||||||
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfile.mockAlice.userID)
|
|
||||||
context.send(viewAction: .toggleUser(.mockAlice))
|
|
||||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,4 +180,38 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssert(context.viewState.dmRecipient?.isIgnored == true)
|
XCTAssert(context.viewState.dmRecipient?.isIgnored == true)
|
||||||
XCTAssertNotNil(context.alertInfo)
|
XCTAssertNotNil(context.alertInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCannotInvitePeople() async {
|
||||||
|
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
||||||
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
|
await context.nextViewState()
|
||||||
|
|
||||||
|
XCTAssertFalse(context.viewState.canInviteUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvitePeople() async {
|
||||||
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||||
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
|
await context.nextViewState()
|
||||||
|
|
||||||
|
XCTAssertTrue(context.viewState.canInviteUsers)
|
||||||
|
|
||||||
|
var callbackCorrectlyCalled = false
|
||||||
|
viewModel.callback = { action in
|
||||||
|
switch action {
|
||||||
|
case .requestInvitePeoplePresentation(let members):
|
||||||
|
callbackCorrectlyCalled = members.map(\.userID) == mockedMembers.map(\.userID)
|
||||||
|
default:
|
||||||
|
callbackCorrectlyCalled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.send(viewAction: .processTapInvite)
|
||||||
|
await Task.yield()
|
||||||
|
XCTAssertTrue(callbackCorrectlyCalled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user