mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Accept and decline invites (#806)
* Add accept/decline invite api * Fix isDirect logic * Add logic for accepting/decling invites * Add error handling structure * Add decline alert * Add navigation to joined room * Add generic error * Add changelog.d file * Cleanup * Fix failing UTs * Add UTs * Add UI tests * Cleanup * Update ref screenshots * Refactor invitesScreen state * Improve switch handling in UserSessionFlowCoordinator * Fix build error
This commit is contained in:
@@ -741,6 +741,40 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
return inviterReturnValue
|
return inviterReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - rejectInvitation
|
||||||
|
|
||||||
|
var rejectInvitationCallsCount = 0
|
||||||
|
var rejectInvitationCalled: Bool {
|
||||||
|
return rejectInvitationCallsCount > 0
|
||||||
|
}
|
||||||
|
var rejectInvitationReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var rejectInvitationClosure: (() async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func rejectInvitation() async -> Result<Void, RoomProxyError> {
|
||||||
|
rejectInvitationCallsCount += 1
|
||||||
|
if let rejectInvitationClosure = rejectInvitationClosure {
|
||||||
|
return await rejectInvitationClosure()
|
||||||
|
} else {
|
||||||
|
return rejectInvitationReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - acceptInvitation
|
||||||
|
|
||||||
|
var acceptInvitationCallsCount = 0
|
||||||
|
var acceptInvitationCalled: Bool {
|
||||||
|
return acceptInvitationCallsCount > 0
|
||||||
|
}
|
||||||
|
var acceptInvitationReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var acceptInvitationClosure: (() async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func acceptInvitation() async -> Result<Void, RoomProxyError> {
|
||||||
|
acceptInvitationCallsCount += 1
|
||||||
|
if let acceptInvitationClosure = acceptInvitationClosure {
|
||||||
|
return await acceptInvitationClosure()
|
||||||
|
} else {
|
||||||
|
return acceptInvitationReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
|
||||||
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
|
||||||
|
|||||||
@@ -118,5 +118,7 @@ struct A11yIdentifiers {
|
|||||||
|
|
||||||
struct InvitesScreen {
|
struct InvitesScreen {
|
||||||
let noInvites = "invites-no_invites"
|
let noInvites = "invites-no_invites"
|
||||||
|
let accept = "invites-accept"
|
||||||
|
let decline = "invites-decline"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
|
|
||||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
self.attributedStringBuilder = attributedStringBuilder
|
self.attributedStringBuilder = attributedStringBuilder
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ struct InvitesCoordinatorParameters {
|
|||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum InvitesCoordinatorAction { }
|
enum InvitesCoordinatorAction {
|
||||||
|
case openRoom(withIdentifier: String)
|
||||||
|
}
|
||||||
|
|
||||||
final class InvitesCoordinator: CoordinatorProtocol {
|
final class InvitesCoordinator: CoordinatorProtocol {
|
||||||
private let parameters: InvitesCoordinatorParameters
|
private let parameters: InvitesCoordinatorParameters
|
||||||
@@ -39,10 +41,15 @@ final class InvitesCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
viewModel.actions.sink { [weak self] _ in
|
viewModel.actions
|
||||||
guard let self else { return }
|
.sink { [weak self] action in
|
||||||
}
|
guard let self else { return }
|
||||||
.store(in: &cancellables)
|
switch action {
|
||||||
|
case .openRoom(let roomID):
|
||||||
|
self.actionsSubject.send(.openRoom(withIdentifier: roomID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
|
|||||||
@@ -14,10 +14,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
enum InvitesViewModelAction { }
|
enum InvitesViewModelAction {
|
||||||
|
case openRoom(withIdentifier: String)
|
||||||
|
}
|
||||||
|
|
||||||
struct InvitesViewState: BindableState {
|
struct InvitesViewState: BindableState {
|
||||||
var invites: [InvitesRoomDetails]?
|
var invites: [InvitesRoomDetails]?
|
||||||
|
var bindings: InvitesViewStateBindings = .init()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvitesViewStateBindings {
|
||||||
|
var alertInfo: AlertInfo<Bool>?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InvitesRoomDetails {
|
struct InvitesRoomDetails {
|
||||||
|
|||||||
@@ -37,17 +37,21 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
|
|||||||
|
|
||||||
override func process(viewAction: InvitesViewAction) {
|
override func process(viewAction: InvitesViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .accept:
|
case .accept(let invite):
|
||||||
break
|
accept(invite: invite)
|
||||||
case .decline:
|
case .decline(let invite):
|
||||||
break
|
startDeclineFlow(invite: invite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var clientProxy: ClientProxyProtocol {
|
||||||
|
userSession.clientProxy
|
||||||
|
}
|
||||||
|
|
||||||
private var invitesSummaryProvider: RoomSummaryProviderProtocol? {
|
private var invitesSummaryProvider: RoomSummaryProviderProtocol? {
|
||||||
userSession.clientProxy.invitesSummaryProvider
|
clientProxy.invitesSummaryProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupSubscriptions() {
|
private func setupSubscriptions() {
|
||||||
@@ -57,6 +61,7 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invitesSummaryProvider.roomListPublisher
|
invitesSummaryProvider.roomListPublisher
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] roomSummaries in
|
.sink { [weak self] roomSummaries in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
|
|||||||
|
|
||||||
private func fetchInviter(for roomID: String) {
|
private func fetchInviter(for roomID: String) {
|
||||||
Task {
|
Task {
|
||||||
guard let room: RoomProxyProtocol = await self.userSession.clientProxy.roomForIdentifier(roomID) else {
|
guard let room: RoomProxyProtocol = await self.clientProxy.roomForIdentifier(roomID) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +90,69 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
|
|||||||
state.invites?[inviteIndex].inviter = inviter
|
state.invites?[inviteIndex].inviter = inviter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startDeclineFlow(invite: InvitesRoomDetails) {
|
||||||
|
let roomPlaceholder = invite.isDirect ? (invite.inviter?.displayName ?? invite.roomDetails.name) : invite.roomDetails.name
|
||||||
|
let title = invite.isDirect ? L10n.screenInvitesDeclineDirectChatTitle : L10n.screenInvitesDeclineChatTitle
|
||||||
|
let message = invite.isDirect ? L10n.screenInvitesDeclineDirectChatMessage(roomPlaceholder) : L10n.screenInvitesDeclineChatMessage(roomPlaceholder)
|
||||||
|
|
||||||
|
state.bindings.alertInfo = .init(id: true,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
|
||||||
|
secondaryButton: .init(title: L10n.actionDecline, role: .destructive, action: { self.decline(invite: invite) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func accept(invite: InvitesRoomDetails) {
|
||||||
|
Task {
|
||||||
|
let roomID = invite.roomDetails.id
|
||||||
|
defer {
|
||||||
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: roomID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
|
|
||||||
|
guard let roomProxy = await clientProxy.roomForIdentifier(roomID) else {
|
||||||
|
displayError(.failedAcceptingInvite)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch await roomProxy.acceptInvitation() {
|
||||||
|
case .success:
|
||||||
|
actionsSubject.send(.openRoom(withIdentifier: roomID))
|
||||||
|
case .failure(let error):
|
||||||
|
displayError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decline(invite: InvitesRoomDetails) {
|
||||||
|
Task {
|
||||||
|
let roomID = invite.roomDetails.id
|
||||||
|
defer {
|
||||||
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: roomID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
|
|
||||||
|
guard let roomProxy = await clientProxy.roomForIdentifier(roomID) else {
|
||||||
|
displayError(.failedRejectingInvite)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await roomProxy.rejectInvitation()
|
||||||
|
|
||||||
|
if case .failure(let error) = result {
|
||||||
|
displayError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayError(_ error: RoomProxyError) {
|
||||||
|
state.bindings.alertInfo = .init(id: true,
|
||||||
|
title: L10n.commonError,
|
||||||
|
message: L10n.errorUnknown)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Array where Element == RoomSummary {
|
private extension Array where Element == RoomSummary {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ struct InvitesScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.actionInvitesList)
|
.navigationTitle(L10n.actionInvitesList)
|
||||||
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ struct InvitesScreenCell: View {
|
|||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button(L10n.actionDecline, action: declineAction)
|
Button(L10n.actionDecline, action: declineAction)
|
||||||
.buttonStyle(.elementCapsule)
|
.buttonStyle(.elementCapsule)
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.invitesScreen.decline)
|
||||||
|
|
||||||
Button(L10n.actionAccept, action: acceptAction)
|
Button(L10n.actionAccept, action: acceptAction)
|
||||||
.buttonStyle(.elementCapsuleProminent)
|
.buttonStyle(.elementCapsuleProminent)
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.invitesScreen.accept)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
return RoomProxyMock(with: .init(displayName: "Empty room"))
|
return RoomProxyMock(with: .init(displayName: "Empty room"))
|
||||||
case .filled(let details), .invalidated(let details):
|
case .filled(let details), .invalidated(let details):
|
||||||
let room = RoomProxyMock(with: .init(displayName: details.name))
|
let room = RoomProxyMock(with: .init(displayName: details.name))
|
||||||
|
room.acceptInvitationReturnValue = .success(())
|
||||||
room.inviterReturnValue = roomInviter
|
room.inviterReturnValue = roomInviter
|
||||||
return room
|
return room
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,6 +348,26 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rejectInvitation() async -> Result<Void, RoomProxyError> {
|
||||||
|
await Task.dispatch(on: .global()) {
|
||||||
|
do {
|
||||||
|
return try .success(self.room.rejectInvitation())
|
||||||
|
} catch {
|
||||||
|
return .failure(.failedRejectingInvite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptInvitation() async -> Result<Void, RoomProxyError> {
|
||||||
|
await Task.dispatch(on: .global()) {
|
||||||
|
do {
|
||||||
|
return try .success(self.room.acceptInvitation())
|
||||||
|
} catch {
|
||||||
|
return .failure(.failedAcceptingInvite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
/// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
/// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ enum RoomProxyError: Error {
|
|||||||
case failedAddingTimelineListener
|
case failedAddingTimelineListener
|
||||||
case failedRetrievingMembers
|
case failedRetrievingMembers
|
||||||
case failedLeavingRoom
|
case failedLeavingRoom
|
||||||
|
case failedAcceptingInvite
|
||||||
|
case failedRejectingInvite
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -90,6 +92,10 @@ protocol RoomProxyProtocol {
|
|||||||
func updateMembers() async
|
func updateMembers() async
|
||||||
|
|
||||||
func inviter() async -> RoomMemberProxyProtocol?
|
func inviter() async -> RoomMemberProxyProtocol?
|
||||||
|
|
||||||
|
func rejectInvitation() async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
|
func acceptInvitation() async -> Result<Void, RoomProxyError>
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomProxyProtocol {
|
extension RoomProxyProtocol {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
|
|
||||||
let details = RoomSummaryDetails(id: room.roomId(),
|
let details = RoomSummaryDetails(id: room.roomId(),
|
||||||
name: room.name() ?? room.roomId(),
|
name: room.name() ?? room.roomId(),
|
||||||
isDirect: room.isDm() ?? false,
|
isDirect: fullRoom?.isDirect() ?? room.isDm() ?? false,
|
||||||
avatarURL: avatarURL,
|
avatarURL: avatarURL,
|
||||||
lastMessage: attributedLastMessage,
|
lastMessage: attributedLastMessage,
|
||||||
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
|
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
self.presentInvitesList(animated: animated)
|
self.presentInvitesList(animated: animated)
|
||||||
case (.invitesScreen, .closedInvitesScreen, .roomList):
|
case (.invitesScreen, .closedInvitesScreen, .roomList):
|
||||||
break
|
break
|
||||||
|
case (.invitesScreen, .selectRoom(let roomId), .invitesScreen(let selectedRoomId)) where roomId == selectedRoomId:
|
||||||
|
self.presentRoomWithIdentifier(roomId)
|
||||||
|
case (.invitesScreen, .deselectRoom, .invitesScreen):
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
fatalError("Unknown transition: \(context)")
|
fatalError("Unknown transition: \(context)")
|
||||||
}
|
}
|
||||||
@@ -200,12 +205,18 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
detailNavigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self, roomIdentifier] in
|
detailNavigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self, roomIdentifier] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
// Move the state machine to no room selected if the room currently being dimissed
|
// Move the state machine to no room selected if the room currently being dismissed
|
||||||
// is the same as the one selected in the state machine.
|
// is the same as the one selected in the state machine.
|
||||||
// This generally happens when popping the room screen while in a compact layout
|
// This generally happens when popping the room screen while in a compact layout
|
||||||
if case let .roomList(selectedRoomId) = self.stateMachine.state, selectedRoomId == roomIdentifier {
|
switch self.stateMachine.state {
|
||||||
|
case
|
||||||
|
let .roomList(selectedRoomId) where selectedRoomId == roomIdentifier,
|
||||||
|
let .invitesScreen(selectedRoomId) where selectedRoomId == roomIdentifier:
|
||||||
|
|
||||||
self.stateMachine.processEvent(.deselectRoom)
|
self.stateMachine.processEvent(.deselectRoom)
|
||||||
self.detailNavigationStackCoordinator.setRootCoordinator(nil)
|
self.detailNavigationStackCoordinator.setRootCoordinator(nil)
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,10 +341,15 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
let coordinator = InvitesCoordinator(parameters: parameters)
|
let coordinator = InvitesCoordinator(parameters: parameters)
|
||||||
|
|
||||||
coordinator.actions
|
coordinator.actions
|
||||||
.sink { _ in }
|
.sink { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .openRoom(let roomId):
|
||||||
|
self?.stateMachine.processEvent(.selectRoom(roomId: roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
navigationSplitCoordinator.setDetailCoordinator(coordinator, animated: animated) { [weak self] in
|
sidebarNavigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||||
self?.stateMachine.processEvent(.closedInvitesScreen)
|
self?.stateMachine.processEvent(.closedInvitesScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
/// The initial state, used before the coordinator starts
|
/// The initial state, used before the coordinator starts
|
||||||
case initial
|
case initial
|
||||||
|
|
||||||
/// Showing the home screen
|
/// Showing the home screen. The `selectedRoomId` represents the timeline shown on the detail panel (if any)
|
||||||
case roomList(selectedRoomId: String?)
|
case roomList(selectedRoomId: String?)
|
||||||
|
|
||||||
/// Showing the session verification flows
|
/// Showing the session verification flows
|
||||||
@@ -125,7 +125,11 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
return .invitesScreen(selectedRoomId: selectedRoomId)
|
return .invitesScreen(selectedRoomId: selectedRoomId)
|
||||||
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
|
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
|
||||||
return .roomList(selectedRoomId: selectedRoomId)
|
return .roomList(selectedRoomId: selectedRoomId)
|
||||||
|
case (.selectRoom(let roomId), .invitesScreen):
|
||||||
|
return .invitesScreen(selectedRoomId: roomId)
|
||||||
|
case (.deselectRoom, .invitesScreen):
|
||||||
|
return .invitesScreen(selectedRoomId: nil)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,13 @@ class InvitesScreenUITests: XCTestCase {
|
|||||||
XCTAssertTrue(app.staticTexts[A11yIdentifiers.invitesScreen.noInvites].exists)
|
XCTAssertTrue(app.staticTexts[A11yIdentifiers.invitesScreen.noInvites].exists)
|
||||||
app.assertScreenshot(.invitesNoInvites)
|
app.assertScreenshot(.invitesNoInvites)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDeclineInvite() {
|
||||||
|
let app = Application.launch(.invites)
|
||||||
|
let declineButton = app.buttons[A11yIdentifiers.invitesScreen.decline].firstMatch
|
||||||
|
XCTAssert(declineButton.exists)
|
||||||
|
declineButton.tap()
|
||||||
|
XCTAssertEqual(app.alerts.count, 1)
|
||||||
|
app.assertScreenshot(.invites, step: 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:227793880d0425d98c0dbb1d8a5f6439432383d9a9506030b9f4838bf9c0ce4d
|
||||||
|
size 188175
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:da9cae78d8cf7592e20aedd014801c912e0feb90a1a9365b6ed99bea5b11bc1f
|
||||||
|
size 356615
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3221977873adc6942d96a283f8625a835277201db4897a72fd6942a8191ed221
|
||||||
|
size 233543
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0fe7a52291eb74567927f6da56a533f1dfc616a899d1174e65d36198ca65ca11
|
||||||
|
size 461856
|
||||||
@@ -32,24 +32,56 @@ class InvitesViewModelTests: XCTestCase {
|
|||||||
userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
func testEmptyState() throws {
|
func testEmptyState() async throws {
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
|
_ = await context.nextViewState()
|
||||||
let invites = try XCTUnwrap(context.viewState.invites)
|
let invites = try XCTUnwrap(context.viewState.invites)
|
||||||
XCTAssertTrue(invites.isEmpty)
|
XCTAssertTrue(invites.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testListState() throws {
|
func testListState() async throws {
|
||||||
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockInvites))
|
setupViewModel(roomSummaries: .mockInvites)
|
||||||
clientProxy.invitesSummaryProvider = summaryProvider
|
_ = await context.nextViewState()
|
||||||
clientProxy.visibleRoomsSummaryProvider = summaryProvider
|
|
||||||
setupViewModel()
|
|
||||||
let invites = try XCTUnwrap(context.viewState.invites)
|
let invites = try XCTUnwrap(context.viewState.invites)
|
||||||
XCTAssertEqual(invites.count, 2)
|
XCTAssertEqual(invites.count, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAcceptInvite() async throws {
|
||||||
|
let invites: [RoomSummary] = .mockInvites
|
||||||
|
guard case .filled(let details) = invites.first else {
|
||||||
|
XCTFail("No invite found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupViewModel(roomSummaries: invites)
|
||||||
|
context.send(viewAction: .accept(.init(roomDetails: details)))
|
||||||
|
let action: InvitesViewModelAction? = await viewModel.actions.values.first()
|
||||||
|
guard case .openRoom(let roomID) = action else {
|
||||||
|
XCTFail("Wrong view model action")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(details.id, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeclineInvite() async throws {
|
||||||
|
let invites: [RoomSummary] = .mockInvites
|
||||||
|
guard case .filled(let details) = invites.first else {
|
||||||
|
XCTFail("No invite found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupViewModel(roomSummaries: invites)
|
||||||
|
context.send(viewAction: .decline(.init(roomDetails: details)))
|
||||||
|
XCTAssertNotNil(context.alertInfo)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupViewModel() {
|
private func setupViewModel(roomSummaries: [RoomSummary]? = nil) {
|
||||||
|
if let roomSummaries {
|
||||||
|
let summaryProvider = MockRoomSummaryProvider(state: .loaded(roomSummaries))
|
||||||
|
clientProxy.invitesSummaryProvider = summaryProvider
|
||||||
|
clientProxy.visibleRoomsSummaryProvider = summaryProvider
|
||||||
|
}
|
||||||
|
|
||||||
viewModel = InvitesViewModel(userSession: userSession)
|
viewModel = InvitesViewModel(userSession: userSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Users can accept and decline invites.
|
||||||
Reference in New Issue
Block a user