Refresh the room members when a mention is started (#7438)

* Refresh the room members when a mention is started

The mention suggestions read room.membersStateFlow, which is a cache filled
only by an explicit updateMembers() call. Nothing calls it while a room is
open, so a member who joined after the room was opened never appears in the
suggestion list until the room is left and entered again.

Call updateMembers() when a mention search starts. The trigger is
distinctUntilChanged on "the current suggestion is a mention", so it fires
once per mention rather than on every keystroke, and fetchRoomMembers already
no-ops while a fetch is in flight.
This commit is contained in:
hayyaksi
2026-08-17 22:10:08 +03:00
committed by GitHub
parent 75607c73ec
commit cb6ac751cc
2 changed files with 65 additions and 2 deletions
@@ -86,6 +86,7 @@ import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType
import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@@ -103,7 +104,9 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -459,6 +462,14 @@ class MessageComposerPresenter(
val mentionTriggerFlow = merge(mentionStartTrigger, mentionCompletionTrigger) val mentionTriggerFlow = merge(mentionStartTrigger, mentionCompletionTrigger)
// Refresh the room members, which are only loaded on demand, when a mention starts
launch {
suggestionSearchTrigger
.distinctUntilChangedBy { it?.type }
.filter { it?.type == SuggestionType.Mention }
.collect { room.updateMembers() }
}
val roomAliasSuggestionsFlow = roomAliasSuggestionsDataSource val roomAliasSuggestionsFlow = roomAliasSuggestionsDataSource
.getAllRoomAliasSuggestions() .getAllRoomAliasSuggestions()
.stateIn(this, SharingStarted.Lazily, emptyList()) .stateIn(this, SharingStarted.Lazily, emptyList())
@@ -1056,7 +1056,8 @@ class MessageComposerPresenterTest : RobolectricTest() {
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(
roomPermissions = FakeRoomPermissions( roomPermissions = FakeRoomPermissions(
canTriggerRoomNotification = true, canTriggerRoomNotification = true,
) ),
updateMembersResult = {},
), ),
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
).apply { ).apply {
@@ -1103,6 +1104,55 @@ class MessageComposerPresenterTest : RobolectricTest() {
} }
} }
@Test
fun `present - mention suggestions refresh the room members`() = runTest {
val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN)
val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN)
val updateMembersResult = lambdaRecorder<Unit> { }
val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom(
roomPermissions = FakeRoomPermissions(
canTriggerRoomNotification = true,
),
updateMembersResult = updateMembersResult,
),
typingNoticeResult = { Result.success(Unit) }
).apply {
givenRoomMembersState(
RoomMembersState.Ready(
persistentListOf(currentUser, bob),
)
)
givenRoomInfo(aRoomInfo(isDirect = false))
}
val presenter = createPresenter(
room = room,
slashCommandService = FakeSlashCommandService(
getSuggestionsResult = { _, _ -> emptyList() },
),
)
presenter.test {
val initialState = awaitItem()
initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
skipItems(1)
assertThat(awaitItem().suggestions).containsExactly(ResolvedSuggestion.AtRoom, ResolvedSuggestion.Member(bob))
updateMembersResult.assertions().isCalledOnce()
// Typing after the `@` must not refresh the members again
initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 1, SuggestionType.Mention, "b")))
initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 2, SuggestionType.Mention, "bo")))
advanceUntilIdle()
updateMembersResult.assertions().isCalledOnce()
// Starting a new mention refreshes them again
initialState.eventSink(MessageComposerEvent.SuggestionReceived(null))
initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
advanceUntilIdle()
updateMembersResult.assertions().isCalledExactly(2)
cancelAndIgnoreRemainingEvents()
}
}
@Test @Test
fun `present - room mention suggestions no permission`() = runTest { fun `present - room mention suggestions no permission`() = runTest {
val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN)
@@ -1113,7 +1163,8 @@ class MessageComposerPresenterTest : RobolectricTest() {
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(
roomPermissions = FakeRoomPermissions( roomPermissions = FakeRoomPermissions(
canTriggerRoomNotification = false, canTriggerRoomNotification = false,
) ),
updateMembersResult = {},
), ),
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
).apply { ).apply {
@@ -1144,6 +1195,7 @@ class MessageComposerPresenterTest : RobolectricTest() {
val room = FakeJoinedRoom( val room = FakeJoinedRoom(
baseRoom = FakeBaseRoom( baseRoom = FakeBaseRoom(
roomPermissions = FakeRoomPermissions(canTriggerRoomNotification = true), roomPermissions = FakeRoomPermissions(canTriggerRoomNotification = true),
updateMembersResult = {},
), ),
typingNoticeResult = { Result.success(Unit) } typingNoticeResult = { Result.success(Unit) }
).apply { ).apply {