Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import io.getstream.chat.android.models.User
import io.getstream.chat.android.models.querysort.QuerySortByField
import io.getstream.chat.android.models.querysort.QuerySorter
import io.getstream.chat.android.ui.common.state.channels.actions.ChannelAction
import io.getstream.chat.android.ui.common.utils.SearchDebounce
import io.getstream.chat.android.ui.common.utils.extensions.defaultChannelListFilter
import io.getstream.chat.android.ui.common.utils.extensions.isOneToOne
import io.getstream.log.taggedLogger
Expand Down Expand Up @@ -94,7 +95,8 @@ import kotlin.coroutines.cancellation.CancellationException
* @param messageLimit How many messages are fetched for each channel item when loading channels.
* When `null`, the server-side default is used.
* @param chatEventHandlerFactory The instance of [ChatEventHandlerFactory] used to create [ChatEventHandler].
* @param searchDebounceMs The debounce time for search queries.
* @param searchDebounceMs The debounce time for search queries. Message search queries of 1-2 characters
* are debounced for at least 500ms.
* @param draftMessagesEnabled If the draft message feature is enabled.
* @param messageSearchSort Sorting for message search results. When `null`, the server-side default is used.
* @param globalState A flow emitting the current [GlobalState].
Expand All @@ -108,7 +110,7 @@ public class ChannelListViewModel internal constructor(
private val memberLimit: Int?,
private val messageLimit: Int?,
private val chatEventHandlerFactory: ChatEventHandlerFactory,
searchDebounceMs: Long,
private val searchDebounceMs: Long,
private val draftMessagesEnabled: Boolean,
private val messageSearchSort: QuerySorter<Message>?,
private val globalState: Flow<GlobalState>,
Expand Down Expand Up @@ -143,7 +145,8 @@ public class ChannelListViewModel internal constructor(
* @param messageLimit How many messages are fetched for each channel item when loading channels.
* When `null`, the server-side default is used.
* @param chatEventHandlerFactory The instance of [ChatEventHandlerFactory] used to create [ChatEventHandler].
* @param searchDebounceMs The debounce time for search queries.
* @param searchDebounceMs The debounce time for search queries. Message search queries of 1-2 characters
Comment thread
andremion marked this conversation as resolved.
* are debounced for at least 500ms.
* @param draftMessagesEnabled If the draft message feature is enabled.
* @param messageSearchSort Sorting for message search results. When `null`, the server-side default is used.
* @param globalState A flow emitting the current [GlobalState].
Expand Down Expand Up @@ -190,7 +193,8 @@ public class ChannelListViewModel internal constructor(
* @param messageLimit How many messages are fetched for each channel item when loading channels.
* When `null`, the server-side default is used.
* @param chatEventHandlerFactory The instance of [ChatEventHandlerFactory] used to create [ChatEventHandler].
* @param searchDebounceMs The debounce time for search queries.
* @param searchDebounceMs The debounce time for search queries. Message search queries of 1-2 characters
* are debounced for at least 500ms.
* @param draftMessagesEnabled If the draft message feature is enabled.
* @param messageSearchSort Sorting for message search results. When `null`, the server-side default is used.
* @param globalState A flow emitting the current [GlobalState].
Expand Down Expand Up @@ -234,7 +238,8 @@ public class ChannelListViewModel internal constructor(
*
* @param groupKey The name of the channels group.
* @param chatClient The prepared [ChatClient] instance required for fetching the data.
* @param searchDebounceMs The debounce time for search queries.
* @param searchDebounceMs The debounce time for search queries. Message search queries of 1-2 characters
* are debounced for at least 500ms.
* @param draftMessagesEnabled If the draft message feature is enabled.
* @param messageSearchSort Sorting for message search results. When `null`, the server-side default is used.
* @param globalState A flow emitting the current [GlobalState].
Expand Down Expand Up @@ -805,8 +810,9 @@ public class ChannelListViewModel internal constructor(
}

private fun handleSearchQuery(query: String) {
logger.d { "[handleSearchQuery] query: '$query'" }
searchDebouncer.submitSuspendable {
val debounceMs = SearchDebounce.debounceMsFor(query.trim(), searchDebounceMs)
logger.d { "[handleSearchQuery] query: '$query', debounceMs: $debounceMs" }
searchDebouncer.submitSuspendable(debounceMs) {
searchMessagesForQuery(query)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ import io.getstream.chat.android.randomMessage
import io.getstream.chat.android.test.TestCoroutineExtension
import io.getstream.chat.android.test.asCall
import io.getstream.chat.android.ui.common.state.channels.actions.DeleteConversation
import io.getstream.chat.android.ui.common.utils.SearchDebounce
import io.getstream.result.Error
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
Expand All @@ -76,6 +79,7 @@ import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
Expand Down Expand Up @@ -594,6 +598,113 @@ internal class ChannelListViewModelTest {
assertEquals(30, captor.secondValue.offset)
}

@Test
fun `Given channel list When setting a short message search query Should debounce it for longer`() =
runTest {
val chatClient: ChatClient = mock()
val viewModel = Fixture(chatClient)
.givenCurrentUser()
.givenChannelsQuery()
.givenChannelsState(
channelsStateData = ChannelsStateData.Result(listOf(channel1)),
loading = false,
)
.givenChannelMutes()
.givenSearchMessagesResult(SearchMessagesResult())
.givenRepositorySelectChannels()
.get(this)

viewModel.setSearchQuery(SearchQuery.Messages("ab"))
advanceTimeBy(SearchDebounce.SHORT_QUERY_DEBOUNCE_MS)

verify(chatClient, never()).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())

runCurrent()

verify(chatClient).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}

@Test
fun `Given channel list When setting a padded short message search query Should debounce it for longer`() =
runTest {
val chatClient: ChatClient = mock()
val viewModel = Fixture(chatClient)
.givenCurrentUser()
.givenChannelsQuery()
.givenChannelsState(
channelsStateData = ChannelsStateData.Result(listOf(channel1)),
loading = false,
)
.givenChannelMutes()
.givenSearchMessagesResult(SearchMessagesResult())
.givenRepositorySelectChannels()
.get(this)

// Whitespace makes this three characters long, while the term searched for is one.
viewModel.setSearchQuery(SearchQuery.Messages("a "))
advanceTimeBy(SearchDebounce.SHORT_QUERY_DEBOUNCE_MS)

verify(chatClient, never()).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())

runCurrent()

verify(chatClient).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}

@Test
fun `Given channel list When setting a regular message search query Should debounce it with the default debounce`() =
runTest {
val chatClient: ChatClient = mock()
val viewModel = Fixture(chatClient)
.givenCurrentUser()
.givenChannelsQuery()
.givenChannelsState(
channelsStateData = ChannelsStateData.Result(listOf(channel1)),
loading = false,
)
.givenChannelMutes()
.givenSearchMessagesResult(SearchMessagesResult())
.givenRepositorySelectChannels()
.get(this)

viewModel.setSearchQuery(SearchQuery.Messages("abc"))
advanceTimeBy(ChannelListViewModel.SEARCH_DEBOUNCE_MS)

verify(chatClient, never()).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())

runCurrent()

verify(chatClient).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}

@Test
fun `Given a debounce longer than the short query one When setting a short search query Should keep it`() =
runTest {
val searchDebounceMs = SearchDebounce.SHORT_QUERY_DEBOUNCE_MS + 300
val chatClient: ChatClient = mock()
val viewModel = Fixture(chatClient)
.givenCurrentUser()
.givenChannelsQuery()
.givenChannelsState(
channelsStateData = ChannelsStateData.Result(listOf(channel1)),
loading = false,
)
.givenChannelMutes()
.givenSearchMessagesResult(SearchMessagesResult())
.givenRepositorySelectChannels()
.givenSearchDebounceMs(searchDebounceMs)
.get(this)

viewModel.setSearchQuery(SearchQuery.Messages("ab"))
advanceTimeBy(searchDebounceMs)

verify(chatClient, never()).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())

runCurrent()

verify(chatClient).searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

@Test
fun `Given channel list When setting message search query Should search messages without offset or cursor`() =
runTest {
Expand Down Expand Up @@ -1270,6 +1381,7 @@ internal class ChannelListViewModelTest {
private var predefinedFilterName: String? = null
private var predefinedFilterValues: Map<String, Any>? = null
private var predefinedSortValues: Map<String, Any>? = null
private var searchDebounceMs: Long = ChannelListViewModel.SEARCH_DEBOUNCE_MS

init {
val statePlugin: StatePlugin = mock()
Expand Down Expand Up @@ -1356,6 +1468,10 @@ internal class ChannelListViewModelTest {
predefinedSortValues = sortValues
}

fun givenSearchDebounceMs(searchDebounceMs: Long) = apply {
this.searchDebounceMs = searchDebounceMs
}

fun givenSearchMessagesResult(result: SearchMessagesResult) = apply {
whenever(
chatClient.searchMessages(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()),
Expand Down Expand Up @@ -1395,6 +1511,7 @@ internal class ChannelListViewModelTest {
groupKey != null -> ChannelListViewModel(
chatClient = chatClient,
groupKey = groupKey,
searchDebounceMs = searchDebounceMs,
draftMessagesEnabled = false,
messageSearchSort = messageSearchSort,
globalState = MutableStateFlow(globalState),
Expand All @@ -1405,6 +1522,7 @@ internal class ChannelListViewModelTest {
predefinedFilterName = name,
filterValues = predefinedFilterValues,
sortValues = predefinedSortValues,
searchDebounceMs = searchDebounceMs,
draftMessagesEnabled = false,
chatEventHandlerFactory = ChatEventHandlerFactory(clientState),
messageSearchSort = messageSearchSort,
Expand All @@ -1415,6 +1533,7 @@ internal class ChannelListViewModelTest {
chatClient = chatClient,
initialSort = initialSort,
initialFilters = initialFilters,
searchDebounceMs = searchDebounceMs,
draftMessagesEnabled = false,
chatEventHandlerFactory = ChatEventHandlerFactory(clientState),
messageSearchSort = messageSearchSort,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.getstream.chat.android.core.utils

import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand All @@ -41,18 +42,36 @@ public class Debouncer(
* containing the new work.
*/
public fun submit(work: () -> Unit) {
job?.cancel()
job = scope.launch {
delay(debounceMs)
work()
}
submitInternal(debounceMs) { work() }
}

/**
* Like [submit], but debounced by the given period instead of the one this [Debouncer] was
* created with.
*/
@InternalStreamChatApi
public fun submit(debounceMs: Long, work: () -> Unit) {
submitInternal(debounceMs) { work() }
}

/**
* Cancels the previous work and launches a new coroutine
* containing the new suspendable work.
*/
public fun submitSuspendable(work: suspend () -> Unit) {
submitInternal(debounceMs, work)
}

/**
* Like [submitSuspendable], but debounced by the given period instead of the one this
* [Debouncer] was created with.
*/
@InternalStreamChatApi
public fun submitSuspendable(debounceMs: Long, work: suspend () -> Unit) {
submitInternal(debounceMs, work)
}

private fun submitInternal(debounceMs: Long, work: suspend () -> Unit) {
job?.cancel()
job = scope.launch {
delay(debounceMs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,64 @@ internal class DebouncerTest {
acc `should be equal to` 2
}

@Test
fun testWorkWithCustomDebounceInterval() = runTest {
// given
val debouncer = Debouncer(200)
var acc = 0
// when
debouncer.submit(debounceMs = 500) {
acc += 1
}
delay(499)
// then
acc `should be equal to` 0
// when
delay(1)
// then
acc `should be equal to` 1
}

@Test
fun testSuspendableWorkWithCustomDebounceInterval() = runTest {
// given
val debouncer = Debouncer(200)
var acc = 0
// when
debouncer.submitSuspendable(debounceMs = 500) {
acc += 1
}
delay(499)
// then
acc `should be equal to` 0
// when
delay(1)
// then
acc `should be equal to` 1
}

@Test
fun testWorkWithCustomDebounceIntervalCancelsPendingWork() = runTest {
// given
val debouncer = Debouncer(200)
var acc = 0
// when
debouncer.submit(debounceMs = 500) {
acc += 1
}
delay(100)
debouncer.submit(debounceMs = 300) {
acc += 10
}
delay(299)
// then
acc `should be equal to` 0
// when
delay(1)
// then
acc `should be equal to` 10
}

@Test
fun testCancelLastDebounce() = runTest {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.getstream.chat.android.models.Filters
import io.getstream.chat.android.models.Member
import io.getstream.chat.android.models.querysort.QuerySortByField
import io.getstream.chat.android.ui.common.state.channel.info.AddMembersViewState
import io.getstream.chat.android.ui.common.utils.SearchDebounce
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
Expand Down Expand Up @@ -86,7 +87,7 @@ public class AddMembersViewController(
// Re-run search whenever the query changes, with debounce.
_state
.map { it.query }
.debounce(TYPING_DEBOUNCE_TIMEOUT_MS)
.debounce { query -> SearchDebounce.debounceMsFor(query.trim(), TYPING_DEBOUNCE_TIMEOUT_MS) }
.distinctUntilChanged()
.onEach { query -> searchUsers(query) }
.launchIn(scope)
Expand Down
Loading
Loading