mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-02 06:06:57 +02:00
Merge branch 'develop' into aringenbach/enable_rte_user_mentions
This commit is contained in:
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
struct QRLoginCode: Codable {
|
||||
let rendezvous: RendezvousDetails
|
||||
let flow: String?
|
||||
let intent: String
|
||||
}
|
||||
|
||||
@@ -42,7 +43,8 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
|
||||
var intent: Intent?
|
||||
var outcome: Outcome?
|
||||
|
||||
var reason: FailureReason?
|
||||
|
||||
// swiftformat:disable:next redundantBackticks
|
||||
var protocols: [`Protocol`]?
|
||||
|
||||
@@ -64,6 +66,7 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
case type
|
||||
case intent
|
||||
case outcome
|
||||
case reason
|
||||
case homeserver
|
||||
case user
|
||||
case protocols
|
||||
@@ -77,9 +80,18 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
}
|
||||
|
||||
enum `Type`: String, Codable {
|
||||
case loginStart = "m.login.start"
|
||||
case loginProgress = "m.login.progress"
|
||||
/**
|
||||
This is only used in MSC3906 v1 and will be removed
|
||||
*/
|
||||
case loginFinish = "m.login.finish"
|
||||
case loginFailure = "m.login.failure"
|
||||
case loginProtocol = "m.login.protocol"
|
||||
case loginProtocols = "m.login.protocols"
|
||||
case loginApproved = "m.login.approved"
|
||||
case loginDeclined = "m.login.declined"
|
||||
case loginSuccess = "m.login.success"
|
||||
case loginVerified = "m.login.verified"
|
||||
}
|
||||
|
||||
enum Intent: String, Codable {
|
||||
@@ -87,6 +99,9 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
case loginReciprocate = "login.reciprocate"
|
||||
}
|
||||
|
||||
/**
|
||||
This is only used in MSC306 v1 and will be removed
|
||||
*/
|
||||
enum Outcome: String, Codable {
|
||||
case success
|
||||
case declined
|
||||
@@ -97,4 +112,11 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
enum `Protocol`: String, Codable {
|
||||
case loginToken = "org.matrix.msc3906.login_token"
|
||||
}
|
||||
|
||||
enum FailureReason: String, Codable {
|
||||
case cancelled
|
||||
case unsupported
|
||||
case e2eeSecurityError = "e2ee_security_error"
|
||||
case incompatibleIntent = "incompatible_intent"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -180,6 +180,12 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
guard let flow = code.flow != nil ? RendezvousFlow(rawValue: code.flow!) : .SETUP_ADDITIONAL_DEVICE_V1 else {
|
||||
MXLog.error("[QRLoginService] Unsupported flow")
|
||||
state = .failed(error: .deviceNotSupported)
|
||||
return
|
||||
}
|
||||
|
||||
// so, this is of an expected algorithm so any bad data can be considered an invalid QR code
|
||||
guard code.intent == QRLoginRendezvousPayload.Intent.loginReciprocate.rawValue,
|
||||
let uri = code.rendezvous.transport?.uri,
|
||||
@@ -223,7 +229,10 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
}
|
||||
|
||||
MXLog.debug("[QRLoginService] Request login with `login_token`")
|
||||
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)),
|
||||
let protocolPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1
|
||||
? QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)
|
||||
: QRLoginRendezvousPayload(type: .loginProtocol, protocol: .loginToken)
|
||||
guard let requestData = try? JSONEncoder().encode(protocolPayload),
|
||||
case .success = await rendezvousService.send(data: requestData) else {
|
||||
MXLog.error("[QRLoginService] Failed sending continue with `login_token` request")
|
||||
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
||||
@@ -282,10 +291,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
}
|
||||
|
||||
MXLog.debug("[QRLoginService] Session created, sending device details")
|
||||
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress,
|
||||
outcome: .success,
|
||||
deviceId: session.myDeviceId,
|
||||
deviceKey: session.crypto.deviceEd25519Key)),
|
||||
let successPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1
|
||||
? QRLoginRendezvousPayload(type: .loginProgress, outcome: .success, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key)
|
||||
: QRLoginRendezvousPayload(type: .loginSuccess, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key)
|
||||
|
||||
guard let requestData = try? JSONEncoder().encode(successPayload),
|
||||
case .success = await rendezvousService.send(data: requestData) else {
|
||||
MXLog.error("[QRLoginService] Failed sending session details")
|
||||
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
||||
@@ -307,7 +317,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
MXLog.debug("[QRLoginService] Wait for cross-signing details")
|
||||
guard case let .success(data) = await rendezvousService.receive(),
|
||||
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data),
|
||||
responsePayload.outcome == .verified,
|
||||
flow == .SETUP_ADDITIONAL_DEVICE_V1 && responsePayload.outcome == .verified || responsePayload.type == .loginVerified,
|
||||
let verifiyingDeviceId = responsePayload.verifyingDeviceId,
|
||||
let verifyingDeviceKey = responsePayload.verifyingDeviceKey else {
|
||||
MXLog.error("[QRLoginService] Received invalid cross-signing details")
|
||||
|
||||
+5
-1
@@ -20,13 +20,16 @@ import SwiftUI
|
||||
|
||||
class MockQRLoginService: QRLoginServiceProtocol {
|
||||
private let mockCanDisplayQR: Bool
|
||||
private let mockFlow: String?
|
||||
|
||||
init(withState state: QRLoginServiceState = .initial,
|
||||
mode: QRLoginServiceMode = .notAuthenticated,
|
||||
canDisplayQR: Bool = true) {
|
||||
canDisplayQR: Bool = true,
|
||||
flow: String? = nil) {
|
||||
self.state = state
|
||||
self.mode = mode
|
||||
mockCanDisplayQR = canDisplayQR
|
||||
mockFlow = flow
|
||||
}
|
||||
|
||||
// MARK: - QRLoginServiceProtocol
|
||||
@@ -57,6 +60,7 @@ class MockQRLoginService: QRLoginServiceProtocol {
|
||||
uri: "https://matrix.org"),
|
||||
key: "some.public.key")
|
||||
return QRLoginCode(rendezvous: details,
|
||||
flow: mockFlow,
|
||||
intent: "login.start")
|
||||
}
|
||||
|
||||
|
||||
+28
-6
@@ -22,12 +22,14 @@ import WysiwygComposer
|
||||
|
||||
protocol UserSuggestionCoordinatorDelegate: AnyObject {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
|
||||
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?)
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat)
|
||||
}
|
||||
|
||||
struct UserSuggestionCoordinatorParameters {
|
||||
let mediaManager: MXMediaManager
|
||||
let room: MXRoom
|
||||
let userID: String
|
||||
}
|
||||
|
||||
/// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c.
|
||||
@@ -66,7 +68,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
init(parameters: UserSuggestionCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room)
|
||||
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID)
|
||||
userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider)
|
||||
|
||||
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
@@ -83,6 +85,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
switch result {
|
||||
case .selectedItemWithIdentifier(let identifier):
|
||||
if identifier == UserSuggestionID.room {
|
||||
self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger)
|
||||
return
|
||||
}
|
||||
|
||||
guard let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first else {
|
||||
return
|
||||
}
|
||||
@@ -148,29 +155,44 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
|
||||
private let room: MXRoom
|
||||
private let userID: String
|
||||
|
||||
var roomMembers: [MXRoomMember] = []
|
||||
var canMentionRoom = false
|
||||
|
||||
init(room: MXRoom) {
|
||||
init(room: MXRoom, userID: String) {
|
||||
self.room = room
|
||||
self.userID = userID
|
||||
updateWithPowerLevels()
|
||||
}
|
||||
|
||||
/// Gets the power levels for the room to update suggestions accordingly.
|
||||
func updateWithPowerLevels() {
|
||||
room.state { [weak self] state in
|
||||
guard let self, let powerLevels = state?.powerLevels else { return }
|
||||
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID)
|
||||
let mentionRoomPowerLevel = powerLevels.minimumPowerLevel(forNotifications: kMXRoomPowerLevelNotificationsRoomKey,
|
||||
defaultPower: kMXRoomPowerLevelNotificationsRoomDefault)
|
||||
self.canMentionRoom = userPowerLevel >= mentionRoomPowerLevel
|
||||
}
|
||||
}
|
||||
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
|
||||
room.members({ [weak self] roomMembers in
|
||||
room.members { [weak self] roomMembers in
|
||||
guard let self = self, let joinedMembers = roomMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
self.roomMembers = joinedMembers
|
||||
members(self.roomMembersToProviderMembers(joinedMembers))
|
||||
}, lazyLoadedMembers: { [weak self] lazyRoomMembers in
|
||||
} lazyLoadedMembers: { [weak self] lazyRoomMembers in
|
||||
guard let self = self, let joinedMembers = lazyRoomMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
self.roomMembers = joinedMembers
|
||||
members(self.roomMembersToProviderMembers(joinedMembers))
|
||||
}, failure: { error in
|
||||
} failure: { error in
|
||||
MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] {
|
||||
|
||||
+7
-2
@@ -19,6 +19,7 @@ import Foundation
|
||||
@objc
|
||||
protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject {
|
||||
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
|
||||
func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?)
|
||||
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat)
|
||||
}
|
||||
|
||||
@@ -31,8 +32,8 @@ final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
|
||||
weak var delegate: UserSuggestionCoordinatorBridgeDelegate?
|
||||
|
||||
init(mediaManager: MXMediaManager, room: MXRoom) {
|
||||
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room)
|
||||
init(mediaManager: MXMediaManager, room: MXRoom, userID: String) {
|
||||
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID)
|
||||
let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters)
|
||||
_userSuggestionCoordinator = userSuggestionCoordinator
|
||||
|
||||
@@ -62,6 +63,10 @@ extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) {
|
||||
delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger)
|
||||
}
|
||||
|
||||
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) {
|
||||
delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger)
|
||||
}
|
||||
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) {
|
||||
delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height)
|
||||
|
||||
@@ -24,7 +24,13 @@ struct RoomMembersProviderMember {
|
||||
var avatarUrl: String
|
||||
}
|
||||
|
||||
class UserSuggestionID: NSObject {
|
||||
/// A special case added for suggesting `@room` mentions.
|
||||
@objc static let room = "@room"
|
||||
}
|
||||
|
||||
protocol RoomMembersProviderProtocol {
|
||||
var canMentionRoom: Bool { get }
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void)
|
||||
}
|
||||
|
||||
@@ -111,7 +117,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
self.suggestionItems = members.map { member in
|
||||
self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in
|
||||
UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)
|
||||
}
|
||||
|
||||
@@ -124,3 +130,11 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == RoomMembersProviderMember {
|
||||
/// Returns the array with an additional member that represents an `@room` mention.
|
||||
func withRoom(_ canMentionRoom: Bool) -> Self {
|
||||
guard canMentionRoom else { return self }
|
||||
return self + [RoomMembersProviderMember(userId: UserSuggestionID.room, displayName: "Everyone", avatarUrl: "")]
|
||||
}
|
||||
}
|
||||
|
||||
+69
-45
@@ -20,87 +20,111 @@ import XCTest
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserSuggestionServiceTests: XCTestCase {
|
||||
var service: UserSuggestionService?
|
||||
var service: UserSuggestionService!
|
||||
var canMentionRoom = false
|
||||
|
||||
override func setUp() {
|
||||
service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false)
|
||||
canMentionRoom = false
|
||||
}
|
||||
|
||||
func testAlice() {
|
||||
service?.processTextMessage("@Al")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@Al")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@al")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@al")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@ice")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@ice")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@Alice")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@Alice")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@alice:matrix.org")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@alice:matrix.org")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
}
|
||||
|
||||
func testBob() {
|
||||
service?.processTextMessage("@ob")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
service.processTextMessage("@ob")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
|
||||
|
||||
service?.processTextMessage("@ob:")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
service.processTextMessage("@ob:")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
|
||||
|
||||
service?.processTextMessage("@b:matrix")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
service.processTextMessage("@b:matrix")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
|
||||
}
|
||||
|
||||
func testBoth() {
|
||||
service?.processTextMessage("@:matrix")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
assert(service?.items.value.last?.displayName == "Bob")
|
||||
service.processTextMessage("@:matrix")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
XCTAssertEqual(service.items.value.last?.displayName, "Bob")
|
||||
|
||||
service?.processTextMessage("@.org")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
assert(service?.items.value.last?.displayName == "Bob")
|
||||
service.processTextMessage("@.org")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
XCTAssertEqual(service.items.value.last?.displayName, "Bob")
|
||||
}
|
||||
|
||||
func testEmptyResult() {
|
||||
service?.processTextMessage("Lorem ipsum idolor")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("Lorem ipsum idolor")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("@")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("@@")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@@")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("alice@matrix.org")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("alice@matrix.org")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testStuff() {
|
||||
service?.processTextMessage("@@")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@@")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testWhitespaces() {
|
||||
service?.processTextMessage("")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage(" ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage(" ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("\n")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("\n")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage(" \n ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage(" \n ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("@A ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@A ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage(" @A ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage(" @A ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testRoomWithoutPower() {
|
||||
// Given a user without the power to mention a room.
|
||||
canMentionRoom = false
|
||||
|
||||
// Given a user without the power to mention a room.
|
||||
service.processTextMessage("@ro")
|
||||
|
||||
// Then the completion for a room mention should not be shown.
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testRoomWithPower() {
|
||||
// Given a user without the power to mention a room.
|
||||
canMentionRoom = true
|
||||
|
||||
// Given a user without the power to mention a room.
|
||||
service.processTextMessage("@ro")
|
||||
|
||||
// Then the completion for a room mention should be shown.
|
||||
XCTAssertEqual(service.items.value.first?.userId, UserSuggestionID.room)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
|
||||
}
|
||||
|
||||
extension MockUserSuggestionScreenState: RoomMembersProviderProtocol {
|
||||
var canMentionRoom: Bool { false }
|
||||
|
||||
func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) {
|
||||
if Self.members == nil {
|
||||
Self.members = generateUsersWithCount(10)
|
||||
|
||||
Reference in New Issue
Block a user