Room mentioning in the composer (#3868)

* refactored the suggestion item structure

to scale with the room pill

* Implemented a way for the rooms

to appear in the suggestions view for the RTE, however I need to add the pills to the composer and the compatibility with the plain text composer

* small code correction

* fix

* fixed a bug where the suggestion wasn't returning

the right suggestion type and the suggestion text properly

* implementation done!

also updated some tests, but we need more of them

* updated toolbar view model tests

* updated tests

* updated preview tests

* renamed the Avatars case for the suggestions
This commit is contained in:
Mauro
2025-03-06 11:32:37 +01:00
committed by GitHub
parent 2d77a1b10b
commit 5f59e867b5
43 changed files with 564 additions and 180 deletions
@@ -21,8 +21,10 @@ final class CompletionSuggestionServiceTests: XCTestCase {
func testUserSuggestions() async throws {
let alice: RoomMemberProxyMock = .mockAlice
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == []
@@ -31,7 +33,7 @@ final class CompletionSuggestionServiceTests: XCTestCase {
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(), rawSuggestionText: "ali"))]
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: "ali")]
}
service.setSuggestionTrigger(.init(type: .user, text: "ali", range: .init()))
try await deferred.fulfill()
@@ -58,8 +60,10 @@ final class CompletionSuggestionServiceTests: XCTestCase {
func testUserSuggestionsIncludingAllUsers() async throws {
let alice: RoomMemberProxyMock = .mockAlice
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members, canUserTriggerRoomNotification: true))
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members, canUserTriggerRoomNotification: true))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == []
@@ -68,13 +72,13 @@ final class CompletionSuggestionServiceTests: XCTestCase {
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.allUsers(item: .allUsersMention(roomAvatar: nil, rawSuggestionText: "ro"))]
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "ro")]
}
service.setSuggestionTrigger(.init(type: .user, text: "ro", range: .init()))
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.allUsers(item: .allUsersMention(roomAvatar: nil, rawSuggestionText: "every"))]
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "every")]
}
service.setSuggestionTrigger(.init(type: .user, text: "every", range: .init()))
try await deferred.fulfill()
@@ -84,8 +88,10 @@ final class CompletionSuggestionServiceTests: XCTestCase {
let alice: RoomMemberProxyMock = .mockAlice
let bob: RoomMemberProxyMock = .mockBob
let members: [RoomMemberProxyMock] = [alice, bob, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members, canUserTriggerRoomNotification: true))
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members, canUserTriggerRoomNotification: true))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == []
@@ -94,34 +100,45 @@ final class CompletionSuggestionServiceTests: XCTestCase {
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.allUsers(item: .allUsersMention(roomAvatar: nil, rawSuggestionText: "")),
.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(), rawSuggestionText: "")),
.user(item: .init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL, range: .init(), rawSuggestionText: ""))]
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: ""),
.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(), rawSuggestionText: "")]
}
service.setSuggestionTrigger(.init(type: .user, text: "", range: .init()))
try await deferred.fulfill()
// Let's test the same with the processTextMessage method
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(location: 0, length: 1), rawSuggestionText: ""),
.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: ""),
.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: "")]
}
service.processTextMessage("@", selectedRange: .init(location: 0, length: 1))
try await deferred.fulfill()
}
func testUserSuggestionInDifferentMessagePositions() async throws {
let alice: RoomMemberProxyMock = .mockAlice
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestion in
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 0, length: 3), rawSuggestionText: "al"))]
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")]
}
service.processTextMessage("@al hello", selectedRange: .init(location: 0, length: 1))
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestion in
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 5, length: 3), rawSuggestionText: "al"))]
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")]
}
service.processTextMessage("test @al", selectedRange: .init(location: 5, length: 1))
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestion in
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 5, length: 3), rawSuggestionText: "al"))]
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")]
}
service.processTextMessage("test @al test", selectedRange: .init(location: 5, length: 1))
try await deferred.fulfill()
@@ -132,16 +149,18 @@ final class CompletionSuggestionServiceTests: XCTestCase {
let bob: RoomMemberProxyMock = .mockBob
let members: [RoomMemberProxyMock] = [alice, bob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 0, length: 3), rawSuggestionText: "al"))]
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")]
}
service.processTextMessage("@al test @bo", selectedRange: .init(location: 0, length: 1))
try await deffered.fulfill()
deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
suggestion == [.user(item: .init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL, range: .init(location: 9, length: 3), rawSuggestionText: "bo"))]
deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "bo")]
}
service.processTextMessage("@al test @bo", selectedRange: .init(location: 9, length: 1))
try await deffered.fulfill()
@@ -152,10 +171,205 @@ final class CompletionSuggestionServiceTests: XCTestCase {
service.processTextMessage("@al test @bo", selectedRange: .init(location: 4, length: 1))
try await deffered.fulfill()
}
}
private extension MentionSuggestionItem {
static func allUsersMention(roomAvatar: URL?, rawSuggestionText: String) -> Self {
MentionSuggestionItem(id: PillConstants.atRoom, displayName: PillConstants.everyone, avatarURL: roomAvatar, range: .init(), rawSuggestionText: rawSuggestionText)
func testRoomSuggestions() async throws {
let alice: RoomMemberProxyMock = .mockAlice
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == []
}
try await deferred.fulfill()
// The empty # should trigger suggestions from any room with an alias
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "2",
canonicalAlias: "#foundation-and-empire:matrix.org",
name: "Foundation and Empire",
avatar: .room(id: "2",
name: "Foundation and Empire",
avatarURL: .mockMXCAvatar))),
range: .init(),
rawSuggestionText: ""),
.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(),
rawSuggestionText: "")]
}
service.setSuggestionTrigger(.init(type: .room, text: "", range: .init()))
try await deferred.fulfill()
// Same but with the processTextMessage method
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "2",
canonicalAlias: "#foundation-and-empire:matrix.org",
name: "Foundation and Empire",
avatar: .room(id: "2",
name: "Foundation and Empire",
avatarURL: .mockMXCAvatar))),
range: .init(location: 0, length: 1),
rawSuggestionText: ""),
.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 0, length: 1),
rawSuggestionText: "")]
}
service.processTextMessage("#", selectedRange: .init(location: 0, length: 1))
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(),
rawSuggestionText: "prelude")]
}
service.setSuggestionTrigger(.init(type: .room, text: "prelude", range: .init()))
try await deferred.fulfill()
}
func testRoomSuggestionInDifferentMessagePositions() async throws {
let alice: RoomMemberProxyMock = .mockAlice
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 0, length: 3),
rawSuggestionText: "pr")]
}
service.processTextMessage("#pr hello", selectedRange: .init(location: 0, length: 1))
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 5, length: 3),
rawSuggestionText: "pr")]
}
service.processTextMessage("test #pr", selectedRange: .init(location: 5, length: 1))
try await deferred.fulfill()
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 5, length: 3),
rawSuggestionText: "pr")]
}
service.processTextMessage("test #pr test", selectedRange: .init(location: 5, length: 1))
try await deferred.fulfill()
}
func testRoomSuggestionWithMultipleMentionSymbol() async throws {
let alice: RoomMemberProxyMock = .mockAlice
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 0, length: 3),
rawSuggestionText: "pr")]
}
service.processTextMessage("#pr test #fo", selectedRange: .init(location: 0, length: 1))
try await deffered.fulfill()
deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "2",
canonicalAlias: "#foundation-and-empire:matrix.org",
name: "Foundation and Empire",
avatar: .room(id: "2",
name: "Foundation and Empire",
avatarURL: .mockMXCAvatar))),
range: .init(location: 9, length: 3),
rawSuggestionText: "fo"),
.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 9, length: 3),
rawSuggestionText: "fo")]
}
service.processTextMessage("#pr test #fo", selectedRange: .init(location: 9, length: 1))
try await deffered.fulfill()
deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
suggestion == []
}
service.processTextMessage("#pr test #fo", selectedRange: .init(location: 4, length: 1))
try await deffered.fulfill()
}
func testSuggestionsWithMultipleDifferentTriggers() async throws {
let alice: RoomMemberProxyMock = .mockAlice
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .room(.init(id: "6",
canonicalAlias: "#prelude-foundation:matrix.org",
name: "Prelude to Foundation",
avatar: .room(id: "6",
name: "Prelude to Foundation",
avatarURL: nil))),
range: .init(location: 0, length: 3),
rawSuggestionText: "pr")]
}
service.processTextMessage("#pr test @al", selectedRange: .init(location: 0, length: 1))
try await deffered.fulfill()
deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "al")]
}
service.processTextMessage("#pr test @al", selectedRange: .init(location: 9, length: 1))
try await deffered.fulfill()
}
}