From e492f0638667f1f1a4af573fef442e161dbbc77a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Jul 2026 22:09:01 +0200 Subject: [PATCH 01/10] Migrate libraries/recentemojis into libraries/emoji --- .../io/element/android/x/di/AppModule.kt | 10 ++----- features/messages/impl/build.gradle.kts | 5 ++-- .../messages/impl/MessagesPresenter.kt | 2 +- .../impl/actionlist/ActionListPresenter.kt | 2 +- .../customreaction/CustomReactionPresenter.kt | 4 +-- .../messages/impl/MessagesPresenterTest.kt | 2 +- .../actionlist/ActionListPresenterTest.kt | 2 +- .../CustomReactionPresenterTest.kt | 2 +- .../api/build.gradle.kts | 7 ++--- .../libraries/emoji}/api/EmojibaseProvider.kt | 2 +- .../emoji/api/recentemojis}/AddRecentEmoji.kt | 2 +- .../api/recentemojis}/GetRecentEmojis.kt | 2 +- .../impl/build.gradle.kts | 28 +++++++++++-------- .../emoji}/impl/DefaultEmojibaseProvider.kt | 12 ++++++-- .../recentemojis}/DefaultAddRecentEmoji.kt | 4 +-- .../recentemojis}/DefaultGetRecentEmojis.kt | 6 ++-- .../DefaultGetRecentEmojisTest.kt | 4 +-- .../test/build.gradle.kts | 9 +++--- .../emoji}/test/FakeEmojibaseProvider.kt | 4 +-- .../kotlin/extension/DependencyHandleScope.kt | 2 +- 20 files changed, 57 insertions(+), 54 deletions(-) rename libraries/{recentemojis => emoji}/api/build.gradle.kts (69%) rename libraries/{recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis => emoji/api/src/main/kotlin/io/element/android/libraries/emoji}/api/EmojibaseProvider.kt (87%) rename libraries/{recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api => emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis}/AddRecentEmoji.kt (84%) rename libraries/{recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api => emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis}/GetRecentEmojis.kt (88%) rename libraries/{recentemojis => emoji}/impl/build.gradle.kts (51%) rename libraries/{recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis => emoji/impl/src/main/kotlin/io/element/android/libraries/emoji}/impl/DefaultEmojibaseProvider.kt (52%) rename libraries/{recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl => emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis}/DefaultAddRecentEmoji.kt (86%) rename libraries/{recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl => emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis}/DefaultGetRecentEmojis.kt (87%) rename libraries/{recentemojis/impl/src/test/kotlin/io/element/android/libraries/recentemojis/impl => emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis}/DefaultGetRecentEmojisTest.kt (95%) rename libraries/{recentemojis => emoji}/test/build.gradle.kts (70%) rename libraries/{recentemojis/test/src/main/kotlin/io/element/android/libraries/recentemojis => emoji/test/src/main/kotlin/io/element/android/libraries/emoji}/test/FakeEmojibaseProvider.kt (85%) 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 87d0ece7a1..80d68bd17d 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 @@ -28,8 +28,8 @@ import io.element.android.libraries.di.BaseDirectory import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext -import io.element.android.libraries.recentemojis.api.EmojibaseProvider -import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider +import io.element.android.libraries.emoji.api.EmojibaseProvider +import io.element.android.libraries.emoji.impl.DefaultEmojibaseProvider import io.element.android.x.BuildConfig import io.element.android.x.R import kotlinx.coroutines.CoroutineName @@ -115,10 +115,4 @@ object AppModule { fun provideSnackbarDispatcher(): SnackbarDispatcher { return SnackbarDispatcher() } - - @Provides - @SingleIn(AppScope::class) - fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider { - return DefaultEmojibaseProvider(context) - } } diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 116a3a7c41..b40aebab39 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) - implementation(projects.libraries.recentemojis.api) + implementation(projects.libraries.emoji.api) implementation(projects.libraries.roomselect.api) implementation(projects.libraries.slashcommands.api) implementation(projects.libraries.audio.api) @@ -106,6 +106,7 @@ dependencies { testImplementation(projects.libraries.testtags) testImplementation(projects.features.poll.test) testImplementation(projects.libraries.eventformatter.test) - testImplementation(projects.libraries.recentemojis.test) + testImplementation(projects.libraries.emoji.impl) + testImplementation(projects.libraries.emoji.test) testImplementation(projects.libraries.slashcommands.test) } 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 1109b2c70d..17127ff6ec 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 @@ -69,6 +69,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.emoji.api.recentemojis.AddRecentEmoji import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.toThreadId @@ -85,7 +86,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.room.getDirectRoomMember -import io.element.android.libraries.recentemojis.api.AddRecentEmoji import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService 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 f17a841c52..8caa95f31d 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 @@ -44,13 +44,13 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis 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.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import io.element.android.libraries.recentemojis.api.GetRecentEmojis import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList 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 a1ebb66ea7..1fddf4e3bb 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 @@ -18,8 +18,8 @@ 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.recentemojis.api.EmojibaseProvider -import io.element.android.libraries.recentemojis.api.GetRecentEmojis +import io.element.android.libraries.emoji.api.EmojibaseProvider +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableSet 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 65aa1d857e..0a0d310b16 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 @@ -52,6 +52,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.emoji.api.recentemojis.AddRecentEmoji import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId @@ -94,7 +95,6 @@ import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListServ import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.recentemojis.api.AddRecentEmoji import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown 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 20b636081a..fb597df290 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 @@ -27,6 +27,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.features.poll.api.pollcontent.aPollAnswerItemList import io.element.android.libraries.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.notification.CallIntent @@ -42,7 +43,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore -import io.element.android.libraries.recentemojis.api.GetRecentEmojis import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.collections.immutable.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 e3317b3d42..d0765bd5e7 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 @@ -11,8 +11,8 @@ package io.element.android.features.messages.impl.timeline.components.customreac import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions +import io.element.android.libraries.emoji.test.FakeEmojibaseProvider import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.recentemojis.test.FakeEmojibaseProvider import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentListOf diff --git a/libraries/recentemojis/api/build.gradle.kts b/libraries/emoji/api/build.gradle.kts similarity index 69% rename from libraries/recentemojis/api/build.gradle.kts rename to libraries/emoji/api/build.gradle.kts index 2fc74c03c7..749e13a4c7 100644 --- a/libraries/recentemojis/api/build.gradle.kts +++ b/libraries/emoji/api/build.gradle.kts @@ -1,17 +1,16 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { - namespace = "io.element.android.libraries.recentemojis.api" + namespace = "io.element.android.libraries.emoji.api" } dependencies { diff --git a/libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/EmojibaseProvider.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/EmojibaseProvider.kt similarity index 87% rename from libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/EmojibaseProvider.kt rename to libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/EmojibaseProvider.kt index 9d6e41eb55..d8f9c92777 100644 --- a/libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/EmojibaseProvider.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/EmojibaseProvider.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.api +package io.element.android.libraries.emoji.api import io.element.android.emojibasebindings.EmojibaseStore diff --git a/libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/AddRecentEmoji.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt similarity index 84% rename from libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/AddRecentEmoji.kt rename to libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt index 5b50823f77..5865568511 100644 --- a/libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/AddRecentEmoji.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.api +package io.element.android.libraries.emoji.api.recentemojis fun interface AddRecentEmoji { suspend operator fun invoke(emoji: String): Result diff --git a/libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/GetRecentEmojis.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt similarity index 88% rename from libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/GetRecentEmojis.kt rename to libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt index 6517512f44..9a9983805e 100644 --- a/libraries/recentemojis/api/src/main/kotlin/io/element/android/libraries/recentemojis/api/GetRecentEmojis.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.api +package io.element.android.libraries.emoji.api.recentemojis import kotlinx.collections.immutable.ImmutableList diff --git a/libraries/recentemojis/impl/build.gradle.kts b/libraries/emoji/impl/build.gradle.kts similarity index 51% rename from libraries/recentemojis/impl/build.gradle.kts rename to libraries/emoji/impl/build.gradle.kts index 061a7ecd89..871c19a41a 100644 --- a/libraries/recentemojis/impl/build.gradle.kts +++ b/libraries/emoji/impl/build.gradle.kts @@ -1,37 +1,41 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. */ import extension.setupDependencyInjection +import extension.testCommonDependencies plugins { id("io.element.android-compose-library") - id("kotlin-parcelize") } android { - namespace = "io.element.android.libraries.recentemojis.impl" + namespace = "io.element.android.libraries.emoji.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupDependencyInjection() dependencies { - api(projects.libraries.recentemojis.api) + api(projects.libraries.emoji.api) implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.uiStrings) + implementation(libs.kotlinx.collections.immutable) implementation(libs.matrix.emojibase.bindings) - testImplementation(projects.libraries.recentemojis.test) - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) + testImplementation(projects.libraries.emoji.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.tests.testutils) + testCommonDependencies(libs) } diff --git a/libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultEmojibaseProvider.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt similarity index 52% rename from libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultEmojibaseProvider.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt index 161dd0cc56..f0ad797648 100644 --- a/libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultEmojibaseProvider.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt @@ -6,14 +6,20 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.impl +package io.element.android.libraries.emoji.impl import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.emojibasebindings.EmojibaseStore -import io.element.android.libraries.recentemojis.api.EmojibaseProvider +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.emoji.api.EmojibaseProvider -class DefaultEmojibaseProvider(val context: Context) : EmojibaseProvider { +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultEmojibaseProvider(@ApplicationContext val context: Context) : EmojibaseProvider { override val emojibaseStore: EmojibaseStore by lazy { EmojibaseDatasource().load(context) } diff --git a/libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultAddRecentEmoji.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultAddRecentEmoji.kt similarity index 86% rename from libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultAddRecentEmoji.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultAddRecentEmoji.kt index 4444d7e01a..cc9da75fd8 100644 --- a/libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultAddRecentEmoji.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultAddRecentEmoji.kt @@ -6,13 +6,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.impl +package io.element.android.libraries.emoji.impl.recentemojis import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.emoji.api.recentemojis.AddRecentEmoji import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.recentemojis.api.AddRecentEmoji import kotlinx.coroutines.withContext @ContributesBinding(SessionScope::class) diff --git a/libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultGetRecentEmojis.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt similarity index 87% rename from libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultGetRecentEmojis.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt index 9c5ed178dd..acd5e012ce 100644 --- a/libraries/recentemojis/impl/src/main/kotlin/io/element/android/libraries/recentemojis/impl/DefaultGetRecentEmojis.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt @@ -6,14 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.impl +package io.element.android.libraries.emoji.impl.recentemojis import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.emoji.api.EmojibaseProvider +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.recentemojis.api.EmojibaseProvider -import io.element.android.libraries.recentemojis.api.GetRecentEmojis import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.withContext diff --git a/libraries/recentemojis/impl/src/test/kotlin/io/element/android/libraries/recentemojis/impl/DefaultGetRecentEmojisTest.kt b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt similarity index 95% rename from libraries/recentemojis/impl/src/test/kotlin/io/element/android/libraries/recentemojis/impl/DefaultGetRecentEmojisTest.kt rename to libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt index 722aaee234..2ae8a86c25 100644 --- a/libraries/recentemojis/impl/src/test/kotlin/io/element/android/libraries/recentemojis/impl/DefaultGetRecentEmojisTest.kt +++ b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt @@ -6,14 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.impl +package io.element.android.libraries.emoji.impl.recentemojis import com.google.common.truth.Truth.assertThat import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseCategory.People +import io.element.android.libraries.emoji.test.FakeEmojibaseProvider import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.recentemojis.test.FakeEmojibaseProvider import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap diff --git a/libraries/recentemojis/test/build.gradle.kts b/libraries/emoji/test/build.gradle.kts similarity index 70% rename from libraries/recentemojis/test/build.gradle.kts rename to libraries/emoji/test/build.gradle.kts index 7c32d162ee..42192c8061 100644 --- a/libraries/recentemojis/test/build.gradle.kts +++ b/libraries/emoji/test/build.gradle.kts @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. @@ -11,16 +10,16 @@ plugins { } android { - namespace = "io.element.android.libraries.recentemojis.test" + namespace = "io.element.android.libraries.emoji.test" } dependencies { + api(projects.libraries.emoji.api) api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.kotlinx.collections.immutable) implementation(libs.coroutines.test) - implementation(projects.tests.testutils) - implementation(projects.libraries.recentemojis.api) implementation(libs.matrix.emojibase.bindings) + implementation(projects.tests.testutils) } diff --git a/libraries/recentemojis/test/src/main/kotlin/io/element/android/libraries/recentemojis/test/FakeEmojibaseProvider.kt b/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojibaseProvider.kt similarity index 85% rename from libraries/recentemojis/test/src/main/kotlin/io/element/android/libraries/recentemojis/test/FakeEmojibaseProvider.kt rename to libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojibaseProvider.kt index 4774144707..0e7c42e538 100644 --- a/libraries/recentemojis/test/src/main/kotlin/io/element/android/libraries/recentemojis/test/FakeEmojibaseProvider.kt +++ b/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojibaseProvider.kt @@ -6,12 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.recentemojis.test +package io.element.android.libraries.emoji.test import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseStore -import io.element.android.libraries.recentemojis.api.EmojibaseProvider +import io.element.android.libraries.emoji.api.EmojibaseProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentMap diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 3e01ffc25f..babd9f27ff 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -123,7 +123,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:wellknown:impl")) implementation(project(":libraries:oauth:impl")) implementation(project(":libraries:workmanager:impl")) - implementation(project(":libraries:recentemojis:impl")) + implementation(project(":libraries:emoji:impl")) } fun DependencyHandlerScope.allServicesImpl() { From 5d8290b24001cbd83e4e17b8b295ba3cca51003d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Jul 2026 22:11:23 +0200 Subject: [PATCH 02/10] Extract EmojiPicker into libraries/emoji with Renderer/Presenter pattern --- .../io/element/android/x/di/AppModule.kt | 2 - features/messages/impl/build.gradle.kts | 1 - .../features/messages/impl/MessagesNode.kt | 12 ++ .../messages/impl/MessagesStateProvider.kt | 3 - .../features/messages/impl/MessagesView.kt | 11 +- .../MessagesViewWithIdentityChangePreview.kt | 1 + .../impl/threads/ThreadedMessagesNode.kt | 1 + .../CustomReactionBottomSheet.kt | 69 +++++---- .../customreaction/CustomReactionPresenter.kt | 67 ++++----- .../customreaction/CustomReactionState.kt | 6 +- .../picker/EmojiPickerPresenter.kt | 114 --------------- .../src/main/res/values-be/translations.xml | 8 -- .../src/main/res/values-bg/translations.xml | 8 -- .../src/main/res/values-ca/translations.xml | 9 -- .../src/main/res/values-cs/translations.xml | 9 -- .../src/main/res/values-cy/translations.xml | 9 -- .../src/main/res/values-da/translations.xml | 9 -- .../src/main/res/values-de/translations.xml | 9 -- .../src/main/res/values-el/translations.xml | 9 -- .../src/main/res/values-es/translations.xml | 8 -- .../src/main/res/values-et/translations.xml | 9 -- .../src/main/res/values-eu/translations.xml | 8 -- .../src/main/res/values-fa/translations.xml | 9 -- .../src/main/res/values-fi/translations.xml | 9 -- .../src/main/res/values-fr/translations.xml | 9 -- .../src/main/res/values-hr/translations.xml | 9 -- .../src/main/res/values-hu/translations.xml | 9 -- .../src/main/res/values-in/translations.xml | 9 -- .../src/main/res/values-it/translations.xml | 9 -- .../src/main/res/values-ja/translations.xml | 9 -- .../src/main/res/values-ka/translations.xml | 8 -- .../src/main/res/values-ko/translations.xml | 9 -- .../src/main/res/values-lt/translations.xml | 8 -- .../src/main/res/values-nb/translations.xml | 9 -- .../src/main/res/values-nl/translations.xml | 8 -- .../src/main/res/values-pl/translations.xml | 9 -- .../main/res/values-pt-rBR/translations.xml | 9 -- .../src/main/res/values-pt/translations.xml | 9 -- .../src/main/res/values-ro/translations.xml | 9 -- .../src/main/res/values-ru/translations.xml | 9 -- .../src/main/res/values-sk/translations.xml | 9 -- .../src/main/res/values-sv/translations.xml | 8 -- .../src/main/res/values-tr/translations.xml | 8 -- .../src/main/res/values-uk/translations.xml | 9 -- .../src/main/res/values-ur/translations.xml | 8 -- .../src/main/res/values-uz/translations.xml | 9 -- .../src/main/res/values-vi/translations.xml | 9 -- .../main/res/values-zh-rTW/translations.xml | 9 -- .../src/main/res/values-zh/translations.xml | 9 -- .../impl/src/main/res/values/localazy.xml | 9 -- .../messages/impl/MessagesViewTest.kt | 41 ++++-- .../CustomReactionPresenterTest.kt | 6 +- .../picker/EmojiPickerPresenterTest.kt | 125 ---------------- .../emoji/api/picker/EmojiPickerPresenter.kt | 12 ++ .../emoji/api/picker/EmojiPickerRenderer.kt | 33 +++++ .../emoji/api/picker/EmojiPickerState.kt | 20 +++ libraries/emoji/impl/build.gradle.kts | 1 - .../emoji/impl/DefaultEmojibaseProvider.kt | 18 ++- .../emoji/impl}/EmojibaseProvider.kt | 7 +- .../picker/DefaultEmojiPickerPresenter.kt | 133 ++++++++++++++++++ .../impl/picker/DefaultEmojiPickerRenderer.kt | 43 ++++++ .../impl/picker/DefaultEmojiPickerState.kt | 16 +-- .../picker/DefaultEmojiPickerStateProvider.kt | 56 ++++---- .../libraries/emoji/impl/picker}/EmojiItem.kt | 28 +--- .../emoji/impl}/picker/EmojiPickerEvent.kt | 4 +- .../emoji/impl/picker/EmojiPickerView.kt | 18 ++- .../emoji/impl/picker}/EmojibaseExtensions.kt | 8 +- .../emoji/impl}/picker/SkinTonePicker.kt | 10 +- .../recentemojis/DefaultGetRecentEmojis.kt | 4 +- .../impl/src/main/res/values/temporary.xml | 13 ++ .../impl/fixtures}/FakeEmojibaseProvider.kt | 12 +- .../impl/picker/EmojiPickerPresenterTest.kt | 112 +++++++++++++++ .../DefaultGetRecentEmojisTest.kt | 2 +- libraries/emoji/test/build.gradle.kts | 25 ---- 74 files changed, 571 insertions(+), 804 deletions(-) delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt delete mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt create mode 100644 libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt create mode 100644 libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt create mode 100644 libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerState.kt rename libraries/emoji/{api/src/main/kotlin/io/element/android/libraries/emoji/api => impl/src/main/kotlin/io/element/android/libraries/emoji/impl}/EmojibaseProvider.kt (60%) create mode 100644 libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt create mode 100644 libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerRenderer.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerState.kt (72%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt (58%) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker}/EmojiItem.kt (86%) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl}/picker/EmojiPickerEvent.kt (71%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt (91%) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker}/EmojibaseExtensions.kt (91%) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction => libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl}/picker/SkinTonePicker.kt (95%) create mode 100644 libraries/emoji/impl/src/main/res/values/temporary.xml rename libraries/emoji/{test/src/main/kotlin/io/element/android/libraries/emoji/test => impl/src/test/kotlin/io/element/android/libraries/emoji/impl/fixtures}/FakeEmojibaseProvider.kt (60%) create mode 100644 libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerPresenterTest.kt delete mode 100644 libraries/emoji/test/build.gradle.kts 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 80d68bd17d..7564db5d95 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 @@ -28,8 +28,6 @@ import io.element.android.libraries.di.BaseDirectory import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext -import io.element.android.libraries.emoji.api.EmojibaseProvider -import io.element.android.libraries.emoji.impl.DefaultEmojibaseProvider import io.element.android.x.BuildConfig import io.element.android.x.R import kotlinx.coroutines.CoroutineName diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index b40aebab39..d442e1bb95 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -107,6 +107,5 @@ dependencies { testImplementation(projects.features.poll.test) testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.emoji.impl) - testImplementation(projects.libraries.emoji.test) testImplementation(projects.libraries.slashcommands.test) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 18c02d8f40..d4fce982e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -54,6 +55,7 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -100,6 +102,7 @@ class MessagesNode( private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer, private val roomMemberModerationRenderer: RoomMemberModerationRenderer, private val eventContentValidationCache: EventContentValidationCache, + private val emojiPickerRenderer: EmojiPickerRenderer, ) : Node(buildContext, plugins = plugins), MessagesNavigator { data class Inputs( val focusedEventId: EventId?, @@ -329,6 +332,15 @@ class MessagesNode( onViewRequestsClick = callback::navigateToKnockRequestsList, ) }, + customReactionBottomSheet = { + CustomReactionBottomSheet( + state = state.customReactionState, + onSelectEmoji = { uniqueId, emoji -> + state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId)) + }, + emojiPickerRenderer = emojiPickerRenderer, + ) + }, onThreadsListClick = callback::navigateToThreadsList, ) roomMemberModerationRenderer.Render( 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 6389089e07..f47e76fbef 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 @@ -53,7 +53,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.libraries.textcomposer.model.aTextEditorStateRich -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf @@ -193,11 +192,9 @@ fun aReactionSummaryState( fun aCustomReactionState( target: CustomReactionState.Target = CustomReactionState.Target.None, - recentEmojis: ImmutableList = persistentListOf(), eventSink: (CustomReactionEvent) -> 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/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index ec014a4ff6..74d6c71aab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -81,7 +81,6 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemDaySepara import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineState import io.element.android.features.messages.impl.timeline.components.CallMenuItem -import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView @@ -149,6 +148,7 @@ fun MessagesView( modifier: Modifier = Modifier, forceJumpToBottomVisibility: Boolean = false, knockRequestsBannerView: @Composable () -> Unit, + customReactionBottomSheet: @Composable () -> Unit, ) { val eventContentValidationState = LocalEventContentValidationState.current @@ -409,12 +409,7 @@ fun MessagesView( }, ) - CustomReactionBottomSheet( - state = state.customReactionState, - onSelectEmoji = { uniqueId, emoji -> - state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId)) - } - ) + customReactionBottomSheet() ReactionSummaryView(state = state.reactionSummaryState) ReadReceiptBottomSheet( @@ -669,6 +664,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onViewAllPinnedMessagesClick = { }, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, + customReactionBottomSheet = {}, onThreadsListClick = {}, ) } @@ -726,5 +722,6 @@ internal fun MessagesViewA11yPreview() = ElementPreview { onThreadsListClick = {}, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, + customReactionBottomSheet = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index d7dd95eb95..4e9bf9249a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -43,6 +43,7 @@ internal fun MessagesViewWithIdentityChangePreview( onJoinCallClick = {}, onViewAllPinnedMessagesClick = {}, knockRequestsBannerView = {}, + customReactionBottomSheet = {}, onThreadsListClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index c3e06b880b..b56268eb02 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -322,6 +322,7 @@ class ThreadedMessagesNode( onViewAllPinnedMessagesClick = {}, modifier = modifier, knockRequestsBannerView = {}, + customReactionBottomSheet = {}, onThreadsListClick = {}, ) 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 5b66a7a9ba..cc971e16ba 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 @@ -13,15 +13,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetState 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.core.coroutine.CoroutineDispatchers +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.hide +import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @OptIn(ExperimentalMaterial3Api::class) @@ -29,46 +27,45 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa fun CustomReactionBottomSheet( state: CustomReactionState, onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit, + emojiPickerRenderer: EmojiPickerRenderer, modifier: Modifier = Modifier, ) { - val sheetState = rememberBottomSheetState( - initialValue = SheetValue.Hidden, - ) + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden) val coroutineScope = rememberCoroutineScope() - val target = state.target as? CustomReactionState.Target.Success - fun onDismiss() { - state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) - } + when (state.target) { + CustomReactionState.Target.None, + is CustomReactionState.Target.Loading -> Unit + is CustomReactionState.Target.Success -> { - fun onEmojiSelectedDismiss(emoji: Emoji) { - if (target?.event == null) return - sheetState.hide(coroutineScope) { - state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) - onSelectEmoji(target.event.eventOrTransactionId, emoji) - } - } + fun onEmojiSelectedDismiss(emoji: Emoji) { + sheetState.hide(coroutineScope) { + state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) + onSelectEmoji(state.target.event.eventOrTransactionId, emoji) + } + } - if (target?.emojibaseStore != null && target.event.eventId != null) { - ModalBottomSheet( - onDismissRequest = ::onDismiss, - sheetState = sheetState, - modifier = modifier, - scrollable = false, - ) { - val presenter = remember { - EmojiPickerPresenter( - emojibaseStore = target.emojibaseStore, - recentEmojis = state.recentEmojis, - coroutineDispatchers = CoroutineDispatchers.Default, + fun onDismiss() { + state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) + } + + + ModalBottomSheet( + onDismissRequest = ::onDismiss, + sheetState = sheetState, + modifier = modifier, + scrollable = false, + ) { + emojiPickerRenderer.Render( + state = state.target.emojiPickerState, + onSelectEmoji = ::onEmojiSelectedDismiss, + selectedEmojis = state.selectedEmoji, + modifier = Modifier.fillMaxSize(), + contentDescription = { emoji, isSelected -> + a11yReactionAction(emoji = emoji.unicode, userAlreadyReacted = isSelected) + }, ) } - EmojiPicker( - onSelectEmoji = ::onEmojiSelectedDismiss, - 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/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index 1fddf4e3bb..7a9f5fe4ff 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,59 +9,50 @@ package io.element.android.features.messages.impl.timeline.components.customreaction 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.emoji.api.EmojibaseProvider -import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf +import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.launch @Inject class CustomReactionPresenter( - private val emojibaseProvider: EmojibaseProvider, - private val getRecentEmojis: GetRecentEmojis, + private val emojiPickerPresenter: EmojiPickerPresenter, ) : Presenter { @Composable override fun present(): CustomReactionState { - val localCoroutineScope = rememberCoroutineScope() - var recentEmojis by remember { mutableStateOf>(persistentListOf()) } - - val target: MutableState = remember { - mutableStateOf(CustomReactionState.Target.None) - } - - fun handleShowCustomReactionSheet(event: TimelineItem.Event) { - target.value = CustomReactionState.Target.Loading(event) - localCoroutineScope.launch { - recentEmojis = getRecentEmojis().getOrNull() ?: persistentListOf() - target.value = CustomReactionState.Target.Success( - event = event, - emojibaseStore = emojibaseProvider.emojibaseStore - ) - } - } - - fun handleDismissCustomReactionSheet() { - target.value = CustomReactionState.Target.None - } + var internalTarget by remember { mutableStateOf(InternalTarget.None) } + val emojiPickerState = emojiPickerPresenter.present() fun handleEvent(event: CustomReactionEvent) { when (event) { - is CustomReactionEvent.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.event) - is CustomReactionEvent.DismissCustomReactionSheet -> handleDismissCustomReactionSheet() + is CustomReactionEvent.ShowCustomReactionSheet -> { + internalTarget = InternalTarget.Showing(event.event) + } + is CustomReactionEvent.DismissCustomReactionSheet -> { + internalTarget = InternalTarget.None + } } } - val event = (target.value as? CustomReactionState.Target.Success)?.event - val selectedEmoji = event + + val computedTarget = when (val target = internalTarget) { + InternalTarget.None -> CustomReactionState.Target.None + is InternalTarget.Showing -> if (emojiPickerState.isReady) { + CustomReactionState.Target.Success( + event = target.event, + emojiPickerState = emojiPickerState, + ) + } else { + CustomReactionState.Target.Loading(target.event) + } + } + + val eventForSelection = (computedTarget as? CustomReactionState.Target.Success)?.event + val selectedEmoji = eventForSelection ?.reactionsState ?.reactions ?.mapNotNull { if (it.isHighlighted) it.key else null } @@ -69,10 +60,14 @@ class CustomReactionPresenter( .toImmutableSet() return CustomReactionState( - target = target.value, + target = computedTarget, selectedEmoji = selectedEmoji, - recentEmojis = recentEmojis, eventSink = ::handleEvent, ) } } + +private sealed interface InternalTarget { + data object None : InternalTarget + data class Showing(val event: TimelineItem.Event) : InternalTarget +} 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 1897d4e5a7..566453b32b 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 @@ -8,15 +8,13 @@ package io.element.android.features.messages.impl.timeline.components.customreaction -import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.features.messages.impl.timeline.model.TimelineItem -import kotlinx.collections.immutable.ImmutableList +import io.element.android.libraries.emoji.api.picker.EmojiPickerState import kotlinx.collections.immutable.ImmutableSet data class CustomReactionState( val target: Target, val selectedEmoji: ImmutableSet, - val recentEmojis: ImmutableList, val eventSink: (CustomReactionEvent) -> Unit, ) { sealed interface Target { @@ -24,7 +22,7 @@ data class CustomReactionState( data class Loading(val event: TimelineItem.Event) : Target data class Success( val event: TimelineItem.Event, - val emojibaseStore: EmojibaseStore, + val emojiPickerState: EmojiPickerState, ) : Target } } 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 deleted file mode 100644 index 3a4cf6c9ce..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.text.input.rememberTextFieldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalInspectionMode -import io.element.android.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.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 { - val queryState = rememberTextFieldState() - var isSearchActive by remember { mutableStateOf(false) } - var emojiResults by remember { mutableStateOf>>(SearchBarResultState.Initial()) } - - 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() - } - } - - val searchQuery = queryState.text.toString() - 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(coroutineDispatchers.computation) { - emojibaseStore.allEmojis - .asSequence() - .filter { emoji -> - emoji.tags.orEmpty().any { it.contains(lowercaseQuery) } || - emoji.shortcodes.any { it.contains(lowercaseQuery) } - } - .take(60) - .toImmutableList() - } - - SearchBarResultState.Results(results) - } - } - - val isInPreview = LocalInspectionMode.current - fun handleEvent(event: EmojiPickerEvent) { - when (event) { - // For some reason, in preview mode the SearchBar emits this event with an `isActive = true` value automatically - is EmojiPickerEvent.ToggleSearchActive -> if (!isInPreview) { - isSearchActive = event.isActive - } - } - } - - return EmojiPickerState( - categories = categories, - allEmojis = emojibaseStore.allEmojis, - searchQuery = queryState, - isSearchActive = isSearchActive, - searchResults = emojiResults, - eventSink = ::handleEvent, - ) - } -} diff --git a/features/messages/impl/src/main/res/values-be/translations.xml b/features/messages/impl/src/main/res/values-be/translations.xml index 8b40c72040..b6cac43616 100644 --- a/features/messages/impl/src/main/res/values-be/translations.xml +++ b/features/messages/impl/src/main/res/values-be/translations.xml @@ -6,14 +6,6 @@ "Зашыфравана невядомай ці выдаленай прыладай." "Зашыфравана прыладай, не пацверджанай яе ўладальнікам." "Зашыфравана неправераным карыстальнікам." - "Актыўнасці" - "Сцягі" - "Ежа & Напоі" - "Жывёлы & Прырода" - "Аб\'екты" - "Усмешкі & Удзельнікі" - "Падарожжы & Месцы" - "Сімвалы" "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз." "Апрацоўка…" diff --git a/features/messages/impl/src/main/res/values-bg/translations.xml b/features/messages/impl/src/main/res/values-bg/translations.xml index 1e675f52ae..2db2e6930f 100644 --- a/features/messages/impl/src/main/res/values-bg/translations.xml +++ b/features/messages/impl/src/main/res/values-bg/translations.xml @@ -1,14 +1,6 @@ "Без шифроване" - "Дейности" - "Знамена" - "Храна & Напитки" - "Животни & Природа" - "Обекти" - "Усмивки & Хора" - "Пътуване & Места" - "Символи" "Докоснете, за да промените качеството на качване на видео" "Файлът не можа да бъде качен." "Неуспешна обработка на мултимедия за качване, моля, опитайте отново." diff --git a/features/messages/impl/src/main/res/values-ca/translations.xml b/features/messages/impl/src/main/res/values-ca/translations.xml index f9b2685c7d..44fe948683 100644 --- a/features/messages/impl/src/main/res/values-ca/translations.xml +++ b/features/messages/impl/src/main/res/values-ca/translations.xml @@ -7,15 +7,6 @@ "Xifrat per un dispositiu desconegut o eliminat." "Xifrat per un dispositiu no verificat pel seu propietari." "Xifrat per un usuari no verificat." - "Activitats" - "Banderes" - "Menjar & begudes" - "Animals i naturalesa" - "Objectes" - "Cares i persones" - "Viatges i llocs" - "Emoticones recents" - "Símbols" "És possible que les llegendes no siguin visibles pels que utilitzin aplicacions antigues." "No s\'ha pogut processar el contingut que s\'havia de pujar. Torna-ho a provar." "No s\'ha pogut pujar el contingut. Torna-ho a provar." diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 9bf1c5c079..6c39d67583 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -7,15 +7,6 @@ "Šifrováno neznámým nebo smazaným zařízením." "Šifrováno zařízením, které nebylo ověřeno jeho vlastníkem." "Šifrováno neověřeným uživatelem." - "Aktivity" - "Vlajky" - "Jídlo a nápoje" - "Zvířata a příroda" - "Předměty" - "Smajlíci a lidé" - "Cestování a místa" - "Nedávné emotikony" - "Symboly" "Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace." "Klepnutím změníte kvalitu nahrávání videa" "Soubor nelze nahrát." diff --git a/features/messages/impl/src/main/res/values-cy/translations.xml b/features/messages/impl/src/main/res/values-cy/translations.xml index 736a9a34f9..f0db099f8e 100644 --- a/features/messages/impl/src/main/res/values-cy/translations.xml +++ b/features/messages/impl/src/main/res/values-cy/translations.xml @@ -7,15 +7,6 @@ "Wedi\'i amgryptio gan ddyfais anhysbys neu wedi\'i dileu." "Wedi\'i amgryptio gan ddyfais nad yw wedi\'i wirio gan ei pherchennog." "Wedi\'i amgryptio gan ddefnyddiwr heb ei wirio." - "Gweithgareddau" - "Baneri" - "Bwyd a Diod" - "Anifeiliaid a Natur" - "Gwrthrychau" - "Wynebau Hapus a Phobl" - "Teithio a Llefydd" - "Emojis diweddar" - "Symbolau" "Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn." "Tapiwch i newid ansawdd llwytho\'r fideo" "Nid oedd modd llwytho\'r ffeil." diff --git a/features/messages/impl/src/main/res/values-da/translations.xml b/features/messages/impl/src/main/res/values-da/translations.xml index e068df293a..f168dd7896 100644 --- a/features/messages/impl/src/main/res/values-da/translations.xml +++ b/features/messages/impl/src/main/res/values-da/translations.xml @@ -7,15 +7,6 @@ "Krypteret af en ukendt eller slettet enhed." "Krypteret af en enhed, der ikke er verificeret af sin ejer." "Krypteret af en ikke-verificeret bruger." - "Aktiviteter" - "Flag" - "Mad og drikke" - "Dyr og natur" - "Objekter" - "Smileys og personer" - "Rejser og steder" - "Seneste emojis" - "Symboler" "Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps." "Tryk for at ændre videokvaliteten i uploadet" "Filen kunne ikke uploades." diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 4848f1f282..1bdb3e0b16 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -7,15 +7,6 @@ "Verschlüsselt von einem unbekannten oder gelöschten Gerät." "Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde." "Verschlüsselt durch einen nicht verifizierten Nutzer." - "Aktivitäten" - "Flaggen" - "Essen & Trinken" - "Tiere & Natur" - "Objekte" - "Smileys & Menschen" - "Reisen & Orte" - "Zuletzt verwendete Emojis" - "Symbole" "Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar." "Tippe, um die Qualität des Video-Uploads zu ändern" "Die Datei konnte nicht hochgeladen werden." diff --git a/features/messages/impl/src/main/res/values-el/translations.xml b/features/messages/impl/src/main/res/values-el/translations.xml index bfecdbb944..4a944d5250 100644 --- a/features/messages/impl/src/main/res/values-el/translations.xml +++ b/features/messages/impl/src/main/res/values-el/translations.xml @@ -7,15 +7,6 @@ "Κρυπτογραφημένο από άγνωστη ή διαγεγραμμένη συσκευή." "Κρυπτογραφημένο από μια συσκευή που δεν έχει επαληθευτεί από τον ιδιοκτήτη της." "Κρυπτογραφημένο από μη επαληθευμένο χρήστη." - "Δραστηριότητες" - "Σημαίες" - "Φαγητό & Ποτό" - "Ζώα & Φύση" - "Αντικείμενα" - "Φατσούλες & Άνθρωποι" - "Ταξίδια & Μέρη" - "Πρόσφατα emoji" - "Σύμβολα" "Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές." "Πατήστε για να αλλάξετε την ποιότητα μεταφόρτωσης βίντεο" "Δεν ήταν δυνατή η μεταφόρτωση του αρχείου." diff --git a/features/messages/impl/src/main/res/values-es/translations.xml b/features/messages/impl/src/main/res/values-es/translations.xml index 81ca655676..0a0b57cc5e 100644 --- a/features/messages/impl/src/main/res/values-es/translations.xml +++ b/features/messages/impl/src/main/res/values-es/translations.xml @@ -7,14 +7,6 @@ "Cifrado por un dispositivo desconocido o eliminado." "Cifrado por un dispositivo no verificado por su propietario." "Cifrado por un usuario no verificado." - "Actividades" - "Banderas" - "Comida y bebida" - "Animales y naturaleza" - "Objetos" - "Emojis y personas" - "Viajes y lugares" - "Símbolos" "Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas." "Error al procesar el contenido multimedia, por favor inténtalo de nuevo." "Error al subir el contenido multimedia, por favor inténtalo de nuevo." diff --git a/features/messages/impl/src/main/res/values-et/translations.xml b/features/messages/impl/src/main/res/values-et/translations.xml index 86b1995f32..aa6576fc17 100644 --- a/features/messages/impl/src/main/res/values-et/translations.xml +++ b/features/messages/impl/src/main/res/values-et/translations.xml @@ -7,15 +7,6 @@ "Krüptitud tundmatu või kustutatud seadme poolt." "Krüptitud seadme poolt, mida tema omanik pole verifitseerinud." "Krüptitud verifitseerimata kasutaja poolt." - "Tegevused" - "Lipud" - "Toit ja jook" - "Loomad ja loodus" - "Esemed" - "Emotikonid ja inimesed" - "Reisimine ja kohad" - "Hiljutised emojid" - "Sümbolid" "Pööra pilti ümber rõhttelje" "Pilt on pööratud ümber rõhttelje" "Pildi algne variant" diff --git a/features/messages/impl/src/main/res/values-eu/translations.xml b/features/messages/impl/src/main/res/values-eu/translations.xml index 6c76b4dfd9..8c158d688a 100644 --- a/features/messages/impl/src/main/res/values-eu/translations.xml +++ b/features/messages/impl/src/main/res/values-eu/translations.xml @@ -1,14 +1,6 @@ "Ez dago zifratuta." - "Jarduerak" - "Banderak" - "Jana eta edana" - "Animaliak eta natura" - "Objektuak" - "Irribartxoak eta jendea" - "Bidaiak eta tokiak" - "Ikurrak" "Huts egin du multimedia igotzeak, saiatu berriro." "Blokeatu erabiltzailea" "Mezua zure zerbitzariko administratzaileari jakinaraziko zaio. Ezingo dute zifratutako mezurik irakurri." diff --git a/features/messages/impl/src/main/res/values-fa/translations.xml b/features/messages/impl/src/main/res/values-fa/translations.xml index 19f55a5559..4f8f737f9c 100644 --- a/features/messages/impl/src/main/res/values-fa/translations.xml +++ b/features/messages/impl/src/main/res/values-fa/translations.xml @@ -6,15 +6,6 @@ "رمز شده به دست افزاره‌ای ناشناخته یا حذف شده." "رمز شده به دست افزاره‌ای که از سوی مالکش تأیید نشده." "رمز شده به دست کاربری تأیید نشده." - "فعّالیت‌ها" - "پرچم‌ها" - "غذا و نوشیدنی" - "حیوانات و طبعیت" - "اشیا" - "شکلک‌ها و افراد" - "سفر و مکان‌ها" - "شکلک‌های اخیر" - "نمادها" "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید." "بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید." "پردازش کردن…" diff --git a/features/messages/impl/src/main/res/values-fi/translations.xml b/features/messages/impl/src/main/res/values-fi/translations.xml index 881a5386e7..3705047087 100644 --- a/features/messages/impl/src/main/res/values-fi/translations.xml +++ b/features/messages/impl/src/main/res/values-fi/translations.xml @@ -7,15 +7,6 @@ "Tuntemattoman tai poistetun laitteen salaama." "Salattu laitteella, jota sen omistaja ei ole vahvistanut." "Vahvistamattoman käyttäjän salaama." - "Aktiviteetit" - "Liput" - "Ruoka ja juoma" - "Eläimet ja luonto" - "Esineet" - "Hymiöt ja ihmiset" - "Matkustaminen ja paikat" - "Viimeaikaiset emojit" - "Symbolit" "Käännä kuva vaakasuunnassa" "Käännetty vaakasuunnassa" "Alkuperäinen" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index 3505fd520d..0062421326 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -7,15 +7,6 @@ "Chiffré par un appareil inconnu ou supprimé." "Chiffré par un appareil non vérifié par son propriétaire." "Chiffré par un utilisateur non vérifié." - "Activités" - "Drapeaux" - "Nourriture et boissons" - "Animaux et nature" - "Objets" - "Émoticônes et personnes" - "Voyages & lieux" - "Emojis récents" - "Symboles" "Retourner l’image horizontalement" "Retournée horizontalement" "Originale" diff --git a/features/messages/impl/src/main/res/values-hr/translations.xml b/features/messages/impl/src/main/res/values-hr/translations.xml index 0f04f2179a..23912aaf78 100644 --- a/features/messages/impl/src/main/res/values-hr/translations.xml +++ b/features/messages/impl/src/main/res/values-hr/translations.xml @@ -7,15 +7,6 @@ "Šifrirano nepoznatim ili izbrisanim uređajem." "Šifrirano uređajem koji vlasnik nije potvrdio." "Šifrirao neprovjereni korisnik." - "Aktivnosti" - "Zastave" - "Hrana i piće" - "Životinje i priroda" - "Objekti" - "Emotikoni i osobe" - "Putovanja i mjesta" - "Nedavni emotikoni" - "Simboli" "Okreni sliku vodoravno" "Okrenuto vodoravno" "Izvornik" diff --git a/features/messages/impl/src/main/res/values-hu/translations.xml b/features/messages/impl/src/main/res/values-hu/translations.xml index bb33520cb6..2b428f92b2 100644 --- a/features/messages/impl/src/main/res/values-hu/translations.xml +++ b/features/messages/impl/src/main/res/values-hu/translations.xml @@ -7,15 +7,6 @@ "Ismeretlen vagy törölt eszköz által titkosítva." "A tulajdonos által nem ellenőrzött eszköz által titkosítva." "Nem ellenőrzött felhasználó által titkosítva." - "Tevékenységek" - "Zászlók" - "Étel és ital" - "Állatok és természet" - "Tárgyak" - "Mosolyok és emberek" - "Utazás és helyek" - "Legutóbbi emodzsik" - "Szimbólumok" "Kép vízszintes tükrözése" "Vízszintesen tükrözve" "Eredeti" diff --git a/features/messages/impl/src/main/res/values-in/translations.xml b/features/messages/impl/src/main/res/values-in/translations.xml index d052efb769..00233facfd 100644 --- a/features/messages/impl/src/main/res/values-in/translations.xml +++ b/features/messages/impl/src/main/res/values-in/translations.xml @@ -7,15 +7,6 @@ "Dienkripsi oleh perangkat yang tidak dikenal atau dihapus." "Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya." "Dienkripsi oleh pengguna yang tidak terverifikasi." - "Aktivitas" - "Bendera" - "Makanan & Minuman" - "Hewan & Alam" - "Objek" - "Senyuman & Orang" - "Wisata & Tempat" - "Emojis Sebelumnya" - "Simbol" "Putar gambar ke kiri" "%1$d derajat" diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml index 0f2e0d37be..53812093ee 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -7,15 +7,6 @@ "Cifrato da un dispositivo sconosciuto o eliminato." "Cifrato da un dispositivo non verificato dal proprietario." "Cifrato da un utente non verificato." - "Attività" - "Bandiere" - "Cibi & Bevande" - "Animali & Natura" - "Oggetti" - "Faccine & Persone" - "Viaggi & Luoghi" - "Emoji recenti" - "Simboli" "Capovolgi l\'immagine orizzontalmente" "Capovolto orizzontalmente" "Originale" diff --git a/features/messages/impl/src/main/res/values-ja/translations.xml b/features/messages/impl/src/main/res/values-ja/translations.xml index 26d33c4aa1..15851b893e 100644 --- a/features/messages/impl/src/main/res/values-ja/translations.xml +++ b/features/messages/impl/src/main/res/values-ja/translations.xml @@ -7,15 +7,6 @@ "削除されたまたは不明な端末により暗号化されています。" "所有者に検証されていない端末により暗号化されています。" "未検証のユーザーにより暗号化されています。" - "アクティビティ" - "旗" - "食べ物" - "動物・自然" - "物" - "顔・人" - "旅・場所" - "最近使用" - "記号" "水平に反転" "垂直に反転" "元画像" diff --git a/features/messages/impl/src/main/res/values-ka/translations.xml b/features/messages/impl/src/main/res/values-ka/translations.xml index 842fa4bf5b..54002be4b2 100644 --- a/features/messages/impl/src/main/res/values-ka/translations.xml +++ b/features/messages/impl/src/main/res/values-ka/translations.xml @@ -1,13 +1,5 @@ - "აქტივობები" - "დროშები" - "Საჭმელ-სასმელი" - "ცხოველები & ბუნება" - "ობიექტები" - "ღიმილები & ხალხი" - "მოგზაურობა და ადგილები" - "სიმბოლოები" "მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა." "მომხმარებლის დაბლოკვა" diff --git a/features/messages/impl/src/main/res/values-ko/translations.xml b/features/messages/impl/src/main/res/values-ko/translations.xml index fbcb0cab52..82d81f42cd 100644 --- a/features/messages/impl/src/main/res/values-ko/translations.xml +++ b/features/messages/impl/src/main/res/values-ko/translations.xml @@ -7,15 +7,6 @@ "알 수 없거나 삭제된 장치에 의해 암호화됩니다." "소유자가 확인하지 않은 장치에 의해 암호화되었습니다." "검증되지 않은 사용자에 의해 암호화되었습니다." - "활동" - "깃발" - "음식 & 음료" - "동물 & 자연" - "사물" - "표정 & 사람" - "여행 & 장소" - "최근 이모지" - "상징" "이미지를 왼쪽으로 회전" "%1$d도" diff --git a/features/messages/impl/src/main/res/values-lt/translations.xml b/features/messages/impl/src/main/res/values-lt/translations.xml index 379f9ae853..39f1b43627 100644 --- a/features/messages/impl/src/main/res/values-lt/translations.xml +++ b/features/messages/impl/src/main/res/values-lt/translations.xml @@ -1,13 +1,5 @@ - "Veikla" - "Vėliavos" - "Maistas ir Gėrimai" - "Gyvūnai ir Gamta" - "Objektai" - "Šypsenėlės ir Žmonės" - "Kelionės ir Vietovės" - "Simboliai" "Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą." "Nepavyko įkelti laikmenos, pabandykite dar kartą." "Blokuoti vartotoją" diff --git a/features/messages/impl/src/main/res/values-nb/translations.xml b/features/messages/impl/src/main/res/values-nb/translations.xml index a0dafdd136..a68aeabcdf 100644 --- a/features/messages/impl/src/main/res/values-nb/translations.xml +++ b/features/messages/impl/src/main/res/values-nb/translations.xml @@ -7,15 +7,6 @@ "Kryptert av en ukjent eller slettet enhet." "Kryptert med en enhet som ikke er verifisert av eieren." "Kryptert av en ubekreftet bruker." - "Aktiviteter" - "Flagg" - "Mat og drikke" - "Dyr og natur" - "Gjenstander" - "Smilefjes og mennesker" - "Reising og steder" - "Nylige emojier" - "Symboler" "Teksting er kanskje ikke synlig for personer som bruker eldre apper." "Trykk for å endre kvaliteten på videoopplastingen" "Filen kunne ikke lastes opp." diff --git a/features/messages/impl/src/main/res/values-nl/translations.xml b/features/messages/impl/src/main/res/values-nl/translations.xml index d2305229a7..dbce48fc97 100644 --- a/features/messages/impl/src/main/res/values-nl/translations.xml +++ b/features/messages/impl/src/main/res/values-nl/translations.xml @@ -6,14 +6,6 @@ "Versleuteld door een onbekend of verwijderd apparaat." "Versleuteld door een apparaat dat niet is geverifieerd door de eigenaar." "Versleuteld door een niet-geverifieerde gebruiker." - "Activiteiten" - "Vlaggen" - "Eten & Drinken" - "Dieren & Natuur" - "Voorwerpen" - "Smileys & Mensen" - "Reizen & Locaties" - "Symbolen" "Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw." "Het uploaden van media is mislukt. Probeer het opnieuw." "Gebruiker blokkeren" diff --git a/features/messages/impl/src/main/res/values-pl/translations.xml b/features/messages/impl/src/main/res/values-pl/translations.xml index d8ffaac54c..651a8e460e 100644 --- a/features/messages/impl/src/main/res/values-pl/translations.xml +++ b/features/messages/impl/src/main/res/values-pl/translations.xml @@ -7,15 +7,6 @@ "Zaszyfrowana przez nieznane lub usunięte urządzenie." "Zaszyfrowana przez urządzenie niezweryfikowane przez jego właściciela." "Zaszyfrowana przez niezweryfikowanego użytkownika." - "Aktywności" - "Flagi" - "Jedzenie i napoje" - "Zwierzęta i natura" - "Obiekty" - "Buźki i osoby" - "Podróż i miejsca" - "Ostatnie emoji" - "Symbole" "Odwróć obraz w poziomie" "Odwrócono w poziomie" "Oryginalny" diff --git a/features/messages/impl/src/main/res/values-pt-rBR/translations.xml b/features/messages/impl/src/main/res/values-pt-rBR/translations.xml index f93f0d71ca..5c8b62e5dd 100644 --- a/features/messages/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/messages/impl/src/main/res/values-pt-rBR/translations.xml @@ -7,15 +7,6 @@ "Criptografado por um dispositivo desconhecido ou apagado." "Criptografado por um dispositivo que não foi verificado pelo seu dono." "Criptografado por um usuário não verificado." - "Atividades" - "Bandeiras" - "Comida & Bebida" - "Animais & Natureza" - "Objetos" - "Sorrisos & Pessoas" - "Viagens & Lugares" - "Emojis recentes" - "Símbolos" "As legendas podem não ser visíveis para pessoas que usam apps mais antigos." "Toque para alterar a qualidade do envio do vídeo" "O arquivo não pôde ser enviado." diff --git a/features/messages/impl/src/main/res/values-pt/translations.xml b/features/messages/impl/src/main/res/values-pt/translations.xml index 2fb5173169..6329956b05 100644 --- a/features/messages/impl/src/main/res/values-pt/translations.xml +++ b/features/messages/impl/src/main/res/values-pt/translations.xml @@ -7,15 +7,6 @@ "Cifragem com origem num dispositivo eliminado ou desconhecido." "Cifragem com origem num dispositivo não verificado pelo seu dono." "Cifragem com origem num utilizador não verificado." - "Atividades" - "Bandeiras" - "Comidas e Bebidas" - "Animais e Natureza" - "Objetos" - "Caras e Pessoas" - "Viagens e Lugares" - "Emojis recentes" - "Símbolos" "As legendas poderão não ser visíveis em versões mais antigas da aplicação." "Toca para alterar a qualidade de carregamento do vídeo" "Não foi possível enviar o ficheiro" diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 67b72dc2a8..e9e4da87c3 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -7,15 +7,6 @@ "Criptat de un dispozitiv necunoscut sau șters." "Criptat de un dispozitiv care nu este verificat de proprietarul său." "Criptat de un utilizator neverificat." - "Activități" - "Steaguri" - "Mâncare & Băutură" - "Animale și Natură" - "Obiecte" - "Fețe zâmbitoare & Oameni" - "Călătorii & Locuri" - "Emoticoane recente" - "Simboluri" "Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi." "Atingeți pentru a modifica calitatea încărcării videoclipului" "Fișierul nu a putut fi încărcat." diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 3744beff8f..32e6e16980 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -7,15 +7,6 @@ "Зашифровано неизвестным или удаленным устройством." "Зашифровано устройством, не проверенным его владельцем." "Зашифровано непроверенным пользователем." - "Активности" - "Флаги" - "Еда и напитки" - "Животные и природа" - "Объекты" - "Эмодзи и люди" - "Путешествия и места" - "Недавние эмодзи" - "Символы" "Перевернуть изображение по горизонтали" "Перевернуто по горизонтали" "Оригинал" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index d2022c4882..c64e5ba62e 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -7,15 +7,6 @@ "Zašifrované neznámym alebo odstráneným zariadením." "Šifrované zariadením, ktoré nie je overené jeho majiteľom." "Šifrované neovereným používateľom." - "Aktivity" - "Vlajky" - "Jedlo a nápoje" - "Zvieratá a príroda" - "Predmety" - "Smajlíky a ľudia" - "Cestovanie a miesta" - "Nedávne emotikony" - "Symboly" "Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie." "Ťuknutím zmeníte kvalitu nahratého videa" "Súbor sa nepodarilo nahrať." diff --git a/features/messages/impl/src/main/res/values-sv/translations.xml b/features/messages/impl/src/main/res/values-sv/translations.xml index 2e10fd3166..6de0b1ebcc 100644 --- a/features/messages/impl/src/main/res/values-sv/translations.xml +++ b/features/messages/impl/src/main/res/values-sv/translations.xml @@ -7,14 +7,6 @@ "Krypterad av en okänd eller raderad enhet." "Krypterad av en enhet som inte verifierats av ägaren." "Krypterad av en overifierad användare." - "Aktiviteter" - "Flaggor" - "Mat & dryck" - "Djur & natur" - "Föremål" - "Smileys & personer" - "Resor & platser" - "Symboler" "Bildtexter kanske inte är synliga för personer som använder äldre appar." "Tryck för att ändra videouppladdningskvaliteten" "Filen kunde inte laddas upp." diff --git a/features/messages/impl/src/main/res/values-tr/translations.xml b/features/messages/impl/src/main/res/values-tr/translations.xml index a40687444b..a987f63ce3 100644 --- a/features/messages/impl/src/main/res/values-tr/translations.xml +++ b/features/messages/impl/src/main/res/values-tr/translations.xml @@ -7,14 +7,6 @@ "Bilinmeyen veya silinmiş bir cihaz tarafından şifrelenmiştir." "Sahibi tarafından doğrulanmamış bir cihaz tarafından şifrelenmiştir." "Doğrulanmamış bir kullanıcı tarafından şifrelenmiştir." - "Aktiviteler" - "Bayraklar" - "Yiyecek ve İçecek" - "Hayvanlar ve Doğa" - "Nesneler" - "İfadeler ve İnsanlar" - "Seyahat ve Yerler" - "Simgeler" "Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir." "Medya yüklenemedi, lütfen tekrar deneyin." "Medya yüklenemedi, lütfen tekrar deneyin." diff --git a/features/messages/impl/src/main/res/values-uk/translations.xml b/features/messages/impl/src/main/res/values-uk/translations.xml index cae9560768..c5f5ddde91 100644 --- a/features/messages/impl/src/main/res/values-uk/translations.xml +++ b/features/messages/impl/src/main/res/values-uk/translations.xml @@ -7,15 +7,6 @@ "Зашифровано невідомим або видаленим пристроєм." "Зашифровано пристроєм, який не звірено його власником." "Зашифровано не звіреним користувачем." - "Дії" - "Прапори" - "Їжа та напої" - "Тварини та природа" - "Об\'єкти" - "Смайлики та люди" - "Подорожі та місця" - "Нещодавні емодзі" - "Символи" "Повернути зображення по горизонталі" "Перевернуто по горизонталі" "Оригінал" diff --git a/features/messages/impl/src/main/res/values-ur/translations.xml b/features/messages/impl/src/main/res/values-ur/translations.xml index cb110ef5c6..6f549a5ba3 100644 --- a/features/messages/impl/src/main/res/values-ur/translations.xml +++ b/features/messages/impl/src/main/res/values-ur/translations.xml @@ -6,14 +6,6 @@ "کسی نامعلوم یا حذف شدہ آلے کے ذریعے مرموز کردہ۔" "کسی ایسے آلے کے ذریعے مرموز کردہ جس کی توثیق اسکے مالک سے نہیں ہوئی۔" "ایک غیر توثیق شدہ صارف کے ذریعے مرموز کردہ۔" - "سرگرمیاں" - "جھنڈے" - "طعم و مشروب" - "حیوانات و فطرت" - "اشیاء" - "مسکراہٹیں و لوگ" - "سفر و مقامات" - "علامتیں" "وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔" "وسائط رفع کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" "صارف کو مسدود کریں" diff --git a/features/messages/impl/src/main/res/values-uz/translations.xml b/features/messages/impl/src/main/res/values-uz/translations.xml index 03a1f6ef7e..8941917f5f 100644 --- a/features/messages/impl/src/main/res/values-uz/translations.xml +++ b/features/messages/impl/src/main/res/values-uz/translations.xml @@ -7,15 +7,6 @@ "Nomaʼlum yoki oʻchirib tashlangan qurilma tomonidan shifrlangan." "Egasi tasdiqlamagan qurilma tomonidan shifrlangan." "Tasdiqlanmagan foydalanuvchi tomonidan shifrlangan." - "Faoliyatlar" - "Bayroqlar" - "Oziq-ovqat va ichimliklar" - "Hayvonlar va tabiat" - "Ob\'ektlar" - "Smayllar va odamlar" - "Sayohat va Joylar" - "Oxirgi kulgichlar" - "Belgilar" "Rasmni gorizontal ravishda aylantiring" "Gorizontal ravishda aylantirildi" "Asl nusxasi" diff --git a/features/messages/impl/src/main/res/values-vi/translations.xml b/features/messages/impl/src/main/res/values-vi/translations.xml index 5104cd5a5b..a16d7378b3 100644 --- a/features/messages/impl/src/main/res/values-vi/translations.xml +++ b/features/messages/impl/src/main/res/values-vi/translations.xml @@ -7,15 +7,6 @@ "Được mã hóa bởi một thiết bị không xác định hoặc đã bị xóa." "Được mã hóa bởi một thiết bị chưa được chủ sở hữu xác minh." "Được mã hóa bởi một người dùng chưa được xác minh." - "Hoạt động" - "Cờ" - "Thực phẩm và đồ uống" - "Động vật và thiên nhiên" - "Đồ vật" - "Mặt cười & mọi người" - "Du lịch và địa danh" - "Biểu tượng cảm xúc gần đây" - "Biểu tượng" "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." "Không thể tải lên tệp phương tiện. Vui lòng thử lại." "Chặn người dùng" diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 0eea6e808e..e8cd285b5e 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -7,15 +7,6 @@ "由未知或已刪除的裝置加密。" "由未經其擁有者驗證的裝置加密。" "由未經驗證的使用者加密。" - "活動" - "旗幟" - "食物與飲料" - "動物與大自然" - "物品" - "表情與人物" - "旅行與景點" - "最近使用的表情符號" - "標誌" "使用舊應用程式的使用者可能看不到標題。" "輕點即可變更影片上傳品質" "無法上傳檔案。" diff --git a/features/messages/impl/src/main/res/values-zh/translations.xml b/features/messages/impl/src/main/res/values-zh/translations.xml index e2300912ae..41e55e1cce 100644 --- a/features/messages/impl/src/main/res/values-zh/translations.xml +++ b/features/messages/impl/src/main/res/values-zh/translations.xml @@ -7,15 +7,6 @@ "由未知或已删除的设备加密。" "由未经其所有者验证的设备加密。" "由未经验证的用户加密。" - "节假日" - "旗帜" - "饮食" - "动物和自然" - "日常物品" - "表情和人物" - "文旅景点" - "最近的 Emoji" - "符号" "水平翻转图像" "水平翻转" "原始" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index e4790feba0..556ab00ee0 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -7,15 +7,6 @@ "Encrypted by an unknown or deleted device." "Encrypted by a device not verified by its owner." "Encrypted by an unverified user." - "Activities" - "Flags" - "Food & Drink" - "Animals & Nature" - "Objects" - "Smileys & People" - "Travel & Places" - "Recent emojis" - "Symbols" "Flip image horizontally" "Flipped horizontally" "Original" 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 5d2dceaf31..2800c737ad 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 @@ -31,7 +31,6 @@ import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.text.AnnotatedString import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory -import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState @@ -48,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent @@ -56,6 +56,8 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.libraries.emoji.impl.picker.DefaultEmojiPickerRenderer +import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom @@ -538,24 +540,23 @@ class MessagesViewTest : RobolectricTest() { eventSink = eventsRecorder, ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event + val emojisByCategory = persistentMapOf( + EmojibaseCategory.People to persistentListOf( + Emoji( + hexcode = "", + label = "", + tags = persistentListOf(), + shortcodes = persistentListOf(), + unicode = aUnicode, + skins = null, + ) + ) + ) val stateWithCustomReactionState = state.copy( customReactionState = aCustomReactionState( target = CustomReactionState.Target.Success( event = timelineItem, - emojibaseStore = EmojibaseStore( - categories = persistentMapOf( - EmojibaseCategory.People to persistentListOf( - Emoji( - hexcode = "", - label = "", - tags = persistentListOf(), - shortcodes = persistentListOf(), - unicode = aUnicode, - skins = null, - ) - ) - ) - ), + emojiPickerState = anEmojiPickerState(emojis = emojisByCategory), ), eventSink = customReactionStateEventsRecorder ), @@ -695,6 +696,7 @@ private fun AndroidComposeUiTest.setMessagesView( onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onThreadsListClicked: () -> Unit = EnsureNeverCalled(), ) { + val emojiPickerRenderer = DefaultEmojiPickerRenderer() setSafeContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode CompositionLocalProvider(LocalInspectionMode provides true) { @@ -711,6 +713,15 @@ private fun AndroidComposeUiTest.setMessagesView( onJoinCallClick = onJoinCallClick, onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, knockRequestsBannerView = {}, + customReactionBottomSheet = { + CustomReactionBottomSheet( + state = state.customReactionState, + onSelectEmoji = { uniqueId, emoji -> + state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId)) + }, + emojiPickerRenderer = emojiPickerRenderer, + ) + }, onThreadsListClick = onThreadsListClicked, ) } 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 d0765bd5e7..be765554de 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 @@ -11,11 +11,10 @@ package io.element.android.features.messages.impl.timeline.components.customreac import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions -import io.element.android.libraries.emoji.test.FakeEmojibaseProvider +import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -25,8 +24,7 @@ class CustomReactionPresenterTest { val warmUpRule = WarmUpRule() private val presenter = CustomReactionPresenter( - emojibaseProvider = FakeEmojibaseProvider(), - getRecentEmojis = { Result.success(persistentListOf()) }, + emojiPickerPresenter = { anEmojiPickerState() }, ) @Test 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 deleted file mode 100644 index 9ebbfd5a3a..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * 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.text.input.setTextAndPlaceCursorAtEnd -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.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.robolectric.RobolectricTest -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class EmojiPickerPresenterTest : RobolectricTest() { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `updating search query loads new results`() = runTest { - testPresenter { - skipItems(1) - - val initialState = awaitItem() - assertThat(initialState.searchQuery.text.toString()).isEmpty() - assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) - - initialState.searchQuery.setTextAndPlaceCursorAtEnd("smile") - assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("smile") - - val stateWithResults = awaitItem() - assertThat(stateWithResults.searchQuery.text.toString()).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(EmojiPickerEvent.ToggleSearchActive(true)) - assertThat(awaitItem().isSearchActive).isTrue() - - initialState.eventSink(EmojiPickerEvent.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().toImmutableMap()), - 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/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt new file mode 100644 index 0000000000..5e29ee01ab --- /dev/null +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations 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.emoji.api.picker + +import io.element.android.libraries.architecture.Presenter + +fun interface EmojiPickerPresenter : Presenter diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt new file mode 100644 index 0000000000..6d2f67a5a6 --- /dev/null +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations 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.emoji.api.picker + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import io.element.android.emojibasebindings.Emoji +import kotlinx.collections.immutable.ImmutableSet + +@Immutable +interface EmojiPickerRenderer { + /** + * @param state opaque state produced by [EmojiPickerPresenter.present]. + * @param onSelectEmoji invoked when the user taps an emoji (including a skin-tone variant). + * @param selectedEmojis emojis to visually mark as already selected. + * @param modifier layout modifier for the picker container. + * @param contentDescription accessibility label for each emoji cell. + */ + @Composable + fun Render( + state: EmojiPickerState, + onSelectEmoji: (Emoji) -> Unit, + selectedEmojis: ImmutableSet, + modifier: Modifier = Modifier, + contentDescription: @Composable (Emoji, Boolean) -> String = { emoji, _ -> emoji.unicode }, + ) +} diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerState.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerState.kt new file mode 100644 index 0000000000..a5c820e648 --- /dev/null +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerState.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations 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.emoji.api.picker + +/** + * Opaque state produced by an [EmojiPickerPresenter] and consumed by an [EmojiPickerRenderer]. + */ +interface EmojiPickerState { + /** + * `true` once the picker has finished loading its data and is safe to display. + * Callers should typically defer showing the picker (or its container, e.g. a bottom sheet) + * until this flips to `true` to avoid flashing an empty picker. + */ + val isReady: Boolean +} diff --git a/libraries/emoji/impl/build.gradle.kts b/libraries/emoji/impl/build.gradle.kts index 871c19a41a..1cb274e6c0 100644 --- a/libraries/emoji/impl/build.gradle.kts +++ b/libraries/emoji/impl/build.gradle.kts @@ -35,7 +35,6 @@ dependencies { implementation(libs.kotlinx.collections.immutable) implementation(libs.matrix.emojibase.bindings) - testImplementation(projects.libraries.emoji.test) testImplementation(projects.libraries.matrix.test) testCommonDependencies(libs) } diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt index f0ad797648..9f64879a80 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/DefaultEmojibaseProvider.kt @@ -11,16 +11,28 @@ package io.element.android.libraries.emoji.impl import android.content.Context import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext -import io.element.android.libraries.emoji.api.EmojibaseProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +@Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultEmojibaseProvider(@ApplicationContext val context: Context) : EmojibaseProvider { - override val emojibaseStore: EmojibaseStore by lazy { +class DefaultEmojibaseProvider( + @ApplicationContext private val context: Context, + dispatchers: CoroutineDispatchers, + @AppCoroutineScope appScope: CoroutineScope, +) : EmojibaseProvider { + private val deferred = appScope.async(dispatchers.io, start = CoroutineStart.LAZY) { EmojibaseDatasource().load(context) } + + override suspend fun getStore(): EmojibaseStore = deferred.await() } diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/EmojibaseProvider.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/EmojibaseProvider.kt similarity index 60% rename from libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/EmojibaseProvider.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/EmojibaseProvider.kt index d8f9c92777..ddd18f8861 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/EmojibaseProvider.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/EmojibaseProvider.kt @@ -1,15 +1,14 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations 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.emoji.api +package io.element.android.libraries.emoji.impl import io.element.android.emojibasebindings.EmojibaseStore interface EmojibaseProvider { - val emojibaseStore: EmojibaseStore + suspend fun getStore(): EmojibaseStore } diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt new file mode 100644 index 0000000000..e7afe50c0f --- /dev/null +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.emoji.impl.picker + +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalInspectionMode +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.emojibasebindings.Emoji +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 io.element.android.libraries.di.SessionScope +import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter +import io.element.android.libraries.emoji.api.picker.EmojiPickerState +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis +import io.element.android.libraries.emoji.impl.EmojibaseProvider +import io.element.android.libraries.emoji.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlin.time.Duration.Companion.milliseconds + +@Inject +@ContributesBinding(SessionScope::class) +class DefaultEmojiPickerPresenter( + private val emojibaseProvider: EmojibaseProvider, + private val getRecentEmojis: GetRecentEmojis, + private val coroutineDispatchers: CoroutineDispatchers, +) : EmojiPickerPresenter { + @Composable + override fun present(): EmojiPickerState { + val queryState = rememberTextFieldState() + var isSearchActive by remember { mutableStateOf(false) } + var emojiResults by remember { mutableStateOf>>(SearchBarResultState.Initial()) } + + val data by produceState(EmojiPickerData.Empty) { + val storeDeferred = async { emojibaseProvider.getStore() } + val recentsDeferred = async { getRecentEmojis().getOrNull() ?: persistentListOf() } + val store = storeDeferred.await() + val recentEmojiUnicodes = recentsDeferred.await() + value = withContext(coroutineDispatchers.computation) { + val baseCategories = store.categories.map { (category, emojis) -> + EmojiCategory( + titleId = category.title, + icon = IconSource.Vector(category.icon), + emojis = emojis, + ) + } + val recentEmojis = recentEmojiUnicodes + .mapNotNull { unicode -> store.allEmojis.find { it.unicode == unicode } } + .toImmutableList() + val categories = if (recentEmojis.isEmpty()) { + baseCategories.toImmutableList() + } else { + val recentCategory = EmojiCategory( + titleId = R.string.emoji_picker_category_recent, + icon = IconSource.Resource(io.element.android.compound.R.drawable.ic_compound_history), + emojis = recentEmojis, + ) + (listOf(recentCategory) + baseCategories).toImmutableList() + } + EmojiPickerData( + categories = categories, + allEmojis = store.allEmojis, + ) + } + } + + val searchQuery = queryState.text.toString() + LaunchedEffect(searchQuery, data) { + if (searchQuery.isEmpty() || data.allEmojis.isEmpty()) { + emojiResults = SearchBarResultState.Initial() + return@LaunchedEffect + } + delay(100.milliseconds) + val lowercaseQuery = searchQuery.lowercase() + val results = withContext(coroutineDispatchers.computation) { + data.allEmojis + .asSequence() + .filter { emoji -> + emoji.tags.orEmpty().any { it.contains(lowercaseQuery) } || + emoji.shortcodes.any { it.contains(lowercaseQuery) } + } + .take(60) + .toImmutableList() + } + emojiResults = SearchBarResultState.Results(results) + } + + val isInPreview = LocalInspectionMode.current + fun handleEvent(event: EmojiPickerEvent) { + when (event) { + is EmojiPickerEvent.ToggleSearchActive -> if (!isInPreview) { + isSearchActive = event.isActive + } + } + } + + return DefaultEmojiPickerState( + categories = data.categories, + searchQuery = queryState, + isSearchActive = isSearchActive, + searchResults = emojiResults, + eventSink = ::handleEvent, + ) + } +} + +private data class EmojiPickerData( + val categories: ImmutableList, + val allEmojis: ImmutableList, +) { + companion object { + val Empty = EmojiPickerData(persistentListOf(), persistentListOf()) + } +} diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerRenderer.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerRenderer.kt new file mode 100644 index 0000000000..4b6fcd8554 --- /dev/null +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerRenderer.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Element Creations 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.emoji.impl.picker + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.emojibasebindings.Emoji +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer +import io.element.android.libraries.emoji.api.picker.EmojiPickerState +import kotlinx.collections.immutable.ImmutableSet + +@Inject +@ContributesBinding(SessionScope::class) +class DefaultEmojiPickerRenderer : EmojiPickerRenderer { + @Composable + override fun Render( + state: EmojiPickerState, + onSelectEmoji: (Emoji) -> Unit, + selectedEmojis: ImmutableSet, + modifier: Modifier, + contentDescription: @Composable (Emoji, Boolean) -> String, + ) { + if (state is DefaultEmojiPickerState) { + EmojiPickerView( + state = state, + onSelectEmoji = onSelectEmoji, + selectedEmojis = selectedEmojis, + contentDescription = contentDescription, + modifier = modifier, + ) + } else { + error("Unsupported state type: ${state::class.java}") + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerState.kt similarity index 72% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerState.kt index c1d306dece..8b3835724b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerState.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction.picker +package io.element.android.libraries.emoji.impl.picker import androidx.annotation.StringRes import androidx.compose.foundation.text.input.TextFieldState @@ -14,23 +14,21 @@ import androidx.compose.runtime.Immutable import io.element.android.emojibasebindings.Emoji import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.emoji.api.picker.EmojiPickerState import kotlinx.collections.immutable.ImmutableList -// Emoji is unstable (because from an external library?), so we annotate with @Immutable @Immutable -data class EmojiPickerState( +internal data class DefaultEmojiPickerState( val categories: ImmutableList, - val allEmojis: ImmutableList, val searchQuery: TextFieldState, val isSearchActive: Boolean, val searchResults: SearchBarResultState>, val eventSink: (EmojiPickerEvent) -> Unit, -) +) : EmojiPickerState { + override val isReady = categories.isNotEmpty() +} -/** - * Represents a category of emojis with a title id, icon, and the list of associated emojis. - */ -data class EmojiCategory( +internal 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/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt similarity index 58% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt index 958b3849f5..0a091a2b66 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt @@ -6,29 +6,26 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction.picker +package io.element.android.libraries.emoji.impl.picker import androidx.compose.foundation.text.input.TextFieldState 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 io.element.android.libraries.emoji.api.picker.EmojiPickerState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -class EmojiPickerStateProvider : PreviewParameterProvider { - override val values: Sequence +internal class DefaultEmojiPickerStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - anEmojiPickerState(), - anEmojiPickerState(isSearchActive = true), - anEmojiPickerState(isSearchActive = true, searchQuery = "smile"), - anEmojiPickerState( + aDefaultEmojiPickerState(), + aDefaultEmojiPickerState(isSearchActive = true), + aDefaultEmojiPickerState(isSearchActive = true, searchQuery = "smile"), + aDefaultEmojiPickerState( isSearchActive = true, searchQuery = "smile", searchResults = SearchBarResultState.Results(emojiList()) @@ -36,12 +33,6 @@ class EmojiPickerStateProvider : PreviewParameterProvider { ) } -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", @@ -56,29 +47,46 @@ private fun emojiList(): ImmutableList = persistentListOf( "crying face", persistentListOf("crying"), persistentListOf("smile, crying"), - "\uD83E\uDD72", + "🥲", null ) ) -internal fun anEmojiPickerState( - categories: ImmutableList = (listOf(recentEmojisCategory()) + EmojibaseCategory.entries.map { +internal fun aDefaultEmojiPickerState( + categories: ImmutableList = EmojibaseCategory.entries.map { EmojiCategory( titleId = it.title, icon = IconSource.Vector(it.icon), emojis = emojiList(), ) - }).toImmutableList(), - allEmojis: ImmutableList = categories.flatMap { it.emojis }.toImmutableList(), + }.toImmutableList(), searchQuery: String = "", isSearchActive: Boolean = false, searchResults: SearchBarResultState> = SearchBarResultState.Initial(), eventSink: (EmojiPickerEvent) -> Unit = {}, -) = EmojiPickerState( +) = DefaultEmojiPickerState( categories = categories, - allEmojis = allEmojis, searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, searchResults = searchResults, eventSink = eventSink, ) + +/** + * Public helper for external tests that need an [EmojiPickerState] instance the impl's + * [DefaultEmojiPickerRenderer] will accept. The no-arg overload returns a ready state + * (see [EmojiPickerState.isReady]); use the [emojis] overload to inject specific categories. + */ +fun anEmojiPickerState(): EmojiPickerState = aDefaultEmojiPickerState() + +fun anEmojiPickerState( + emojis: Map>, +): EmojiPickerState = aDefaultEmojiPickerState( + categories = emojis.map { (category, list) -> + EmojiCategory( + titleId = category.title, + icon = IconSource.Vector(category.icon), + emojis = list, + ) + }.toImmutableList(), +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt similarity index 86% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt index db3a70715d..27c80af961 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction +package io.element.android.libraries.emoji.impl.picker import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -52,12 +52,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import io.element.android.compound.theme.ElementTheme import io.element.android.emojibasebindings.Emoji -import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction -import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePadding -import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePicker -import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSize -import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSpacing -import io.element.android.features.messages.impl.timeline.components.customreaction.picker.aSkinList import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text @@ -66,7 +60,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf @Composable -fun EmojiItem( +internal fun EmojiItem( item: Emoji, isSelected: Boolean, onSelectEmoji: (Emoji) -> Unit, @@ -75,6 +69,7 @@ fun EmojiItem( skinPickerEmoji: Emoji?, selectedSkinUnicodes: ImmutableSet, hasSelectedSkin: Boolean, + contentDescription: @Composable (emoji: Emoji, isSelected: Boolean) -> String = { emoji, _ -> emoji.unicode }, modifier: Modifier = Modifier, emojiSize: TextUnit = 20.sp, ) { @@ -89,18 +84,13 @@ fun EmojiItem( var hoveredIndex by remember { mutableIntStateOf(-1) } var dismissed by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } - val description = a11yReactionAction( - emoji = item.unicode, - userAlreadyReacted = isSelected, - ) + val description = contentDescription(item, isSelected) val hasSkinTones = !item.skins.isNullOrEmpty() Box( modifier = modifier .onPlaced { coordinates -> val position = coordinates.positionInParent() val parentBounds = coordinates.parentLayoutCoordinates?.size ?: return@onPlaced - // Calculate the offset of the item inside its parent as a percentage, to be able to set the initial offset - // for the selection inside the skin tone picker relative to its size. itemOffsetPercent = if (parentBounds.width > 0) { position.x / parentBounds.width } else { @@ -112,7 +102,6 @@ fun EmojiItem( .background(backgroundColor, CircleShape) .indication(interactionSource, ripple()) .pointerInput(item) { - // Always detect long press and tap gestures detectTapGestures( onPress = { pressOffset -> val press = PressInteraction.Press(pressOffset) @@ -130,7 +119,6 @@ fun EmojiItem( ) } .then( - // Only detect drag after long press for those items which have a skin tone picker if (hasSkinTones) { Modifier.pointerInput(item) { val slotWidthPx = with(density) { SkinToneSlotSize.toPx() } @@ -147,8 +135,6 @@ fun EmojiItem( onDrag = { change, _ -> if (!dismissed) { change.consume() - // It's a valid drag event if it's within ~2 items height above, so, on top of the current one or the skin tone selector, - // or 1 item below, which should be far enough to not be triggered by mistake val isValidDrag = if (change.position.y < startOffset.y) { startOffset.y - change.position.y <= itemSize.height * 2f } else { @@ -160,15 +146,11 @@ fun EmojiItem( onDismissSkinPicker() } else { val skinItemsCount = item.skins!!.size - // Original + variants val totalSlots = 1 + skinItemsCount - // Calculate the whole size of the skin tone picker val pickerWidthPx = 2 * paddingPx + totalSlots * slotWidthPx + (totalSlots - 1) * spacingPx - // Calculate the initial offset inside the picker, given the relative position of the item inside its parent val initialOffset = pickerWidthPx * itemOffsetPercent val xInPicker = initialOffset + change.position.x - // If it's a valid offset, calculate the hovered index, otherwise, set it to -1 to indicate that no item is hovered val index = if (xInPicker in 0f..pickerWidthPx) { (xInPicker / slotWidthPx).toInt().coerceIn(0, totalSlots - 1) } else { @@ -210,7 +192,7 @@ fun EmojiItem( } ) .clearAndSetSemantics { - contentDescription = description + this.contentDescription = description }, contentAlignment = Alignment.Center ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvent.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerEvent.kt similarity index 71% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvent.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerEvent.kt index 6ea5eea0f6..57cbf8e08b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvent.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerEvent.kt @@ -6,8 +6,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction.picker +package io.element.android.libraries.emoji.impl.picker -sealed interface EmojiPickerEvent { +internal sealed interface EmojiPickerEvent { data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt similarity index 91% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt index ce65a435f4..325f1d792b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction.picker +package io.element.android.libraries.emoji.impl.picker import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,7 +35,6 @@ 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.features.messages.impl.timeline.components.customreaction.EmojiItem import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp @@ -50,11 +49,12 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun EmojiPicker( +internal fun EmojiPickerView( + state: DefaultEmojiPickerState, onSelectEmoji: (Emoji) -> Unit, - state: EmojiPickerState, selectedEmojis: ImmutableSet, modifier: Modifier = Modifier, + contentDescription: @Composable (emoji: Emoji, isSelected: Boolean) -> String = { emoji, _ -> emoji.unicode }, ) { val coroutineScope = rememberCoroutineScope() val pagerState = rememberPagerState(pageCount = { state.categories.size }) @@ -78,6 +78,7 @@ fun EmojiPicker( skinPickerEmoji = skinPickerEmoji, onDismissSkinPicker = { skinPickerEmoji = null }, selectedEmojis = selectedEmojis, + contentDescription = contentDescription, ) } @@ -120,6 +121,7 @@ fun EmojiPicker( skinPickerEmoji = skinPickerEmoji, onDismissSkinPicker = { skinPickerEmoji = null }, selectedEmojis = selectedEmojis, + contentDescription = contentDescription, ) } } @@ -135,6 +137,7 @@ private fun EmojiResults( skinPickerEmoji: Emoji?, onDismissSkinPicker: () -> Unit, selectedEmojis: ImmutableSet, + contentDescription: @Composable (emoji: Emoji, isSelected: Boolean) -> String, ) { LazyVerticalGrid( modifier = Modifier.fillMaxSize(), @@ -155,6 +158,7 @@ private fun EmojiResults( emojiSize = 32.dp.toSp(), selectedSkinUnicodes = selectedEmojis, hasSelectedSkin = item.skins?.any { skin -> skin.unicode in selectedEmojis } == true, + contentDescription = contentDescription, ) } } @@ -162,10 +166,10 @@ private fun EmojiResults( @PreviewsDayNight @Composable -internal fun EmojiPickerPreview(@PreviewParameter(EmojiPickerStateProvider::class) state: EmojiPickerState) = ElementPreview { - EmojiPicker( - onSelectEmoji = {}, +internal fun EmojiPickerViewPreview(@PreviewParameter(DefaultEmojiPickerStateProvider::class) state: DefaultEmojiPickerState) = ElementPreview { + EmojiPickerView( state = state, + onSelectEmoji = {}, selectedEmojis = persistentSetOf("😀", "😄", "😃"), modifier = Modifier.fillMaxWidth(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojibaseExtensions.kt similarity index 91% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojibaseExtensions.kt index 0f967cc41d..c6b428a13d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojibaseExtensions.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction +package io.element.android.libraries.emoji.impl.picker import androidx.annotation.StringRes import androidx.compose.material.icons.Icons @@ -20,10 +20,10 @@ import androidx.compose.material.icons.outlined.EmojiSymbols import androidx.compose.material.icons.outlined.EmojiTransportation import androidx.compose.ui.graphics.vector.ImageVector import io.element.android.emojibasebindings.EmojibaseCategory -import io.element.android.features.messages.impl.R +import io.element.android.libraries.emoji.impl.R @get:StringRes -val EmojibaseCategory.title: Int +internal val EmojibaseCategory.title: Int get() = when (this) { EmojibaseCategory.People -> R.string.emoji_picker_category_people EmojibaseCategory.Nature -> R.string.emoji_picker_category_nature @@ -35,7 +35,7 @@ val EmojibaseCategory.title: Int EmojibaseCategory.Flags -> R.string.emoji_picker_category_flags } -val EmojibaseCategory.icon: ImageVector +internal val EmojibaseCategory.icon: ImageVector get() = when (this) { EmojibaseCategory.People -> Icons.Outlined.EmojiPeople EmojibaseCategory.Nature -> Icons.Outlined.EmojiNature diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/SkinTonePicker.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/SkinTonePicker.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/SkinTonePicker.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/SkinTonePicker.kt index 1ac66b5378..6d2618873a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/SkinTonePicker.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/SkinTonePicker.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.messages.impl.timeline.components.customreaction.picker +package io.element.android.libraries.emoji.impl.picker import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -36,12 +36,12 @@ import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf -val SkinToneSlotSize = 48.dp -val SkinToneSlotSpacing = 2.dp -val SkinTonePadding = 4.dp +internal val SkinToneSlotSize = 48.dp +internal val SkinToneSlotSpacing = 2.dp +internal val SkinTonePadding = 4.dp @Composable -fun SkinTonePicker( +internal fun SkinTonePicker( emoji: Emoji, hoveredIndex: Int, selectedUnicodes: ImmutableSet, diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt index acd5e012ce..412332e984 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojis.kt @@ -11,8 +11,8 @@ package io.element.android.libraries.emoji.impl.recentemojis import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.emoji.api.EmojibaseProvider import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis +import io.element.android.libraries.emoji.impl.EmojibaseProvider import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -25,7 +25,7 @@ class DefaultGetRecentEmojis( private val emojibaseProvider: EmojibaseProvider, ) : GetRecentEmojis { override suspend operator fun invoke(): Result> = withContext(dispatchers.io) { - val allEmojis = emojibaseProvider.emojibaseStore.allEmojis + val allEmojis = emojibaseProvider.getStore().allEmojis client.getRecentEmojis() .map { emojis -> // Remove any possible duplicates diff --git a/libraries/emoji/impl/src/main/res/values/temporary.xml b/libraries/emoji/impl/src/main/res/values/temporary.xml new file mode 100644 index 0000000000..34467795b9 --- /dev/null +++ b/libraries/emoji/impl/src/main/res/values/temporary.xml @@ -0,0 +1,13 @@ + + + + "Activities" + "Flags" + "Food & Drink" + "Animals & Nature" + "Objects" + "Smileys & People" + "Travel & Places" + "Recent emojis" + "Symbols" + diff --git a/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojibaseProvider.kt b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/fixtures/FakeEmojibaseProvider.kt similarity index 60% rename from libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojibaseProvider.kt rename to libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/fixtures/FakeEmojibaseProvider.kt index 0e7c42e538..f01b6a1268 100644 --- a/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojibaseProvider.kt +++ b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/fixtures/FakeEmojibaseProvider.kt @@ -6,18 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.emoji.test +package io.element.android.libraries.emoji.impl.fixtures import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseStore -import io.element.android.libraries.emoji.api.EmojibaseProvider +import io.element.android.libraries.emoji.impl.EmojibaseProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentMap -class FakeEmojibaseProvider( - val emojis: Map> = mapOf(), +internal class FakeEmojibaseProvider( + emojis: Map> = mapOf(), ) : EmojibaseProvider { - override val emojibaseStore: EmojibaseStore - get() = EmojibaseStore(emojis.toPersistentMap()) + private val store = EmojibaseStore(emojis.toPersistentMap()) + override suspend fun getStore(): EmojibaseStore = store } diff --git a/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerPresenterTest.kt b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerPresenterTest.kt new file mode 100644 index 0000000000..cf1a68f9d5 --- /dev/null +++ b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerPresenterTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * 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.emoji.impl.picker + +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import app.cash.turbine.TurbineTestContext +import com.google.common.truth.Truth.assertThat +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.emoji.api.picker.EmojiPickerState +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis +import io.element.android.libraries.emoji.impl.fixtures.FakeEmojibaseProvider +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.robolectric.RobolectricTest +import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class EmojiPickerPresenterTest : RobolectricTest() { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `updating search query loads new results`() = runTest { + createPresenter().test { + val initialState = awaitStoreLoaded() + assertThat(initialState.searchQuery.text.toString()).isEmpty() + assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) + + initialState.searchQuery.setTextAndPlaceCursorAtEnd("smile") + val stateAfterQuery = awaitItem() as DefaultEmojiPickerState + assertThat(stateAfterQuery.searchQuery.text.toString()).isEqualTo("smile") + + val stateWithResults = awaitItem() as DefaultEmojiPickerState + assertThat(stateWithResults.searchQuery.text.toString()).isEqualTo("smile") + assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) + } + } + + @Test + fun `recent emojis are prepended as a category when the source returns any`() = runTest { + createPresenter( + categories = persistentListOf(emojiCategory(EmojibaseCategory.Activity)), + getRecentEmojis = GetRecentEmojis { Result.success(persistentListOf("😊")) }, + ).test { + val state = awaitStoreLoaded() + assertThat(state.categories.size).isEqualTo(2) + } + } + + @Test + fun `ToggleSearchActive toggles the search state`() = runTest { + createPresenter().test { + val initialState = awaitStoreLoaded() + assertThat(initialState.isSearchActive).isFalse() + + initialState.eventSink(EmojiPickerEvent.ToggleSearchActive(true)) + assertThat((awaitItem() as DefaultEmojiPickerState).isSearchActive).isTrue() + + initialState.eventSink(EmojiPickerEvent.ToggleSearchActive(false)) + assertThat((awaitItem() as DefaultEmojiPickerState).isSearchActive).isFalse() + } + } + + @Test + fun `presenter exposes categories from the emoji store when the recent source is empty`() = runTest { + val providedCategories = persistentListOf( + emojiCategory(EmojibaseCategory.Activity), + emojiCategory(EmojibaseCategory.People), + ) + createPresenter(categories = providedCategories).test { + val state = awaitStoreLoaded() + assertThat(state.categories.size).isEqualTo(providedCategories.size) + } + } + + private suspend fun TurbineTestContext.awaitStoreLoaded(): DefaultEmojiPickerState { + var state = awaitItem() as DefaultEmojiPickerState + while (state.categories.isEmpty()) { + state = awaitItem() as DefaultEmojiPickerState + } + return state + } + + private fun TestScope.createPresenter( + categories: ImmutableList>> = persistentListOf(emojiCategory()), + getRecentEmojis: GetRecentEmojis = GetRecentEmojis { Result.success(persistentListOf()) }, + ) = DefaultEmojiPickerPresenter( + emojibaseProvider = FakeEmojibaseProvider(emojis = categories.toMap()), + getRecentEmojis = getRecentEmojis, + coroutineDispatchers = testCoroutineDispatchers(), + ) + + private fun emojiCategory( + category: EmojibaseCategory = EmojibaseCategory.Activity, + emojis: ImmutableList = persistentListOf( + Emoji("1F3C3", "Smile", persistentListOf("smile"), persistentListOf("smile"), "😊", skins = null) + ) + ) = category to emojis +} diff --git a/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt index 2ae8a86c25..822e6e652a 100644 --- a/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt +++ b/libraries/emoji/impl/src/test/kotlin/io/element/android/libraries/emoji/impl/recentemojis/DefaultGetRecentEmojisTest.kt @@ -12,7 +12,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseCategory.People -import io.element.android.libraries.emoji.test.FakeEmojibaseProvider +import io.element.android.libraries.emoji.impl.fixtures.FakeEmojibaseProvider import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.ImmutableList diff --git a/libraries/emoji/test/build.gradle.kts b/libraries/emoji/test/build.gradle.kts deleted file mode 100644 index 42192c8061..0000000000 --- a/libraries/emoji/test/build.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -plugins { - id("io.element.android-library") -} - -android { - namespace = "io.element.android.libraries.emoji.test" -} - -dependencies { - api(projects.libraries.emoji.api) - api(projects.libraries.matrix.api) - api(libs.coroutines.core) - - implementation(libs.kotlinx.collections.immutable) - implementation(libs.coroutines.test) - implementation(libs.matrix.emojibase.bindings) - implementation(projects.tests.testutils) -} From 93d6d5c84c6edbd0eb93bc6dab6d967d0a198050 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jul 2026 15:31:40 +0200 Subject: [PATCH 03/10] Migrate EmojiPickerPresenter to assisted-factory pattern with EmptyGetRecentEmojis --- .../CustomReactionBottomSheet.kt | 2 -- .../customreaction/CustomReactionPresenter.kt | 5 ++++- .../CustomReactionPresenterTest.kt | 9 ++++----- libraries/emoji/api/build.gradle.kts | 2 +- .../emoji/api/picker/EmojiPickerPresenter.kt | 7 ++++++- .../emoji/api/recentemojis/GetRecentEmojis.kt | 9 +++++++++ .../picker/DefaultEmojiPickerPresenter.kt | 19 ++++++++++++++----- 7 files changed, 38 insertions(+), 15 deletions(-) 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 cc971e16ba..c90a0e5acc 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 @@ -37,7 +37,6 @@ fun CustomReactionBottomSheet( CustomReactionState.Target.None, is CustomReactionState.Target.Loading -> Unit is CustomReactionState.Target.Success -> { - fun onEmojiSelectedDismiss(emoji: Emoji) { sheetState.hide(coroutineScope) { state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) @@ -49,7 +48,6 @@ fun CustomReactionBottomSheet( state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) } - ModalBottomSheet( onDismissRequest = ::onDismiss, sheetState = sheetState, 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 7a9f5fe4ff..fe68092be0 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 @@ -17,12 +17,15 @@ 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.emoji.api.picker.EmojiPickerPresenter +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis import kotlinx.collections.immutable.toImmutableSet @Inject class CustomReactionPresenter( - private val emojiPickerPresenter: EmojiPickerPresenter, + emojiPickerPresenterFactory: EmojiPickerPresenter.Factory, + getRecentEmojis: GetRecentEmojis, ) : Presenter { + private val emojiPickerPresenter = emojiPickerPresenterFactory.create(getRecentEmojis) @Composable override fun present(): CustomReactionState { var internalTarget by remember { mutableStateOf(InternalTarget.None) } 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 be765554de..8ba316ea4d 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 @@ -11,6 +11,8 @@ package io.element.android.features.messages.impl.timeline.components.customreac import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions +import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter +import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.tests.testutils.WarmUpRule @@ -24,7 +26,8 @@ class CustomReactionPresenterTest { val warmUpRule = WarmUpRule() private val presenter = CustomReactionPresenter( - emojiPickerPresenter = { anEmojiPickerState() }, + emojiPickerPresenterFactory = EmojiPickerPresenter.Factory { EmojiPickerPresenter { anEmojiPickerState() } }, + getRecentEmojis = EmptyGetRecentEmojis, ) @Test @@ -36,8 +39,6 @@ class CustomReactionPresenterTest { initialState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event)) - assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event)) - val eventId = (awaitItem().target as? CustomReactionState.Target.Success)?.event?.eventId assertThat(eventId).isEqualTo(AN_EVENT_ID) @@ -57,8 +58,6 @@ class CustomReactionPresenterTest { val key = reactions.reactions.first().key initialState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event)) - assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event)) - val stateWithSelectedEmojis = awaitItem() val eventId = (stateWithSelectedEmojis.target as? CustomReactionState.Target.Success)?.event?.eventId assertThat(eventId).isEqualTo(AN_EVENT_ID) diff --git a/libraries/emoji/api/build.gradle.kts b/libraries/emoji/api/build.gradle.kts index 749e13a4c7..48143d9512 100644 --- a/libraries/emoji/api/build.gradle.kts +++ b/libraries/emoji/api/build.gradle.kts @@ -18,5 +18,5 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(libs.kotlinx.collections.immutable) - implementation(libs.matrix.emojibase.bindings) + api(libs.matrix.emojibase.bindings) } diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt index 5e29ee01ab..a20b53a480 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt @@ -8,5 +8,10 @@ package io.element.android.libraries.emoji.api.picker import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis -fun interface EmojiPickerPresenter : Presenter +fun interface EmojiPickerPresenter : Presenter { + fun interface Factory { + fun create(getRecentEmojis: GetRecentEmojis): EmojiPickerPresenter + } +} diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt index 9a9983805e..cf6015d799 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.emoji.api.recentemojis import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf /** * Returns the list of recently used emojis for reactions. @@ -16,3 +17,11 @@ import kotlinx.collections.immutable.ImmutableList fun interface GetRecentEmojis { suspend operator fun invoke(): Result> } + +/** + * A [GetRecentEmojis] that never returns any recent emojis. Handy for pickers that don't want + * a "Recent" tab (e.g. the user-status picker). + */ +object EmptyGetRecentEmojis : GetRecentEmojis { + override suspend fun invoke(): Result> = Result.success(persistentListOf()) +} diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt index e7afe50c0f..6a0c4924b3 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt @@ -17,8 +17,10 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalInspectionMode +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Inject import io.element.android.emojibasebindings.Emoji import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.IconSource @@ -37,13 +39,18 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlin.time.Duration.Companion.milliseconds -@Inject -@ContributesBinding(SessionScope::class) +@AssistedInject class DefaultEmojiPickerPresenter( private val emojibaseProvider: EmojibaseProvider, - private val getRecentEmojis: GetRecentEmojis, + @Assisted private val getRecentEmojis: GetRecentEmojis, private val coroutineDispatchers: CoroutineDispatchers, ) : EmojiPickerPresenter { + @AssistedFactory + @ContributesBinding(SessionScope::class) + interface Factory : EmojiPickerPresenter.Factory { + override fun create(getRecentEmojis: GetRecentEmojis): DefaultEmojiPickerPresenter + } + @Composable override fun present(): EmojiPickerState { val queryState = rememberTextFieldState() @@ -86,7 +93,9 @@ class DefaultEmojiPickerPresenter( val searchQuery = queryState.text.toString() LaunchedEffect(searchQuery, data) { if (searchQuery.isEmpty() || data.allEmojis.isEmpty()) { - emojiResults = SearchBarResultState.Initial() + if (emojiResults !is SearchBarResultState.Initial) { + emojiResults = SearchBarResultState.Initial() + } return@LaunchedEffect } delay(100.milliseconds) From 89deff8a5321f430690ac672544113c07f6f1218 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jul 2026 15:33:54 +0200 Subject: [PATCH 04/10] Add libraries/emoji/test module with picker fakes --- features/messages/impl/build.gradle.kts | 1 + .../CustomReactionPresenterTest.kt | 5 ++--- libraries/emoji/test/build.gradle.kts | 19 +++++++++++++++++++ .../emoji/test/FakeEmojiPickerPresenter.kt | 19 +++++++++++++++++++ .../emoji/test/FakeEmojiPickerState.kt | 18 ++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 libraries/emoji/test/build.gradle.kts create mode 100644 libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerPresenter.kt create mode 100644 libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerState.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index d442e1bb95..b40aebab39 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -107,5 +107,6 @@ dependencies { testImplementation(projects.features.poll.test) testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.emoji.impl) + testImplementation(projects.libraries.emoji.test) testImplementation(projects.libraries.slashcommands.test) } 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 8ba316ea4d..4d3c4ab41a 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 @@ -11,9 +11,8 @@ package io.element.android.features.messages.impl.timeline.components.customreac import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions -import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis -import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState +import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test @@ -26,7 +25,7 @@ class CustomReactionPresenterTest { val warmUpRule = WarmUpRule() private val presenter = CustomReactionPresenter( - emojiPickerPresenterFactory = EmojiPickerPresenter.Factory { EmojiPickerPresenter { anEmojiPickerState() } }, + emojiPickerPresenterFactory = fakeEmojiPickerPresenterFactory(), getRecentEmojis = EmptyGetRecentEmojis, ) diff --git a/libraries/emoji/test/build.gradle.kts b/libraries/emoji/test/build.gradle.kts new file mode 100644 index 0000000000..3a18a239ad --- /dev/null +++ b/libraries/emoji/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.emoji.test" +} + +dependencies { + api(projects.libraries.emoji.api) + implementation(projects.libraries.architecture) +} diff --git a/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerPresenter.kt b/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerPresenter.kt new file mode 100644 index 0000000000..76a239ac49 --- /dev/null +++ b/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerPresenter.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations 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.emoji.test + +import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter +import io.element.android.libraries.emoji.api.picker.EmojiPickerState + +fun fakeEmojiPickerPresenter( + state: EmojiPickerState = FakeEmojiPickerState(), +): EmojiPickerPresenter = EmojiPickerPresenter { state } + +fun fakeEmojiPickerPresenterFactory( + presenter: EmojiPickerPresenter = fakeEmojiPickerPresenter(), +): EmojiPickerPresenter.Factory = EmojiPickerPresenter.Factory { presenter } diff --git a/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerState.kt b/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerState.kt new file mode 100644 index 0000000000..234fe2e9b2 --- /dev/null +++ b/libraries/emoji/test/src/main/kotlin/io/element/android/libraries/emoji/test/FakeEmojiPickerState.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations 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.emoji.test + +import io.element.android.libraries.emoji.api.picker.EmojiPickerState + +/** + * Minimal [EmojiPickerState] for use in tests that don't render the picker (typically presenter + * tests). + */ +data class FakeEmojiPickerState( + override val isReady: Boolean = true, +) : EmojiPickerState From 4dadb3c8c8463756c55b796ae09b28980ddd6dee Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Jul 2026 15:35:21 +0200 Subject: [PATCH 05/10] Wire the emoji picker into the user-status feature --- .idea/codeStyles/Project.xml | 34 +++++++++++ features/preferences/impl/build.gradle.kts | 2 + .../impl/root/PreferencesRootNode.kt | 3 + .../impl/root/PreferencesRootView.kt | 12 +++- .../impl/userstatus/UserStatusEvent.kt | 2 + .../impl/userstatus/UserStatusPresenter.kt | 61 ++++++++++++++----- .../impl/userstatus/UserStatusState.kt | 9 +++ .../userstatus/UserStatusStateProvider.kt | 16 ++++- .../impl/userstatus/UserStatusView.kt | 51 +++++++++++++++- .../impl/root/PreferencesRootViewTest.kt | 2 + .../userstatus/UserStatusPresenterTest.kt | 6 +- .../emoji/api/picker/EmojiPickerRenderer.kt | 11 ++++ 12 files changed, 189 insertions(+), 20 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index cdef735570..8151cb4ea3 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,39 @@ + + + "Y maint ffeil mwyaf sy\'n cael ei ganiatáu yw: %1$s" "Dewiswch ansawdd y fideo rydych chi am ei llwytho." "Dewiswch ansawdd llwytho fideo" - "Chwilio emojis" "Rydych chi eisoes wedi mewngofnodi ar y ddyfais hon fel %1$s ." "Mae angen uwchraddio eich gweinydd cartref i gefnogi Gwasanaeth Dilysu Matrix a chreu cyfrif." "Wedi methu creu\'r ddolen barhaol" diff --git a/libraries/ui-strings/src/main/res/values-da/translations.xml b/libraries/ui-strings/src/main/res/values-da/translations.xml index 41fe346190..614cec39cf 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -440,7 +440,6 @@ Er du sikker på, at du vil fortsætte?" "Den maksimalt tilladte filstørrelse er: %1$s" "Vælg den kvalitet, du ønsker at uploade videoen i." "Vælg kvalitet for video-overførsel" - "Søg emojis" "Du er allerede logget ind på denne enhed som %1$s." "Din hjemmeserver skal opgraderes for at understøtte Matrix Authentication Service og kontooprettelse." "Oprettelse af permalink mislykkedes" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index fe1eaac312..d83fb1ee64 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -433,7 +433,6 @@ Möchtest du wirklich fortfahren?" "Die maximal erlaubte Dateigröße ist: %1$s" "Wähle die Qualität des Videos, das du hochladen möchtest." "Wähle die Video-Upload-Qualität" - "Emojis suchen" "Du bist auf diesem Gerät bereits als %1$s angemeldet." "Dein Homeserver muss aktualisiert werden, um den Matrix Authentication Services und die Erstellung von Konten zu unterstützen." "Fehler beim Erstellen des Permalinks" diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index dab809f74d..938e1a5e9a 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -477,7 +477,6 @@ Kas sa oled kindel, et soovid jätkata?" "Suurim lubatud failisuurus on: %1$s" "Vali üleslaaditava video kvaliteet." "Vali video kvaliteet." - "Otsi emojisid" "Sa juba oled sellesse seadmesse sisseloginud kasutajana %1$s." "Selleks et koos kasutajakonto loomisega toimiks Matrix Authentication Service\'i tugi, vajab sinu koduserver uuendamist." "Püsilingi loomine ei õnnestumud" diff --git a/libraries/ui-strings/src/main/res/values-fa/translations.xml b/libraries/ui-strings/src/main/res/values-fa/translations.xml index 6d377a9652..03d1172040 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -348,7 +348,6 @@ "تغییراتی ذخیره نشده دارید." "تغییراتتان ذخیره نشده‌اند. مطمئنید که می‌خواهید برگردید؟" "ذخیرهٔ تغییرات؟" - "حست‌وجوی شکلک‌ها" "شکست در ایجاد پایاپیوند" "%1$s نتوانست نقشه را بارگیری کند. لطفا بعدا دوباره امتحان کنید." "شکست در بار کردن پیام‌ها" diff --git a/libraries/ui-strings/src/main/res/values-hr/translations.xml b/libraries/ui-strings/src/main/res/values-hr/translations.xml index 03e24aebd3..6ca10c229e 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -475,7 +475,6 @@ Jeste li sigurni da želite nastaviti?" "Maksimalna dopuštena veličina datoteke je: %1$s" "Odaberite kvalitetu videozapisa koji želite prenijeti." "Odaberi kvalitetu prijenosa videozapisa" - "Pretraživanje emotikona" "Već ste prijavljeni na ovom uređaju kao %1$s." "Vaš matični poslužitelj potrebno je nadograditi kako bi podržavao uslugu Matrix Authentication Service i stvaranje računa." "Nije uspjelo stvaranje trajne poveznice" diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index 60ee544965..492268c38f 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -429,7 +429,6 @@ Er du sikker på at du vil fortsette?" "Maksimal tillatt filstørrelse er: %1$s" "Velg kvaliteten på videoen du vil laste opp." "Velg kvalitet for videoopplasting" - "Søk etter emojier" "Du er allerede logget inn på denne enheten som %1$s." "Hjemmeserveren din må oppgraderes for å støtte Matrix Authentication Service og kontooppretting." "Opprettelse av permalenken mislyktes" diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml index fcea19da49..e5c8755a96 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -477,7 +477,6 @@ Czy na pewno chcesz kontynuować?" "Maksymalny dozwolony rozmiar pliku to: %1$s" "Wybierz jakość filmu, który chcesz przesłać." "Wybierz jakość przesyłania wideo" - "Wyszukaj emoji" "Jesteś już zalogowany na tym urządzeniu jako %1$s." "Twój serwer domowy wymaga aktualizacji, aby uzyskać wsparcie usługi Matrix Authentication Service i tworzenia kont." "Nie udało się utworzyć linku bezpośredniego" diff --git a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml index 474d1c8917..49decfa49b 100644 --- a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml @@ -410,7 +410,6 @@ Você tem certeza de que deseja continuar?" "O tamanho máximo permitido de arquivos é: %1$s" "Selecione a qualidade do video que quer enviar." "Selecione a qualidade de envio dos vídeos" - "Pesquisar emojis" "Você já está conectado neste dispositivo como %1$s ." "Seu servidor-casa precisa ser atualizado para oferecer suporte ao Matrix Authentication Service e à criação de contas." "Falha ao criar o link permanente" diff --git a/libraries/ui-strings/src/main/res/values-pt/translations.xml b/libraries/ui-strings/src/main/res/values-pt/translations.xml index e8d32ee72e..617ecad2db 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -391,7 +391,6 @@ Tens a certeza de que queres continuar?" "O tamanho máximo de ficheiro permitido é: %1$s" "Seleciona a qualidade do vídeo que pretendes carregar." "Seleciona a qualidade de carregamento do vídeo" - "Pesquisar emojis" "Já tens sessão iniciada como %1$s neste dispositivo." "Seu homeserver precisa ser atualizado para suportar o Matrix Authentication Service e a criação de conta." "Falha ao criar ligação permanente" diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index bdb220c9a0..4eba83312b 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -451,7 +451,6 @@ Sunteți sigur că doriți să continuați?" "Dimensiunea maximă permisă pentru fișiere este: %1$s" "Selectați calitatea videoclipului pe care doriți să îl încărcați." "Selectați calitatea de încărcare a videoclipurilor" - "Căutați emoticoane" "Sunteți deja conectat pe acest dispozitiv ca %1$s." "Serverul dumneavoastră trebuie actualizat pentru a suporta serviciul de autentificare Matrix și crearea de conturi." "Crearea permalink-ului a eșuat" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index cd711bf18b..892d4348c1 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -476,7 +476,6 @@ "Максимально допустимый размер файла: %1$s" "Выберите качество видео, которое Вы хотите загрузить." "Выберите качество загружаемого видео" - "Поиск эмодзи" "Вы уже вошли на данном устройстве как %1$s." "Домашний сервер необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учётных записей." "Не удалось создать постоянную ссылку" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index f4f03fd3b8..4fdf503c97 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -423,7 +423,6 @@ Naozaj chcete pokračovať?" "Maximálna povolená veľkosť súboru je: %1$s" "Vyberte kvalitu videa, ktoré chcete nahrať." "Vyberte kvalitu nahrávania videa" - "Hľadať emotikony" "Už ste prihlásený/á na tomto zariadení ako %1$s ." "Váš domovský server musí byť aktualizovaný tak, aby podporoval Matrix Authentication Service a vytvorenie účtu." "Nepodarilo sa vytvoriť trvalý odkaz" diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml index ea7ba155c4..4e46cd4653 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -379,7 +379,6 @@ Anledning:%1$s." "Den maximala tillåtna filstorleken är: %1$s" "Välj kvaliteten på videon du vill ladda upp." "Välj videouppladdningskvalitet" - "Sök emojier" "Du är redan inloggad på den här enheten som %1$s." "Din hemserver måste uppgraderas för att stödja Matrix Authentication Service och skapande av konto." "Misslyckades att skapa permalänken" diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index aabea67405..4f92b9547f 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -476,7 +476,6 @@ "Максимально дозволений розмір файлу: %1$s" "Виберіть якість відео, яке ви хочете вивантажити." "Виберіть якість вивантажуваного відео" - "Пошук емоджі" "Ви вже ввійшли на цьому пристрої як %1$s." "Ваш домашній сервер потрібно оновити, щоб він підтримував службу автентифікації Matrix і створення облікових записів." "Не вдалося створити постійне посилання" diff --git a/libraries/ui-strings/src/main/res/values-uz/translations.xml b/libraries/ui-strings/src/main/res/values-uz/translations.xml index 6a21c6aea6..120de32b66 100644 --- a/libraries/ui-strings/src/main/res/values-uz/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uz/translations.xml @@ -450,7 +450,6 @@ Davom etasizmi?" "Ruxsat etilgan maksimal fayl hajmi: %1$s" "Yuklanadigan video sifatini tanlang." "Video yuklash sifatini tanlang" - "Emojilarni qidiring" "Bu qurilmada allaqachon %1$s hisobiga kirgansiz." "Matrix autentifikatsiya xizmati va hisob yaratish imkoniyatini qo‘llab-quvvatlash uchun uy serveringizni yangilash talab etiladi." "Doimiy havola yaratilmadi" diff --git a/libraries/ui-strings/src/main/res/values-vi/translations.xml b/libraries/ui-strings/src/main/res/values-vi/translations.xml index 3da8d7f394..87baf98c47 100644 --- a/libraries/ui-strings/src/main/res/values-vi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-vi/translations.xml @@ -422,7 +422,6 @@ Bạn có chắc muốn tiếp tục không?" "Kích thước tệp tối đa cho phép là: %1$s" "Chọn chất lượng video bạn muốn tải lên." "Chọn chất lượng tải lên video" - "Tìm kiếm biểu tượng cảm xúc" "Bạn đã đăng nhập trên thiết bị này với tư cách là%1$s ." "Máy chủ của bạn cần được nâng cấp để hỗ trợ Dịch vụ Xác thực và tạo tài khoản." "Không tạo được liên kết cố định" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index f54a9cc5a5..c2ff955eac 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -425,7 +425,6 @@ "最大允許的檔案大小為:%1$s" "選取您要上傳的視訊的品質。" "選取視訊上傳品質" - "搜尋表情符號" "您已在此裝置上以 %1$s 的身份登入。" "您的家伺服器需要升級才能支援 Matrix Authentication Service 與帳號建立。" "無法建立永久連結" diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml index e91509ab3f..99f0d20b14 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -466,7 +466,6 @@ "允许的最大文件大小为:%1$s" "选择你要上传的视频的质量。" "选择视频上传质量" - "搜索 Emoji" "你已在此设备以 %1$s 的身份登录。" "你的主服务器需要升级,以支持 Matrix 认证服务和账户创建。" "永久链接创建失败" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 485b9b9d59..ed45f312e3 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -477,7 +477,6 @@ Are you sure you want to continue?" "The max file size allowed is: %1$s" "Select the quality of the video you want to upload." "Select video upload quality" - "Search emojis" "You\'re already logged in on this device as %1$s." "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation." "Failed creating the permalink" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 5753fe5f47..ac5b1295fe 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -261,7 +261,6 @@ { "name" : ":features:messages:impl", "includeRegex" : [ - "emoji_picker_category_.*", "screen_report_content_.*", "screen_room_attachment.*", "screen_room_encrypted.*", @@ -426,6 +425,12 @@ "screen\\.share_location\\..*", "screen\\.view_location\\..*" ] + }, + { + "name" : ":libraries:emoji:impl", + "includeRegex" : [ + "emoji_picker_.*" + ] } ] } From 14feeca2a5881938eb33b2947697fd4490080075 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jul 2026 11:25:40 +0200 Subject: [PATCH 09/10] Add some comments after PR review --- .../preferences/impl/userstatus/UserStatusPresenter.kt | 2 ++ .../emoji/impl/picker/DefaultEmojiPickerPresenter.kt | 2 ++ .../android/libraries/emoji/impl/picker/EmojiItem.kt | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt index 6079ae04cc..60f02ad05a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusPresenter.kt @@ -79,6 +79,8 @@ class UserStatusPresenter( } } + // In CustomInput mode, override the picker state's `emojiPickerSheetState` with the freshly composed emoji-picker + // sub-state so recompositions of the picker propagate up. Other modes pass through unchanged. val effectivePickerState = when (val current = pickerState) { is UserStatusPickerState.CustomInput -> { val emojiPickerState = presentEmojiPickerSheetState(isEmojiPickerVisible) diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt index 6a0c4924b3..9d7dbfdce8 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerPresenter.kt @@ -93,6 +93,8 @@ class DefaultEmojiPickerPresenter( val searchQuery = queryState.text.toString() LaunchedEffect(searchQuery, data) { if (searchQuery.isEmpty() || data.allEmojis.isEmpty()) { + // `SearchBarResultState.Initial` has no `equals` override, so reassigning a new instance + // would trigger a redundant emission each time this effect re-runs while the query is empty. if (emojiResults !is SearchBarResultState.Initial) { emojiResults = SearchBarResultState.Initial() } diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt index 02ed46c0ae..930b42c789 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiItem.kt @@ -91,6 +91,8 @@ internal fun EmojiItem( .onPlaced { coordinates -> val position = coordinates.positionInParent() val parentBounds = coordinates.parentLayoutCoordinates?.size ?: return@onPlaced + // Calculate the offset of the item inside its parent as a percentage, to be able to set the initial offset + // for the selection inside the skin tone picker relative to its size. itemOffsetPercent = if (parentBounds.width > 0) { position.x / parentBounds.width } else { @@ -102,6 +104,7 @@ internal fun EmojiItem( .background(backgroundColor, CircleShape) .indication(interactionSource, ripple()) .pointerInput(item) { + // Always detect long press and tap gestures detectTapGestures( onPress = { pressOffset -> val press = PressInteraction.Press(pressOffset) @@ -119,6 +122,7 @@ internal fun EmojiItem( ) } .then( + // Only detect drag after long press for those items which have a skin tone picker if (hasSkinTones) { Modifier.pointerInput(item) { val slotWidthPx = with(density) { SkinToneSlotSize.toPx() } @@ -135,6 +139,8 @@ internal fun EmojiItem( onDrag = { change, _ -> if (!dismissed) { change.consume() + // It's a valid drag event if it's within ~2 items height above, so, on top of the current one or the skin tone selector, + // or 1 item below, which should be far enough to not be triggered by mistake val isValidDrag = if (change.position.y < startOffset.y) { startOffset.y - change.position.y <= itemSize.height * 2f } else { @@ -146,11 +152,15 @@ internal fun EmojiItem( onDismissSkinPicker() } else { val skinItemsCount = item.skins!!.size + // Original + variants val totalSlots = 1 + skinItemsCount + // Calculate the whole size of the skin tone picker val pickerWidthPx = 2 * paddingPx + totalSlots * slotWidthPx + (totalSlots - 1) * spacingPx + // Calculate the initial offset inside the picker, given the relative position of the item inside its parent val initialOffset = pickerWidthPx * itemOffsetPercent val xInPicker = initialOffset + change.position.x + // If it's a valid offset, calculate the hovered index, otherwise, set it to -1 to indicate that no item is hovered val index = if (xInPicker in 0f..pickerWidthPx) { (xInPicker / slotWidthPx).toInt().coerceIn(0, totalSlots - 1) } else { From 075cca2d21c6e4eea8cf0837457e4ddf904d6f00 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Jul 2026 13:56:30 +0200 Subject: [PATCH 10/10] Sync emoji picker translations from Localazy --- libraries/ui-strings/src/main/res/values-el/translations.xml | 1 - libraries/ui-strings/src/main/res/values-fi/translations.xml | 1 - libraries/ui-strings/src/main/res/values-fr/translations.xml | 1 - libraries/ui-strings/src/main/res/values-hu/translations.xml | 1 - libraries/ui-strings/src/main/res/values-in/translations.xml | 1 - libraries/ui-strings/src/main/res/values-it/translations.xml | 1 - libraries/ui-strings/src/main/res/values-ja/translations.xml | 1 - libraries/ui-strings/src/main/res/values-ko/translations.xml | 1 - 8 files changed, 8 deletions(-) diff --git a/libraries/ui-strings/src/main/res/values-el/translations.xml b/libraries/ui-strings/src/main/res/values-el/translations.xml index 5bbfeb20e5..1b69bb001f 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -434,7 +434,6 @@ "Το μέγιστο επιτρεπόμενο μέγεθος αρχείου είναι: %1$s" "Επιλέξτε την ποιότητα του βίντεο που θέλετε να ανεβάσετε." "Επιλέξτε την ποιότητα μεταφόρτωσης βίντεο" - "Αναζήτηση emoji" "Έχετε ήδη συνδεθεί σε αυτή τη συσκευή ως %1$s." "Ο οικιακός διακομιστής σου πρέπει να αναβαθμιστεί για να υποστηρίζει το Matrix Authentication Service και τη δημιουργία λογαριασμού." "Αποτυχία δημιουργίας του μόνιμου συνδέσμου" diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index ef7d518779..52d3bf16e5 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -477,7 +477,6 @@ Haluatko varmasti jatkaa?" "Suurin sallittu tiedostokoko on: %1$s" "Valitse lähetettävän videon laatu." "Valitse videon lähetyslaatu" - "Etsi emojeja" "Olet jo kirjautuneena tälle laitteelle käyttäjällä %1$s." "Kotipalvelimesi on päivitettävä tukemaan Matrix Authentication Serviceä ja tilin luomista." "Pysyvän linkin luominen epäonnistui" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index a6cbd9b48e..8d550ae51e 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -452,7 +452,6 @@ Raison : %1$s." "La taille maximale de fichier autorisée est: %1$s" "Sélectionnez la qualité des vidéos que vous souhaitez envoyer." "Sélectionnez la qualité d’envoi des vidéos" - "Chercher des emojis" "Vous êtes déjà connecté sur cet appareil en tant que %1$s." "Votre serveur d’accueil doit être mis à jour pour prendre en charge le protocole MAS (Matrix Authentication Service) et la création de compte." "Échec de la création du permalien" diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index af1c071227..0a37baf199 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -467,7 +467,6 @@ Biztos, hogy folytatja?" "A legnagyobb megengedett fájlméret: %1$s" "Válassza ki a feltöltendő videó minőségét." "Feltöltött videó minőségének kiválasztása" - "Emodzsik keresése" "Már bejelentkezett erre az eszközre, mint %1$s." "A Matrix-kiszolgálót frissíteni kell a Matrix Authentication Service és a fióklétrehozás támogatásához." "Nem sikerült létrehozni az állandó hivatkozást" diff --git a/libraries/ui-strings/src/main/res/values-in/translations.xml b/libraries/ui-strings/src/main/res/values-in/translations.xml index ac74be0e8e..5527a10802 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -439,7 +439,6 @@ Apakah Anda yakin ingin melanjutkan?" "Ukuran file maksimum yang diizinkan adalah: %1$s" "Pilih kualitas video yang ingin Anda unggah." "Pilih kualitas unggahan video" - "Cari emoji" "Anda sudah masuk ke akun di perangkat ini sebagai%1$s ." "Homeserver Anda perlu ditingkatkan untuk mendukung Matrix Authentication Service dan pembuatan akun." "Gagal membuat tautan permanen" diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 394e043e07..3b47c41665 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -467,7 +467,6 @@ Sei sicuro di voler continuare?" "La dimensione massima consentita per il file è: %1$s" "Seleziona la qualità del video che vuoi caricare." "Seleziona la qualità di caricamento del video" - "Cerca emoji" "Hai già effettuato l\'accesso su questo dispositivo come %1$s ." "Il tuo homeserver deve essere aggiornato per supportare il Matrix Authentication Service e la creazione di account." "Impossibile creare il collegamento permanente" diff --git a/libraries/ui-strings/src/main/res/values-ja/translations.xml b/libraries/ui-strings/src/main/res/values-ja/translations.xml index 9ab50981dc..7f54e420b8 100644 --- a/libraries/ui-strings/src/main/res/values-ja/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ja/translations.xml @@ -452,7 +452,6 @@ "アップロードできる最大サイズは %1$s です。" "アップロードする画質を選択してください。" "動画のアップロード品質を選択" - "絵文字を検索" "この端末では既に %1$s としてログインしています。" "Matrix Authentication Service とアカウント作成に対応するため、このホームサーバーはアップグレードが必要です。" "固定リンクの作成に失敗しました" diff --git a/libraries/ui-strings/src/main/res/values-ko/translations.xml b/libraries/ui-strings/src/main/res/values-ko/translations.xml index 3010ef136c..e97fe17ca5 100644 --- a/libraries/ui-strings/src/main/res/values-ko/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -443,7 +443,6 @@ " "업로드할 비디오의 품질을 선택하세요." "비디오 업로드 품질 선택" - "이모지 검색" "이 기기에서 이미 %1$s님으로 로그인되어 있습니다." "Matrix Authentication Service 및 계정 생성을 지원하려면 홈서버를 업그레이드해야 합니다." "퍼머링크 생성 실패"