mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Implement Knock Logic (#3573)
* WIP RequestToJoin struct * implemented the logic to display the cells * knock request banner accept flow * mark all knocks as seen implemented * details logic * implemented accept, decline and ban in the list * added a loader and modified the stacked view of the banner * pr suggestions * updated naming and loading strings * added the initial loading state improved code and the tests * updated a string that has changed * code improvement * tests for the room screen view model * room details tests and improved the knock requests tests for the room screen * knock requests list tests * added error state alerts with retry * struct has been renamed on the sdk so I renamed it also on the app side * update SDK
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
enum RoomScreenViewModelAction {
|
||||
enum RoomScreenViewModelAction: Equatable {
|
||||
case focusEvent(eventID: String)
|
||||
case displayPinnedEventsTimeline
|
||||
case displayRoomDetails
|
||||
@@ -23,7 +23,7 @@ enum RoomScreenViewAction {
|
||||
case displayRoomDetails
|
||||
case displayCall
|
||||
case footerViewAction(RoomScreenFooterViewAction)
|
||||
case acceptKnock(userID: String)
|
||||
case acceptKnock(eventID: String)
|
||||
case dismissKnockRequests
|
||||
case viewKnockRequests
|
||||
}
|
||||
@@ -48,11 +48,18 @@ struct RoomScreenViewState: BindableState {
|
||||
var canAcceptKnocks = false
|
||||
var canDeclineKnocks = false
|
||||
var canBan = false
|
||||
// TODO: We still don't know how to get these, but these will be the non already seen knock requests of the room, for now we are using this as a mock for testing purposes
|
||||
var unseenKnockRequests: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Helloooo")]
|
||||
var unseenKnockRequests: [KnockRequestInfo] = []
|
||||
var handledEventIDs: Set<String> = []
|
||||
|
||||
var displayedKnockRequests: [KnockRequestInfo] {
|
||||
unseenKnockRequests.filter { !handledEventIDs.contains($0.eventID) }
|
||||
}
|
||||
|
||||
var shouldSeeKnockRequests: Bool {
|
||||
isKnockingEnabled && isKnockableRoom && !unseenKnockRequests.isEmpty && (canAcceptKnocks || canDeclineKnocks || canBan)
|
||||
isKnockingEnabled &&
|
||||
isKnockableRoom &&
|
||||
!displayedKnockRequests.isEmpty &&
|
||||
(canAcceptKnocks || canDeclineKnocks || canBan)
|
||||
}
|
||||
|
||||
var footerDetails: RoomScreenFooterViewDetails?
|
||||
|
||||
@@ -103,12 +103,10 @@ 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 .acceptKnock(let eventID):
|
||||
Task { await acceptKnock(eventID: eventID) }
|
||||
case .dismissKnockRequests:
|
||||
// TODO: API to mark knocks as seen required
|
||||
break
|
||||
Task { await markAllKnocksAsSeen() }
|
||||
case .viewKnockRequests:
|
||||
actionsSubject.send(.displayKnockRequests)
|
||||
}
|
||||
@@ -181,6 +179,23 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.shouldShowCallButton = ongoingCallRoomID != roomProxy.id
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomProxy.knockRequestsStatePublisher
|
||||
// We only care about unseen requests
|
||||
.map { knockRequestsState in
|
||||
guard case let .loaded(requests) = knockRequestsState else {
|
||||
return []
|
||||
}
|
||||
|
||||
return requests
|
||||
.filter { !$0.isSeen }
|
||||
.map(KnockRequestInfo.init)
|
||||
}
|
||||
// If the requests have the same event ids we can discard the output
|
||||
.removeDuplicates { Set($0.map(\.eventID)) == Set($1.map(\.eventID)) }
|
||||
.throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true)
|
||||
.weakAssign(to: \.state.unseenKnockRequests, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async {
|
||||
@@ -277,10 +292,49 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func acceptKnock(eventID: String) async {
|
||||
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value,
|
||||
let request = requests.first(where: { $0.eventID == eventID }) else {
|
||||
return
|
||||
}
|
||||
|
||||
state.handledEventIDs.insert(eventID)
|
||||
switch await request.accept() {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
userIndicatorController.submitIndicator(.init(id: Self.errorIndicatorIdentifier, type: .toast, title: L10n.errorUnknown))
|
||||
state.handledEventIDs.remove(eventID)
|
||||
}
|
||||
}
|
||||
|
||||
private func markAllKnocksAsSeen() async {
|
||||
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value else {
|
||||
return
|
||||
}
|
||||
state.handledEventIDs.formUnion(Set(requests.map(\.eventID)))
|
||||
|
||||
let failedIDs = await withTaskGroup(of: (String, Result<Void, KnockRequestProxyError>).self) { group in
|
||||
for request in requests {
|
||||
group.addTask {
|
||||
await (request.eventID, request.markAsSeen())
|
||||
}
|
||||
}
|
||||
|
||||
var failedIDs = [String]()
|
||||
for await result in group where result.1.isFailure {
|
||||
failedIDs.append(result.0)
|
||||
}
|
||||
return failedIDs
|
||||
}
|
||||
state.handledEventIDs.subtract(failedIDs)
|
||||
}
|
||||
|
||||
// MARK: Loading indicators
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"
|
||||
private static let errorIndicatorIdentifier = "\(RoomScreenViewModel.self)-Error"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier, type: .toast, title: L10n.commonLoading))
|
||||
@@ -304,3 +358,13 @@ extension RoomScreenViewModel {
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}
|
||||
}
|
||||
|
||||
private extension KnockRequestInfo {
|
||||
init(from proxy: KnockRequestProxyProtocol) {
|
||||
self.init(displayName: proxy.displayName,
|
||||
avatarURL: proxy.avatarURL,
|
||||
userID: proxy.userID,
|
||||
reason: proxy.reason,
|
||||
eventID: proxy.eventID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct KnockRequestInfo {
|
||||
struct KnockRequestInfo: Equatable {
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
let userID: String
|
||||
let reason: String?
|
||||
let eventID: String
|
||||
}
|
||||
|
||||
struct KnockRequestsBannerView: View {
|
||||
@@ -102,10 +103,8 @@ private struct SingleKnockRequestBannerContent: View {
|
||||
Button(L10n.screenRoomSingleKnockRequestViewButtonTitle, action: onViewAll)
|
||||
.buttonStyle(.compound(.secondary, size: .medium))
|
||||
if let onAccept {
|
||||
Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle) {
|
||||
onAccept(request.userID)
|
||||
}
|
||||
.buttonStyle(.compound(.primary, size: .medium))
|
||||
Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle) { onAccept(request.eventID) }
|
||||
.buttonStyle(.compound(.primary, size: .medium))
|
||||
}
|
||||
}
|
||||
.padding(.top, request.reason == nil ? 0 : 2)
|
||||
@@ -123,7 +122,6 @@ private struct MultipleKnockRequestsBannerContent: View {
|
||||
requests
|
||||
.prefix(3)
|
||||
.map { .init(url: $0.avatarURL, name: $0.displayName, contentID: $0.userID) }
|
||||
.reversed()
|
||||
}
|
||||
|
||||
private var multipleKnockRequestsTitle: String {
|
||||
@@ -138,7 +136,7 @@ private struct MultipleKnockRequestsBannerContent: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 14) {
|
||||
HStack(spacing: 10) {
|
||||
StackedAvatarsView(overlap: 16, lineWidth: 2, shouldStackFromLast: true, avatars: avatars, avatarSize: .user(on: .knockingUsersBannerStack), mediaProvider: mediaProvider)
|
||||
StackedAvatarsView(overlap: 16, lineWidth: 2, avatars: avatars, avatarSize: .user(on: .knockingUsersBannerStack), mediaProvider: mediaProvider)
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Text(multipleKnockRequestsTitle)
|
||||
.lineLimit(2)
|
||||
@@ -173,18 +171,22 @@ private struct KnockRequestsBannerDismissButton: View {
|
||||
}
|
||||
|
||||
struct KnockRequestsBannerView_Previews: PreviewProvider, TestablePreview {
|
||||
static let singleRequest: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil)]
|
||||
static let singleRequest: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil, eventID: "1")]
|
||||
|
||||
static let singleRequestWithReason: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hey, I’d like to join this room because of xyz topic and I’d like to participate in the room.")]
|
||||
static let singleRequestWithReason: [KnockRequestInfo] = [.init(displayName: "Alice",
|
||||
avatarURL: nil,
|
||||
userID: "@alice:matrix.org",
|
||||
reason: "Hey, I’d like to join this room because of xyz topic and I’d like to participate in the room.",
|
||||
eventID: "1")]
|
||||
|
||||
static let singleRequestNoDisplayName: [KnockRequestInfo] = [.init(displayName: nil, avatarURL: nil, userID: "@alice:matrix.org", reason: nil)]
|
||||
static let singleRequestNoDisplayName: [KnockRequestInfo] = [.init(displayName: nil, avatarURL: nil, userID: "@alice:matrix.org", reason: nil, eventID: "1")]
|
||||
|
||||
static let multipleRequests: [KnockRequestInfo] = [
|
||||
.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil),
|
||||
.init(displayName: "Bob", avatarURL: nil, userID: "@bob:matrix.org", reason: nil),
|
||||
.init(displayName: "Charlie", avatarURL: nil, userID: "@charlie:matrix.org", reason: nil),
|
||||
.init(displayName: "Dan", avatarURL: nil, userID: "@dan:matrix.org", reason: nil),
|
||||
.init(displayName: "Test", avatarURL: nil, userID: "@dan:matrix.org", reason: nil)
|
||||
.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil, eventID: "1"),
|
||||
.init(displayName: "Bob", avatarURL: nil, userID: "@bob:matrix.org", reason: nil, eventID: "2"),
|
||||
.init(displayName: "Charlie", avatarURL: nil, userID: "@charlie:matrix.org", reason: nil, eventID: "3"),
|
||||
.init(displayName: "Dan", avatarURL: nil, userID: "@dan:matrix.org", reason: nil, eventID: "4"),
|
||||
.init(displayName: "Test", avatarURL: nil, userID: "@dan:matrix.org", reason: nil, eventID: "5")
|
||||
]
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -137,7 +137,7 @@ struct RoomScreen: View {
|
||||
private var knockRequestsBanner: some View {
|
||||
Group {
|
||||
if roomContext.viewState.shouldSeeKnockRequests {
|
||||
KnockRequestsBannerView(requests: roomContext.viewState.unseenKnockRequests,
|
||||
KnockRequestsBannerView(requests: roomContext.viewState.displayedKnockRequests,
|
||||
onDismiss: dismissKnockRequestsBanner,
|
||||
onAccept: roomContext.viewState.canAcceptKnocks ? acceptKnockRequest : nil,
|
||||
onViewAll: onViewAllKnockRequests,
|
||||
@@ -153,8 +153,8 @@ struct RoomScreen: View {
|
||||
roomContext.send(viewAction: .dismissKnockRequests)
|
||||
}
|
||||
|
||||
private func acceptKnockRequest(userID: String) {
|
||||
roomContext.send(viewAction: .acceptKnock(userID: userID))
|
||||
private func acceptKnockRequest(eventID: String) {
|
||||
roomContext.send(viewAction: .acceptKnock(eventID: eventID))
|
||||
}
|
||||
|
||||
private func onViewAllKnockRequests() {
|
||||
|
||||
Reference in New Issue
Block a user