Add emoji search to the reaction emoji picker (#5255)
* Add emoji search to the reaction emoji picker * Update screenshots * Fix tests and lint issues. Fixing the tests required addressing some underlying issues in `SearchBar` --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
d977ed25a4
commit
a050f64196
18 changed files with 395 additions and 121 deletions
|
|
@ -11,9 +11,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPicker
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPickerPresenter
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.theme.components.hide
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
|
|
@ -47,9 +50,10 @@ fun CustomReactionBottomSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
|
val presenter = remember { EmojiPickerPresenter(target.emojibaseStore) }
|
||||||
EmojiPicker(
|
EmojiPicker(
|
||||||
onSelectEmoji = ::onEmojiSelectedDismiss,
|
onSelectEmoji = ::onEmojiSelectedDismiss,
|
||||||
emojibaseStore = target.emojibaseStore,
|
state = presenter.present(),
|
||||||
selectedEmojis = state.selectedEmoji,
|
selectedEmojis = state.selectedEmoji,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.SecondaryTabRow
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.emojibasebindings.Emoji
|
|
||||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
|
||||||
import io.element.android.emojibasebindings.EmojibaseDatasource
|
|
||||||
import io.element.android.emojibasebindings.EmojibaseStore
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.text.toSp
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun EmojiPicker(
|
|
||||||
onSelectEmoji: (Emoji) -> Unit,
|
|
||||||
emojibaseStore: EmojibaseStore,
|
|
||||||
selectedEmojis: ImmutableSet<String>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val categories = remember { emojibaseStore.categories }
|
|
||||||
val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size })
|
|
||||||
Column(modifier) {
|
|
||||||
SecondaryTabRow(
|
|
||||||
selectedTabIndex = pagerState.currentPage,
|
|
||||||
) {
|
|
||||||
EmojibaseCategory.entries.forEachIndexed { index, category ->
|
|
||||||
Tab(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = category.icon,
|
|
||||||
contentDescription = stringResource(id = category.title)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
selected = pagerState.currentPage == index,
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) { index ->
|
|
||||||
val category = EmojibaseCategory.entries[index]
|
|
||||||
val emojis = categories[category] ?: listOf()
|
|
||||||
LazyVerticalGrid(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
columns = GridCells.Adaptive(minSize = 48.dp),
|
|
||||||
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
|
||||||
) {
|
|
||||||
items(emojis, key = { it.unicode }) { item ->
|
|
||||||
EmojiItem(
|
|
||||||
modifier = Modifier.aspectRatio(1f),
|
|
||||||
item = item,
|
|
||||||
isSelected = selectedEmojis.contains(item.unicode),
|
|
||||||
onSelectEmoji = onSelectEmoji,
|
|
||||||
emojiSize = 32.dp.toSp(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun EmojiPickerPreview() = ElementPreview {
|
|
||||||
EmojiPicker(
|
|
||||||
onSelectEmoji = {},
|
|
||||||
emojibaseStore = EmojibaseDatasource().load(LocalContext.current),
|
|
||||||
selectedEmojis = persistentSetOf("😀", "😄", "😃"),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SecondaryTabRow
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.emojibasebindings.Emoji
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojiItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.icon
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.title
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.text.toSp
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EmojiPicker(
|
||||||
|
onSelectEmoji: (Emoji) -> Unit,
|
||||||
|
state: EmojiPickerState,
|
||||||
|
selectedEmojis: ImmutableSet<String>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val categories = state.categories
|
||||||
|
val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size })
|
||||||
|
|
||||||
|
Column(modifier) {
|
||||||
|
SearchBar(
|
||||||
|
modifier = Modifier.padding(bottom = 10.dp),
|
||||||
|
query = state.searchQuery,
|
||||||
|
onQueryChange = { state.eventSink(EmojiPickerEvents.UpdateSearchQuery(it)) },
|
||||||
|
resultState = state.searchResults,
|
||||||
|
active = state.isSearchActive,
|
||||||
|
onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) },
|
||||||
|
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
placeHolderTitle = stringResource(CommonStrings.emoji_picker_search_placeholder),
|
||||||
|
) { results ->
|
||||||
|
val emojis = results
|
||||||
|
LazyVerticalGrid(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
items(emojis, key = { it.unicode }) { item ->
|
||||||
|
SelectableEmojiItem(
|
||||||
|
item = item,
|
||||||
|
selectedEmojis = selectedEmojis,
|
||||||
|
onSelectEmoji = onSelectEmoji,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isSearchActive) {
|
||||||
|
SecondaryTabRow(
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
) {
|
||||||
|
EmojibaseCategory.entries.forEachIndexed { index, category ->
|
||||||
|
Tab(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = category.icon,
|
||||||
|
contentDescription = stringResource(id = category.title)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) { index ->
|
||||||
|
val category = EmojibaseCategory.entries[index]
|
||||||
|
val emojis = categories[category] ?: listOf()
|
||||||
|
LazyVerticalGrid(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
items(emojis, key = { it.unicode }) { item ->
|
||||||
|
SelectableEmojiItem(
|
||||||
|
item = item,
|
||||||
|
selectedEmojis = selectedEmojis,
|
||||||
|
onSelectEmoji = onSelectEmoji,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectableEmojiItem(
|
||||||
|
item: Emoji,
|
||||||
|
selectedEmojis: ImmutableSet<String>,
|
||||||
|
onSelectEmoji: (Emoji) -> Unit,
|
||||||
|
) {
|
||||||
|
EmojiItem(
|
||||||
|
modifier = Modifier.aspectRatio(1f),
|
||||||
|
item = item,
|
||||||
|
isSelected = selectedEmojis.contains(item.unicode),
|
||||||
|
onSelectEmoji = onSelectEmoji,
|
||||||
|
emojiSize = 32.dp.toSp(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun EmojiPickerPreview(@PreviewParameter(EmojiPickerStateProvider::class) state: EmojiPickerState) = ElementPreview {
|
||||||
|
EmojiPicker(
|
||||||
|
onSelectEmoji = {},
|
||||||
|
state = state,
|
||||||
|
selectedEmojis = persistentSetOf("😀", "😄", "😃"),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
sealed interface EmojiPickerEvents {
|
||||||
|
data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvents
|
||||||
|
data class UpdateSearchQuery(val query: String) : EmojiPickerEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import io.element.android.emojibasebindings.Emoji
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class EmojiPickerPresenter(
|
||||||
|
private val emojibaseStore: EmojibaseStore,
|
||||||
|
) : Presenter<EmojiPickerState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): EmojiPickerState {
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
|
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(SearchBarResultState.Initial()) }
|
||||||
|
val categories = remember { emojibaseStore.categories }
|
||||||
|
|
||||||
|
LaunchedEffect(searchQuery) {
|
||||||
|
emojiResults = if (searchQuery.isEmpty()) {
|
||||||
|
SearchBarResultState.Initial()
|
||||||
|
} else {
|
||||||
|
// Add a small delay to avoid doing too many computations when the user is typing quickly
|
||||||
|
delay(100.milliseconds)
|
||||||
|
|
||||||
|
val lowercaseQuery = searchQuery.lowercase()
|
||||||
|
val results = withContext(Dispatchers.Default) {
|
||||||
|
emojibaseStore.allEmojis
|
||||||
|
.asSequence()
|
||||||
|
.filter { emoji ->
|
||||||
|
emoji.tags.orEmpty().any { it.contains(lowercaseQuery) } ||
|
||||||
|
emoji.shortcodes.any { it.contains(lowercaseQuery) }
|
||||||
|
}
|
||||||
|
.take(60)
|
||||||
|
.toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchBarResultState.Results(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isInPreview = LocalInspectionMode.current
|
||||||
|
fun handleEvents(event: EmojiPickerEvents) {
|
||||||
|
when (event) {
|
||||||
|
// For some reason, in preview mode the SearchBar emits this event with an `isActive = true` value automatically
|
||||||
|
is EmojiPickerEvents.ToggleSearchActive -> if (!isInPreview) {
|
||||||
|
isSearchActive = event.isActive
|
||||||
|
}
|
||||||
|
is EmojiPickerEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmojiPickerState(
|
||||||
|
categories = categories,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
searchResults = emojiResults,
|
||||||
|
eventSink = ::handleEvents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
import io.element.android.emojibasebindings.Emoji
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
|
||||||
|
data class EmojiPickerState(
|
||||||
|
val categories: ImmutableMap<EmojibaseCategory, ImmutableList<Emoji>>,
|
||||||
|
val searchQuery: String,
|
||||||
|
val isSearchActive: Boolean,
|
||||||
|
val searchResults: SearchBarResultState<ImmutableList<Emoji>>,
|
||||||
|
val eventSink: (EmojiPickerEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.emojibasebindings.Emoji
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
|
|
||||||
|
class EmojiPickerStateProvider : PreviewParameterProvider<EmojiPickerState> {
|
||||||
|
override val values: Sequence<EmojiPickerState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
anEmojiPickerState(),
|
||||||
|
anEmojiPickerState(isSearchActive = true),
|
||||||
|
anEmojiPickerState(isSearchActive = true, searchQuery = "smile"),
|
||||||
|
anEmojiPickerState(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchQuery = "smile",
|
||||||
|
searchResults = SearchBarResultState.Results(
|
||||||
|
persistentListOf(
|
||||||
|
Emoji(
|
||||||
|
"0x00",
|
||||||
|
"grinning face",
|
||||||
|
persistentListOf("grinning"),
|
||||||
|
persistentListOf("smile, grin"),
|
||||||
|
"😀",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
"0x01",
|
||||||
|
"crying face",
|
||||||
|
persistentListOf("crying"),
|
||||||
|
persistentListOf("smile, crying"),
|
||||||
|
"\uD83E\uDD72",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun anEmojiPickerState(
|
||||||
|
categories: ImmutableMap<EmojibaseCategory, ImmutableList<Emoji>> = EmojibaseCategory.entries.associateWith {
|
||||||
|
persistentListOf(
|
||||||
|
Emoji(
|
||||||
|
"0x00",
|
||||||
|
"grinning face",
|
||||||
|
persistentListOf("grinning"),
|
||||||
|
persistentListOf("smile, grin"),
|
||||||
|
"😀",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
"0x01",
|
||||||
|
"crying face",
|
||||||
|
persistentListOf("crying"),
|
||||||
|
persistentListOf("smile, crying"),
|
||||||
|
"\uD83E\uDD72",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.toImmutableMap(),
|
||||||
|
searchQuery: String = "",
|
||||||
|
isSearchActive: Boolean = false,
|
||||||
|
searchResults: SearchBarResultState<ImmutableList<Emoji>> = SearchBarResultState.Initial(),
|
||||||
|
eventSink: (EmojiPickerEvents) -> Unit = {},
|
||||||
|
) = EmojiPickerState(
|
||||||
|
categories = categories,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
searchResults = searchResults,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -23,7 +23,10 @@ import androidx.compose.material3.TextFieldColors
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
|
@ -67,16 +70,19 @@ fun <T> SearchBar(
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
if (!active) {
|
val updatedOnQueryChange by rememberUpdatedState(onQueryChange)
|
||||||
onQueryChange("")
|
LaunchedEffect(active) {
|
||||||
focusManager.clearFocus()
|
if (!active) {
|
||||||
|
updatedOnQueryChange("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
inputField = {
|
inputField = {
|
||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
query = query,
|
query = query,
|
||||||
onQueryChange = onQueryChange,
|
onQueryChange = updatedOnQueryChange,
|
||||||
onSearch = { focusManager.clearFocus() },
|
onSearch = { focusManager.clearFocus() },
|
||||||
expanded = active,
|
expanded = active,
|
||||||
onExpandedChange = onActiveChange,
|
onExpandedChange = onActiveChange,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:af699468bfa61662538fd3b187ebb1490f6a8e692c40bd13fc44beeb26744c93
|
||||||
|
size 21790
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9b7fdc137bb92ef23da15f9caad89a095355c719a4151b10d3f6156d04ab7055
|
||||||
|
size 6807
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a9ea09c61e22ba6bf59a93f06f89ae738d4cde1199fe79d2b0aed86805ddabab
|
||||||
|
size 5636
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:da07b6a6c81f2d819247b77cb6452bf808a6ba9097143e8ca6acf85025ab5b3d
|
||||||
|
size 14438
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d79e3b8f699dab27d180942bc8f68f0193148e0e04602c03ec7592e4b73278f0
|
||||||
|
size 20876
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:12be2dd55a8f06cb5fe3ffcd0364a0cfee77a4ea502a31044c2c201ace026e49
|
||||||
|
size 6702
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3ffe383d6b566d8ea19d335012e8aeff51de67fb75b208532a3c07bc02d76464
|
||||||
|
size 5523
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e8b0196422fbee4892e4909c027de98ca9f98c1d4d9c5d09fe8606b120f0e4de
|
||||||
|
size 14264
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5bbf091e13abc9a1d23fb01419d2fd17cf8c2e9e87cdd25d45e52d3c97ae04f4
|
|
||||||
size 231965
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:cf9a76ede9a298829f4df8f1b6d5abcc25d841dd16be2d9331b1c4da8e84e8f6
|
|
||||||
size 234898
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue