Merge branch 'develop' into aringenbach/enable_rte_user_mentions

This commit is contained in:
aringenbach
2023-04-11 14:21:31 +02:00
34 changed files with 520 additions and 164 deletions
@@ -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] {
@@ -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: "")]
}
}
@@ -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)