diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 5f12bf4be1..2974b99696 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -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.MessageComposerMode 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.rememberMarkdownTextEditorState 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.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -459,6 +462,14 @@ class MessageComposerPresenter( 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 .getAllRoomAliasSuggestions() .stateIn(this, SharingStarted.Lazily, emptyList()) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 07b9a4e330..4170ae41ab 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1056,7 +1056,8 @@ class MessageComposerPresenterTest : RobolectricTest() { baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions( canTriggerRoomNotification = true, - ) + ), + updateMembersResult = {}, ), typingNoticeResult = { Result.success(Unit) } ).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 { } + 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 fun `present - room mention suggestions no permission`() = runTest { val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) @@ -1113,7 +1163,8 @@ class MessageComposerPresenterTest : RobolectricTest() { baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions( canTriggerRoomNotification = false, - ) + ), + updateMembersResult = {}, ), typingNoticeResult = { Result.success(Unit) } ).apply { @@ -1144,6 +1195,7 @@ class MessageComposerPresenterTest : RobolectricTest() { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions(canTriggerRoomNotification = true), + updateMembersResult = {}, ), typingNoticeResult = { Result.success(Unit) } ).apply {