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
@@ -31,7 +31,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
var callback: ((HomeScreenViewModelAction) -> Void)?
// swiftlint:disable:next function_body_length
// swiftlint:disable:next function_body_length cyclomatic_complexity
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
self.userSession = userSession
self.attributedStringBuilder = attributedStringBuilder
@@ -21,7 +21,9 @@ struct InvitesCoordinatorParameters {
let userSession: UserSessionProtocol
}
enum InvitesCoordinatorAction { }
enum InvitesCoordinatorAction {
case openRoom(withIdentifier: String)
}
final class InvitesCoordinator: CoordinatorProtocol {
private let parameters: InvitesCoordinatorParameters
@@ -39,10 +41,15 @@ final class InvitesCoordinator: CoordinatorProtocol {
}
func start() {
viewModel.actions.sink { [weak self] _ in
guard let self else { return }
}
.store(in: &cancellables)
viewModel.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .openRoom(let roomID):
self.actionsSubject.send(.openRoom(withIdentifier: roomID))
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
@@ -14,10 +14,17 @@
// limitations under the License.
//
enum InvitesViewModelAction { }
enum InvitesViewModelAction {
case openRoom(withIdentifier: String)
}
struct InvitesViewState: BindableState {
var invites: [InvitesRoomDetails]?
var bindings: InvitesViewStateBindings = .init()
}
struct InvitesViewStateBindings {
var alertInfo: AlertInfo<Bool>?
}
struct InvitesRoomDetails {
@@ -37,17 +37,21 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
override func process(viewAction: InvitesViewAction) {
switch viewAction {
case .accept:
break
case .decline:
break
case .accept(let invite):
accept(invite: invite)
case .decline(let invite):
startDeclineFlow(invite: invite)
}
}
// MARK: - Private
private var clientProxy: ClientProxyProtocol {
userSession.clientProxy
}
private var invitesSummaryProvider: RoomSummaryProviderProtocol? {
userSession.clientProxy.invitesSummaryProvider
clientProxy.invitesSummaryProvider
}
private func setupSubscriptions() {
@@ -57,6 +61,7 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
}
invitesSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] roomSummaries in
guard let self else { return }
@@ -72,7 +77,7 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
private func fetchInviter(for roomID: String) {
Task {
guard let room: RoomProxyProtocol = await self.userSession.clientProxy.roomForIdentifier(roomID) else {
guard let room: RoomProxyProtocol = await self.clientProxy.roomForIdentifier(roomID) else {
return
}
@@ -85,6 +90,69 @@ class InvitesViewModel: InvitesViewModelType, InvitesViewModelProtocol {
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 {
@@ -35,6 +35,7 @@ struct InvitesScreen: View {
}
}
.navigationTitle(L10n.actionInvitesList)
.alert(item: $context.alertInfo) { $0.alert }
}
// MARK: - Private
@@ -86,9 +86,11 @@ struct InvitesScreenCell: View {
HStack(spacing: 12) {
Button(L10n.actionDecline, action: declineAction)
.buttonStyle(.elementCapsule)
.accessibilityIdentifier(A11yIdentifiers.invitesScreen.decline)
Button(L10n.actionAccept, action: acceptAction)
.buttonStyle(.elementCapsuleProminent)
.accessibilityIdentifier(A11yIdentifiers.invitesScreen.accept)
}
}