mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Add remove item and clear history buttons to search history (#7816)
* Add remove and clear history buttons to search * Rename `SearchHistoryResultItem` to `SearchHistoryListItem` and `MessagesSearchResultItem` to `MessageSearchListItem` Also, add some docs. * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
d589ff37dc
commit
6cde0706be
+7
-1
@@ -29,5 +29,11 @@ sealed interface GlobalSearchEvent {
|
|||||||
data class SaveRoomToHistory(val roomId: RoomId) : GlobalSearchEvent
|
data class SaveRoomToHistory(val roomId: RoomId) : GlobalSearchEvent
|
||||||
|
|
||||||
/** The user selected a search result from the search history. */
|
/** The user selected a search result from the search history. */
|
||||||
data class SearchHistoryResultSelected(val resultItem: SearchHistoryResultItem) : GlobalSearchEvent
|
data class SearchHistoryResultSelected(val resultItem: SearchHistoryListItem) : GlobalSearchEvent
|
||||||
|
|
||||||
|
/** The user removed a search history entry. */
|
||||||
|
data class RemoveSearchHistoryResult(val result: SearchHistoryListItem) : GlobalSearchEvent
|
||||||
|
|
||||||
|
/** The user cleared the search history. */
|
||||||
|
data object ClearSearchHistory : GlobalSearchEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-9
@@ -57,6 +57,7 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -87,14 +88,14 @@ class GlobalSearchPresenter(
|
|||||||
var currentTarget: GlobalSearchTarget by remember { mutableStateOf(GlobalSearchTarget.ROOMS) }
|
var currentTarget: GlobalSearchTarget by remember { mutableStateOf(GlobalSearchTarget.ROOMS) }
|
||||||
val currentMessageSearch: MessageSearch = remember { messageSearchService.createMessageSearch(scope = coroutineScope) }
|
val currentMessageSearch: MessageSearch = remember { messageSearchService.createMessageSearch(scope = coroutineScope) }
|
||||||
|
|
||||||
val searchHistory by produceState<AsyncData<ImmutableList<SearchHistoryResultItem>>>(initialValue = AsyncData.Uninitialized) {
|
val searchHistory by produceState<AsyncData<ImmutableList<SearchHistoryListItem>>>(initialValue = AsyncData.Uninitialized) {
|
||||||
searchHistoryStore.history.collectLatest { history ->
|
searchHistoryStore.history.collectLatest { history ->
|
||||||
val mappedHistoryItems = history.mapNotNull { result ->
|
val mappedHistoryItems = history.mapNotNull { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is SearchHistoryResult.Query -> SearchHistoryResultItem.Query(term = result.term)
|
is SearchHistoryResult.Query -> SearchHistoryListItem.Query(term = result.term)
|
||||||
is SearchHistoryResult.Room -> {
|
is SearchHistoryResult.Room -> {
|
||||||
val roomInfo = matrixClient.getRoomInfo(result.roomId).getOrNull() ?: return@mapNotNull null
|
val roomInfo = matrixClient.getRoomInfo(result.roomId).getOrNull() ?: return@mapNotNull null
|
||||||
SearchHistoryResultItem.Room(roomId = result.roomId, roomInfo = roomInfo)
|
SearchHistoryListItem.Room(roomId = result.roomId, roomInfo = roomInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +237,7 @@ class GlobalSearchPresenter(
|
|||||||
}
|
}
|
||||||
is GlobalSearchEvent.SearchHistoryResultSelected -> {
|
is GlobalSearchEvent.SearchHistoryResultSelected -> {
|
||||||
when (event.resultItem) {
|
when (event.resultItem) {
|
||||||
is SearchHistoryResultItem.Query -> coroutineScope.launch {
|
is SearchHistoryListItem.Query -> coroutineScope.launch {
|
||||||
delay(300.milliseconds)
|
delay(300.milliseconds)
|
||||||
queryState.edit {
|
queryState.edit {
|
||||||
replace(0, length, event.resultItem.term)
|
replace(0, length, event.resultItem.term)
|
||||||
@@ -244,9 +245,21 @@ class GlobalSearchPresenter(
|
|||||||
searchResults = AsyncData.Loading()
|
searchResults = AsyncData.Loading()
|
||||||
currentTarget = GlobalSearchTarget.ROOMS
|
currentTarget = GlobalSearchTarget.ROOMS
|
||||||
}
|
}
|
||||||
is SearchHistoryResultItem.Room -> Unit
|
is SearchHistoryListItem.Room -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is GlobalSearchEvent.RemoveSearchHistoryResult -> coroutineScope.launch {
|
||||||
|
val currentHistory = searchHistoryStore.history.first()
|
||||||
|
val id = when (event.result) {
|
||||||
|
is SearchHistoryListItem.Query -> event.result.term
|
||||||
|
is SearchHistoryListItem.Room -> event.result.roomId.value
|
||||||
|
}
|
||||||
|
val result = currentHistory.find { it.id == id } ?: return@launch
|
||||||
|
searchHistoryStore.remove(result)
|
||||||
|
}
|
||||||
|
GlobalSearchEvent.ClearSearchHistory -> coroutineScope.launch {
|
||||||
|
searchHistoryStore.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +278,7 @@ class GlobalSearchPresenter(
|
|||||||
result: MessageSearchResult,
|
result: MessageSearchResult,
|
||||||
roomInfo: RoomInfo,
|
roomInfo: RoomInfo,
|
||||||
formattedTimestamp: String,
|
formattedTimestamp: String,
|
||||||
): MessageSearchResultItem.Message? {
|
): MessageSearchListItem.Message? {
|
||||||
val body = latestEventFormatter.format(
|
val body = latestEventFormatter.format(
|
||||||
latestEvent = LatestEventValue.Remote(
|
latestEvent = LatestEventValue.Remote(
|
||||||
timestamp = result.timestamp,
|
timestamp = result.timestamp,
|
||||||
@@ -276,7 +289,7 @@ class GlobalSearchPresenter(
|
|||||||
),
|
),
|
||||||
isDmRoom = false,
|
isDmRoom = false,
|
||||||
)
|
)
|
||||||
return MessageSearchResultItem.Message(
|
return MessageSearchListItem.Message(
|
||||||
messageSearchResult = result,
|
messageSearchResult = result,
|
||||||
body = body?.toString() ?: "",
|
body = body?.toString() ?: "",
|
||||||
formattedTimestamp = formattedTimestamp,
|
formattedTimestamp = formattedTimestamp,
|
||||||
@@ -288,7 +301,7 @@ class GlobalSearchPresenter(
|
|||||||
result: MessageSearchResult,
|
result: MessageSearchResult,
|
||||||
roomInfo: RoomInfo,
|
roomInfo: RoomInfo,
|
||||||
formattedTimestamp: String,
|
formattedTimestamp: String,
|
||||||
): MessageSearchResultItem.Media? {
|
): MessageSearchListItem.Media? {
|
||||||
val messageType = when (val content = result.content) {
|
val messageType = when (val content = result.content) {
|
||||||
is MessageContent if content.type is MessageTypeWithAttachment -> content.type as MessageTypeWithAttachment
|
is MessageContent if content.type is MessageTypeWithAttachment -> content.type as MessageTypeWithAttachment
|
||||||
else -> return null
|
else -> return null
|
||||||
@@ -342,7 +355,7 @@ class GlobalSearchPresenter(
|
|||||||
blurhash = blurhash
|
blurhash = blurhash
|
||||||
)
|
)
|
||||||
|
|
||||||
return MessageSearchResultItem.Media(
|
return MessageSearchListItem.Media(
|
||||||
messageSearchResult = result,
|
messageSearchResult = result,
|
||||||
mediaContent = mediaContent,
|
mediaContent = mediaContent,
|
||||||
formattedTimestamp = formattedTimestamp,
|
formattedTimestamp = formattedTimestamp,
|
||||||
|
|||||||
+31
-8
@@ -28,20 +28,26 @@ data class GlobalSearchState(
|
|||||||
val queryState: TextFieldState,
|
val queryState: TextFieldState,
|
||||||
val currentTarget: GlobalSearchTarget,
|
val currentTarget: GlobalSearchTarget,
|
||||||
val results: AsyncData<GlobalSearchResults>,
|
val results: AsyncData<GlobalSearchResults>,
|
||||||
val history: AsyncData<ImmutableList<SearchHistoryResultItem>>,
|
val history: AsyncData<ImmutableList<SearchHistoryListItem>>,
|
||||||
val eventSink: (GlobalSearchEvent) -> Unit
|
val eventSink: (GlobalSearchEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target of the global search: either rooms or messages.
|
||||||
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
enum class GlobalSearchTarget {
|
enum class GlobalSearchTarget {
|
||||||
ROOMS,
|
ROOMS,
|
||||||
MESSAGES,
|
MESSAGES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The results of a global search, either a list of rooms or a list of messages.
|
||||||
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface GlobalSearchResults {
|
sealed interface GlobalSearchResults {
|
||||||
data class RoomListResults(val results: ImmutableList<RoomListRoomSummary>) : GlobalSearchResults
|
data class RoomListResults(val results: ImmutableList<RoomListRoomSummary>) : GlobalSearchResults
|
||||||
data class MessageSearchResults(val results: ImmutableList<MessageSearchResultItem>) : GlobalSearchResults
|
data class MessageSearchResults(val results: ImmutableList<MessageSearchListItem>) : GlobalSearchResults
|
||||||
|
|
||||||
fun isEmpty(): Boolean = when (this) {
|
fun isEmpty(): Boolean = when (this) {
|
||||||
is RoomListResults -> results.isEmpty()
|
is RoomListResults -> results.isEmpty()
|
||||||
@@ -49,7 +55,10 @@ sealed interface GlobalSearchResults {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface MessageSearchResultItem {
|
/**
|
||||||
|
* A single item in the message search results. It's the UI representation of [MessageSearchResult], which is the mapped model returned by the Matrix SDK.
|
||||||
|
*/
|
||||||
|
sealed interface MessageSearchListItem {
|
||||||
val roomId: RoomId
|
val roomId: RoomId
|
||||||
val eventId: EventId
|
val eventId: EventId
|
||||||
val senderId: UserId
|
val senderId: UserId
|
||||||
@@ -57,24 +66,30 @@ sealed interface MessageSearchResultItem {
|
|||||||
val roomInfo: RoomInfo
|
val roomInfo: RoomInfo
|
||||||
val formattedTimestamp: String
|
val formattedTimestamp: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message item in the message search results.
|
||||||
|
*/
|
||||||
data class Message(
|
data class Message(
|
||||||
val messageSearchResult: MessageSearchResult,
|
val messageSearchResult: MessageSearchResult,
|
||||||
val body: String,
|
val body: String,
|
||||||
override val roomInfo: RoomInfo,
|
override val roomInfo: RoomInfo,
|
||||||
override val formattedTimestamp: String,
|
override val formattedTimestamp: String,
|
||||||
) : MessageSearchResultItem {
|
) : MessageSearchListItem {
|
||||||
override val roomId: RoomId = messageSearchResult.roomId
|
override val roomId: RoomId = messageSearchResult.roomId
|
||||||
override val eventId: EventId = messageSearchResult.eventId
|
override val eventId: EventId = messageSearchResult.eventId
|
||||||
override val senderId: UserId = messageSearchResult.senderId
|
override val senderId: UserId = messageSearchResult.senderId
|
||||||
override val senderName: String = messageSearchResult.senderProfile.getDisambiguatedDisplayName(senderId)
|
override val senderName: String = messageSearchResult.senderProfile.getDisambiguatedDisplayName(senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message with media item in the message search results.
|
||||||
|
*/
|
||||||
data class Media(
|
data class Media(
|
||||||
val messageSearchResult: MessageSearchResult,
|
val messageSearchResult: MessageSearchResult,
|
||||||
val mediaContent: MediaSearchResultContent,
|
val mediaContent: MediaSearchResultContent,
|
||||||
override val roomInfo: RoomInfo,
|
override val roomInfo: RoomInfo,
|
||||||
override val formattedTimestamp: String,
|
override val formattedTimestamp: String,
|
||||||
) : MessageSearchResultItem {
|
) : MessageSearchListItem {
|
||||||
override val roomId: RoomId = messageSearchResult.roomId
|
override val roomId: RoomId = messageSearchResult.roomId
|
||||||
override val eventId: EventId = messageSearchResult.eventId
|
override val eventId: EventId = messageSearchResult.eventId
|
||||||
override val senderId: UserId = messageSearchResult.senderId
|
override val senderId: UserId = messageSearchResult.senderId
|
||||||
@@ -82,20 +97,28 @@ sealed interface MessageSearchResultItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface SearchHistoryResultItem {
|
/**
|
||||||
|
* A single entry in the global search history, displayed in the list of recent searches.
|
||||||
|
*
|
||||||
|
* It's the UI representation of [io.element.android.features.home.impl.search.history.SearchHistoryResult], which is the persisted model.
|
||||||
|
*/
|
||||||
|
sealed interface SearchHistoryListItem {
|
||||||
val id: String
|
val id: String
|
||||||
|
|
||||||
data class Query(val term: String) : SearchHistoryResultItem {
|
data class Query(val term: String) : SearchHistoryListItem {
|
||||||
override val id: String = "query:$term"
|
override val id: String = "query:$term"
|
||||||
}
|
}
|
||||||
data class Room(
|
data class Room(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val roomInfo: RoomInfo,
|
val roomInfo: RoomInfo,
|
||||||
) : SearchHistoryResultItem {
|
) : SearchHistoryListItem {
|
||||||
override val id: String = "room:${roomId.value}"
|
override val id: String = "room:${roomId.value}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the media content of a message search result.
|
||||||
|
*/
|
||||||
data class MediaSearchResultContent(
|
data class MediaSearchResultContent(
|
||||||
val filename: String,
|
val filename: String,
|
||||||
val extension: String?,
|
val extension: String?,
|
||||||
|
|||||||
+9
-9
@@ -79,13 +79,13 @@ class GlobalSearchStatePreviewParam : PreviewParameterProvider<GlobalSearchState
|
|||||||
currentTarget = GlobalSearchTarget.MESSAGES,
|
currentTarget = GlobalSearchTarget.MESSAGES,
|
||||||
queryState = TextFieldState("Query"),
|
queryState = TextFieldState("Query"),
|
||||||
results = AsyncData.Success(GlobalSearchResults.MessageSearchResults(persistentListOf(
|
results = AsyncData.Success(GlobalSearchResults.MessageSearchResults(persistentListOf(
|
||||||
MessageSearchResultItem.Message(
|
MessageSearchListItem.Message(
|
||||||
messageSearchResult = aMessageSearchResult(eventId = EventId("\$eventId1:server.org")),
|
messageSearchResult = aMessageSearchResult(eventId = EventId("\$eventId1:server.org")),
|
||||||
body = "A message with Query",
|
body = "A message with Query",
|
||||||
roomInfo = aRoomInfo(),
|
roomInfo = aRoomInfo(),
|
||||||
formattedTimestamp = "12:00",
|
formattedTimestamp = "12:00",
|
||||||
),
|
),
|
||||||
MessageSearchResultItem.Media(
|
MessageSearchListItem.Media(
|
||||||
messageSearchResult = aMessageSearchResult(eventId = EventId("\$eventId2:server.org")),
|
messageSearchResult = aMessageSearchResult(eventId = EventId("\$eventId2:server.org")),
|
||||||
mediaContent = MediaSearchResultContent(
|
mediaContent = MediaSearchResultContent(
|
||||||
filename = "file.png",
|
filename = "file.png",
|
||||||
@@ -114,9 +114,9 @@ class GlobalSearchStatePreviewParam : PreviewParameterProvider<GlobalSearchState
|
|||||||
results = AsyncData.Uninitialized,
|
results = AsyncData.Uninitialized,
|
||||||
history = AsyncData.Success(
|
history = AsyncData.Success(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
SearchHistoryResultItem.Query("Query 1"),
|
SearchHistoryListItem.Query("Query 1"),
|
||||||
SearchHistoryResultItem.Query("Query 2"),
|
SearchHistoryListItem.Query("Query 2"),
|
||||||
SearchHistoryResultItem.Room(
|
SearchHistoryListItem.Room(
|
||||||
roomId = roomId1,
|
roomId = roomId1,
|
||||||
roomInfo = aRoomInfo(
|
roomInfo = aRoomInfo(
|
||||||
id = roomId1,
|
id = roomId1,
|
||||||
@@ -125,7 +125,7 @@ class GlobalSearchStatePreviewParam : PreviewParameterProvider<GlobalSearchState
|
|||||||
canonicalAlias = aCanonicalAlias,
|
canonicalAlias = aCanonicalAlias,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
SearchHistoryResultItem.Room(
|
SearchHistoryListItem.Room(
|
||||||
roomId = roomId2,
|
roomId = roomId2,
|
||||||
roomInfo = aRoomInfo(
|
roomInfo = aRoomInfo(
|
||||||
id = roomId2,
|
id = roomId2,
|
||||||
@@ -133,7 +133,7 @@ class GlobalSearchStatePreviewParam : PreviewParameterProvider<GlobalSearchState
|
|||||||
avatarUrl = AN_AVATAR_URL,
|
avatarUrl = AN_AVATAR_URL,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
SearchHistoryResultItem.Room(
|
SearchHistoryListItem.Room(
|
||||||
roomId = dmRoomId,
|
roomId = dmRoomId,
|
||||||
roomInfo = aRoomInfo(
|
roomInfo = aRoomInfo(
|
||||||
id = dmRoomId,
|
id = dmRoomId,
|
||||||
@@ -144,7 +144,7 @@ class GlobalSearchStatePreviewParam : PreviewParameterProvider<GlobalSearchState
|
|||||||
canonicalAlias = aCanonicalAlias,
|
canonicalAlias = aCanonicalAlias,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
SearchHistoryResultItem.Room(
|
SearchHistoryListItem.Room(
|
||||||
roomId = tombstonedRoomId,
|
roomId = tombstonedRoomId,
|
||||||
roomInfo = aRoomInfo(
|
roomInfo = aRoomInfo(
|
||||||
id = tombstonedRoomId,
|
id = tombstonedRoomId,
|
||||||
@@ -300,7 +300,7 @@ internal fun aGlobalSearchState(
|
|||||||
queryState: TextFieldState = TextFieldState(),
|
queryState: TextFieldState = TextFieldState(),
|
||||||
currentTarget: GlobalSearchTarget = GlobalSearchTarget.ROOMS,
|
currentTarget: GlobalSearchTarget = GlobalSearchTarget.ROOMS,
|
||||||
results: AsyncData<GlobalSearchResults> = AsyncData.Uninitialized,
|
results: AsyncData<GlobalSearchResults> = AsyncData.Uninitialized,
|
||||||
history: AsyncData<ImmutableList<SearchHistoryResultItem>> = AsyncData.Uninitialized,
|
history: AsyncData<ImmutableList<SearchHistoryListItem>> = AsyncData.Uninitialized,
|
||||||
eventSink: (GlobalSearchEvent) -> Unit = {},
|
eventSink: (GlobalSearchEvent) -> Unit = {},
|
||||||
) = GlobalSearchState(
|
) = GlobalSearchState(
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
|
|||||||
+63
-20
@@ -12,12 +12,14 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -31,9 +33,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.text.input.KeyboardActionHandler
|
import androidx.compose.foundation.text.input.KeyboardActionHandler
|
||||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -67,6 +71,8 @@ import io.element.android.libraries.designsystem.modifiers.niceClickable
|
|||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.text.roundToPx
|
import io.element.android.libraries.designsystem.text.roundToPx
|
||||||
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
import io.element.android.libraries.designsystem.theme.components.FilledTextField
|
import io.element.android.libraries.designsystem.theme.components.FilledTextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
@@ -75,6 +81,7 @@ import io.element.android.libraries.designsystem.theme.components.LinearProgress
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.SegmentedButton
|
import io.element.android.libraries.designsystem.theme.components.SegmentedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
|
||||||
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
@@ -155,10 +162,14 @@ private fun GlobalSearchContent(
|
|||||||
val historyResults = state.history.dataOrNull()?.toImmutableList() ?: persistentListOf()
|
val historyResults = state.history.dataOrNull()?.toImmutableList() ?: persistentListOf()
|
||||||
when {
|
when {
|
||||||
state.results.isUninitialized() -> if (state.queryState.text.isBlank() && historyResults.isNotEmpty()) {
|
state.results.isUninitialized() -> if (state.queryState.text.isBlank() && historyResults.isNotEmpty()) {
|
||||||
searchHistory(historyResults) { result ->
|
searchHistory(
|
||||||
|
historyResults = historyResults,
|
||||||
|
onRemoveResult = { item -> state.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(item)) },
|
||||||
|
onClearAllResults = { state.eventSink(GlobalSearchEvent.ClearSearchHistory) },
|
||||||
|
) { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is SearchHistoryResultItem.Query -> Unit
|
is SearchHistoryListItem.Query -> Unit
|
||||||
is SearchHistoryResultItem.Room -> onSelectSearchResult(result.roomInfo.id, null)
|
is SearchHistoryListItem.Room -> onSelectSearchResult(result.roomInfo.id, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(result))
|
state.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(result))
|
||||||
@@ -214,7 +225,7 @@ private fun SearchTopAppBarContent(state: GlobalSearchState) {
|
|||||||
modifier = Modifier.clip(CircleShape).size(20.dp),
|
modifier = Modifier.clip(CircleShape).size(20.dp),
|
||||||
onClick = { state.eventSink(GlobalSearchEvent.ClearQuery) },
|
onClick = { state.eventSink(GlobalSearchEvent.ClearQuery) },
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
containerColor = ElementTheme.colors.borderInteractivePrimary,
|
containerColor = ElementTheme.colors.iconSecondary,
|
||||||
contentColor = ElementTheme.colors.iconOnSolidPrimary
|
contentColor = ElementTheme.colors.iconOnSolidPrimary
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -279,31 +290,52 @@ private fun LazyListScope.startSearching() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun LazyListScope.searchHistory(
|
private fun LazyListScope.searchHistory(
|
||||||
historyResults: ImmutableList<SearchHistoryResultItem>,
|
historyResults: ImmutableList<SearchHistoryListItem>,
|
||||||
onSelectSearchHistoryResult: (SearchHistoryResultItem) -> Unit,
|
onRemoveResult: (SearchHistoryListItem) -> Unit,
|
||||||
|
onClearAllResults: () -> Unit,
|
||||||
|
onSelectSearchHistoryResult: (SearchHistoryListItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Row(
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp),
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
text = stringResource(R.string.screen_search_recent_searches_section_title),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
) {
|
||||||
color = ElementTheme.colors.textPrimary,
|
Text(
|
||||||
)
|
modifier = Modifier.padding(start = 16.dp).weight(1f),
|
||||||
|
text = stringResource(R.string.screen_search_recent_searches_section_title),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(CommonStrings.action_clear_all),
|
||||||
|
size = ButtonSize.Small,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = ElementTheme.colors.textActionAccent),
|
||||||
|
onClick = onClearAllResults,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items(
|
items(
|
||||||
items = historyResults,
|
items = historyResults,
|
||||||
key = { it.id },
|
key = { it.id },
|
||||||
) { result ->
|
) { result ->
|
||||||
|
val twoLineHeight =
|
||||||
|
// Vertical padding
|
||||||
|
12.dp * 2 +
|
||||||
|
// Text line heights
|
||||||
|
ElementTheme.typography.fontBodyMdRegular.lineHeight.toDp() +
|
||||||
|
ElementTheme.typography.fontBodyLgRegular.lineHeight.toDp()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.niceClickable(onClick = { onSelectSearchHistoryResult(result) })
|
.clickable(onClick = { onSelectSearchHistoryResult(result) })
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(start = 16.dp, end = 2.dp)
|
||||||
|
.height(twoLineHeight),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
when (result) {
|
when (result) {
|
||||||
is SearchHistoryResultItem.Query -> {
|
is SearchHistoryListItem.Query -> {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
@@ -315,13 +347,14 @@ private fun LazyListScope.searchHistory(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.weight(1f, fill = true),
|
||||||
text = result.term,
|
text = result.term,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is SearchHistoryResultItem.Room -> {
|
is SearchHistoryListItem.Room -> {
|
||||||
val heroAvatars = remember(result.roomInfo.heroes) {
|
val heroAvatars = remember(result.roomInfo.heroes) {
|
||||||
result.roomInfo.heroes.map { it.getAvatarData(AvatarSize.SearchRoomListItem) }.toImmutableList()
|
result.roomInfo.heroes.map { it.getAvatarData(AvatarSize.SearchRoomListItem) }.toImmutableList()
|
||||||
}
|
}
|
||||||
@@ -333,7 +366,7 @@ private fun LazyListScope.searchHistory(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.weight(1f, fill = true)) {
|
||||||
Text(
|
Text(
|
||||||
text = result.roomInfo.name.orEmpty(),
|
text = result.roomInfo.name.orEmpty(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -365,6 +398,16 @@ private fun LazyListScope.searchHistory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.minimumInteractiveComponentSize()
|
||||||
|
.niceClickable {
|
||||||
|
onRemoveResult(result)
|
||||||
|
},
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
contentDescription = stringResource(CommonStrings.action_remove),
|
||||||
|
tint = ElementTheme.colors.iconSecondary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,7 +494,7 @@ private fun LazyListScope.roomListResults(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun LazyListScope.messageSearchResults(
|
private fun LazyListScope.messageSearchResults(
|
||||||
results: ImmutableList<MessageSearchResultItem>,
|
results: ImmutableList<MessageSearchListItem>,
|
||||||
onSearchResultSelected: (RoomId, EventId?) -> Unit,
|
onSearchResultSelected: (RoomId, EventId?) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (results.isNotEmpty()) {
|
if (results.isNotEmpty()) {
|
||||||
@@ -471,7 +514,7 @@ private fun LazyListScope.messageSearchResults(
|
|||||||
) { index, result ->
|
) { index, result ->
|
||||||
val clickableModifier = Modifier.niceClickable(onClick = { onSearchResultSelected(result.roomId, result.eventId) })
|
val clickableModifier = Modifier.niceClickable(onClick = { onSearchResultSelected(result.roomId, result.eventId) })
|
||||||
when (result) {
|
when (result) {
|
||||||
is MessageSearchResultItem.Message -> TextMessageSearchResultItemView(
|
is MessageSearchListItem.Message -> TextMessageSearchResultItemView(
|
||||||
modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex),
|
modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex),
|
||||||
avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem),
|
avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem),
|
||||||
avatarType = AvatarType.Room(
|
avatarType = AvatarType.Room(
|
||||||
@@ -482,7 +525,7 @@ private fun LazyListScope.messageSearchResults(
|
|||||||
body = result.body,
|
body = result.body,
|
||||||
formattedTimestamp = result.formattedTimestamp,
|
formattedTimestamp = result.formattedTimestamp,
|
||||||
)
|
)
|
||||||
is MessageSearchResultItem.Media -> {
|
is MessageSearchListItem.Media -> {
|
||||||
MediaMessageSearchResultItemView(
|
MediaMessageSearchResultItemView(
|
||||||
modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex),
|
modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex),
|
||||||
avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem),
|
avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem),
|
||||||
|
|||||||
+11
@@ -54,6 +54,17 @@ class DefaultSearchHistoryStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(result: SearchHistoryResult) {
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
mutex.withLock {
|
||||||
|
ensureLoadedLocked()
|
||||||
|
val updated = cache.value.filterNot { it == result }
|
||||||
|
cache.value = updated
|
||||||
|
persist(updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clear() = withContext<Unit>(dispatchers.io) {
|
override suspend fun clear() = withContext<Unit>(dispatchers.io) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
isLoaded = true
|
isLoaded = true
|
||||||
|
|||||||
+9
-3
@@ -18,9 +18,15 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface SearchHistoryResult {
|
sealed interface SearchHistoryResult {
|
||||||
@Serializable
|
val id: String
|
||||||
data class Query(val term: String) : SearchHistoryResult
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Room(val roomId: RoomId) : SearchHistoryResult
|
data class Query(val term: String) : SearchHistoryResult {
|
||||||
|
override val id: String = term
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Room(val roomId: RoomId) : SearchHistoryResult {
|
||||||
|
override val id: String = roomId.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -30,6 +30,11 @@ interface SearchHistoryStore {
|
|||||||
*/
|
*/
|
||||||
suspend fun add(result: SearchHistoryResult)
|
suspend fun add(result: SearchHistoryResult)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a [result] from the history.
|
||||||
|
*/
|
||||||
|
suspend fun remove(result: SearchHistoryResult)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove every entry from the history.
|
* Remove every entry from the history.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+69
-4
@@ -224,7 +224,7 @@ class GlobalSearchPresenterTest {
|
|||||||
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
|
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
|
||||||
val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
|
val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
|
||||||
assertThat(results).hasSize(1)
|
assertThat(results).hasSize(1)
|
||||||
assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java)
|
assertThat(results.first()).isInstanceOf(MessageSearchListItem.Message::class.java)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ class GlobalSearchPresenterTest {
|
|||||||
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
|
val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
|
||||||
val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
|
val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
|
||||||
assertThat(results).hasSize(1)
|
assertThat(results).hasSize(1)
|
||||||
assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java)
|
assertThat(results.first()).isInstanceOf(MessageSearchListItem.Message::class.java)
|
||||||
|
|
||||||
// Remove the query
|
// Remove the query
|
||||||
successState.queryState.edit { replace(0, length, "") }
|
successState.queryState.edit { replace(0, length, "") }
|
||||||
@@ -337,6 +337,71 @@ class GlobalSearchPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - RemoveSearchHistoryResult removes a query entry`() = runTest {
|
||||||
|
val searchHistoryStore = FakeSearchHistoryStore().apply {
|
||||||
|
add(SearchHistoryResult.Query("keep"))
|
||||||
|
add(SearchHistoryResult.Query("drop"))
|
||||||
|
}
|
||||||
|
val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(SearchHistoryListItem.Query("drop")))
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(searchHistoryStore.history.first()).containsExactly(SearchHistoryResult.Query("keep"))
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - RemoveSearchHistoryResult removes a room entry`() = runTest {
|
||||||
|
val searchHistoryStore = FakeSearchHistoryStore().apply {
|
||||||
|
add(SearchHistoryResult.Query("keep"))
|
||||||
|
add(SearchHistoryResult.Room(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(SearchHistoryListItem.Room(A_ROOM_ID, aRoomInfo())))
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(searchHistoryStore.history.first()).containsExactly(SearchHistoryResult.Query("keep"))
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - RemoveSearchHistoryResult with an unknown id does nothing`() = runTest {
|
||||||
|
val searchHistoryStore = FakeSearchHistoryStore().apply { add(SearchHistoryResult.Query("keep")) }
|
||||||
|
val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(SearchHistoryListItem.Query("query:unknown")))
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(searchHistoryStore.history.first()).containsExactly(SearchHistoryResult.Query("keep"))
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ClearSearchHistory removes every entry`() = runTest {
|
||||||
|
val searchHistoryStore = FakeSearchHistoryStore().apply {
|
||||||
|
add(SearchHistoryResult.Query("a"))
|
||||||
|
add(SearchHistoryResult.Room(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(GlobalSearchEvent.ClearSearchHistory)
|
||||||
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(searchHistoryStore.history.first()).isEmpty()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - SearchHistoryResultSelected does nothing for a room result`() = runTest {
|
fun `present - SearchHistoryResultSelected does nothing for a room result`() = runTest {
|
||||||
val searchHistoryStore = FakeSearchHistoryStore()
|
val searchHistoryStore = FakeSearchHistoryStore()
|
||||||
@@ -345,7 +410,7 @@ class GlobalSearchPresenterTest {
|
|||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryResultItem.Room(A_ROOM_ID, aRoomInfo())))
|
initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryListItem.Room(A_ROOM_ID, aRoomInfo())))
|
||||||
testScheduler.advanceUntilIdle()
|
testScheduler.advanceUntilIdle()
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
@@ -361,7 +426,7 @@ class GlobalSearchPresenterTest {
|
|||||||
// Skip loading search results state
|
// Skip loading search results state
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryResultItem.Query("Test")))
|
initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryListItem.Query("Test")))
|
||||||
|
|
||||||
assertThat(awaitItem().queryState.text.toString()).isEqualTo("Test")
|
assertThat(awaitItem().queryState.text.toString()).isEqualTo("Test")
|
||||||
assertThat(awaitItem().results.isLoading()).isTrue()
|
assertThat(awaitItem().results.isLoading()).isTrue()
|
||||||
|
|||||||
+40
-3
@@ -162,7 +162,7 @@ class GlobalSearchViewTest : RobolectricTest() {
|
|||||||
results = AsyncData.Success(
|
results = AsyncData.Success(
|
||||||
GlobalSearchResults.MessageSearchResults(
|
GlobalSearchResults.MessageSearchResults(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
MessageSearchResultItem.Message(
|
MessageSearchListItem.Message(
|
||||||
messageSearchResult = aMessageSearchResult(roomId = roomId, eventId = eventId),
|
messageSearchResult = aMessageSearchResult(roomId = roomId, eventId = eventId),
|
||||||
body = "A message body",
|
body = "A message body",
|
||||||
roomInfo = aRoomInfo(id = roomId, name = "A message room"),
|
roomInfo = aRoomInfo(id = roomId, name = "A message room"),
|
||||||
@@ -184,7 +184,7 @@ class GlobalSearchViewTest : RobolectricTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on a history query result - emits the expected Event`() = runAndroidComposeUiTest {
|
fun `clicking on a history query result - emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val queryResult = SearchHistoryResultItem.Query("Recent term")
|
val queryResult = SearchHistoryListItem.Query("Recent term")
|
||||||
val eventsRecorder = EventsRecorder<GlobalSearchEvent>()
|
val eventsRecorder = EventsRecorder<GlobalSearchEvent>()
|
||||||
setGlobalSearchView(
|
setGlobalSearchView(
|
||||||
state = aGlobalSearchState(
|
state = aGlobalSearchState(
|
||||||
@@ -203,10 +203,47 @@ class GlobalSearchViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(GlobalSearchEvent.SearchHistoryResultSelected(queryResult))
|
eventsRecorder.assertSingle(GlobalSearchEvent.SearchHistoryResultSelected(queryResult))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on the remove button of a history result - emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
|
val queryResult = SearchHistoryListItem.Query("Recent term")
|
||||||
|
val eventsRecorder = EventsRecorder<GlobalSearchEvent>()
|
||||||
|
setGlobalSearchView(
|
||||||
|
state = aGlobalSearchState(
|
||||||
|
isSearchActive = true,
|
||||||
|
queryState = TextFieldState(),
|
||||||
|
results = AsyncData.Uninitialized,
|
||||||
|
history = AsyncData.Success(persistentListOf(queryResult)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Remove automatic initial events
|
||||||
|
eventsRecorder.clear()
|
||||||
|
onNodeWithContentDescription(activity!!.getString(CommonStrings.action_remove)).performClick()
|
||||||
|
eventsRecorder.assertSingle(GlobalSearchEvent.RemoveSearchHistoryResult(queryResult))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on clear all in the history - emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<GlobalSearchEvent>()
|
||||||
|
setGlobalSearchView(
|
||||||
|
state = aGlobalSearchState(
|
||||||
|
isSearchActive = true,
|
||||||
|
queryState = TextFieldState(),
|
||||||
|
results = AsyncData.Uninitialized,
|
||||||
|
history = AsyncData.Success(persistentListOf(SearchHistoryListItem.Query("Recent term"))),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Remove automatic initial events
|
||||||
|
eventsRecorder.clear()
|
||||||
|
onNodeWithText("Clear all").performClick()
|
||||||
|
eventsRecorder.assertSingle(GlobalSearchEvent.ClearSearchHistory)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on a history room result - invokes the callback and emits the expected Events`() = runAndroidComposeUiTest {
|
fun `clicking on a history room result - invokes the callback and emits the expected Events`() = runAndroidComposeUiTest {
|
||||||
val roomId = RoomId("!aHistoryRoom:server.org")
|
val roomId = RoomId("!aHistoryRoom:server.org")
|
||||||
val roomResult = SearchHistoryResultItem.Room(
|
val roomResult = SearchHistoryListItem.Room(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomInfo = aRoomInfo(id = roomId, name = "A history room"),
|
roomInfo = aRoomInfo(id = roomId, name = "A history room"),
|
||||||
)
|
)
|
||||||
|
|||||||
+25
@@ -89,6 +89,31 @@ class DefaultSearchHistoryStoreTest {
|
|||||||
).inOrder()
|
).inOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `remove deletes only the given entry and persists the change`() = runTest {
|
||||||
|
val fileStore = FakeSearchHistoryFileStore()
|
||||||
|
val store = createStore(fileStore)
|
||||||
|
store.add(SearchHistoryResult.Query("a"))
|
||||||
|
store.add(SearchHistoryResult.Room(A_ROOM_ID))
|
||||||
|
store.add(SearchHistoryResult.Query("b"))
|
||||||
|
|
||||||
|
store.remove(SearchHistoryResult.Room(A_ROOM_ID))
|
||||||
|
|
||||||
|
val expected = listOf(SearchHistoryResult.Query("b"), SearchHistoryResult.Query("a"))
|
||||||
|
assertThat(store.history.first()).containsExactlyElementsIn(expected).inOrder()
|
||||||
|
assertThat(createStore(fileStore).history.first()).containsExactlyElementsIn(expected).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `remove of an unknown entry leaves the history unchanged`() = runTest {
|
||||||
|
val store = createStore()
|
||||||
|
store.add(SearchHistoryResult.Query("a"))
|
||||||
|
|
||||||
|
store.remove(SearchHistoryResult.Query("unknown"))
|
||||||
|
|
||||||
|
assertThat(store.history.first()).containsExactly(SearchHistoryResult.Query("a"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clear removes every entry and deletes the persisted content`() = runTest {
|
fun `clear removes every entry and deletes the persisted content`() = runTest {
|
||||||
val fileStore = FakeSearchHistoryFileStore()
|
val fileStore = FakeSearchHistoryFileStore()
|
||||||
|
|||||||
+4
@@ -22,6 +22,10 @@ class FakeSearchHistoryStore : SearchHistoryStore {
|
|||||||
}.take(MAX_SEARCH_HISTORY_SIZE)
|
}.take(MAX_SEARCH_HISTORY_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(result: SearchHistoryResult) {
|
||||||
|
state.value = state.value.filterNot { it == result }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clear() {
|
override suspend fun clear() {
|
||||||
state.value = emptyList()
|
state.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -109,6 +109,7 @@ fun TextButton(
|
|||||||
showProgress: Boolean = false,
|
showProgress: Boolean = false,
|
||||||
destructive: Boolean = false,
|
destructive: Boolean = false,
|
||||||
leadingIcon: IconSource? = null,
|
leadingIcon: IconSource? = null,
|
||||||
|
colors: ButtonColors = ButtonStyle.Text.getColors(destructive),
|
||||||
) = ButtonInternal(
|
) = ButtonInternal(
|
||||||
text = text,
|
text = text,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -118,7 +119,8 @@ fun TextButton(
|
|||||||
size = size,
|
size = size,
|
||||||
showProgress = showProgress,
|
showProgress = showProgress,
|
||||||
destructive = destructive,
|
destructive = destructive,
|
||||||
leadingIcon = leadingIcon
|
leadingIcon = leadingIcon,
|
||||||
|
colors = colors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
<string name="action_choose_file">"Choose file"</string>
|
<string name="action_choose_file">"Choose file"</string>
|
||||||
<string name="action_choose_photo">"Choose photo"</string>
|
<string name="action_choose_photo">"Choose photo"</string>
|
||||||
<string name="action_clear">"Clear"</string>
|
<string name="action_clear">"Clear"</string>
|
||||||
|
<string name="action_clear_all">"Clear all"</string>
|
||||||
<string name="action_close">"Close"</string>
|
<string name="action_close">"Close"</string>
|
||||||
<string name="action_complete_verification">"Complete verification"</string>
|
<string name="action_complete_verification">"Complete verification"</string>
|
||||||
<string name="action_confirm">"Confirm"</string>
|
<string name="action_confirm">"Confirm"</string>
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
Reference in New Issue
Block a user