mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user