mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
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:
+11
@@ -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())
|
||||||
|
|||||||
+54
-2
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user