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:
Mauro
2024-12-16 15:32:45 +01:00
committed by GitHub
parent 016cdc687a
commit 45a630dd85
54 changed files with 1700 additions and 150 deletions
@@ -1146,10 +1146,31 @@ private extension RoomPreviewDetails {
topic: roomPreviewInfo.topic,
avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)),
memberCount: UInt(roomPreviewInfo.numJoinedMembers),
isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable,
isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable ?? false,
isJoined: roomPreviewInfo.membership == .joined,
isInvited: roomPreviewInfo.membership == .invited,
isPublic: roomPreviewInfo.joinRule == .public,
canKnock: roomPreviewInfo.joinRule == .knock)
isPublic: roomPreviewInfo.isPublic,
canKnock: roomPreviewInfo.canKnock)
}
}
private extension RoomPreviewInfo {
var canKnock: Bool {
switch joinRule {
case .knock, .knockRestricted:
return true
default:
return false
}
}
var isPublic: Bool {
switch joinRule {
// for restricted rooms we want to show optimistically that the we may be able to join the room
case .public, .restricted:
return true
default:
return false
}
}
}
@@ -60,6 +60,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private var typingNotificationObservationToken: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var identityStatusChangesObservationToken: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var knockRequestsChangesObservationToken: TaskHandle?
private var subscribedForUpdates = false
@@ -83,6 +85,11 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
identityStatusChangesSubject.asCurrentValuePublisher()
}
private let knockRequestsStateSubject = CurrentValueSubject<KnockRequestsState, Never>(.loading)
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> {
knockRequestsStateSubject.asCurrentValuePublisher()
}
// A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI
lazy var id: String = room.id()
@@ -131,6 +138,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
}
subscribeToTypingNotifications()
await subscribeToKnockRequests()
}
func subscribeToRoomInfoUpdates() {
@@ -645,6 +654,19 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
identityStatusChangesSubject.send(changes)
})
}
private func subscribeToKnockRequests() async {
do {
knockRequestsChangesObservationToken = try await room.subscribeToKnockRequests(listener: RoomKnockRequestsListener { [weak self] requests in
guard let self else { return }
MXLog.info("Received requests to join update, requests id: \(requests.map(\.eventId))")
knockRequestsStateSubject.send(.loaded(requests.map(KnockRequestProxy.init)))
})
} catch {
MXLog.error("Failed observing requests to join with error: \(error)")
}
}
}
private final class RoomInfoUpdateListener: RoomInfoListener {
@@ -682,3 +704,15 @@ private final class RoomIdentityStatusChangeListener: IdentityStatusChangeListen
onUpdateClosure(identityStatusChange)
}
}
private final class RoomKnockRequestsListener: KnockRequestsListener {
private let onUpdateClosure: ([KnockRequest]) -> Void
init(_ onUpdateClosure: @escaping ([KnockRequest]) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func call(joinRequests: [KnockRequest]) {
onUpdateClosure(joinRequests)
}
}
@@ -0,0 +1,90 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
struct KnockRequestProxy: KnockRequestProxyProtocol {
private let knockRequest: KnockRequest
init(knockRequest: KnockRequest) {
self.knockRequest = knockRequest
}
var eventID: String {
knockRequest.eventId
}
var userID: String {
knockRequest.userId
}
var displayName: String? {
knockRequest.displayName
}
var avatarURL: URL? {
knockRequest.avatarUrl.flatMap(URL.init)
}
var reason: String? {
knockRequest.reason
}
var formattedTimestamp: String? {
guard let timestamp = knockRequest.timestamp else {
return nil
}
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)).formattedMinimal()
}
var isSeen: Bool {
knockRequest.isSeen
}
func accept() async -> Result<Void, KnockRequestProxyError> {
do {
try await knockRequest.actions.accept()
return .success(())
} catch {
MXLog.error("Failed accepting request with eventID: \(eventID) to join error: \(error)")
return .failure(.sdkError(error))
}
}
func decline() async -> Result<Void, KnockRequestProxyError> {
do {
// As of right now we don't provide reasons in the app for declining
try await knockRequest.actions.decline(reason: nil)
return .success(())
} catch {
MXLog.error("Failed declining request with eventID: \(eventID) to join error: \(error)")
return .failure(.sdkError(error))
}
}
func ban() async -> Result<Void, KnockRequestProxyError> {
do {
// As of right now we don't provide reasons in the app for declining and banning
try await knockRequest.actions.declineAndBan(reason: nil)
return .success(())
} catch {
MXLog.error("Failed declining and banning user for request with eventID: \(eventID) with error: \(error)")
return .failure(.sdkError(error))
}
}
func markAsSeen() async -> Result<Void, KnockRequestProxyError> {
do {
try await knockRequest.actions.markAsSeen()
return .success(())
} catch {
MXLog.error("Failed marking request with eventID: \(eventID) to join as seen error: \(error)")
return .failure(.sdkError(error))
}
}
}
@@ -0,0 +1,28 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
enum KnockRequestProxyError: Error {
case sdkError(Error)
}
// sourcery: AutoMockable
protocol KnockRequestProxyProtocol {
var eventID: String { get }
var userID: String { get }
var displayName: String? { get }
var avatarURL: URL? { get }
var reason: String? { get }
var formattedTimestamp: String? { get }
var isSeen: Bool { get }
func accept() async -> Result<Void, KnockRequestProxyError>
func decline() async -> Result<Void, KnockRequestProxyError>
func ban() async -> Result<Void, KnockRequestProxyError>
func markAsSeen() async -> Result<Void, KnockRequestProxyError>
}
@@ -48,6 +48,11 @@ enum JoinedRoomProxyAction: Equatable {
case roomInfoUpdate
}
enum KnockRequestsState {
case loading
case loaded([KnockRequestProxyProtocol])
}
// sourcery: AutoMockable
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var isEncrypted: Bool { get }
@@ -60,6 +65,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> { get }
var timeline: TimelineProxyProtocol { get }
var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
@@ -9,7 +9,7 @@ import Foundation
import MatrixRustSDK
struct RoomSummary {
enum JoinRequestType {
enum KnockRequestType {
case invite(inviter: RoomMemberProxyProtocol?)
case knock
@@ -34,7 +34,7 @@ struct RoomSummary {
let id: String
let joinRequestType: JoinRequestType?
let knockRequestType: KnockRequestType?
let name: String
let isDirect: Bool
@@ -103,7 +103,7 @@ extension RoomSummary {
canonicalAlias = nil
hasOngoingCall = false
joinRequestType = nil
knockRequestType = nil
isMarkedUnread = false
isFavourite = false
}
@@ -255,7 +255,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership {
let knockRequestType: RoomSummary.KnockRequestType? = switch roomInfo.membership {
case .invited: .invite(inviter: inviterProxy)
case .knocked: .knock
default: nil
@@ -263,7 +263,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
return RoomSummary(roomListItem: roomListItem,
id: roomInfo.id,
joinRequestType: joinRequestType,
knockRequestType: knockRequestType,
name: roomInfo.displayName ?? roomInfo.id,
isDirect: roomInfo.isDirect,
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
@@ -59,7 +59,7 @@ struct RoomStateEventStringBuilder {
case .knocked:
return memberIsYou ? L10n.stateEventRoomKnockByYou : L10n.stateEventRoomKnock(member)
case .knockAccepted:
return senderIsYou ? L10n.stateEventRoomKnockAcceptedByYou(senderDisplayName) : L10n.stateEventRoomKnockAccepted(senderDisplayName, member)
return senderIsYou ? L10n.stateEventRoomKnockAcceptedByYou(member) : L10n.stateEventRoomKnockAccepted(senderDisplayName, member)
case .knockRetracted:
return memberIsYou ? L10n.stateEventRoomKnockRetractedByYou : L10n.stateEventRoomKnockRetracted(member)
case .knockDenied:
@@ -153,7 +153,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
case .sentBeforeWeJoined:
encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .sentBeforeWeJoined)
errorLabel = L10n.commonUnableToDecryptNoAccess
case .historicalMessage:
case .historicalMessageAndBackupIsDisabled, .historicalMessageAndDeviceIsUnverified:
encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .historicalMessage)
errorLabel = L10n.timelineDecryptionFailureHistoricalEventNoKeyBackup
case .withheldForUnverifiedOrInsecureDevice: