mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Room details: improve members count UX (#1050)
* Inject user id into room’s details * Refactor member fetch logic in RoomDetailsScreenViewModel * Refactor DI * Fetch members in RoomMembersListScreenViewModel * Fetch members in InviteUsersScreen * Use joinedMembersCount in room members list * Cleanup * Limit members refresh * Cleanup binding * Fix UT build errors * Fix RoomDetailsScreenViewModelTests * Fix RoomMembersListScreenViewModelTests * Fix UTs * Fix RoomDetailsScreenUITests * Refine RoomDetailsScreenViewModelTests * Add roomProxy extension * PR comments * Refactor room’s counter to Int * Improve updateMembers() performances * Fix memory leaks
This commit is contained in:
@@ -317,7 +317,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let params = RoomDetailsScreenCoordinatorParameters(accountUserID: userSession.userID,
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
|
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
|
||||||
|
|||||||
@@ -437,21 +437,21 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
set(value) { underlyingMembersPublisher = value }
|
set(value) { underlyingMembersPublisher = value }
|
||||||
}
|
}
|
||||||
var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>!
|
var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>!
|
||||||
var invitedMembersCount: UInt {
|
var invitedMembersCount: Int {
|
||||||
get { return underlyingInvitedMembersCount }
|
get { return underlyingInvitedMembersCount }
|
||||||
set(value) { underlyingInvitedMembersCount = value }
|
set(value) { underlyingInvitedMembersCount = value }
|
||||||
}
|
}
|
||||||
var underlyingInvitedMembersCount: UInt!
|
var underlyingInvitedMembersCount: Int!
|
||||||
var joinedMembersCount: UInt {
|
var joinedMembersCount: Int {
|
||||||
get { return underlyingJoinedMembersCount }
|
get { return underlyingJoinedMembersCount }
|
||||||
set(value) { underlyingJoinedMembersCount = value }
|
set(value) { underlyingJoinedMembersCount = value }
|
||||||
}
|
}
|
||||||
var underlyingJoinedMembersCount: UInt!
|
var underlyingJoinedMembersCount: Int!
|
||||||
var activeMembersCount: UInt {
|
var activeMembersCount: Int {
|
||||||
get { return underlyingActiveMembersCount }
|
get { return underlyingActiveMembersCount }
|
||||||
set(value) { underlyingActiveMembersCount = value }
|
set(value) { underlyingActiveMembersCount = value }
|
||||||
}
|
}
|
||||||
var underlyingActiveMembersCount: UInt!
|
var underlyingActiveMembersCount: Int!
|
||||||
var updatesPublisher: AnyPublisher<TimelineDiff, Never> {
|
var updatesPublisher: AnyPublisher<TimelineDiff, Never> {
|
||||||
get { return underlyingUpdatesPublisher }
|
get { return underlyingUpdatesPublisher }
|
||||||
set(value) { underlyingUpdatesPublisher = value }
|
set(value) { underlyingUpdatesPublisher = value }
|
||||||
|
|||||||
@@ -100,11 +100,12 @@ extension RoomMemberProxyMock {
|
|||||||
isIgnored: true))
|
isIgnored: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mockOwner(allowedStateEvents: [StateEventType]) -> RoomMemberProxyMock {
|
static func mockOwner(allowedStateEvents: [StateEventType], canInviteUsers: Bool = true) -> RoomMemberProxyMock {
|
||||||
RoomMemberProxyMock(with: .init(userID: "@foo:some.org",
|
RoomMemberProxyMock(with: .init(userID: "@foo:some.org",
|
||||||
displayName: "User owner",
|
displayName: "User owner",
|
||||||
membership: .join,
|
membership: .join,
|
||||||
isAccountOwner: true,
|
isAccountOwner: true,
|
||||||
|
canInviteUsers: canInviteUsers,
|
||||||
canSendStateEvent: { allowedStateEvents.contains($0) }))
|
canSendStateEvent: { allowedStateEvents.contains($0) }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ struct RoomProxyMockConfiguration {
|
|||||||
var hasUnreadNotifications = Bool.random()
|
var hasUnreadNotifications = Bool.random()
|
||||||
var members: [RoomMemberProxyProtocol]?
|
var members: [RoomMemberProxyProtocol]?
|
||||||
var inviter: RoomMemberProxyMock?
|
var inviter: RoomMemberProxyMock?
|
||||||
|
var memberForID: RoomMemberProxyMock = .mockMe
|
||||||
|
|
||||||
var invitedMembersCount: UInt = 100
|
var invitedMembersCount = 100
|
||||||
var joinedMembersCount: UInt = 50
|
var joinedMembersCount = 50
|
||||||
var activeMembersCount: UInt = 25
|
var activeMembersCount = 25
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomProxyMock {
|
extension RoomProxyMock {
|
||||||
@@ -77,5 +78,6 @@ extension RoomProxyMock {
|
|||||||
underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
|
underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
|
||||||
setNameClosure = { _ in .success(()) }
|
setNameClosure = { _ in .success(()) }
|
||||||
setTopicClosure = { _ in .success(()) }
|
setTopicClosure = { _ in .success(()) }
|
||||||
|
getMemberUserIDReturnValue = .success(configuration.memberForID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ protocol UserIndicatorControllerProtocol: CoordinatorProtocol {
|
|||||||
extension UserIndicatorControllerProtocol {
|
extension UserIndicatorControllerProtocol {
|
||||||
/// Allows to submit a delayed indicator, this returns a Task so that it's also possible to cancel the action
|
/// Allows to submit a delayed indicator, this returns a Task so that it's also possible to cancel the action
|
||||||
func submitIndicator(_ indicator: UserIndicator, delay: Duration) -> Task<Void, Never> {
|
func submitIndicator(_ indicator: UserIndicator, delay: Duration) -> Task<Void, Never> {
|
||||||
Task {
|
Task { @MainActor in
|
||||||
try? await Task.sleep(for: delay)
|
try? await Task.sleep(for: delay)
|
||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
submitIndicator(indicator)
|
submitIndicator(indicator)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct InviteUsersScreenCoordinatorParameters {
|
|||||||
let roomType: InviteUsersScreenRoomType
|
let roomType: InviteUsersScreenRoomType
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
|
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InviteUsersScreenCoordinatorAction {
|
enum InviteUsersScreenCoordinatorAction {
|
||||||
@@ -46,7 +47,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
|
|||||||
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers,
|
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers,
|
||||||
roomType: parameters.roomType,
|
roomType: parameters.roomType,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
userDiscoveryService: parameters.userDiscoveryService)
|
userDiscoveryService: parameters.userDiscoveryService,
|
||||||
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ enum InviteUsersScreenViewModelAction {
|
|||||||
|
|
||||||
enum InviteUsersScreenRoomType {
|
enum InviteUsersScreenRoomType {
|
||||||
case draft
|
case draft
|
||||||
case room(members: [RoomMemberProxyProtocol], userIndicatorController: UserIndicatorControllerProtocol)
|
case room(roomProxy: RoomProxyProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InviteUsersScreenViewState: BindableState {
|
struct InviteUsersScreenViewState: BindableState {
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
private let mediaProvider: MediaProviderProtocol
|
private let mediaProvider: MediaProviderProtocol
|
||||||
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
private let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
private let roomType: InviteUsersScreenRoomType
|
private let roomType: InviteUsersScreenRoomType
|
||||||
|
private weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
|
||||||
|
@CancellableTask private var showLoaderTask: Task<Void, Never>?
|
||||||
|
|
||||||
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
|
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
@@ -33,14 +35,16 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
|
init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
|
||||||
roomType: InviteUsersScreenRoomType,
|
roomType: InviteUsersScreenRoomType,
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
userDiscoveryService: UserDiscoveryServiceProtocol) {
|
userDiscoveryService: UserDiscoveryServiceProtocol,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol?) {
|
||||||
self.roomType = roomType
|
self.roomType = roomType
|
||||||
self.mediaProvider = mediaProvider
|
self.mediaProvider = mediaProvider
|
||||||
self.userDiscoveryService = userDiscoveryService
|
self.userDiscoveryService = userDiscoveryService
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
|
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
|
||||||
|
|
||||||
buildMembershipStateIfNeeded()
|
|
||||||
setupSubscriptions(selectedUsers: selectedUsers)
|
setupSubscriptions(selectedUsers: selectedUsers)
|
||||||
|
fetchMembersIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -60,13 +64,11 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
actionsSubject.send(.toggleUser(user))
|
actionsSubject.send(.toggleUser(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
private func buildMembershipStateIfNeeded() {
|
private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
|
||||||
guard case let .room(members, userIndicatorController) = roomType else {
|
showLoader()
|
||||||
return
|
|
||||||
}
|
|
||||||
let indicatorID = UUID().uuidString
|
|
||||||
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID, type: .modal, title: L10n.commonLoading, persistent: true))
|
|
||||||
|
|
||||||
Task.detached { [members] in
|
Task.detached { [members] in
|
||||||
// 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.
|
||||||
@@ -77,12 +79,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.state.membershipState = membershipState
|
self.state.membershipState = membershipState
|
||||||
userIndicatorController.retractIndicatorWithId(indicatorID)
|
self.hideLoader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
|
|
||||||
@CancellableTask
|
@CancellableTask
|
||||||
private var fetchUsersTask: Task<Void, Never>?
|
private var fetchUsersTask: Task<Void, Never>?
|
||||||
@@ -103,6 +103,26 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fetchMembersIfNeeded() {
|
||||||
|
guard case let .room(roomProxy) = roomType else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
showLoader()
|
||||||
|
await roomProxy.updateMembers()
|
||||||
|
hideLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
roomProxy.membersPublisher
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.first()
|
||||||
|
.sink { [weak self] members in
|
||||||
|
self?.buildMembershipStateIfNeeded(members: members)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
private func fetchUsers() {
|
private func fetchUsers() {
|
||||||
guard searchQuery.count >= 3 else {
|
guard searchQuery.count >= 3 else {
|
||||||
fetchSuggestions()
|
fetchSuggestions()
|
||||||
@@ -139,6 +159,17 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
|||||||
private var searchQuery: String {
|
private var searchQuery: String {
|
||||||
context.searchQuery
|
context.searchQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let userIndicatorID = UUID().uuidString
|
||||||
|
|
||||||
|
private func showLoader() {
|
||||||
|
showLoaderTask = userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideLoader() {
|
||||||
|
showLoaderTask = nil
|
||||||
|
userIndicatorController?.retractIndicatorWithId(userIndicatorID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension InviteUsersScreenRoomType {
|
private extension InviteUsersScreenRoomType {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ struct InviteUsersScreen_Previews: PreviewProvider {
|
|||||||
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([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService)
|
return InviteUsersScreenViewModel(selectedUsers: .init([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
+5
-5
@@ -20,9 +20,9 @@ import SwiftUI
|
|||||||
struct RoomDetailsEditScreenCoordinatorParameters {
|
struct RoomDetailsEditScreenCoordinatorParameters {
|
||||||
let accountOwner: RoomMemberProxyProtocol
|
let accountOwner: RoomMemberProxyProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let roomProxy: RoomProxyProtocol
|
let roomProxy: RoomProxyProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomDetailsEditScreenCoordinatorAction {
|
enum RoomDetailsEditScreenCoordinatorAction {
|
||||||
@@ -77,14 +77,14 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
parameters.navigationStackCoordinator.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
case .selectMediaAtURL(let url):
|
case .selectMediaAtURL(let url):
|
||||||
parameters.navigationStackCoordinator.setSheetCoordinator(nil)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
viewModel.didSelectMediaUrl(url: url)
|
viewModel.didSelectMediaUrl(url: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
||||||
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -22,17 +22,17 @@ typealias RoomDetailsEditScreenViewModelType = StateStoreViewModel<RoomDetailsEd
|
|||||||
class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDetailsEditScreenViewModelProtocol {
|
class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDetailsEditScreenViewModelProtocol {
|
||||||
private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenViewModelAction, Never> = .init()
|
||||||
private let roomProxy: RoomProxyProtocol
|
private let roomProxy: RoomProxyProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
private let mediaPreprocessor: MediaUploadingPreprocessor = .init()
|
private let mediaPreprocessor: MediaUploadingPreprocessor = .init()
|
||||||
|
|
||||||
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> {
|
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(accountOwner: RoomMemberProxyProtocol,
|
init(accountOwner: RoomMemberProxyProtocol,
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
roomProxy: RoomProxyProtocol,
|
roomProxy: RoomProxyProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol?) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
|
||||||
@@ -75,12 +75,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
|
|||||||
Task {
|
Task {
|
||||||
let userIndicatorID = UUID().uuidString
|
let userIndicatorID = UUID().uuidString
|
||||||
defer {
|
defer {
|
||||||
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
userIndicatorController?.retractIndicatorWithId(userIndicatorID)
|
||||||
}
|
}
|
||||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
|
userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID,
|
||||||
type: .modal(interactiveDismissDisabled: true),
|
type: .modal(interactiveDismissDisabled: true),
|
||||||
title: L10n.commonLoading,
|
title: L10n.commonLoading,
|
||||||
persistent: true))
|
persistent: true))
|
||||||
|
|
||||||
let mediaResult = await mediaPreprocessor.processMedia(at: url)
|
let mediaResult = await mediaPreprocessor.processMedia(at: url)
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
|
|||||||
case .success(.image):
|
case .success(.image):
|
||||||
state.localMedia = try? mediaResult.get()
|
state.localMedia = try? mediaResult.get()
|
||||||
case .failure, .success:
|
case .failure, .success:
|
||||||
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown)
|
userIndicatorController?.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,12 +99,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
|
|||||||
Task {
|
Task {
|
||||||
let userIndicatorID = UUID().uuidString
|
let userIndicatorID = UUID().uuidString
|
||||||
defer {
|
defer {
|
||||||
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
userIndicatorController?.retractIndicatorWithId(userIndicatorID)
|
||||||
}
|
}
|
||||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
|
userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID,
|
||||||
type: .modal(interactiveDismissDisabled: true),
|
type: .modal(interactiveDismissDisabled: true),
|
||||||
title: L10n.screenRoomDetailsUpdatingRoom,
|
title: L10n.screenRoomDetailsUpdatingRoom,
|
||||||
persistent: true))
|
persistent: true))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||||
@@ -135,9 +135,9 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
|
|||||||
|
|
||||||
actionsSubject.send(.saveFinished)
|
actionsSubject.send(.saveFinished)
|
||||||
} catch {
|
} catch {
|
||||||
userIndicatorController.alertInfo = .init(id: .init(),
|
userIndicatorController?.alertInfo = .init(id: .init(),
|
||||||
title: L10n.screenRoomDetailsEditionErrorTitle,
|
title: L10n.screenRoomDetailsEditionErrorTitle,
|
||||||
message: L10n.screenRoomDetailsEditionError)
|
message: L10n.screenRoomDetailsEditionError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct RoomDetailsScreenCoordinatorParameters {
|
struct RoomDetailsScreenCoordinatorParameters {
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator
|
let accountUserID: String
|
||||||
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let roomProxy: RoomProxyProtocol
|
let roomProxy: RoomProxyProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
@@ -33,7 +34,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
private var viewModel: RoomDetailsScreenViewModelProtocol
|
private var viewModel: RoomDetailsScreenViewModelProtocol
|
||||||
private var cancellables: Set<AnyCancellable> = .init()
|
private var cancellables: Set<AnyCancellable> = .init()
|
||||||
private let selectedUsers: CurrentValueSubject<[UserProfileProxy], Never> = .init([])
|
private let selectedUsers: CurrentValueSubject<[UserProfileProxy], Never> = .init([])
|
||||||
private var navigationStackCoordinator: NavigationStackCoordinator {
|
private var navigationStackCoordinator: NavigationStackCoordinator? {
|
||||||
parameters.navigationStackCoordinator
|
parameters.navigationStackCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
init(parameters: RoomDetailsScreenCoordinatorParameters) {
|
init(parameters: RoomDetailsScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy,
|
viewModel = RoomDetailsScreenViewModel(accountUserID: parameters.accountUserID,
|
||||||
|
roomProxy: parameters.roomProxy,
|
||||||
mediaProvider: parameters.mediaProvider)
|
mediaProvider: parameters.mediaProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +55,10 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .requestMemberDetailsPresentation(let members):
|
case .requestMemberDetailsPresentation:
|
||||||
self.presentRoomMembersList(members)
|
self.presentRoomMembersList()
|
||||||
case .requestInvitePeoplePresentation(let members):
|
case .requestInvitePeoplePresentation:
|
||||||
self.presentInviteUsersScreen(members: members)
|
self.presentInviteUsersScreen()
|
||||||
case .leftRoom:
|
case .leftRoom:
|
||||||
self.callback?(.leftRoom)
|
self.callback?(.leftRoom)
|
||||||
case .requestEditDetailsPresentation(let accountOwner):
|
case .requestEditDetailsPresentation(let accountOwner):
|
||||||
@@ -71,32 +73,32 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
|
private func presentRoomMembersList() {
|
||||||
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
members: members)
|
roomProxy: parameters.roomProxy)
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
coordinator.callback = { [weak self] action in
|
coordinator.callback = { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .invite:
|
case .invite:
|
||||||
self?.presentInviteUsersScreen(members: members)
|
self?.presentInviteUsersScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator?.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentInviteUsersScreen(members: [RoomMemberProxyProtocol]) {
|
private func presentInviteUsersScreen() {
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let inviteUsersStackCoordinator = NavigationStackCoordinator()
|
||||||
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator)
|
let userIndicatorController = UserIndicatorController(rootCoordinator: inviteUsersStackCoordinator)
|
||||||
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers),
|
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers),
|
||||||
roomType: .room(members: members, userIndicatorController: userIndicatorController),
|
roomType: .room(roomProxy: parameters.roomProxy),
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
userDiscoveryService: parameters.userDiscoveryService)
|
userDiscoveryService: parameters.userDiscoveryService, userIndicatorController: userIndicatorController)
|
||||||
|
|
||||||
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
inviteUsersStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
|
||||||
coordinator.actions.sink { [weak self] result in
|
coordinator.actions.sink { [weak self] result in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -112,7 +114,9 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in
|
||||||
|
self?.selectedUsers.value = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) {
|
private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) {
|
||||||
@@ -129,14 +133,14 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
roomDetailsEditCoordinator.actions.sink { [weak self] action in
|
roomDetailsEditCoordinator.actions.sink { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil)
|
self?.navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator)
|
navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator)
|
||||||
|
|
||||||
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
self.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleUser(_ user: UserProfileProxy) {
|
private func toggleUser(_ user: UserProfileProxy) {
|
||||||
@@ -150,7 +154,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) {
|
private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) {
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
|
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import UIKit
|
|||||||
// MARK: View model
|
// MARK: View model
|
||||||
|
|
||||||
enum RoomDetailsScreenViewModelAction {
|
enum RoomDetailsScreenViewModelAction {
|
||||||
case requestMemberDetailsPresentation([RoomMemberProxyProtocol])
|
case requestMemberDetailsPresentation
|
||||||
case requestInvitePeoplePresentation([RoomMemberProxyProtocol])
|
case requestInvitePeoplePresentation
|
||||||
case leftRoom
|
case leftRoom
|
||||||
case requestEditDetailsPresentation(RoomMemberProxyProtocol)
|
case requestEditDetailsPresentation(RoomMemberProxyProtocol)
|
||||||
}
|
}
|
||||||
@@ -35,22 +35,18 @@ struct RoomDetailsScreenViewState: BindableState {
|
|||||||
let canonicalAlias: String?
|
let canonicalAlias: String?
|
||||||
let isEncrypted: Bool
|
let isEncrypted: Bool
|
||||||
let isDirect: Bool
|
let isDirect: Bool
|
||||||
|
let permalink: URL?
|
||||||
|
|
||||||
var title = ""
|
var title = ""
|
||||||
var topic: String?
|
var topic: String?
|
||||||
var avatarURL: URL?
|
var avatarURL: URL?
|
||||||
let permalink: URL?
|
var joinedMembersCount: Int
|
||||||
var members: [RoomMemberDetails] = []
|
|
||||||
var joinedMembersCount = 0
|
|
||||||
var isProcessingIgnoreRequest = false
|
var isProcessingIgnoreRequest = false
|
||||||
var canInviteUsers = false
|
var canInviteUsers = false
|
||||||
var canEditRoomName = false
|
var canEditRoomName = false
|
||||||
var canEditRoomTopic = false
|
var canEditRoomTopic = false
|
||||||
var canEditRoomAvatar = false
|
var canEditRoomAvatar = false
|
||||||
|
|
||||||
var isLoadingMembers: Bool {
|
|
||||||
members.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
var canEdit: Bool {
|
var canEdit: Bool {
|
||||||
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
|
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
|
||||||
}
|
}
|
||||||
@@ -62,10 +58,6 @@ struct RoomDetailsScreenViewState: BindableState {
|
|||||||
var bindings: RoomDetailsScreenViewStateBindings
|
var bindings: RoomDetailsScreenViewStateBindings
|
||||||
|
|
||||||
var dmRecipient: RoomMemberDetails?
|
var dmRecipient: RoomMemberDetails?
|
||||||
|
|
||||||
private var isDMRoom: Bool {
|
|
||||||
isEncrypted && isDirect && members.count == 2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomDetailsScreenViewStateBindings {
|
struct RoomDetailsScreenViewStateBindings {
|
||||||
|
|||||||
@@ -20,35 +20,36 @@ import SwiftUI
|
|||||||
typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreenViewState, RoomDetailsScreenViewAction>
|
typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreenViewState, RoomDetailsScreenViewAction>
|
||||||
|
|
||||||
class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol {
|
class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol {
|
||||||
|
private let accountUserID: String
|
||||||
private let roomProxy: RoomProxyProtocol
|
private let roomProxy: RoomProxyProtocol
|
||||||
private var members: [RoomMemberProxyProtocol] = []
|
|
||||||
|
private var accountOwner: RoomMemberProxyProtocol? {
|
||||||
|
didSet { updatePowerLevelPermissions() }
|
||||||
|
}
|
||||||
|
|
||||||
private var dmRecipient: RoomMemberProxyProtocol?
|
private var dmRecipient: RoomMemberProxyProtocol?
|
||||||
private var accountOwner: RoomMemberProxyProtocol?
|
|
||||||
|
|
||||||
@CancellableTask
|
|
||||||
private var buildMembersDetailsTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var callback: ((RoomDetailsScreenViewModelAction) -> Void)?
|
var callback: ((RoomDetailsScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
init(roomProxy: RoomProxyProtocol,
|
init(accountUserID: String,
|
||||||
|
roomProxy: RoomProxyProtocol,
|
||||||
mediaProvider: MediaProviderProtocol) {
|
mediaProvider: MediaProviderProtocol) {
|
||||||
|
self.accountUserID = accountUserID
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
super.init(initialViewState: .init(roomId: roomProxy.id,
|
super.init(initialViewState: .init(roomId: roomProxy.id,
|
||||||
canonicalAlias: roomProxy.canonicalAlias,
|
canonicalAlias: roomProxy.canonicalAlias,
|
||||||
isEncrypted: roomProxy.isEncrypted,
|
isEncrypted: roomProxy.isEncrypted,
|
||||||
isDirect: roomProxy.isDirect,
|
isDirect: roomProxy.isDirect,
|
||||||
|
permalink: roomProxy.permalink,
|
||||||
title: roomProxy.roomTitle,
|
title: roomProxy.roomTitle,
|
||||||
topic: roomProxy.topic,
|
topic: roomProxy.topic,
|
||||||
avatarURL: roomProxy.avatarURL,
|
avatarURL: roomProxy.avatarURL,
|
||||||
permalink: roomProxy.permalink,
|
joinedMembersCount: roomProxy.joinedMembersCount,
|
||||||
bindings: .init()),
|
bindings: .init()),
|
||||||
imageProvider: mediaProvider)
|
imageProvider: mediaProvider)
|
||||||
|
|
||||||
setupSubscriptions()
|
setupRoomSubscription()
|
||||||
|
fetchMembers()
|
||||||
Task {
|
|
||||||
await roomProxy.updateMembers()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -57,12 +58,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
override func process(viewAction: RoomDetailsScreenViewAction) {
|
override func process(viewAction: RoomDetailsScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .processTapPeople:
|
case .processTapPeople:
|
||||||
callback?(.requestMemberDetailsPresentation(members))
|
callback?(.requestMemberDetailsPresentation)
|
||||||
case .processTapInvite:
|
case .processTapInvite:
|
||||||
callback?(.requestInvitePeoplePresentation(members))
|
callback?(.requestInvitePeoplePresentation)
|
||||||
case .processTapLeave:
|
case .processTapLeave:
|
||||||
let joinedMembers = members.filter { $0.membership == .join }
|
guard state.joinedMembersCount > 1 else {
|
||||||
guard joinedMembers.count > 1 else {
|
|
||||||
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty)
|
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupSubscriptions() {
|
private func setupRoomSubscription() {
|
||||||
switch roomProxy.registerTimelineListenerIfNeeded() {
|
switch roomProxy.registerTimelineListenerIfNeeded() {
|
||||||
case .success, .failure(.roomListenerAlreadyRegistered):
|
case .success, .failure(.roomListenerAlreadyRegistered):
|
||||||
break
|
break
|
||||||
@@ -96,32 +96,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)")
|
MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
roomProxy.membersPublisher
|
|
||||||
.sink { [weak self] members in
|
|
||||||
guard let self else { return }
|
|
||||||
|
|
||||||
buildMembersDetailsTask = Task {
|
|
||||||
let roomMembersDetails = await self.buildMembersDetails(members: members)
|
|
||||||
|
|
||||||
guard !Task.isCancelled else { return }
|
|
||||||
|
|
||||||
if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 {
|
|
||||||
self.dmRecipient = members.first(where: { !$0.isAccountOwner })
|
|
||||||
}
|
|
||||||
|
|
||||||
self.state.members = roomMembersDetails.members
|
|
||||||
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
|
|
||||||
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
|
|
||||||
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
|
|
||||||
self.state.canEditRoomName = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomName) ?? false
|
|
||||||
self.state.canEditRoomTopic = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomTopic) ?? false
|
|
||||||
self.state.canEditRoomAvatar = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
|
|
||||||
self.members = members
|
|
||||||
self.accountOwner = roomMembersDetails.accountOwner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
roomProxy.updatesPublisher
|
roomProxy.updatesPublisher
|
||||||
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
|
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
@@ -129,33 +103,50 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
self.state.title = self.roomProxy.roomTitle
|
self.state.title = self.roomProxy.roomTitle
|
||||||
self.state.topic = self.roomProxy.topic
|
self.state.topic = self.roomProxy.topic
|
||||||
self.state.avatarURL = self.roomProxy.avatarURL
|
self.state.avatarURL = self.roomProxy.avatarURL
|
||||||
|
self.state.joinedMembersCount = self.roomProxy.joinedMembersCount
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
|
private func fetchMembers() {
|
||||||
await Task.detached {
|
Task {
|
||||||
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
|
await fetchMembersIfNeeded()
|
||||||
var roomMembersDetails: [RoomMemberDetails] = []
|
await fetchAccountOwner()
|
||||||
var accountOwner: RoomMemberProxyProtocol?
|
|
||||||
var joinedMembersCount = 0
|
|
||||||
roomMembersDetails.reserveCapacity(members.count)
|
|
||||||
|
|
||||||
for member in members {
|
|
||||||
roomMembersDetails.append(RoomMemberDetails(withProxy: member))
|
|
||||||
|
|
||||||
if member.membership == .join {
|
|
||||||
joinedMembersCount += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if accountOwner == nil, member.isAccountOwner {
|
|
||||||
accountOwner = member
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .init(members: roomMembersDetails, joinedMembersCount: joinedMembersCount, accountOwner: accountOwner)
|
|
||||||
}
|
}
|
||||||
.value
|
}
|
||||||
|
|
||||||
|
private func fetchMembersIfNeeded() async {
|
||||||
|
// We need to fetch members just in 1-to-1 chat to get the member object for the other person
|
||||||
|
guard roomProxy.isEncryptedOneToOneRoom else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roomProxy.membersPublisher
|
||||||
|
.sink { [weak self] members in
|
||||||
|
guard let self else { return }
|
||||||
|
let dmRecipient = members.first(where: { !$0.isAccountOwner })
|
||||||
|
self.dmRecipient = dmRecipient
|
||||||
|
self.state.dmRecipient = dmRecipient.map(RoomMemberDetails.init(withProxy:))
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
await roomProxy.updateMembers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchAccountOwner() async {
|
||||||
|
switch await roomProxy.getMember(userID: accountUserID) {
|
||||||
|
case .success(let member):
|
||||||
|
accountOwner = member
|
||||||
|
case .failure(let error):
|
||||||
|
MXLog.error("Failed (error: \(error) to get account owner member with id: \(accountUserID), in room: \(roomProxy.id)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePowerLevelPermissions() {
|
||||||
|
state.canInviteUsers = accountOwner?.canInviteUsers ?? false
|
||||||
|
state.canEditRoomName = accountOwner?.canSendStateEvent(type: .roomName) ?? false
|
||||||
|
state.canEditRoomTopic = accountOwner?.canSendStateEvent(type: .roomTopic) ?? false
|
||||||
|
state.canEditRoomAvatar = accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let leaveRoomLoadingID = "LeaveRoomLoading"
|
private static let leaveRoomLoadingID = "LeaveRoomLoading"
|
||||||
@@ -196,9 +187,3 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct RoomMembersDetails {
|
|
||||||
let members: [RoomMemberDetails]
|
|
||||||
let joinedMembersCount: Int
|
|
||||||
let accountOwner: RoomMemberProxyProtocol?
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -137,13 +137,9 @@ struct RoomDetailsScreen: View {
|
|||||||
context.send(viewAction: .processTapPeople)
|
context.send(viewAction: .processTapPeople)
|
||||||
} label: {
|
} label: {
|
||||||
LabeledContent {
|
LabeledContent {
|
||||||
if context.viewState.isLoadingMembers {
|
Text(String(context.viewState.joinedMembersCount))
|
||||||
ProgressView()
|
.foregroundColor(.element.tertiaryContent)
|
||||||
} else {
|
.font(.compound.bodyLG)
|
||||||
Text(String(context.viewState.joinedMembersCount))
|
|
||||||
.foregroundColor(.element.tertiaryContent)
|
|
||||||
.font(.compound.bodyLG)
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Label(L10n.commonPeople, systemImage: "person")
|
Label(L10n.commonPeople, systemImage: "person")
|
||||||
}
|
}
|
||||||
@@ -160,9 +156,8 @@ struct RoomDetailsScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowSeparatorTint(.element.quinaryContent)
|
.listRowSeparatorTint(.element.quinaryContent)
|
||||||
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
|
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(.element.primaryContent)
|
||||||
.disabled(context.viewState.isLoadingMembers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -257,7 +252,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
|
|||||||
canonicalAlias: "#alias:domain.com",
|
canonicalAlias: "#alias:domain.com",
|
||||||
members: members))
|
members: members))
|
||||||
|
|
||||||
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
|
return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com",
|
||||||
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -274,7 +270,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
|
|||||||
canonicalAlias: "#alias:domain.com",
|
canonicalAlias: "#alias:domain.com",
|
||||||
members: members))
|
members: members))
|
||||||
|
|
||||||
return RoomDetailsScreenViewModel(roomProxy: roomProxy,
|
return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com",
|
||||||
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
+8
-6
@@ -17,9 +17,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct RoomMembersListScreenCoordinatorParameters {
|
struct RoomMembersListScreenCoordinatorParameters {
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let members: [RoomMemberProxyProtocol]
|
let roomProxy: RoomProxyProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomMembersListScreenCoordinatorAction {
|
enum RoomMembersListScreenCoordinatorAction {
|
||||||
@@ -29,15 +29,17 @@ enum RoomMembersListScreenCoordinatorAction {
|
|||||||
final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
||||||
private let parameters: RoomMembersListScreenCoordinatorParameters
|
private let parameters: RoomMembersListScreenCoordinatorParameters
|
||||||
private var viewModel: RoomMembersListScreenViewModelProtocol
|
private var viewModel: RoomMembersListScreenViewModelProtocol
|
||||||
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
|
private var navigationStackCoordinator: NavigationStackCoordinator? {
|
||||||
|
parameters.navigationStackCoordinator
|
||||||
|
}
|
||||||
|
|
||||||
var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)?
|
var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
init(parameters: RoomMembersListScreenCoordinatorParameters) {
|
init(parameters: RoomMembersListScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = RoomMembersListScreenViewModel(mediaProvider: parameters.mediaProvider,
|
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
|
||||||
members: parameters.members)
|
mediaProvider: parameters.mediaProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -63,6 +65,6 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
|||||||
let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider)
|
let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider)
|
||||||
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters)
|
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator?.push(coordinator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ enum RoomMembersListScreenViewModelAction {
|
|||||||
struct RoomMembersListScreenViewState: BindableState {
|
struct RoomMembersListScreenViewState: BindableState {
|
||||||
private var joinedMembers: [RoomMemberDetails]
|
private var joinedMembers: [RoomMemberDetails]
|
||||||
private var invitedMembers: [RoomMemberDetails]
|
private var invitedMembers: [RoomMemberDetails]
|
||||||
|
|
||||||
|
let joinedMembersCount: Int
|
||||||
var canInviteUsers = false
|
var canInviteUsers = false
|
||||||
var bindings: RoomMembersListScreenViewStateBindings
|
var bindings: RoomMembersListScreenViewStateBindings
|
||||||
|
|
||||||
init(joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings = .init()) {
|
init(joinedMembersCount: Int,
|
||||||
|
joinedMembers: [RoomMemberDetails] = [],
|
||||||
|
invitedMembers: [RoomMemberDetails] = [],
|
||||||
|
bindings: RoomMembersListScreenViewStateBindings = .init()) {
|
||||||
|
self.joinedMembersCount = joinedMembersCount
|
||||||
self.joinedMembers = joinedMembers
|
self.joinedMembers = joinedMembers
|
||||||
self.invitedMembers = invitedMembers
|
self.invitedMembers = invitedMembers
|
||||||
self.bindings = bindings
|
self.bindings = bindings
|
||||||
@@ -46,10 +52,6 @@ struct RoomMembersListScreenViewState: BindableState {
|
|||||||
member.matches(searchQuery: bindings.searchQuery)
|
member.matches(searchQuery: bindings.searchQuery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var joinedMembersCount: Int {
|
|
||||||
joinedMembers.count
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomMembersListScreenViewStateBindings {
|
struct RoomMembersListScreenViewStateBindings {
|
||||||
|
|||||||
+41
-14
@@ -19,17 +19,18 @@ import SwiftUI
|
|||||||
typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction>
|
typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction>
|
||||||
|
|
||||||
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
|
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
|
||||||
private let mediaProvider: MediaProviderProtocol
|
private let roomProxy: RoomProxyProtocol
|
||||||
private let members: [RoomMemberProxyProtocol]
|
private var members: [RoomMemberProxyProtocol] = []
|
||||||
|
@CancellableTask private var showLoaderTask: Task<Void, Never>?
|
||||||
|
|
||||||
var callback: ((RoomMembersListScreenViewModelAction) -> Void)?
|
var callback: ((RoomMembersListScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
init(mediaProvider: MediaProviderProtocol, members: [RoomMemberProxyProtocol]) {
|
init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||||
self.mediaProvider = mediaProvider
|
self.roomProxy = roomProxy
|
||||||
self.members = members
|
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount),
|
||||||
super.init(initialViewState: .init(), imageProvider: mediaProvider)
|
imageProvider: mediaProvider)
|
||||||
|
|
||||||
setupState(members: members)
|
setupMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -49,17 +50,32 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupState(members: [RoomMemberProxyProtocol]) {
|
private func setupMembers() {
|
||||||
Task {
|
Task {
|
||||||
let indicatorId = UUID().uuidString
|
showLoader()
|
||||||
defer {
|
await roomProxy.updateMembers()
|
||||||
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(indicatorId)
|
hideLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
roomProxy.membersPublisher
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.first()
|
||||||
|
.sink { [weak self] members in
|
||||||
|
self?.updateState(members: members)
|
||||||
}
|
}
|
||||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true))
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateState(members: [RoomMemberProxyProtocol]) {
|
||||||
|
Task {
|
||||||
|
showLoader()
|
||||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||||
self.state = .init(joinedMembers: roomMembersDetails.joinedMembers, invitedMembers: roomMembersDetails.invitedMembers)
|
self.members = members
|
||||||
|
self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount,
|
||||||
|
joinedMembers: roomMembersDetails.joinedMembers,
|
||||||
|
invitedMembers: roomMembersDetails.invitedMembers)
|
||||||
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
|
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
|
||||||
|
hideLoader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +105,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
}
|
}
|
||||||
.value
|
.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let userIndicatorID = UUID().uuidString
|
||||||
|
|
||||||
|
private func showLoader() {
|
||||||
|
showLoaderTask = ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideLoader() {
|
||||||
|
showLoaderTask = nil
|
||||||
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct RoomMembersDetails {
|
private struct RoomMembersDetails {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct RoomMembersListScreen: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading) {
|
LazyVStack(alignment: .leading) {
|
||||||
membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
|
membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
|
||||||
membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(context.viewState.joinedMembersCount))
|
membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
|
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
@@ -80,8 +80,8 @@ struct RoomMembersListScreen_Previews: PreviewProvider {
|
|||||||
.mockBob,
|
.mockBob,
|
||||||
.mockCharlie
|
.mockCharlie
|
||||||
]
|
]
|
||||||
return RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(),
|
return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
|
||||||
members: members)
|
mediaProvider: MockMediaProvider())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
+2
-2
@@ -53,8 +53,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider {
|
|||||||
.mockBob,
|
.mockBob,
|
||||||
.mockCharlie
|
.mockCharlie
|
||||||
]
|
]
|
||||||
let viewModel = RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(),
|
let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
|
||||||
members: members)
|
mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
return VStack {
|
return VStack {
|
||||||
ForEach(members, id: \.userID) { member in
|
ForEach(members, id: \.userID) { member in
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import SwiftUI
|
|||||||
struct StartChatScreenCoordinatorParameters {
|
struct StartChatScreenCoordinatorParameters {
|
||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
let navigationStackCoordinator: NavigationStackCoordinator
|
weak var navigationStackCoordinator: NavigationStackCoordinator?
|
||||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
selectedUsers.asCurrentValuePublisher()
|
selectedUsers.asCurrentValuePublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var navigationStackCoordinator: NavigationStackCoordinator {
|
private var navigationStackCoordinator: NavigationStackCoordinator? {
|
||||||
parameters.navigationStackCoordinator
|
parameters.navigationStackCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
navigationStackCoordinator?.push(coordinator) { [weak self] in
|
||||||
self?.createRoomParameters.send(.init())
|
self?.createRoomParameters.send(.init())
|
||||||
self?.selectedUsers.send([])
|
self?.selectedUsers.send([])
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator?.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@@ -152,7 +152,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
case .selectMediaAtURL(let url):
|
case .selectMediaAtURL(let url):
|
||||||
processAvatar(from: url)
|
processAvatar(from: url)
|
||||||
}
|
}
|
||||||
@@ -160,11 +160,11 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
||||||
|
|
||||||
navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
navigationStackCoordinator?.setSheetCoordinator(userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processAvatar(from url: URL) {
|
private func processAvatar(from url: URL) {
|
||||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
navigationStackCoordinator?.setSheetCoordinator(nil)
|
||||||
showLoadingIndicator()
|
showLoadingIndicator()
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -120,16 +120,16 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
return Asset.Images.encryptionTrusted.image
|
return Asset.Images.encryptionTrusted.image
|
||||||
}
|
}
|
||||||
|
|
||||||
var invitedMembersCount: UInt {
|
var invitedMembersCount: Int {
|
||||||
UInt(room.invitedMembersCount())
|
Int(room.invitedMembersCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
var joinedMembersCount: UInt {
|
var joinedMembersCount: Int {
|
||||||
UInt(room.joinedMembersCount())
|
Int(room.joinedMembersCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeMembersCount: UInt {
|
var activeMembersCount: Int {
|
||||||
UInt(room.activeMembersCount())
|
Int(room.activeMembersCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> {
|
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> {
|
||||||
@@ -380,11 +380,13 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
|
|
||||||
func updateMembers() async {
|
func updateMembers() async {
|
||||||
do {
|
do {
|
||||||
let roomMembers = try await Task.dispatch(on: .global()) {
|
let roomMembersProxies = try await Task.dispatch(on: .global()) {
|
||||||
try self.room.members()
|
try self.room.members().map {
|
||||||
|
RoomMemberProxy(member: $0, backgroundTaskService: self.backgroundTaskService)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
membersSubject.value = buildRoomMemberProxies(members: roomMembers)
|
membersSubject.value = roomMembersProxies
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -422,11 +424,6 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func buildRoomMemberProxies(members: [RoomMember]) -> [RoomMemberProxy] {
|
|
||||||
members.map { RoomMemberProxy(member: $0, backgroundTaskService: backgroundTaskService) }
|
|
||||||
}
|
|
||||||
|
|
||||||
func retryDecryption(for sessionID: String) async {
|
func retryDecryption(for sessionID: String) async {
|
||||||
await Task.dispatch(on: .global()) { [weak self] in
|
await Task.dispatch(on: .global()) { [weak self] in
|
||||||
self?.room.retryDecryption(sessionIds: [sessionID])
|
self?.room.retryDecryption(sessionIds: [sessionID])
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get }
|
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get }
|
||||||
|
|
||||||
var invitedMembersCount: UInt { get }
|
var invitedMembersCount: Int { get }
|
||||||
|
|
||||||
var joinedMembersCount: UInt { get }
|
var joinedMembersCount: Int { get }
|
||||||
|
|
||||||
var activeMembersCount: UInt { get }
|
var activeMembersCount: Int { get }
|
||||||
|
|
||||||
/// Publishes the room's updates.
|
/// Publishes the room's updates.
|
||||||
/// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()`
|
/// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()`
|
||||||
@@ -157,4 +157,8 @@ extension RoomProxyProtocol {
|
|||||||
var roomTitle: String {
|
var roomTitle: String {
|
||||||
displayName ?? name ?? "Unknown room 💥"
|
displayName ?? name ?? "Unknown room 💥"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isEncryptedOneToOneRoom: Bool {
|
||||||
|
isDirect && isEncrypted && joinedMembersCount == 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,8 +306,11 @@ class MockScreen: Identifiable {
|
|||||||
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||||
displayName: "Room",
|
displayName: "Room",
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
members: members))
|
members: members,
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
|
||||||
|
joinedMembersCount: members.count))
|
||||||
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
@@ -322,8 +325,11 @@ class MockScreen: Identifiable {
|
|||||||
avatarURL: URL.picturesDirectory,
|
avatarURL: URL.picturesDirectory,
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
canonicalAlias: "#mock:room.org",
|
canonicalAlias: "#mock:room.org",
|
||||||
members: members))
|
members: members,
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
|
||||||
|
joinedMembersCount: members.count))
|
||||||
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
@@ -331,7 +337,8 @@ class MockScreen: Identifiable {
|
|||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomDetailsScreenWithEmptyTopic:
|
case .roomDetailsScreenWithEmptyTopic:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let members: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockCharlie]
|
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic], canInviteUsers: false)
|
||||||
|
let members: [RoomMemberProxyMock] = [owner, .mockBob, .mockCharlie]
|
||||||
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||||
displayName: "Room",
|
displayName: "Room",
|
||||||
topic: nil,
|
topic: nil,
|
||||||
@@ -339,8 +346,11 @@ class MockScreen: Identifiable {
|
|||||||
isDirect: false,
|
isDirect: false,
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
canonicalAlias: "#mock:room.org",
|
canonicalAlias: "#mock:room.org",
|
||||||
members: members))
|
members: members,
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
memberForID: owner,
|
||||||
|
joinedMembersCount: members.count))
|
||||||
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
@@ -348,12 +358,34 @@ class MockScreen: Identifiable {
|
|||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomDetailsScreenWithInvite:
|
case .roomDetailsScreenWithInvite:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let members: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockCharlie]
|
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [], canInviteUsers: true)
|
||||||
|
let members: [RoomMemberProxyMock] = [owner, .mockBob, .mockCharlie]
|
||||||
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||||
displayName: "Room",
|
displayName: "Room",
|
||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
members: members))
|
members: members,
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
memberForID: owner,
|
||||||
|
joinedMembersCount: members.count))
|
||||||
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
|
roomProxy: roomProxy,
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
return navigationStackCoordinator
|
||||||
|
case .roomDetailsScreenDmDetails:
|
||||||
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
|
let members: [RoomMemberProxyMock] = [.mockMe, .mockDan]
|
||||||
|
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
||||||
|
displayName: "Room",
|
||||||
|
topic: "test",
|
||||||
|
isDirect: true,
|
||||||
|
isEncrypted: true,
|
||||||
|
members: members,
|
||||||
|
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
|
||||||
|
joinedMembersCount: members.count))
|
||||||
|
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
|
||||||
|
navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: roomProxy,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
userDiscoveryService: UserDiscoveryServiceMock()))
|
||||||
@@ -379,7 +411,7 @@ class MockScreen: Identifiable {
|
|||||||
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
members: members))
|
roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members))))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomMembersListScreenPendingInvites:
|
case .roomMembersListScreenPendingInvites:
|
||||||
@@ -387,7 +419,7 @@ class MockScreen: Identifiable {
|
|||||||
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
|
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
members: members))
|
roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members))))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .reportContent:
|
case .reportContent:
|
||||||
@@ -433,21 +465,6 @@ class MockScreen: Identifiable {
|
|||||||
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider()))
|
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider()))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomDetailsScreenDmDetails:
|
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
|
||||||
let members: [RoomMemberProxyMock] = [.mockMe, .mockDan]
|
|
||||||
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
|
|
||||||
displayName: "Room",
|
|
||||||
topic: "test",
|
|
||||||
isDirect: true,
|
|
||||||
isEncrypted: true,
|
|
||||||
members: members))
|
|
||||||
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
|
||||||
roomProxy: roomProxy,
|
|
||||||
mediaProvider: MockMediaProvider(),
|
|
||||||
userDiscoveryService: UserDiscoveryServiceMock()))
|
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
|
||||||
return navigationStackCoordinator
|
|
||||||
case .invitesWithBadges:
|
case .invitesWithBadges:
|
||||||
ServiceLocator.shared.settings.seenInvites = Set([RoomSummary].mockInvites.dropFirst(1).compactMap(\.id))
|
ServiceLocator.shared.settings.seenInvites = Set([RoomSummary].mockInvites.dropFirst(1).compactMap(\.id))
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
@@ -487,7 +504,8 @@ class MockScreen: Identifiable {
|
|||||||
let mediaProvider = MockMediaProvider()
|
let mediaProvider = MockMediaProvider()
|
||||||
let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
|
let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
|
||||||
let members: [RoomMemberProxyMock] = id == .inviteUsersInRoomExistingMembers ? [.mockInvitedAlice, .mockBob] : []
|
let members: [RoomMemberProxyMock] = id == .inviteUsersInRoomExistingMembers ? [.mockInvitedAlice, .mockBob] : []
|
||||||
let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(members: members, userIndicatorController: UserIndicatorControllerMock.default)
|
let roomProxy = RoomProxyMock(with: .init(displayName: "test", members: members))
|
||||||
|
let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(roomProxy: roomProxy)
|
||||||
let coordinator = InviteUsersScreenCoordinator(parameters: .init(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: mediaProvider, userDiscoveryService: userDiscoveryMock))
|
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 {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
func testInviteButton() async {
|
func testInviteButton() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||||
setupWithRoomType(roomType: .room(members: mockedMembers, userIndicatorController: UserIndicatorControllerMock.default))
|
setupWithRoomType(roomType: .room(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: mockedMembers))))
|
||||||
_ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false })
|
_ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false })
|
||||||
context.send(viewAction: .toggleUser(.mockAlice))
|
context.send(viewAction: .toggleUser(.mockAlice))
|
||||||
|
|
||||||
@@ -86,7 +86,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
|||||||
userDiscoveryService.fetchSuggestionsReturnValue = .success([])
|
userDiscoveryService.fetchSuggestionsReturnValue = .success([])
|
||||||
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
||||||
usersSubject.send([])
|
usersSubject.send([])
|
||||||
let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService)
|
let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(),
|
||||||
|
roomType: roomType, mediaProvider: MockMediaProvider(),
|
||||||
|
userDiscoveryService: userDiscoveryService,
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class RoomDetailsScreenViewModelTests: XCTestCase {
|
class RoomDetailsScreenViewModelTests: XCTestCase {
|
||||||
var viewModel: RoomDetailsScreenViewModelProtocol!
|
var viewModel: RoomDetailsScreenViewModel!
|
||||||
var roomProxyMock: RoomProxyMock!
|
var roomProxyMock: RoomProxyMock!
|
||||||
var context: RoomDetailsScreenViewModelType.Context { viewModel.context }
|
var context: RoomDetailsScreenViewModelType.Context { viewModel.context }
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test"))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
AppSettings.reset()
|
AppSettings.reset()
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
func testLeaveRoomTappedWhenPublic() async {
|
func testLeaveRoomTappedWhenPublic() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
context.send(viewAction: .processTapLeave)
|
context.send(viewAction: .processTapLeave)
|
||||||
XCTAssertEqual(context.leaveRoomAlertItem?.state, .public)
|
XCTAssertEqual(context.leaveRoomAlertItem?.state, .public)
|
||||||
@@ -45,7 +45,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
func testLeaveRoomTappedWhenRoomNotPublic() async {
|
func testLeaveRoomTappedWhenRoomNotPublic() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: mockedMembers))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
context.send(viewAction: .processTapLeave)
|
context.send(viewAction: .processTapLeave)
|
||||||
XCTAssertEqual(context.leaveRoomAlertItem?.state, .private)
|
XCTAssertEqual(context.leaveRoomAlertItem?.state, .private)
|
||||||
@@ -88,8 +88,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
func testInitialDMDetailsState() async {
|
func testInitialDMDetailsState() async {
|
||||||
let recipient = RoomMemberProxyMock.mockDan
|
let recipient = RoomMemberProxyMock.mockDan
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
||||||
}
|
}
|
||||||
@@ -101,8 +101,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
return .failure(.ignoreUserFailed)
|
return .failure(.ignoreUserFailed)
|
||||||
}
|
}
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
@@ -168,8 +168,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
return .failure(.unignoreUserFailed)
|
return .failure(.unignoreUserFailed)
|
||||||
}
|
}
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
await context.nextViewState()
|
await context.nextViewState()
|
||||||
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
@@ -186,28 +186,32 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
func testCannotInvitePeople() async {
|
func testCannotInvitePeople() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test",
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
isPublic: true,
|
||||||
|
members: mockedMembers,
|
||||||
|
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
|
||||||
|
joinedMembersCount: mockedMembers.count))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
await context.nextViewState()
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.canInviteUsers)
|
XCTAssertFalse(context.viewState.canInviteUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInvitePeople() async {
|
func testInvitePeople() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
await context.nextViewState()
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertTrue(context.viewState.canInviteUsers)
|
XCTAssertTrue(context.viewState.canInviteUsers)
|
||||||
|
|
||||||
var callbackCorrectlyCalled = false
|
var callbackCorrectlyCalled = false
|
||||||
viewModel.callback = { action in
|
viewModel.callback = { action in
|
||||||
switch action {
|
switch action {
|
||||||
case .requestInvitePeoplePresentation(let members):
|
case .requestInvitePeoplePresentation:
|
||||||
callbackCorrectlyCalled = members.map(\.userID) == mockedMembers.map(\.userID)
|
callbackCorrectlyCalled = true
|
||||||
default:
|
default:
|
||||||
callbackCorrectlyCalled = false
|
callbackCorrectlyCalled = false
|
||||||
}
|
}
|
||||||
@@ -219,16 +223,16 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testRoomSubscription() async {
|
func testRoomSubscription() async {
|
||||||
await context.nextViewState()
|
|
||||||
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
|
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCanEditAvatar() async {
|
func testCanEditAvatar() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar]), .mockBob, .mockAlice]
|
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomAvatar])
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
|
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertTrue(context.viewState.canEditRoomAvatar)
|
XCTAssertTrue(context.viewState.canEditRoomAvatar)
|
||||||
XCTAssertFalse(context.viewState.canEditRoomName)
|
XCTAssertFalse(context.viewState.canEditRoomName)
|
||||||
@@ -237,11 +241,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testCanEditName() async {
|
func testCanEditName() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomName]), .mockBob, .mockAlice]
|
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomName])
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
|
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
||||||
XCTAssertTrue(context.viewState.canEditRoomName)
|
XCTAssertTrue(context.viewState.canEditRoomName)
|
||||||
@@ -250,11 +255,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testCanEditTopic() async {
|
func testCanEditTopic() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockAlice]
|
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic])
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
|
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
||||||
XCTAssertFalse(context.viewState.canEditRoomName)
|
XCTAssertFalse(context.viewState.canEditRoomName)
|
||||||
@@ -263,11 +269,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testCannotEditRoom() async {
|
func testCannotEditRoom() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: []), .mockBob, .mockAlice]
|
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [])
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
|
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
XCTAssertFalse(context.viewState.canEditRoomAvatar)
|
||||||
XCTAssertFalse(context.viewState.canEditRoomName)
|
XCTAssertFalse(context.viewState.canEditRoomName)
|
||||||
@@ -278,9 +285,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
func testCannotEditDirectRoom() async {
|
func testCannotEditDirectRoom() async {
|
||||||
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice]
|
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice]
|
||||||
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers))
|
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers))
|
||||||
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
|
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.canEdit)
|
XCTAssertFalse(context.viewState.canEdit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setup(with members: [RoomMemberProxyMock]) {
|
private func setup(with members: [RoomMemberProxyMock]) {
|
||||||
viewModel = .init(mediaProvider: MockMediaProvider(), members: members)
|
viewModel = .init(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members, joinedMembersCount: members.filter { $0.membership == .join }.count)),
|
||||||
|
mediaProvider: MockMediaProvider())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user