From e5ebf7fc737d030e82a5106596c15d39eadef6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 2 Sep 2025 10:00:00 +0200 Subject: [PATCH 1/3] Add emoji search to the reaction emoji picker --- .../CustomReactionBottomSheet.kt | 6 +- .../components/customreaction/EmojiPicker.kt | 110 ------------ .../customreaction/picker/EmojiPicker.kt | 158 ++++++++++++++++++ .../picker/EmojiPickerEvents.kt | 13 ++ .../picker/EmojiPickerPresenter.kt | 76 +++++++++ .../customreaction/picker/EmojiPickerState.kt | 22 +++ .../picker/EmojiPickerStateProvider.kt | 83 +++++++++ 7 files changed, 357 insertions(+), 111 deletions(-) delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt index b2bd0de2800..23abe8ea747 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -11,9 +11,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier 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.hide import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -47,9 +50,10 @@ fun CustomReactionBottomSheet( sheetState = sheetState, modifier = modifier ) { + val presenter = remember { EmojiPickerPresenter(target.emojibaseStore) } EmojiPicker( onSelectEmoji = ::onEmojiSelectedDismiss, - emojibaseStore = target.emojibaseStore, + state = presenter.present(), selectedEmojis = state.selectedEmoji, modifier = Modifier.fillMaxSize(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt deleted file mode 100644 index fcaac65e820..00000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt +++ /dev/null @@ -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, - 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(), - ) -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt new file mode 100644 index 00000000000..2bf6afafb5c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt @@ -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, + 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, + 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(), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt new file mode 100644 index 00000000000..53fa6b7b7af --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt @@ -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 +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt new file mode 100644 index 00000000000..d1f604587f1 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt @@ -0,0 +1,76 @@ +/* + * 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 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 { + @Composable + override fun present(): EmojiPickerState { + var searchQuery by remember { mutableStateOf("") } + var isSearchActive by remember { mutableStateOf(false) } + var emojiResults by remember { mutableStateOf>>(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) + } + } + + fun handleEvents(event: EmojiPickerEvents) { + when (event) { + is EmojiPickerEvents.ToggleSearchActive -> isSearchActive = event.isActive + is EmojiPickerEvents.UpdateSearchQuery -> searchQuery = event.query + } + } + + return EmojiPickerState( + categories = categories, + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = emojiResults, + eventSink = ::handleEvents, + ) + } +} + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt new file mode 100644 index 00000000000..761f2f5bcd5 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt @@ -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>, + val searchQuery: String, + val isSearchActive: Boolean, + val searchResults: SearchBarResultState>, + val eventSink: (EmojiPickerEvents) -> Unit, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt new file mode 100644 index 00000000000..5cff7cf9e03 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt @@ -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 { + override val values: Sequence + 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.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> = SearchBarResultState.Initial(), + eventSink: (EmojiPickerEvents) -> Unit = {}, +) = EmojiPickerState( + categories = categories, + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = searchResults, + eventSink = eventSink, +) From 6d81c71d844ea9c179fb055b6811a6efc9ca0f10 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 2 Sep 2025 08:13:56 +0000 Subject: [PATCH 2/3] Update screenshots --- ...e.components.customreaction.picker_EmojiPicker_Day_0_en.png | 3 +++ ...e.components.customreaction.picker_EmojiPicker_Day_1_en.png | 3 +++ ...e.components.customreaction.picker_EmojiPicker_Day_2_en.png | 3 +++ ...e.components.customreaction.picker_EmojiPicker_Day_3_en.png | 3 +++ ...components.customreaction.picker_EmojiPicker_Night_0_en.png | 3 +++ ...components.customreaction.picker_EmojiPicker_Night_1_en.png | 3 +++ ...components.customreaction.picker_EmojiPicker_Night_2_en.png | 3 +++ ...components.customreaction.picker_EmojiPicker_Night_3_en.png | 3 +++ ...timeline.components.customreaction_EmojiPicker_Day_0_en.png | 3 --- ...meline.components.customreaction_EmojiPicker_Night_0_en.png | 3 --- 10 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png new file mode 100644 index 00000000000..93a249ccd90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af699468bfa61662538fd3b187ebb1490f6a8e692c40bd13fc44beeb26744c93 +size 21790 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png new file mode 100644 index 00000000000..045c8dba15c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b7fdc137bb92ef23da15f9caad89a095355c719a4151b10d3f6156d04ab7055 +size 6807 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png new file mode 100644 index 00000000000..3e93b95261a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9ea09c61e22ba6bf59a93f06f89ae738d4cde1199fe79d2b0aed86805ddabab +size 5636 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png new file mode 100644 index 00000000000..cc8c7de1bad --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da07b6a6c81f2d819247b77cb6452bf808a6ba9097143e8ca6acf85025ab5b3d +size 14438 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png new file mode 100644 index 00000000000..54208948db5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d79e3b8f699dab27d180942bc8f68f0193148e0e04602c03ec7592e4b73278f0 +size 20876 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png new file mode 100644 index 00000000000..493c2947a16 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12be2dd55a8f06cb5fe3ffcd0364a0cfee77a4ea502a31044c2c201ace026e49 +size 6702 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png new file mode 100644 index 00000000000..136b09b57ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ffe383d6b566d8ea19d335012e8aeff51de67fb75b208532a3c07bc02d76464 +size 5523 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png new file mode 100644 index 00000000000..cc96f69e93d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8b0196422fbee4892e4909c027de98ca9f98c1d4d9c5d09fe8606b120f0e4de +size 14264 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png deleted file mode 100644 index aaef7bf5552..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5bbf091e13abc9a1d23fb01419d2fd17cf8c2e9e87cdd25d45e52d3c97ae04f4 -size 231965 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png deleted file mode 100644 index 46be57e49be..00000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf9a76ede9a298829f4df8f1b6d5abcc25d841dd16be2d9331b1c4da8e84e8f6 -size 234898 From 60e266b9116d16e7a767884eec07f5553fdb37cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 2 Sep 2025 12:23:39 +0200 Subject: [PATCH 3/3] Fix tests and lint issues. Fixing the tests required addressing some underlying issues in `SearchBar` --- .../customreaction/picker/EmojiPickerPresenter.kt | 8 ++++++-- .../designsystem/theme/components/SearchBar.kt | 14 ++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt index d1f604587f1..de5b9f17a5c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt @@ -13,6 +13,7 @@ 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 @@ -57,9 +58,13 @@ class EmojiPickerPresenter( } } + val isInPreview = LocalInspectionMode.current fun handleEvents(event: EmojiPickerEvents) { when (event) { - is EmojiPickerEvents.ToggleSearchActive -> isSearchActive = event.isActive + // 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 } } @@ -73,4 +78,3 @@ class EmojiPickerPresenter( ) } } - diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index e75b7727ebf..bfceb2263e2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -23,7 +23,10 @@ import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -67,16 +70,19 @@ fun SearchBar( ) { val focusManager = LocalFocusManager.current - if (!active) { - onQueryChange("") - focusManager.clearFocus() + val updatedOnQueryChange by rememberUpdatedState(onQueryChange) + LaunchedEffect(active) { + if (!active) { + updatedOnQueryChange("") + focusManager.clearFocus() + } } SearchBar( inputField = { SearchBarDefaults.InputField( query = query, - onQueryChange = onQueryChange, + onQueryChange = updatedOnQueryChange, onSearch = { focusManager.clearFocus() }, expanded = active, onExpandedChange = onActiveChange,