mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Add global search backfill worker (#7860)
* Add search backfill worker This will run on background at most once an hour while the device has internet connectivity and the device is charging and start back-paginating through the user's rooms to make sure the search results are populated. It'll run for the maximum time the default workers allow, 10 minutes. * Create `RustMatrixClient.Factory` AssistedFactory to help with DI and add `SearchBackfillService` * Start search backfill when entering the search screen, just once. This should help getting initial search results if none or few are found at first
This commit is contained in:
committed by
GitHub
parent
9805422a8a
commit
3fe0e09e3c
+35
@@ -10,12 +10,14 @@ package io.element.android.features.home.impl.search
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.home.impl.search.history.SearchHistoryResult
|
||||
@@ -38,6 +40,7 @@ import io.element.android.libraries.matrix.api.search.MessageSearch
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchPaginationState
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchResult
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import io.element.android.libraries.matrix.api.search.SearchBackfillStrategy
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
@@ -81,6 +84,7 @@ class GlobalSearchPresenter(
|
||||
val isEnabled by produceState(false) {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MessageSearch).collectLatest { value = it }
|
||||
}
|
||||
|
||||
val roomListSearchDataSource = remember { roomListSearchDataSourceFactory.create(coroutineScope = coroutineScope) }
|
||||
val queryState = rememberTextFieldState()
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
@@ -88,6 +92,8 @@ class GlobalSearchPresenter(
|
||||
var currentTarget: GlobalSearchTarget by remember { mutableStateOf(GlobalSearchTarget.ROOMS) }
|
||||
val currentMessageSearch: MessageSearch = remember { messageSearchService.createMessageSearch(scope = coroutineScope) }
|
||||
|
||||
StartSearchBackfillWhenVisible(isEnabled = isEnabled, isSearchActive = isSearchActive)
|
||||
|
||||
val searchHistory by produceState<AsyncData<ImmutableList<SearchHistoryListItem>>>(initialValue = AsyncData.Uninitialized) {
|
||||
searchHistoryStore.history.collectLatest { history ->
|
||||
val mappedHistoryItems = history.mapNotNull { result ->
|
||||
@@ -297,6 +303,35 @@ class GlobalSearchPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartSearchBackfillWhenVisible(
|
||||
isEnabled: Boolean,
|
||||
isSearchActive: Boolean,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var hasBackfillRun by rememberSaveable { mutableStateOf(false) }
|
||||
if (isEnabled && isSearchActive && !hasBackfillRun) {
|
||||
DisposableEffect(Unit) {
|
||||
// When we open the message search, we want to backfill the search results so that we can show the user more results if they scroll down.
|
||||
val handle = matrixClient.searchBackfillService.startSearchBackfill(SearchBackfillStrategy.FOREGROUND).getOrNull()
|
||||
?: return@DisposableEffect onDispose {
|
||||
Timber.w("Could not start search backfill, it may already be running.")
|
||||
}
|
||||
|
||||
val job = coroutineScope.launch(coroutineDispatchers.io) {
|
||||
while (handle.isRunning()) {
|
||||
delay(100.milliseconds)
|
||||
}
|
||||
hasBackfillRun = true
|
||||
}
|
||||
onDispose {
|
||||
job.cancel()
|
||||
handle.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapMediaContent(
|
||||
result: MessageSearchResult,
|
||||
roomInfo: RoomInfo,
|
||||
|
||||
+52
-4
@@ -33,13 +33,16 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeSdkPendingTask
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.search.FakeMessageSearch
|
||||
import io.element.android.libraries.matrix.test.search.FakeMessageSearchService
|
||||
import io.element.android.libraries.matrix.test.search.FakeSearchBackfillService
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -205,7 +208,9 @@ class GlobalSearchPresenterTest {
|
||||
@Test
|
||||
fun `present - message search results are mapped when the message search emits`() = runTest {
|
||||
val messageSearch = FakeMessageSearch()
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) }),
|
||||
).apply {
|
||||
getRoomInfoLambda = { Result.success(aRoomInfo()) }
|
||||
}
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
@@ -232,7 +237,9 @@ class GlobalSearchPresenterTest {
|
||||
@Test
|
||||
fun `present - message search displays the uninitialized state after removing the query`() = runTest {
|
||||
val messageSearch = FakeMessageSearch()
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) }),
|
||||
).apply {
|
||||
getRoomInfoLambda = { Result.success(aRoomInfo()) }
|
||||
}
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
@@ -267,7 +274,9 @@ class GlobalSearchPresenterTest {
|
||||
@Test
|
||||
fun `present - UpdateVisibleRange triggers pagination for messages when near the end`() = runTest {
|
||||
val messageSearch = FakeMessageSearch()
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) }),
|
||||
).apply {
|
||||
getRoomInfoLambda = { Result.success(aRoomInfo()) }
|
||||
}
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
@@ -432,6 +441,44 @@ class GlobalSearchPresenterTest {
|
||||
assertThat(awaitItem().results.isLoading()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - will automatically start search backfill and it will be cancelled when the presenter is closed`() = runTest {
|
||||
var isRunning = false
|
||||
val closeLambda = lambdaRecorder<Unit> { isRunning = false }
|
||||
val matrixClient = FakeMatrixClient(
|
||||
searchBackfillService = FakeSearchBackfillService(
|
||||
startSearchBackfillLambda = {
|
||||
isRunning = true
|
||||
Result.success(FakeSdkPendingTask(isRunningLambda = { isRunning }, closeLambda = closeLambda))
|
||||
},
|
||||
),
|
||||
)
|
||||
val presenter = createGlobalSearchPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.MessageSearch.key to true),
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
// Await the initial state, with the feature enabled
|
||||
val initialState = consumeItemsUntilPredicate { it.isEnabled }.last()
|
||||
assertThat(isRunning).isFalse()
|
||||
|
||||
// Opening the search starts the backfill
|
||||
initialState.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
|
||||
consumeItemsUntilPredicate { it.isSearchActive }
|
||||
// The backfill polling loop never ends while the task is running, so don't wait until the scheduler is idle
|
||||
testScheduler.runCurrent()
|
||||
assertThat(isRunning).isTrue()
|
||||
closeLambda.assertions().isNeverCalled()
|
||||
|
||||
// Closing the presenter closes the task
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
closeLambda.assertions().isCalledOnce()
|
||||
assertThat(isRunning).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createGlobalSearchPresenter(
|
||||
@@ -442,7 +489,8 @@ private fun TestScope.createGlobalSearchPresenter(
|
||||
dateFormatter: DateFormatter = FakeDateFormatter(),
|
||||
fileSizeFormatter: FileSizeFormatter = FakeFileSizeFormatter(),
|
||||
permalinkParser: PermalinkParser = FakePermalinkParser(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
matrixClient: MatrixClient =
|
||||
FakeMatrixClient(searchBackfillService = FakeSearchBackfillService(startSearchBackfillLambda = { Result.success(FakeSdkPendingTask()) })),
|
||||
searchHistoryStore: SearchHistoryStore = FakeSearchHistoryStore(),
|
||||
): GlobalSearchPresenter {
|
||||
return GlobalSearchPresenter(
|
||||
|
||||
Reference in New Issue
Block a user