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:
Alfonso Grillo
2023-04-19 17:05:24 +02:00
committed by GitHub
parent 616b8453b5
commit 9921b315b3
21 changed files with 249 additions and 27 deletions
@@ -65,6 +65,7 @@ class MockClientProxy: ClientProxyProtocol {
return RoomProxyMock(with: .init(displayName: "Empty room"))
case .filled(let details), .invalidated(let details):
let room = RoomProxyMock(with: .init(displayName: details.name))
room.acceptInvitationReturnValue = .success(())
room.inviterReturnValue = roomInviter
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
/// 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 failedRetrievingMembers
case failedLeavingRoom
case failedAcceptingInvite
case failedRejectingInvite
}
@MainActor
@@ -90,6 +92,10 @@ protocol RoomProxyProtocol {
func updateMembers() async
func inviter() async -> RoomMemberProxyProtocol?
func rejectInvitation() async -> Result<Void, RoomProxyError>
func acceptInvitation() async -> Result<Void, RoomProxyError>
}
extension RoomProxyProtocol {
@@ -129,7 +129,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
let details = RoomSummaryDetails(id: room.roomId(),
name: room.name() ?? room.roomId(),
isDirect: room.isDm() ?? false,
isDirect: fullRoom?.isDirect() ?? room.isDm() ?? false,
avatarURL: avatarURL,
lastMessage: attributedLastMessage,
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
@@ -121,6 +121,11 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
self.presentInvitesList(animated: animated)
case (.invitesScreen, .closedInvitesScreen, .roomList):
break
case (.invitesScreen, .selectRoom(let roomId), .invitesScreen(let selectedRoomId)) where roomId == selectedRoomId:
self.presentRoomWithIdentifier(roomId)
case (.invitesScreen, .deselectRoom, .invitesScreen):
break
default:
fatalError("Unknown transition: \(context)")
}
@@ -200,12 +205,18 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
detailNavigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self, roomIdentifier] in
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.
// 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.detailNavigationStackCoordinator.setRootCoordinator(nil)
default:
break
}
}
@@ -330,10 +341,15 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
let coordinator = InvitesCoordinator(parameters: parameters)
coordinator.actions
.sink { _ in }
.sink { [weak self] action in
switch action {
case .openRoom(let roomId):
self?.stateMachine.processEvent(.selectRoom(roomId: roomId))
}
}
.store(in: &cancellables)
navigationSplitCoordinator.setDetailCoordinator(coordinator, animated: animated) { [weak self] in
sidebarNavigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
self?.stateMachine.processEvent(.closedInvitesScreen)
}
}
@@ -23,7 +23,7 @@ class UserSessionFlowCoordinatorStateMachine {
/// The initial state, used before the coordinator starts
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?)
/// Showing the session verification flows
@@ -125,7 +125,11 @@ class UserSessionFlowCoordinatorStateMachine {
return .invitesScreen(selectedRoomId: selectedRoomId)
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
return .roomList(selectedRoomId: selectedRoomId)
case (.selectRoom(let roomId), .invitesScreen):
return .invitesScreen(selectedRoomId: roomId)
case (.deselectRoom, .invitesScreen):
return .invitesScreen(selectedRoomId: nil)
default:
return nil
}