diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index 55d52457357..d98a05321d1 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -33,7 +33,6 @@ import io.element.android.x.BuildConfig import io.element.android.x.R import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.plus import java.io.File @@ -107,11 +106,7 @@ object AppModule { @Provides @SingleIn(AppScope::class) fun providesCoroutineDispatchers(): CoroutineDispatchers { - return CoroutineDispatchers( - io = Dispatchers.IO, - computation = Dispatchers.Default, - main = Dispatchers.Main, - ) + return CoroutineDispatchers.Default } @Provides diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 04a456d71b7..be45ac54563 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -57,6 +57,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -70,6 +71,7 @@ import io.element.android.libraries.matrix.api.core.toThreadId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.matrix.api.recentemojis.AddRecentEmoji import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomInfo @@ -121,6 +123,7 @@ class MessagesPresenter( private val analyticsService: AnalyticsService, private val encryptionService: EncryptionService, private val featureFlagService: FeatureFlagService, + private val addRecentEmoji: AddRecentEmoji, ) : Presenter { @AssistedFactory interface Factory { @@ -398,6 +401,7 @@ class MessagesPresenter( ) = launch(dispatchers.io) { timelineController.invokeOnCurrentTimeline { toggleReaction(emoji, eventOrTransactionId) + .flatMap { added -> if (added) addRecentEmoji(emoji) else Result.success(Unit) } .onFailure { Timber.e(it) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 8217cb977cd..ddd905cb8c3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -49,6 +49,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.aTextEditorStateRich +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf @@ -186,9 +187,11 @@ fun aReactionSummaryState( fun aCustomReactionState( target: CustomReactionState.Target = CustomReactionState.Target.None, + recentEmojis: ImmutableList = persistentListOf(), eventSink: (CustomReactionEvents) -> Unit = {}, ) = CustomReactionState( target = target, + recentEmojis = recentEmojis, selectedEmoji = persistentSetOf(), eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 20dc45ccc8f..e79d1424008 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -73,6 +74,7 @@ class DefaultActionListPresenter( private val userSendFailureFactory: VerifiedUserSendFailureFactory, private val dateFormatter: DateFormatter, private val featureFlagService: FeatureFlagService, + private val getRecentEmojis: GetRecentEmojis, ) : ActionListPresenter { @AssistedFactory @ContributesBinding(RoomScope::class) @@ -153,14 +155,15 @@ class DefaultActionListPresenter( ), displayEmojiReactions = displayEmojiReactions, verifiedUserSendFailure = verifiedUserSendFailure, - actions = actions.toImmutableList() + actions = actions.toImmutableList(), + recentEmojis = getRecentEmojis().getOrNull()?.toImmutableList() ?: persistentListOf() ) } else { target.value = ActionListState.Target.None } } - private suspend fun buildActions( + private fun buildActions( timelineItem: TimelineItem.Event, usersEventPermissions: UserEventPermissions, isDeveloperModeEnabled: Boolean, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt index 8082c3e4159..7524a737ffe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt @@ -26,6 +26,7 @@ data class ActionListState( val event: TimelineItem.Event, val sentTimeFull: String, val displayEmojiReactions: Boolean, + val recentEmojis: ImmutableList, val verifiedUserSendFailure: VerifiedUserSendFailure, val actions: ImmutableList, ) : Target diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 28e62978ded..243df7f0551 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList open class ActionListStateProvider : PreviewParameterProvider { @@ -41,6 +42,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -56,6 +58,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = TimelineItemAction.CopyCaption, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -70,6 +73,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = TimelineItemAction.CopyCaption, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -84,6 +88,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = null, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -98,6 +103,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = TimelineItemAction.CopyCaption, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -112,6 +118,7 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList( copyAction = null, ), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -124,6 +131,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ), ), anActionListState( @@ -148,6 +157,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemPollActionList(), + recentEmojis = persistentListOf(), ), ), anActionListState( @@ -160,6 +170,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), anActionListState( @@ -169,6 +180,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayEmojiReactions = true, verifiedUserSendFailure = anUnsignedDeviceSendFailure(), actions = aTimelineItemActionList(), + recentEmojis = persistentListOf(), ) ), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index bb57cc82d46..a891e9d5874 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -20,9 +21,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api @@ -35,6 +38,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -90,6 +97,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -218,6 +227,7 @@ private fun ActionListViewContent( if (target.displayEmojiReactions) { item { EmojiReactionsRow( + recentEmojis = target.recentEmojis, highlightedEmojis = target.event.reactionsState.highlightedKeys, onEmojiReactionClick = onEmojiReactionClick, onCustomReactionClick = onCustomReactionClick, @@ -335,43 +345,67 @@ private fun MessageSummary( } private val emojiRippleRadius = 24.dp +private val suggestedEmojis = persistentListOf("👍️", "👎️", "🔥", "❤️", "👏") @Composable private fun EmojiReactionsRow( + recentEmojis: ImmutableList, highlightedEmojis: ImmutableList, onEmojiReactionClick: (String) -> Unit, onCustomReactionClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = modifier.padding(horizontal = 24.dp, vertical = 16.dp) + modifier = modifier.padding(end = 16.dp, top = 16.dp, bottom = 16.dp), ) { - // TODO use most recently used emojis here when available from the Rust SDK - val defaultEmojis = sequenceOf( - "👍️", - "👎️", - "🔥", - "❤️", - "👏" - ) - for (emoji in defaultEmojis) { - val isHighlighted = highlightedEmojis.contains(emoji) - EmojiButton( - modifier = Modifier - // Make it appear after the more useful actions for the accessibility service - .semantics { - traversalIndex = 1f - }, - emoji = emoji, - isHighlighted = isHighlighted, - onClick = onEmojiReactionClick - ) + val backgroundColor = ElementTheme.colors.bgCanvasDefault + + val emojis = remember(recentEmojis) { + (suggestedEmojis + recentEmojis.filter { it !in suggestedEmojis }) + .take(100) + .toImmutableList() } - Box( + + LazyRow( modifier = Modifier - .size(48.dp), - contentAlignment = Alignment.Center, + .weight(1f, fill = true) + .drawWithContent { + val gradientWidth = 24.dp.toPx() + val width = size.width + drawContent() + + drawRect( + brush = Brush.horizontalGradient( + 0.0f to Color.Transparent, + 1.0f to backgroundColor, + startX = width - gradientWidth, + endX = width, + ), + topLeft = Offset(width - gradientWidth, 0f), + size = Size(gradientWidth, size.height) + ) + }, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(emojis) { emoji -> + val isHighlighted = highlightedEmojis.contains(emoji) + EmojiButton( + modifier = Modifier + // Make it appear after the more useful actions for the accessibility service + .semantics { + traversalIndex = 1f + }, + emoji = emoji, + isHighlighted = isHighlighted, + onClick = onEmojiReactionClick + ) + } + } + + Box( + modifier = Modifier.padding(end = 10.dp).requiredSize(48.dp), + contentAlignment = Alignment.CenterEnd, ) { Icon( imageVector = CompoundIcons.ReactionAdd(), 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 23abe8ea747..d0c848e3935 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 @@ -17,6 +17,7 @@ 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.core.coroutine.CoroutineDispatchers 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 @@ -50,7 +51,13 @@ fun CustomReactionBottomSheet( sheetState = sheetState, modifier = modifier ) { - val presenter = remember { EmojiPickerPresenter(target.emojibaseStore) } + val presenter = remember { + EmojiPickerPresenter( + emojibaseStore = target.emojibaseStore, + recentEmojis = state.recentEmojis, + coroutineDispatchers = CoroutineDispatchers.Default, + ) + } EmojiPicker( onSelectEmoji = ::onEmojiSelectedDismiss, state = presenter.present(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index b7d674d1de0..ba13c461e49 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt @@ -9,29 +9,39 @@ package io.element.android.features.messages.impl.timeline.components.customreac import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.launch @Inject class CustomReactionPresenter( - private val emojibaseProvider: EmojibaseProvider + private val emojibaseProvider: EmojibaseProvider, + private val getRecentEmojis: GetRecentEmojis, ) : Presenter { @Composable override fun present(): CustomReactionState { + val localCoroutineScope = rememberCoroutineScope() + var recentEmojis by remember { mutableStateOf>(persistentListOf()) } + val target: MutableState = remember { mutableStateOf(CustomReactionState.Target.None) } - val localCoroutineScope = rememberCoroutineScope() fun handleShowCustomReactionSheet(event: TimelineItem.Event) { target.value = CustomReactionState.Target.Loading(event) localCoroutineScope.launch { + recentEmojis = getRecentEmojis().getOrNull().orEmpty().toImmutableList() target.value = CustomReactionState.Target.Success( event = event, emojibaseStore = emojibaseProvider.emojibaseStore @@ -56,9 +66,11 @@ class CustomReactionPresenter( ?.mapNotNull { if (it.isHighlighted) it.key else null } .orEmpty() .toImmutableSet() + return CustomReactionState( target = target.value, selectedEmoji = selectedEmoji, + recentEmojis = recentEmojis, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt index 61fb0d7dde5..9a9a985e621 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt @@ -9,11 +9,13 @@ package io.element.android.features.messages.impl.timeline.components.customreac import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.features.messages.impl.timeline.model.TimelineItem +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet data class CustomReactionState( val target: Target, val selectedEmoji: ImmutableSet, + val recentEmojis: ImmutableList, val eventSink: (CustomReactionEvents) -> Unit, ) { sealed interface Target { 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 index 2bf6afafb5c..83b21df092f 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2025 New Vector Ltd. + * 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. @@ -30,16 +30,16 @@ 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.IconSource import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.launch @@ -53,9 +53,7 @@ fun EmojiPicker( modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() - val categories = state.categories - val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size }) - + val pagerState = rememberPagerState(pageCount = { state.categories.size }) Column(modifier) { SearchBar( modifier = Modifier.padding(bottom = 10.dp), @@ -66,36 +64,31 @@ fun EmojiPicker( 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, - ) - } - } + ) { emojis -> + EmojiResults( + emojis = emojis, + isEmojiSelected = { selectedEmojis.contains(it.unicode) }, + onSelectEmoji = onSelectEmoji, + ) } if (!state.isSearchActive) { SecondaryTabRow( selectedTabIndex = pagerState.currentPage, ) { - EmojibaseCategory.entries.forEachIndexed { index, category -> + state.categories.forEachIndexed { index, category -> Tab( icon = { - Icon( - imageVector = category.icon, - contentDescription = stringResource(id = category.title) - ) + when (category.icon) { + is IconSource.Resource -> Icon( + resourceId = category.icon.id, + contentDescription = stringResource(id = category.titleId) + ) + is IconSource.Vector -> Icon( + imageVector = category.icon.vector, + contentDescription = stringResource(id = category.titleId) + ) + } }, selected = pagerState.currentPage == index, onClick = { @@ -109,41 +102,40 @@ fun EmojiPicker( 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, - ) - } - } + val emojis = state.categories[index].emojis + EmojiResults( + emojis = emojis, + isEmojiSelected = { selectedEmojis.contains(it.unicode) }, + onSelectEmoji = onSelectEmoji, + ) } } } } @Composable -private fun SelectableEmojiItem( - item: Emoji, - selectedEmojis: ImmutableSet, +private fun EmojiResults( + emojis: ImmutableList, + isEmojiSelected: (Emoji) -> Boolean, onSelectEmoji: (Emoji) -> Unit, ) { - EmojiItem( - modifier = Modifier.aspectRatio(1f), - item = item, - isSelected = selectedEmojis.contains(item.unicode), - onSelectEmoji = onSelectEmoji, - emojiSize = 32.dp.toSp(), - ) + 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 = isEmojiSelected(item), + onSelectEmoji = onSelectEmoji, + emojiSize = 32.dp.toSp(), + ) + } + } } @PreviewsDayNight 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 de5b9f17a5c..ce9600b1f7b 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 @@ -14,26 +14,57 @@ 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.compound.tokens.generated.CompoundIcons import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.features.messages.impl.R +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.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.theme.components.IconSource 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, + private val recentEmojis: ImmutableList, + private val coroutineDispatchers: CoroutineDispatchers, ) : 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 } + + val recentEmojiIcon = CompoundIcons.History() + val categories = remember { + val providedCategories = emojibaseStore.categories.map { (category, emojis) -> + EmojiCategory( + titleId = category.title, + icon = IconSource.Vector(category.icon), + emojis = emojis + ) + } + if (recentEmojis.isNotEmpty()) { + val recentEmojis = recentEmojis.mapNotNull { recentEmoji -> + emojibaseStore.allEmojis.find { it.unicode == recentEmoji } + }.toImmutableList() + val recentCategory = + EmojiCategory( + titleId = R.string.emoji_picker_category_recent, + icon = IconSource.Vector(recentEmojiIcon), + emojis = recentEmojis + ) + (listOf(recentCategory) + providedCategories).toImmutableList() + } else { + providedCategories.toImmutableList() + } + } LaunchedEffect(searchQuery) { emojiResults = if (searchQuery.isEmpty()) { @@ -43,7 +74,7 @@ class EmojiPickerPresenter( delay(100.milliseconds) val lowercaseQuery = searchQuery.lowercase() - val results = withContext(Dispatchers.Default) { + val results = withContext(coroutineDispatchers.computation) { emojibaseStore.allEmojis .asSequence() .filter { emoji -> @@ -71,6 +102,7 @@ class EmojiPickerPresenter( return EmojiPickerState( categories = categories, + allEmojis = emojibaseStore.allEmojis, searchQuery = searchQuery, isSearchActive = isSearchActive, searchResults = emojiResults, 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 index 761f2f5bcd5..595349a5035 100644 --- 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 @@ -7,16 +7,26 @@ package io.element.android.features.messages.impl.timeline.components.customreaction.picker +import androidx.annotation.StringRes import io.element.android.emojibasebindings.Emoji -import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.libraries.designsystem.theme.components.IconSource 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 categories: ImmutableList, + val allEmojis: ImmutableList, val searchQuery: String, val isSearchActive: Boolean, val searchResults: SearchBarResultState>, val eventSink: (EmojiPickerEvents) -> Unit, ) + +/** + * Represents a category of emojis with a title id, icon, and the list of associated emojis. + */ +data class EmojiCategory( + @StringRes val titleId: Int, + val icon: IconSource, + val emojis: ImmutableList, +) 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 index 5cff7cf9e03..f248efe8939 100644 --- 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 @@ -10,11 +10,15 @@ package io.element.android.features.messages.impl.timeline.components.customreac import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.features.messages.impl.R +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.icons.CompoundDrawables +import io.element.android.libraries.designsystem.theme.components.IconSource 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 +import kotlinx.collections.immutable.toImmutableList class EmojiPickerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,57 +29,52 @@ class EmojiPickerStateProvider : PreviewParameterProvider { 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 - ), - ) - ) + searchResults = SearchBarResultState.Results(emojiList()) ), ) } +private fun recentEmojisCategory() = EmojiCategory( + titleId = R.string.emoji_picker_category_recent, + icon = IconSource.Resource(CompoundDrawables.ic_compound_history), + emojis = emojiList(), +) + +private fun emojiList(): ImmutableList = 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 - ), + categories: ImmutableList = (listOf(recentEmojisCategory()) + EmojibaseCategory.entries.map { + EmojiCategory( + titleId = it.title, + icon = IconSource.Vector(it.icon), + emojis = emojiList(), ) - }.toImmutableMap(), + }).toImmutableList(), + allEmojis: ImmutableList = categories.flatMap { it.emojis }.toImmutableList(), searchQuery: String = "", isSearchActive: Boolean = false, searchResults: SearchBarResultState> = SearchBarResultState.Initial(), eventSink: (EmojiPickerEvents) -> Unit = {}, ) = EmojiPickerState( categories = categories, + allEmojis = allEmojis, searchQuery = searchQuery, isSearchActive = isSearchActive, searchResults = searchResults, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 52c5ee22596..6c99723e456 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -57,6 +57,7 @@ import io.element.android.libraries.matrix.api.core.toThreadId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.matrix.api.recentemojis.AddRecentEmoji import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -75,6 +76,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser @@ -1269,6 +1271,7 @@ class MessagesPresenterTest { encryptionService: FakeEncryptionService = FakeEncryptionService(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), actionListEventSink: (ActionListEvents) -> Unit = {}, + addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()), ): MessagesPresenter { return MessagesPresenter( room = joinedRoom, @@ -1297,6 +1300,7 @@ class MessagesPresenterTest { encryptionService = encryptionService, analyticsService = analyticsService, featureFlagService = featureFlagService, + addRecentEmoji = addRecentEmoji, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index c716b986b6d..85027eb75e6 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -370,6 +370,7 @@ class MessagesViewTest { displayEmojiReactions = true, actions = persistentListOf(TimelineItemAction.Edit), verifiedUserSendFailure = VerifiedUserSendFailure.None, + recentEmojis = persistentListOf(), ) ), ) @@ -462,6 +463,7 @@ class MessagesViewTest { displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf(TimelineItemAction.Edit), + recentEmojis = persistentListOf(), ), ), customReactionState = aCustomReactionState( @@ -491,6 +493,7 @@ class MessagesViewTest { displayEmojiReactions = true, verifiedUserSendFailure = aChangedIdentitySendFailure(), actions = persistentListOf(), + recentEmojis = persistentListOf(), ), ), timelineState = aTimelineState(eventSink = eventsRecorder) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index e8766798cd0..52118a400da 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -94,7 +94,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -135,7 +136,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -182,7 +184,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -228,7 +231,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -274,7 +278,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -322,7 +327,8 @@ class ActionListPresenterTest { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -370,7 +376,8 @@ class ActionListPresenterTest { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -417,7 +424,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -463,7 +471,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -509,7 +518,8 @@ class ActionListPresenterTest { TimelineItemAction.Pin, TimelineItemAction.CopyText, TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -552,7 +562,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.CopyText, TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -599,7 +610,8 @@ class ActionListPresenterTest { TimelineItemAction.Pin, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -650,7 +662,8 @@ class ActionListPresenterTest { TimelineItemAction.RemoveCaption, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -699,7 +712,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyCaption, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -739,7 +753,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -812,7 +827,8 @@ class ActionListPresenterTest { TimelineItemAction.Pin, TimelineItemAction.CopyText, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -858,7 +874,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -911,7 +928,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyText, TimelineItemAction.ViewSource, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) initialState.eventSink.invoke(ActionListEvents.Clear) @@ -1004,7 +1022,8 @@ class ActionListPresenterTest { TimelineItemAction.Edit, TimelineItemAction.CopyText, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1048,7 +1067,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1091,7 +1111,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1133,7 +1154,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1178,7 +1200,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1215,7 +1238,8 @@ class ActionListPresenterTest { verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( TimelineItemAction.ViewSource - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1292,7 +1316,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1345,7 +1370,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1399,7 +1425,8 @@ class ActionListPresenterTest { TimelineItemAction.CopyLink, TimelineItemAction.Pin, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1450,7 +1477,8 @@ class ActionListPresenterTest { // Can't reply in thread for local events TimelineItemAction.Reply, TimelineItemAction.Redact, - ) + ), + recentEmojis = persistentListOf(), ) ) } @@ -1472,5 +1500,6 @@ private fun createActionListPresenter( dateFormatter = FakeDateFormatter(), timelineMode = timelineMode, featureFlagService = featureFlagService, + getRecentEmojis = { Result.success(persistentListOf()) }, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt index 034d5aa3a8b..e34bbdcbef5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenterTest.kt @@ -23,7 +23,10 @@ class CustomReactionPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) + private val presenter = CustomReactionPresenter( + emojibaseProvider = FakeEmojibaseProvider(), + getRecentEmojis = { Result.success(emptyList()) }, + ) @Test fun `present - handle selecting and de-selecting an event`() = runTest { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt new file mode 100644 index 00000000000..74f72a9c314 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt @@ -0,0 +1,125 @@ +/* + * 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.InternalComposeApi +import androidx.compose.runtime.currentComposer +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EmojiPickerPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `UpdateSearchQuery loads new results`() = runTest { + testPresenter { + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) + + initialState.eventSink(EmojiPickerEvents.UpdateSearchQuery("smile")) + assertThat(awaitItem().searchQuery).isEqualTo("smile") + + val stateWithResults = awaitItem() + assertThat(stateWithResults.searchQuery).isEqualTo("smile") + assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + } + } + + @Test + fun `ToggleSearchActive toggles the search state`() = runTest { + testPresenter { + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.isSearchActive).isFalse() + + initialState.eventSink(EmojiPickerEvents.ToggleSearchActive(true)) + assertThat(awaitItem().isSearchActive).isTrue() + + initialState.eventSink(EmojiPickerEvents.ToggleSearchActive(false)) + assertThat(awaitItem().isSearchActive).isFalse() + } + } + + @Test + fun `recent emojis are automatically added to the categories if present`() = runTest { + val providedCategories = persistentListOf(emojiCategory(EmojibaseCategory.Activity)) + val presenter = createPresenter( + categories = providedCategories, + recentEmojis = persistentListOf("😊"), + ) + testPresenter(presenter) { + skipItems(1) + + val initialState = awaitItem() + assertThat(providedCategories.size).isNotEqualTo(initialState.categories.size) + assertThat(initialState.categories.size).isEqualTo(2) + } + } + + private fun TestScope.createPresenter( + categories: ImmutableList>> = persistentListOf(emojiCategory()), + recentEmojis: ImmutableList = persistentListOf(), + ) = EmojiPickerPresenter( + emojibaseStore = EmojibaseStore(categories.toMap().toPersistentMap()), + recentEmojis = recentEmojis, + coroutineDispatchers = testCoroutineDispatchers(), + ) + + private fun emojiCategory( + category: EmojibaseCategory = EmojibaseCategory.Activity, + emojis: ImmutableList = persistentListOf( + Emoji("1F3C3", "Smile", persistentListOf("smile"), persistentListOf("smile"), "😊", skins = null) + ) + ) = category to emojis + + @OptIn(InternalComposeApi::class) + private suspend fun TestScope.testPresenter( + presenter: EmojiPickerPresenter = createPresenter(), + testBlock: suspend TurbineTestContext.() -> Unit, + ) { + moleculeFlow(RecompositionMode.Immediate) { + // These are needed to load the history icon in the presenter + currentComposer.startProviders(arrayOf( + LocalContext provides InstrumentationRegistry.getInstrumentation().context, + LocalConfiguration provides InstrumentationRegistry.getInstrumentation().context.resources.configuration, + )) + val state = presenter.present() + currentComposer.endProviders() + state + }.test { + testBlock() + } + } +} diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt index 94bfa8e3242..606fe201585 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/CoroutineDispatchers.kt @@ -8,9 +8,18 @@ package io.element.android.libraries.core.coroutine import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers data class CoroutineDispatchers( val io: CoroutineDispatcher, val computation: CoroutineDispatcher, val main: CoroutineDispatcher, -) +) { + companion object { + val Default = CoroutineDispatchers( + io = Dispatchers.IO, + computation = Dispatchers.Default, + main = Dispatchers.Main, + ) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index db4439332b6..5c495c5fd72 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -173,6 +173,16 @@ interface MatrixClient { * Returns the maximum file upload size allowed by the Matrix server. */ suspend fun getMaxFileUploadSize(): Result + + /** + * Returns the list of shared recent emoji reactions for this account. + */ + suspend fun getRecentEmojis(): Result> + + /** + * Adds an emoji to the list of recent emoji reactions for this account. + */ + suspend fun addRecentEmoji(emoji: String): Result } /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt new file mode 100644 index 00000000000..da657ea78a4 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/AddRecentEmoji.kt @@ -0,0 +1,23 @@ +/* + * 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.libraries.matrix.api.recentemojis + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.withContext + +@Inject +class AddRecentEmoji( + private val client: MatrixClient, + private val dispatchers: CoroutineDispatchers, +) { + suspend operator fun invoke(emoji: String): Result = withContext(dispatchers.io) { + client.addRecentEmoji(emoji) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt new file mode 100644 index 00000000000..53adf88c379 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/recentemojis/GetRecentEmojis.kt @@ -0,0 +1,30 @@ +/* + * 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.libraries.matrix.api.recentemojis + +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.withContext + +fun interface GetRecentEmojis { + suspend operator fun invoke(): Result> +} + +@ContributesBinding(SessionScope::class) +@Inject +class DefaultGetRecentEmojis( + private val client: MatrixClient, + private val dispatchers: CoroutineDispatchers, +) : GetRecentEmojis { + override suspend operator fun invoke(): Result> = withContext(dispatchers.io) { + client.getRecentEmojis() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 956e645571f..e3f84e9095c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -708,6 +708,18 @@ class RustMatrixClient( runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() } } + override suspend fun addRecentEmoji(emoji: String): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.addRecentEmoji(emoji) + } + } + + override suspend fun getRecentEmojis(): Result> = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.getRecentEmojis().map { it.emoji } + } + } + private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 4f7d42e5fb8..67e75619720 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -97,6 +97,8 @@ class FakeMatrixClient( override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), private val getMaxUploadSizeResult: () -> Result = { lambdaError() }, private val getJoinedRoomIdsResult: () -> Result> = { Result.success(emptySet()) }, + private val getRecentEmojisLambda: () -> Result> = { Result.success(emptyList()) }, + private val addRecentEmojiLambda: (String) -> Result = { Result.success(Unit) }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -349,4 +351,12 @@ class FakeMatrixClient( override suspend fun getMaxFileUploadSize(): Result { return getMaxUploadSizeResult() } + + override suspend fun addRecentEmoji(emoji: String): Result { + return addRecentEmojiLambda(emoji) + } + + override suspend fun getRecentEmojis(): Result> { + return getRecentEmojisLambda() + } } diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png index 0e59b57f97e..d7254dca407 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e85eb3d968eca031af366d557a1afff7aa6f427f9c5761c4568b807562248fb9 -size 48880 +oid sha256:cefbe57afc5b78b598d004fa9f6cc9d42e00127ffb7900a2f5346eeee0b1531c +size 49183 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png index 743bb729998..789df60a7c9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29276b02a32bfb0eacc9fc4853711f0d43b4e9e4dc927d7066ed0d0b1ee12680 -size 50357 +oid sha256:03fbd054c197e783d2079cf4b3953f1a4f15dc7c38889dc826cb5de3b7c1f4d0 +size 50597 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png index c4ce6ff8840..4568d23092e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:494e17d6cb91effd9351b9e331ffb4d8c4da927b28bfa8e740fdb7d865bc7fe3 -size 43609 +oid sha256:34c5fd10902041682c6ab2c3022ea44977035f7de6ea9844d94876b921327ced +size 43960 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png index d9507346e2d..dca90661571 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ec6dd722d802baa463784587d4d8844edb9a38e623fffe2fc347970e437cac6 -size 46583 +oid sha256:531e376ef91fdb4f535a951222f5e25ecacc7ded6d6693e30551ec68a16dcd33 +size 46911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png index adadb27faf1..c051c9bd748 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:204de297913cef7d13569bb8b51135c6a296ea6eec058dc1ce31ebd547137887 -size 44947 +oid sha256:c18f38af96a7f2d0e6dd30827d9ef5cae25d7c864df81ba763044f782f3bb7c7 +size 45233 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png index 7aa0b405eda..13a4439f269 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66bb9985aee2e4988f3beaf58e7979b0812876570d5905a31406069546aaf0e9 -size 42010 +oid sha256:96472d116b4c961b4dc44e7a5e8fcf9a7751ed7662e8c4a169722ff53a53ceed +size 42380 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png index 5437089abeb..10ee8e348d2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7729240a99b1fbf3caba5c480806f88f441d4c17f796808d6f50979ae76ecbe9 -size 45125 +oid sha256:055df03c64585976d1453c0c3b5b0460cefaf1aaa93a47d17b98cc80fba11f29 +size 45525 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png index 90b6b9c5d86..05e6b2c7b71 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f6bec6647ac0678ef3420e222ea2373c4d618f7eee32347548c0fe35a7620b2 -size 43261 +oid sha256:21a5e088364ec86448df788e0d54c27dcba3c6f696a86e7f97289da0792ede75 +size 43598 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png index 91c0d21b398..36ecb96acc1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a97331fddcbc4c89743e15bd79cc6e753b59be0f33f41de2d8cc184b3e1f3ec -size 45084 +oid sha256:e356e49f62609b0c96b294a51af0c3dba861906d0cf18492460b2d199dcd1003 +size 45368 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png index 6fdacd6bb36..e762a68e922 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48ca8baddefc71d98aa4060d730f7785e654ed078f5c9aa21459df5b730b3acc -size 47969 +oid sha256:a883e703a5f8c9a794ca274e52b493c490815f39c84c7c7b9c9cef348ecfb766 +size 48368 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png index bb894571fbd..67b93b5e8bd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0d19fe576a9cb4895ccae8c3f862239220b9542aa0c4c102cf5a99a0184ad94 -size 49392 +oid sha256:c44806a243336438401a21e265c8d513fff7e4f2cbf68a0c745f5918c89d639a +size 49828 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png index 15896e77fcc..ff279906657 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d81fdfb3340f8d8d63a0529eb2f4d035ebd4789aed0c98272bdbcb9c4340c7c9 -size 42544 +oid sha256:9725a74d334baddaf3946675b871abcc24f20e3518fe572f6eaffdcd5e0bf98e +size 43028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png index 08b5933d268..ddf220b94fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e882786b51e7692f52f1938cf1a29581ec299e4bcb7a3253ba69009140a29233 -size 45837 +oid sha256:78379d56dda45b77f6bcfbf32e12e78f72e88c760d7a0911bb9e8c9f244c39b8 +size 46151 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png index d66e2f116ba..39a4ed61707 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf6bda60c2f68bfb41405919d669b7aec8db408d49593ab0520b5b9afec8627 -size 44217 +oid sha256:b8213b5f1c9620cc8edaaa9971906f09c8b466d42ce253a9f52d30419af2e732 +size 44722 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png index 54af19efc9e..05aa90f6c5f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d68f839ba6d6aa3d9bd9fde6d71a68d95130011aebaf43c87eaf8ff826f48a4d -size 40689 +oid sha256:5c94f65df581c57f83346e9c8d697a50eb95cffa5ac3fc4198017a41b9d72537 +size 41215 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png index 840cf053e82..68632978442 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c139a1a328cef4752219fd11df4c79b19c47501df26278811cb7775269988c4 -size 44501 +oid sha256:9fc2b4034704979785701cb42fe2b967256a6151ac90934cecd5b8b1922f780b +size 44987 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png index 9140f6932e0..cedfe0a8915 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f17461d5b74d2c1c493e58236793978a5e96bf5bf15b2ab0179dd0b39b9e626a -size 41948 +oid sha256:d14b2d18fb60fcabb3e657001c97d95f55b82e87cf8efa34c8a5fb2ab237a359 +size 42385 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png index 9c6ca432dfa..ed3e934f075 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5a920e128caf9d5609d9030e5d37cf951011bd51009c988d84873c993fff95d -size 44331 +oid sha256:5d081025bedebd0993968f3522a6e4266ecd20526284c37794a7180305d07c7c +size 44806 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 index 93a249ccd90..04c099106f1 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af699468bfa61662538fd3b187ebb1490f6a8e692c40bd13fc44beeb26744c93 -size 21790 +oid sha256:65a59efc8cfd19f4dddf0ed9d39c4cab634ebc720c2da7737f382dd2e325ad37 +size 22517 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 index 54208948db5..42c3854905a 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d79e3b8f699dab27d180942bc8f68f0193148e0e04602c03ec7592e4b73278f0 -size 20876 +oid sha256:18b5a57b061028e9b0d265f725e5c2b08439589c778c49f054822ab3baab68bb +size 21551