Knock Requests navigation flows (#3555)

* implemented navigations

* better naming for the enum

* removed list to banned users navigation

* polished the code

* avatarURL
This commit is contained in:
Mauro 2024-11-26 12:47:34 +01:00 committed by GitHub
parent 03aaf849ee
commit fe984a1301
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 118 additions and 48 deletions

View File

@ -370,7 +370,16 @@
"screen_join_room_knock_message_description" = "Message (optional)";
"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted.";
"screen_join_room_knock_sent_title" = "Request to join sent";
"screen_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all";
"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?";
"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests";
"screen_knock_requests_list_accept_all_button_title" = "Accept all";
"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban";
"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user wont be able to request access to join this room again.";
"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing";
"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline";
"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?";
"screen_knock_requests_list_decline_alert_title" = "Decline access";
"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban";
"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, youll be able to see their request here.";
"screen_knock_requests_list_empty_state_title" = "No pending request to join";

View File

@ -316,10 +316,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.roomMembersList, .dismissRoomMembersList):
return .roomDetails(isRoot: false)
case (.room, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, previousState: .room)
case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, previousState: .roomMembersList)
case (_, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, previousState: fromState)
case (.roomMemberDetails(_, let previousState), .dismissRoomMemberDetails):
return previousState
@ -328,12 +326,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.userProfile(_, let previousState), .dismissUserProfile):
return previousState
case (.roomDetails, .presentInviteUsersScreen):
return .inviteUsersScreen(fromRoomMembersList: false)
case (.roomMembersList, .presentInviteUsersScreen):
return .inviteUsersScreen(fromRoomMembersList: true)
case (.inviteUsersScreen(let fromRoomMembersList), .dismissInviteUsersScreen):
return fromRoomMembersList ? .roomMembersList : .roomDetails(isRoot: false)
case (_, .presentInviteUsersScreen):
return .inviteUsersScreen(previousState: fromState)
case (.inviteUsersScreen(let previousState), .dismissInviteUsersScreen):
return previousState
case (.room, .presentReportContent(let itemID, let senderID)):
return .reportContent(itemID: itemID, senderID: senderID)
@ -398,9 +394,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return .rolesAndPermissions
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen):
return .roomDetails(isRoot: false)
case (.roomDetails, .presentRoomMemberDetails(let userID)):
return .roomMemberDetails(userID: userID, previousState: fromState)
case (.room, .presentResolveSendFailure):
return .resolveSendFailure
@ -414,10 +407,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.presentingChild(_, let previousState), .dismissChildFlow):
return previousState
case (.roomDetails, .presentKnockRequestsListScreen):
return .knockRequestsList
case (.knockRequestsList, .dismissKnockRequestsListScreen):
return .roomDetails(isRoot: false)
case (_, .presentKnockRequestsListScreen):
return .knockRequestsList(previousState: fromState)
case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen):
return previousState
default:
return nil
@ -575,6 +568,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentKnockRequestsList()
case (.knockRequestsList, .dismissKnockRequestsListScreen, .roomDetails):
break
case (.room, .presentKnockRequestsListScreen, .knockRequestsList):
presentKnockRequestsList()
case (.knockRequestsList, .dismissKnockRequestsListScreen, .room):
break
// Child flow
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
@ -735,6 +732,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentPinnedEventsTimeline)
case .presentResolveSendFailure(failure: let failure, sendHandle: let sendHandle):
stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
case .presentKnockRequestsList:
stateMachine.tryEvent(.presentKnockRequestsListScreen)
}
}
.store(in: &cancellables)
@ -899,11 +898,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let parameters = KnockRequestsListScreenCoordinatorParameters(roomProxy: roomProxy, mediaProvider: userSession.mediaProvider)
let coordinator = KnockRequestsListScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher
.sink { [weak self] _ in
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissKnockRequestsListScreen)
}
@ -1559,7 +1553,7 @@ private extension RoomFlowCoordinator {
case roomMembersList
case roomMemberDetails(userID: String, previousState: State)
case userProfile(userID: String, previousState: State)
case inviteUsersScreen(fromRoomMembersList: Bool)
case inviteUsersScreen(previousState: State)
case mediaUploadPicker(source: MediaPickerScreenSource)
case mediaUploadPreview(fileURL: URL)
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
@ -1572,7 +1566,7 @@ private extension RoomFlowCoordinator {
case rolesAndPermissions
case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource)
case resolveSendFailure
case knockRequestsList
case knockRequestsList(previousState: State)
/// A child flow is in progress.
case presentingChild(childRoomID: String, previousState: State)

View File

@ -1298,8 +1298,30 @@ internal enum L10n {
}
/// Are you sure you want to turn off key storage and delete it?
internal static var screenKeyBackupDisableTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_title") }
/// Yes, accept all
internal static var screenKnockRequestsListAcceptAllAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_confirm_button_title") }
/// Are you sure you want to accept all requests to join?
internal static var screenKnockRequestsListAcceptAllAlertDescription: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_description") }
/// Accept all requests
internal static var screenKnockRequestsListAcceptAllAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_title") }
/// Accept all
internal static var screenKnockRequestsListAcceptAllButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_button_title") }
/// Yes, decline and ban
internal static var screenKnockRequestsListBanAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_confirm_button_title") }
/// Are you sure you want to decline and ban %1$@? This user wont be able to request access to join this room again.
internal static func screenKnockRequestsListBanAlertDescription(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_description", String(describing: p1))
}
/// Decline and ban from accessing
internal static var screenKnockRequestsListBanAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_title") }
/// Yes, decline
internal static var screenKnockRequestsListDeclineAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_confirm_button_title") }
/// Are you sure you want to decline %1$@ request to join this room?
internal static func screenKnockRequestsListDeclineAlertDescription(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_description", String(describing: p1))
}
/// Decline access
internal static var screenKnockRequestsListDeclineAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_title") }
/// Decline and ban
internal static var screenKnockRequestsListDeclineAndBanActionTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_and_ban_action_title") }
/// When somebody will ask to join the room, youll be able to see their request here.

View File

@ -32,12 +32,7 @@ final class KnockRequestsListScreenCoordinator: CoordinatorProtocol {
mediaProvider: parameters.mediaProvider)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
}
.store(in: &cancellables)
}
func start() { }
func toPresentable() -> AnyView {
AnyView(KnockRequestsListScreen(context: viewModel.context))

View File

@ -10,7 +10,8 @@ import Foundation
enum KnockRequestsListScreenViewModelAction { }
struct KnockRequestsListScreenViewState: BindableState {
var requests: [KnockRequestCellInfo] = []
// TODO: Not sure yet how we will fetch this, this is just for testing purposes
var requests: [KnockRequestCellInfo] = [.init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello")]
// If you are in this view one of these must have been true so by default we assume all of them to be true
var canAccept = true
var canDecline = true
@ -22,6 +23,18 @@ struct KnockRequestsListScreenViewState: BindableState {
var shouldDisplayRequests: Bool {
!requests.isEmpty && isKnockableRoom && (canAccept || canDecline || canBan)
}
var bindings = KnockRequestsListStateBindings()
}
struct KnockRequestsListStateBindings {
var alertInfo: AlertInfo<KnockRequestsListAlertType>?
}
enum KnockRequestsListAlertType {
case acceptAllRequests
case declineRequest
case declineAndBan
}
enum KnockRequestsListScreenViewAction {

View File

@ -35,13 +35,34 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
override func process(viewAction: KnockRequestsListScreenViewAction) {
switch viewAction {
case .acceptAllRequests:
break
state.bindings.alertInfo = .init(id: .acceptAllRequests,
title: L10n.screenKnockRequestsListAcceptAllAlertTitle,
message: L10n.screenKnockRequestsListAcceptAllAlertDescription,
primaryButton: .init(title: L10n.screenKnockRequestsListAcceptAllAlertConfirmButtonTitle,
// TODO: Implement action
action: nil),
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
case .acceptRequest(let userID):
// TODO: Implement
break
case .declineRequest(let userID):
break
state.bindings.alertInfo = .init(id: .declineRequest,
title: L10n.screenKnockRequestsListDeclineAlertTitle,
message: L10n.screenKnockRequestsListDeclineAlertDescription(userID),
primaryButton: .init(title: L10n.screenKnockRequestsListDeclineAlertConfirmButtonTitle,
role: .destructive,
// TODO: Implement action
action: nil),
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
case .ban(let userID):
break
state.bindings.alertInfo = .init(id: .declineAndBan,
title: L10n.screenKnockRequestsListBanAlertTitle,
message: L10n.screenKnockRequestsListBanAlertDescription(userID),
// TODO: Implement action
primaryButton: .init(title: L10n.screenKnockRequestsListBanAlertConfirmButtonTitle,
role: .destructive,
action: nil),
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
}
}

View File

@ -19,7 +19,7 @@ struct KnockRequestCellInfo: Identifiable {
/// user identifier of the usee that sent the request
let id: String
let displayName: String?
let avatarUrl: URL?
let avatarURL: URL?
let timestamp: String?
let reason: String?
}
@ -33,7 +33,7 @@ struct KnockRequestCell: View {
var body: some View {
HStack(alignment: .top, spacing: 16) {
LoadableAvatarImage(url: cellInfo.avatarUrl,
LoadableAvatarImage(url: cellInfo.avatarURL,
name: cellInfo.displayName,
contentID: cellInfo.id,
avatarSize: .user(on: .knockingUserList),
@ -168,13 +168,13 @@ private struct DisclosableText: View {
struct KnockRequestCell_Previews: PreviewProvider, TestablePreview {
// swiftlint:disable:next line_length
static let aliceWithLongReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarUrl: nil, timestamp: "20 Nov 2024", reason: "Hello would like to join this room, also this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason")
static let aliceWithLongReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello would like to join this room, also this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason")
static let aliceWithShortReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarUrl: nil, timestamp: "20 Nov 2024", reason: "Hello, I am Alice and would like to join this room, please")
static let aliceWithShortReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello, I am Alice and would like to join this room, please")
static let aliceWithNoReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarUrl: nil, timestamp: "20 Nov 2024", reason: nil)
static let aliceWithNoReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
static let aliceWithNoName = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: nil, avatarUrl: nil, timestamp: "20 Nov 2024", reason: nil)
static let aliceWithNoName = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: nil, avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
static var previews: some View {
KnockRequestCell(cellInfo: aliceWithLongReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in })

View File

@ -26,6 +26,7 @@ struct KnockRequestsListScreen: View {
acceptAllButton
}
}
.alert(item: $context.alertInfo)
}
@ViewBuilder
@ -75,13 +76,13 @@ struct KnockRequestsListScreen: View {
// MARK: - Previews
struct KnockRequestsListScreen_Previews: PreviewProvider, TestablePreview {
static let emptyViewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init())
static let emptyViewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: []))
static let viewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: [.init(id: "@alice:matrix.org", displayName: "Alice", avatarUrl: nil, timestamp: "Now", reason: "Hello"),
static let viewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: [.init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello"),
// swiftlint:disable:next line_length
.init(id: "@bob:matrix.org", displayName: "Bob", avatarUrl: nil, timestamp: "Now", reason: "Hello this one is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason"),
.init(id: "@charlie:matrix.org", displayName: "Charlie", avatarUrl: nil, timestamp: "Now", reason: nil),
.init(id: "@dan:matrix.org", displayName: "Dan", avatarUrl: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")]))
.init(id: "@bob:matrix.org", displayName: "Bob", avatarURL: nil, timestamp: "Now", reason: "Hello this one is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason"),
.init(id: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil, timestamp: "Now", reason: nil),
.init(id: "@dan:matrix.org", displayName: "Dan", avatarURL: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")]))
static var previews: some View {
NavigationStack {

View File

@ -42,6 +42,7 @@ enum RoomScreenCoordinatorAction {
case presentCallScreen
case presentPinnedEventsTimeline
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
case presentKnockRequestsList
}
final class RoomScreenCoordinator: CoordinatorProtocol {
@ -169,6 +170,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentCallScreen)
case .removeComposerFocus:
composerViewModel.process(timelineAction: .removeFocus)
case .displayKnockRequests:
actionsSubject.send(.presentKnockRequestsList)
}
}
.store(in: &cancellables)

View File

@ -14,6 +14,7 @@ enum RoomScreenViewModelAction {
case displayRoomDetails
case displayCall
case removeComposerFocus
case displayKnockRequests
}
enum RoomScreenViewAction {
@ -22,6 +23,9 @@ enum RoomScreenViewAction {
case displayRoomDetails
case displayCall
case footerViewAction(RoomScreenFooterViewAction)
case acceptKnock(userID: String)
case dismissKnockRequests
case viewKnockRequests
}
struct RoomScreenViewState: BindableState {

View File

@ -103,6 +103,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .resolvePinViolation(let userID):
Task { await resolveIdentityPinningViolation(userID) }
}
case .acceptKnock(userID: let userID):
// TODO: API to accept a knock required
break
case .dismissKnockRequests:
// TODO: API to mark knocks as seen required
break
case .viewKnockRequests:
actionsSubject.send(.displayKnockRequests)
}
}

View File

@ -148,15 +148,15 @@ struct RoomScreen: View {
}
private func dismissKnockRequestsBanner() {
// TODO: Implement
roomContext.send(viewAction: .dismissKnockRequests)
}
private func acceptKnockRequest(userID: String) {
// TODO: Implement
roomContext.send(viewAction: .acceptKnock(userID: userID))
}
private func onViewAllKnockRequests() {
// TODO: Implement
roomContext.send(viewAction: .viewKnockRequests)
}
private var scrollToBottomButton: some View {