Merge pull request #7257 from element-hq/feature/fga/user-status-emoji-picker

Plug emoji picker for custom user status
This commit is contained in:
ganfra
2026-07-23 14:39:55 +02:00
committed by GitHub
202 changed files with 1428 additions and 950 deletions
+34
View File
@@ -1,5 +1,39 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
@@ -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.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
import io.element.android.x.BuildConfig
import io.element.android.x.R
import kotlinx.coroutines.CoroutineName
@@ -115,10 +113,4 @@ object AppModule {
fun provideSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher()
}
@Provides
@SingleIn(AppScope::class)
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
return DefaultEmojibaseProvider(context)
}
}
+3 -2
View File
@@ -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)
}
@@ -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(
@@ -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
@@ -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<String> = persistentListOf(),
eventSink: (CustomReactionEvent) -> Unit = {},
) = CustomReactionState(
target = target,
recentEmojis = recentEmojis,
selectedEmoji = persistentSetOf(),
eventSink = eventSink,
)
@@ -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
@@ -146,9 +145,10 @@ fun MessagesView(
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit,
onThreadsListClick: () -> Unit,
knockRequestsBannerView: @Composable () -> Unit,
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 = {},
)
}
@@ -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
@@ -43,6 +43,7 @@ internal fun MessagesViewWithIdentityChangePreview(
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
knockRequestsBannerView = {},
customReactionBottomSheet = {},
onThreadsListClick = {},
)
}
@@ -322,6 +322,7 @@ class ThreadedMessagesNode(
onViewAllPinnedMessagesClick = {},
modifier = modifier,
knockRequestsBannerView = {},
customReactionBottomSheet = {},
onThreadsListClick = {},
)
@@ -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,43 @@ 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) {
sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvent.DismissCustomReactionSheet)
onSelectEmoji(state.target.event.eventOrTransactionId, emoji)
}
}
fun onEmojiSelectedDismiss(emoji: Emoji) {
if (target?.event == null) return
sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvent.DismissCustomReactionSheet)
onSelectEmoji(target.event.eventOrTransactionId, emoji)
}
}
fun onDismiss() {
state.eventSink(CustomReactionEvent.DismissCustomReactionSheet)
}
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,
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(),
)
}
}
}
@@ -9,59 +9,53 @@
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.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.recentemojis.api.GetRecentEmojis
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.launch
@Inject
class CustomReactionPresenter(
private val emojibaseProvider: EmojibaseProvider,
private val getRecentEmojis: GetRecentEmojis,
emojiPickerPresenterFactory: EmojiPickerPresenter.Factory,
getRecentEmojis: GetRecentEmojis,
) : Presenter<CustomReactionState> {
private val emojiPickerPresenter = emojiPickerPresenterFactory.create(getRecentEmojis)
@Composable
override fun present(): CustomReactionState {
val localCoroutineScope = rememberCoroutineScope()
var recentEmojis by remember { mutableStateOf<ImmutableList<String>>(persistentListOf()) }
val target: MutableState<CustomReactionState.Target> = 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>(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 +63,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
}
@@ -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<String>,
val recentEmojis: ImmutableList<String>,
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
}
}
@@ -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<String>,
private val coroutineDispatchers: CoroutineDispatchers,
) : Presenter<EmojiPickerState> {
@Composable
override fun present(): EmojiPickerState {
val queryState = rememberTextFieldState()
var isSearchActive by remember { mutableStateOf(false) }
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(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,
)
}
}
@@ -6,14 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"Зашыфравана невядомай ці выдаленай прыладай."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашыфравана прыладай, не пацверджанай яе ўладальнікам."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашыфравана неправераным карыстальнікам."</string>
<string name="emoji_picker_category_activity">"Актыўнасці"</string>
<string name="emoji_picker_category_flags">"Сцягі"</string>
<string name="emoji_picker_category_foods">"Ежа &amp; Напоі"</string>
<string name="emoji_picker_category_nature">"Жывёлы &amp; Прырода"</string>
<string name="emoji_picker_category_objects">"Аб\'екты"</string>
<string name="emoji_picker_category_people">"Усмешкі &amp; Удзельнікі"</string>
<string name="emoji_picker_category_places">"Падарожжы &amp; Месцы"</string>
<string name="emoji_picker_category_symbols">"Сімвалы"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
<string name="screen_media_upload_preview_processing">"Апрацоўка…"</string>
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_event_authenticity_sent_in_clear">"Без шифроване"</string>
<string name="emoji_picker_category_activity">"Дейности"</string>
<string name="emoji_picker_category_flags">"Знамена"</string>
<string name="emoji_picker_category_foods">"Храна &amp; Напитки"</string>
<string name="emoji_picker_category_nature">"Животни &amp; Природа"</string>
<string name="emoji_picker_category_objects">"Обекти"</string>
<string name="emoji_picker_category_people">"Усмивки &amp; Хора"</string>
<string name="emoji_picker_category_places">"Пътуване &amp; Места"</string>
<string name="emoji_picker_category_symbols">"Символи"</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Докоснете, за да промените качеството на качване на видео"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Файлът не можа да бъде качен."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Неуспешна обработка на мултимедия за качване, моля, опитайте отново."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Xifrat per un dispositiu desconegut o eliminat."</string>
<string name="crypto_event_authenticity_unsigned_device">"Xifrat per un dispositiu no verificat pel seu propietari."</string>
<string name="crypto_event_authenticity_unverified_identity">"Xifrat per un usuari no verificat."</string>
<string name="emoji_picker_category_activity">"Activitats"</string>
<string name="emoji_picker_category_flags">"Banderes"</string>
<string name="emoji_picker_category_foods">"Menjar &amp; begudes"</string>
<string name="emoji_picker_category_nature">"Animals i naturalesa"</string>
<string name="emoji_picker_category_objects">"Objectes"</string>
<string name="emoji_picker_category_people">"Cares i persones"</string>
<string name="emoji_picker_category_places">"Viatges i llocs"</string>
<string name="emoji_picker_category_recent">"Emoticones recents"</string>
<string name="emoji_picker_category_symbols">"Símbols"</string>
<string name="screen_media_upload_preview_caption_warning">"És possible que les llegendes no siguin visibles pels que utilitzin aplicacions antigues."</string>
<string name="screen_media_upload_preview_error_failed_processing">"No s\'ha pogut processar el contingut que s\'havia de pujar. Torna-ho a provar."</string>
<string name="screen_media_upload_preview_error_failed_sending">"No s\'ha pogut pujar el contingut. Torna-ho a provar."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Šifrováno neznámým nebo smazaným zařízením."</string>
<string name="crypto_event_authenticity_unsigned_device">"Šifrováno zařízením, které nebylo ověřeno jeho vlastníkem."</string>
<string name="crypto_event_authenticity_unverified_identity">"Šifrováno neověřeným uživatelem."</string>
<string name="emoji_picker_category_activity">"Aktivity"</string>
<string name="emoji_picker_category_flags">"Vlajky"</string>
<string name="emoji_picker_category_foods">"Jídlo a nápoje"</string>
<string name="emoji_picker_category_nature">"Zvířata a příroda"</string>
<string name="emoji_picker_category_objects">"Předměty"</string>
<string name="emoji_picker_category_people">"Smajlíci a lidé"</string>
<string name="emoji_picker_category_places">"Cestování a místa"</string>
<string name="emoji_picker_category_recent">"Nedávné emotikony"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Klepnutím změníte kvalitu nahrávání videa"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Soubor nelze nahrát."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Wedi\'i amgryptio gan ddyfais anhysbys neu wedi\'i dileu."</string>
<string name="crypto_event_authenticity_unsigned_device">"Wedi\'i amgryptio gan ddyfais nad yw wedi\'i wirio gan ei pherchennog."</string>
<string name="crypto_event_authenticity_unverified_identity">"Wedi\'i amgryptio gan ddefnyddiwr heb ei wirio."</string>
<string name="emoji_picker_category_activity">"Gweithgareddau"</string>
<string name="emoji_picker_category_flags">"Baneri"</string>
<string name="emoji_picker_category_foods">"Bwyd a Diod"</string>
<string name="emoji_picker_category_nature">"Anifeiliaid a Natur"</string>
<string name="emoji_picker_category_objects">"Gwrthrychau"</string>
<string name="emoji_picker_category_people">"Wynebau Hapus a Phobl"</string>
<string name="emoji_picker_category_places">"Teithio a Llefydd"</string>
<string name="emoji_picker_category_recent">"Emojis diweddar"</string>
<string name="emoji_picker_category_symbols">"Symbolau"</string>
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tapiwch i newid ansawdd llwytho\'r fideo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Nid oedd modd llwytho\'r ffeil."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Krypteret af en ukendt eller slettet enhed."</string>
<string name="crypto_event_authenticity_unsigned_device">"Krypteret af en enhed, der ikke er verificeret af sin ejer."</string>
<string name="crypto_event_authenticity_unverified_identity">"Krypteret af en ikke-verificeret bruger."</string>
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flag"</string>
<string name="emoji_picker_category_foods">"Mad og drikke"</string>
<string name="emoji_picker_category_nature">"Dyr og natur"</string>
<string name="emoji_picker_category_objects">"Objekter"</string>
<string name="emoji_picker_category_people">"Smileys og personer"</string>
<string name="emoji_picker_category_places">"Rejser og steder"</string>
<string name="emoji_picker_category_recent">"Seneste emojis"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="screen_media_upload_preview_caption_warning">"Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tryk for at ændre videokvaliteten i uploadet"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke uploades."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Verschlüsselt von einem unbekannten oder gelöschten Gerät."</string>
<string name="crypto_event_authenticity_unsigned_device">"Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde."</string>
<string name="crypto_event_authenticity_unverified_identity">"Verschlüsselt durch einen nicht verifizierten Nutzer."</string>
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
<string name="emoji_picker_category_flags">"Flaggen"</string>
<string name="emoji_picker_category_foods">"Essen &amp; Trinken"</string>
<string name="emoji_picker_category_nature">"Tiere &amp; Natur"</string>
<string name="emoji_picker_category_objects">"Objekte"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Menschen"</string>
<string name="emoji_picker_category_places">"Reisen &amp; Orte"</string>
<string name="emoji_picker_category_recent">"Zuletzt verwendete Emojis"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="screen_media_upload_preview_caption_warning">"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tippe, um die Qualität des Video-Uploads zu ändern"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Die Datei konnte nicht hochgeladen werden."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Κρυπτογραφημένο από άγνωστη ή διαγεγραμμένη συσκευή."</string>
<string name="crypto_event_authenticity_unsigned_device">"Κρυπτογραφημένο από μια συσκευή που δεν έχει επαληθευτεί από τον ιδιοκτήτη της."</string>
<string name="crypto_event_authenticity_unverified_identity">"Κρυπτογραφημένο από μη επαληθευμένο χρήστη."</string>
<string name="emoji_picker_category_activity">"Δραστηριότητες"</string>
<string name="emoji_picker_category_flags">"Σημαίες"</string>
<string name="emoji_picker_category_foods">"Φαγητό &amp; Ποτό"</string>
<string name="emoji_picker_category_nature">"Ζώα &amp; Φύση"</string>
<string name="emoji_picker_category_objects">"Αντικείμενα"</string>
<string name="emoji_picker_category_people">"Φατσούλες &amp; Άνθρωποι"</string>
<string name="emoji_picker_category_places">"Ταξίδια &amp; Μέρη"</string>
<string name="emoji_picker_category_recent">"Πρόσφατα emoji"</string>
<string name="emoji_picker_category_symbols">"Σύμβολα"</string>
<string name="screen_media_upload_preview_caption_warning">"Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Πατήστε για να αλλάξετε την ποιότητα μεταφόρτωσης βίντεο"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Δεν ήταν δυνατή η μεταφόρτωση του αρχείου."</string>
@@ -7,14 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Cifrado por un dispositivo desconocido o eliminado."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifrado por un dispositivo no verificado por su propietario."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifrado por un usuario no verificado."</string>
<string name="emoji_picker_category_activity">"Actividades"</string>
<string name="emoji_picker_category_flags">"Banderas"</string>
<string name="emoji_picker_category_foods">"Comida y bebida"</string>
<string name="emoji_picker_category_nature">"Animales y naturaleza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Emojis y personas"</string>
<string name="emoji_picker_category_places">"Viajes y lugares"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_media_upload_preview_caption_warning">"Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Krüptitud tundmatu või kustutatud seadme poolt."</string>
<string name="crypto_event_authenticity_unsigned_device">"Krüptitud seadme poolt, mida tema omanik pole verifitseerinud."</string>
<string name="crypto_event_authenticity_unverified_identity">"Krüptitud verifitseerimata kasutaja poolt."</string>
<string name="emoji_picker_category_activity">"Tegevused"</string>
<string name="emoji_picker_category_flags">"Lipud"</string>
<string name="emoji_picker_category_foods">"Toit ja jook"</string>
<string name="emoji_picker_category_nature">"Loomad ja loodus"</string>
<string name="emoji_picker_category_objects">"Esemed"</string>
<string name="emoji_picker_category_people">"Emotikonid ja inimesed"</string>
<string name="emoji_picker_category_places">"Reisimine ja kohad"</string>
<string name="emoji_picker_category_recent">"Hiljutised emojid"</string>
<string name="emoji_picker_category_symbols">"Sümbolid"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Pööra pilti ümber rõhttelje"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Pilt on pööratud ümber rõhttelje"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Pildi algne variant"</string>
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_event_authenticity_sent_in_clear">"Ez dago zifratuta."</string>
<string name="emoji_picker_category_activity">"Jarduerak"</string>
<string name="emoji_picker_category_flags">"Banderak"</string>
<string name="emoji_picker_category_foods">"Jana eta edana"</string>
<string name="emoji_picker_category_nature">"Animaliak eta natura"</string>
<string name="emoji_picker_category_objects">"Objektuak"</string>
<string name="emoji_picker_category_people">"Irribartxoak eta jendea"</string>
<string name="emoji_picker_category_places">"Bidaiak eta tokiak"</string>
<string name="emoji_picker_category_symbols">"Ikurrak"</string>
<string name="screen_media_upload_preview_error_failed_sending">"Huts egin du multimedia igotzeak, saiatu berriro."</string>
<string name="screen_report_content_block_user">"Blokeatu erabiltzailea"</string>
<string name="screen_report_content_explanation">"Mezua zure zerbitzariko administratzaileari jakinaraziko zaio. Ezingo dute zifratutako mezurik irakurri."</string>
@@ -6,15 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"رمز شده به دست افزاره‌ای ناشناخته یا حذف شده."</string>
<string name="crypto_event_authenticity_unsigned_device">"رمز شده به دست افزاره‌ای که از سوی مالکش تأیید نشده."</string>
<string name="crypto_event_authenticity_unverified_identity">"رمز شده به دست کاربری تأیید نشده."</string>
<string name="emoji_picker_category_activity">"فعّالیت‌ها"</string>
<string name="emoji_picker_category_flags">"پرچم‌ها"</string>
<string name="emoji_picker_category_foods">"غذا و نوشیدنی"</string>
<string name="emoji_picker_category_nature">"حیوانات و طبعیت"</string>
<string name="emoji_picker_category_objects">"اشیا"</string>
<string name="emoji_picker_category_people">"شکلک‌ها و افراد"</string>
<string name="emoji_picker_category_places">"سفر و مکان‌ها"</string>
<string name="emoji_picker_category_recent">"شکلک‌های اخیر"</string>
<string name="emoji_picker_category_symbols">"نمادها"</string>
<string name="screen_media_upload_preview_error_failed_processing">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_error_failed_sending">"بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_processing">"پردازش کردن…"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Tuntemattoman tai poistetun laitteen salaama."</string>
<string name="crypto_event_authenticity_unsigned_device">"Salattu laitteella, jota sen omistaja ei ole vahvistanut."</string>
<string name="crypto_event_authenticity_unverified_identity">"Vahvistamattoman käyttäjän salaama."</string>
<string name="emoji_picker_category_activity">"Aktiviteetit"</string>
<string name="emoji_picker_category_flags">"Liput"</string>
<string name="emoji_picker_category_foods">"Ruoka ja juoma"</string>
<string name="emoji_picker_category_nature">"Eläimet ja luonto"</string>
<string name="emoji_picker_category_objects">"Esineet"</string>
<string name="emoji_picker_category_people">"Hymiöt ja ihmiset"</string>
<string name="emoji_picker_category_places">"Matkustaminen ja paikat"</string>
<string name="emoji_picker_category_recent">"Viimeaikaiset emojit"</string>
<string name="emoji_picker_category_symbols">"Symbolit"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Käännä kuva vaakasuunnassa"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Käännetty vaakasuunnassa"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Alkuperäinen"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Chiffré par un appareil inconnu ou supprimé."</string>
<string name="crypto_event_authenticity_unsigned_device">"Chiffré par un appareil non vérifié par son propriétaire."</string>
<string name="crypto_event_authenticity_unverified_identity">"Chiffré par un utilisateur non vérifié."</string>
<string name="emoji_picker_category_activity">"Activités"</string>
<string name="emoji_picker_category_flags">"Drapeaux"</string>
<string name="emoji_picker_category_foods">"Nourriture et boissons"</string>
<string name="emoji_picker_category_nature">"Animaux et nature"</string>
<string name="emoji_picker_category_objects">"Objets"</string>
<string name="emoji_picker_category_people">"Émoticônes et personnes"</string>
<string name="emoji_picker_category_places">"Voyages &amp; lieux"</string>
<string name="emoji_picker_category_recent">"Emojis récents"</string>
<string name="emoji_picker_category_symbols">"Symboles"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Retourner l’image horizontalement"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Retournée horizontalement"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Šifrirano nepoznatim ili izbrisanim uređajem."</string>
<string name="crypto_event_authenticity_unsigned_device">"Šifrirano uređajem koji vlasnik nije potvrdio."</string>
<string name="crypto_event_authenticity_unverified_identity">"Šifrirao neprovjereni korisnik."</string>
<string name="emoji_picker_category_activity">"Aktivnosti"</string>
<string name="emoji_picker_category_flags">"Zastave"</string>
<string name="emoji_picker_category_foods">"Hrana i piće"</string>
<string name="emoji_picker_category_nature">"Životinje i priroda"</string>
<string name="emoji_picker_category_objects">"Objekti"</string>
<string name="emoji_picker_category_people">"Emotikoni i osobe"</string>
<string name="emoji_picker_category_places">"Putovanja i mjesta"</string>
<string name="emoji_picker_category_recent">"Nedavni emotikoni"</string>
<string name="emoji_picker_category_symbols">"Simboli"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Okreni sliku vodoravno"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Okrenuto vodoravno"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Izvornik"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Ismeretlen vagy törölt eszköz által titkosítva."</string>
<string name="crypto_event_authenticity_unsigned_device">"A tulajdonos által nem ellenőrzött eszköz által titkosítva."</string>
<string name="crypto_event_authenticity_unverified_identity">"Nem ellenőrzött felhasználó által titkosítva."</string>
<string name="emoji_picker_category_activity">"Tevékenységek"</string>
<string name="emoji_picker_category_flags">"Zászlók"</string>
<string name="emoji_picker_category_foods">"Étel és ital"</string>
<string name="emoji_picker_category_nature">"Állatok és természet"</string>
<string name="emoji_picker_category_objects">"Tárgyak"</string>
<string name="emoji_picker_category_people">"Mosolyok és emberek"</string>
<string name="emoji_picker_category_places">"Utazás és helyek"</string>
<string name="emoji_picker_category_recent">"Legutóbbi emodzsik"</string>
<string name="emoji_picker_category_symbols">"Szimbólumok"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Kép vízszintes tükrözése"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Vízszintesen tükrözve"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Eredeti"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Dienkripsi oleh perangkat yang tidak dikenal atau dihapus."</string>
<string name="crypto_event_authenticity_unsigned_device">"Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya."</string>
<string name="crypto_event_authenticity_unverified_identity">"Dienkripsi oleh pengguna yang tidak terverifikasi."</string>
<string name="emoji_picker_category_activity">"Aktivitas"</string>
<string name="emoji_picker_category_flags">"Bendera"</string>
<string name="emoji_picker_category_foods">"Makanan &amp; Minuman"</string>
<string name="emoji_picker_category_nature">"Hewan &amp; Alam"</string>
<string name="emoji_picker_category_objects">"Objek"</string>
<string name="emoji_picker_category_people">"Senyuman &amp; Orang"</string>
<string name="emoji_picker_category_places">"Wisata &amp; Tempat"</string>
<string name="emoji_picker_category_recent">"Emojis Sebelumnya"</string>
<string name="emoji_picker_category_symbols">"Simbol"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"Putar gambar ke kiri"</string>
<plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="other">"%1$d derajat"</item>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Cifrato da un dispositivo sconosciuto o eliminato."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifrato da un dispositivo non verificato dal proprietario."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifrato da un utente non verificato."</string>
<string name="emoji_picker_category_activity">"Attività"</string>
<string name="emoji_picker_category_flags">"Bandiere"</string>
<string name="emoji_picker_category_foods">"Cibi &amp; Bevande"</string>
<string name="emoji_picker_category_nature">"Animali &amp; Natura"</string>
<string name="emoji_picker_category_objects">"Oggetti"</string>
<string name="emoji_picker_category_people">"Faccine &amp; Persone"</string>
<string name="emoji_picker_category_places">"Viaggi &amp; Luoghi"</string>
<string name="emoji_picker_category_recent">"Emoji recenti"</string>
<string name="emoji_picker_category_symbols">"Simboli"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Capovolgi l\'immagine orizzontalmente"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Capovolto orizzontalmente"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"削除されたまたは不明な端末により暗号化されています。"</string>
<string name="crypto_event_authenticity_unsigned_device">"所有者に検証されていない端末により暗号化されています。"</string>
<string name="crypto_event_authenticity_unverified_identity">"未検証のユーザーにより暗号化されています。"</string>
<string name="emoji_picker_category_activity">"アクティビティ"</string>
<string name="emoji_picker_category_flags">"旗"</string>
<string name="emoji_picker_category_foods">"食べ物"</string>
<string name="emoji_picker_category_nature">"動物・自然"</string>
<string name="emoji_picker_category_objects">"物"</string>
<string name="emoji_picker_category_people">"顔・人"</string>
<string name="emoji_picker_category_places">"旅・場所"</string>
<string name="emoji_picker_category_recent">"最近使用"</string>
<string name="emoji_picker_category_symbols">"記号"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平に反転"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"垂直に反転"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"元画像"</string>
@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"აქტივობები"</string>
<string name="emoji_picker_category_flags">"დროშები"</string>
<string name="emoji_picker_category_foods">"Საჭმელ-სასმელი"</string>
<string name="emoji_picker_category_nature">"ცხოველები &amp; ბუნება"</string>
<string name="emoji_picker_category_objects">"ობიექტები"</string>
<string name="emoji_picker_category_people">"ღიმილები &amp; ხალხი"</string>
<string name="emoji_picker_category_places">"მოგზაურობა და ადგილები"</string>
<string name="emoji_picker_category_symbols">"სიმბოლოები"</string>
<string name="screen_media_upload_preview_error_failed_processing">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_media_upload_preview_error_failed_sending">"მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_report_content_block_user">"მომხმარებლის დაბლოკვა"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"알 수 없거나 삭제된 장치에 의해 암호화됩니다."</string>
<string name="crypto_event_authenticity_unsigned_device">"소유자가 확인하지 않은 장치에 의해 암호화되었습니다."</string>
<string name="crypto_event_authenticity_unverified_identity">"검증되지 않은 사용자에 의해 암호화되었습니다."</string>
<string name="emoji_picker_category_activity">"활동"</string>
<string name="emoji_picker_category_flags">"깃발"</string>
<string name="emoji_picker_category_foods">"음식 &amp; 음료"</string>
<string name="emoji_picker_category_nature">"동물 &amp; 자연"</string>
<string name="emoji_picker_category_objects">"사물"</string>
<string name="emoji_picker_category_people">"표정 &amp; 사람"</string>
<string name="emoji_picker_category_places">"여행 &amp; 장소"</string>
<string name="emoji_picker_category_recent">"최근 이모지"</string>
<string name="emoji_picker_category_symbols">"상징"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"이미지를 왼쪽으로 회전"</string>
<plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="other">"%1$d도"</item>
@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Veikla"</string>
<string name="emoji_picker_category_flags">"Vėliavos"</string>
<string name="emoji_picker_category_foods">"Maistas ir Gėrimai"</string>
<string name="emoji_picker_category_nature">"Gyvūnai ir Gamta"</string>
<string name="emoji_picker_category_objects">"Objektai"</string>
<string name="emoji_picker_category_people">"Šypsenėlės ir Žmonės"</string>
<string name="emoji_picker_category_places">"Kelionės ir Vietovės"</string>
<string name="emoji_picker_category_symbols">"Simboliai"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nepavyko įkelti laikmenos, pabandykite dar kartą."</string>
<string name="screen_report_content_block_user">"Blokuoti vartotoją"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Kryptert av en ukjent eller slettet enhet."</string>
<string name="crypto_event_authenticity_unsigned_device">"Kryptert med en enhet som ikke er verifisert av eieren."</string>
<string name="crypto_event_authenticity_unverified_identity">"Kryptert av en ubekreftet bruker."</string>
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flagg"</string>
<string name="emoji_picker_category_foods">"Mat og drikke"</string>
<string name="emoji_picker_category_nature">"Dyr og natur"</string>
<string name="emoji_picker_category_objects">"Gjenstander"</string>
<string name="emoji_picker_category_people">"Smilefjes og mennesker"</string>
<string name="emoji_picker_category_places">"Reising og steder"</string>
<string name="emoji_picker_category_recent">"Nylige emojier"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="screen_media_upload_preview_caption_warning">"Teksting er kanskje ikke synlig for personer som bruker eldre apper."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Trykk for å endre kvaliteten på videoopplastingen"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke lastes opp."</string>
@@ -6,14 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"Versleuteld door een onbekend of verwijderd apparaat."</string>
<string name="crypto_event_authenticity_unsigned_device">"Versleuteld door een apparaat dat niet is geverifieerd door de eigenaar."</string>
<string name="crypto_event_authenticity_unverified_identity">"Versleuteld door een niet-geverifieerde gebruiker."</string>
<string name="emoji_picker_category_activity">"Activiteiten"</string>
<string name="emoji_picker_category_flags">"Vlaggen"</string>
<string name="emoji_picker_category_foods">"Eten &amp; Drinken"</string>
<string name="emoji_picker_category_nature">"Dieren &amp; Natuur"</string>
<string name="emoji_picker_category_objects">"Voorwerpen"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Mensen"</string>
<string name="emoji_picker_category_places">"Reizen &amp; Locaties"</string>
<string name="emoji_picker_category_symbols">"Symbolen"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Het uploaden van media is mislukt. Probeer het opnieuw."</string>
<string name="screen_report_content_block_user">"Gebruiker blokkeren"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Zaszyfrowana przez nieznane lub usunięte urządzenie."</string>
<string name="crypto_event_authenticity_unsigned_device">"Zaszyfrowana przez urządzenie niezweryfikowane przez jego właściciela."</string>
<string name="crypto_event_authenticity_unverified_identity">"Zaszyfrowana przez niezweryfikowanego użytkownika."</string>
<string name="emoji_picker_category_activity">"Aktywności"</string>
<string name="emoji_picker_category_flags">"Flagi"</string>
<string name="emoji_picker_category_foods">"Jedzenie i napoje"</string>
<string name="emoji_picker_category_nature">"Zwierzęta i natura"</string>
<string name="emoji_picker_category_objects">"Obiekty"</string>
<string name="emoji_picker_category_people">"Buźki i osoby"</string>
<string name="emoji_picker_category_places">"Podróż i miejsca"</string>
<string name="emoji_picker_category_recent">"Ostatnie emoji"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Odwróć obraz w poziomie"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Odwrócono w poziomie"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Oryginalny"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Criptografado por um dispositivo desconhecido ou apagado."</string>
<string name="crypto_event_authenticity_unsigned_device">"Criptografado por um dispositivo que não foi verificado pelo seu dono."</string>
<string name="crypto_event_authenticity_unverified_identity">"Criptografado por um usuário não verificado."</string>
<string name="emoji_picker_category_activity">"Atividades"</string>
<string name="emoji_picker_category_flags">"Bandeiras"</string>
<string name="emoji_picker_category_foods">"Comida &amp; Bebida"</string>
<string name="emoji_picker_category_nature">"Animais &amp; Natureza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Sorrisos &amp; Pessoas"</string>
<string name="emoji_picker_category_places">"Viagens &amp; Lugares"</string>
<string name="emoji_picker_category_recent">"Emojis recentes"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_media_upload_preview_caption_warning">"As legendas podem não ser visíveis para pessoas que usam apps mais antigos."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Toque para alterar a qualidade do envio do vídeo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"O arquivo não pôde ser enviado."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Cifragem com origem num dispositivo eliminado ou desconhecido."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifragem com origem num dispositivo não verificado pelo seu dono."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifragem com origem num utilizador não verificado."</string>
<string name="emoji_picker_category_activity">"Atividades"</string>
<string name="emoji_picker_category_flags">"Bandeiras"</string>
<string name="emoji_picker_category_foods">"Comidas e Bebidas"</string>
<string name="emoji_picker_category_nature">"Animais e Natureza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Caras e Pessoas"</string>
<string name="emoji_picker_category_places">"Viagens e Lugares"</string>
<string name="emoji_picker_category_recent">"Emojis recentes"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_media_upload_preview_caption_warning">"As legendas poderão não ser visíveis em versões mais antigas da aplicação."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Toca para alterar a qualidade de carregamento do vídeo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Não foi possível enviar o ficheiro"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Criptat de un dispozitiv necunoscut sau șters."</string>
<string name="crypto_event_authenticity_unsigned_device">"Criptat de un dispozitiv care nu este verificat de proprietarul său."</string>
<string name="crypto_event_authenticity_unverified_identity">"Criptat de un utilizator neverificat."</string>
<string name="emoji_picker_category_activity">"Activități"</string>
<string name="emoji_picker_category_flags">"Steaguri"</string>
<string name="emoji_picker_category_foods">"Mâncare &amp; Băutură"</string>
<string name="emoji_picker_category_nature">"Animale și Natură"</string>
<string name="emoji_picker_category_objects">"Obiecte"</string>
<string name="emoji_picker_category_people">"Fețe zâmbitoare &amp; Oameni"</string>
<string name="emoji_picker_category_places">"Călătorii &amp; Locuri"</string>
<string name="emoji_picker_category_recent">"Emoticoane recente"</string>
<string name="emoji_picker_category_symbols">"Simboluri"</string>
<string name="screen_media_upload_preview_caption_warning">"Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Atingeți pentru a modifica calitatea încărcării videoclipului"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Fișierul nu a putut fi încărcat."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Зашифровано неизвестным или удаленным устройством."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашифровано устройством, не проверенным его владельцем."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашифровано непроверенным пользователем."</string>
<string name="emoji_picker_category_activity">"Активности"</string>
<string name="emoji_picker_category_flags">"Флаги"</string>
<string name="emoji_picker_category_foods">"Еда и напитки"</string>
<string name="emoji_picker_category_nature">"Животные и природа"</string>
<string name="emoji_picker_category_objects">"Объекты"</string>
<string name="emoji_picker_category_people">"Эмодзи и люди"</string>
<string name="emoji_picker_category_places">"Путешествия и места"</string>
<string name="emoji_picker_category_recent">"Недавние эмодзи"</string>
<string name="emoji_picker_category_symbols">"Символы"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Перевернуть изображение по горизонтали"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонтали"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригинал"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Zašifrované neznámym alebo odstráneným zariadením."</string>
<string name="crypto_event_authenticity_unsigned_device">"Šifrované zariadením, ktoré nie je overené jeho majiteľom."</string>
<string name="crypto_event_authenticity_unverified_identity">"Šifrované neovereným používateľom."</string>
<string name="emoji_picker_category_activity">"Aktivity"</string>
<string name="emoji_picker_category_flags">"Vlajky"</string>
<string name="emoji_picker_category_foods">"Jedlo a nápoje"</string>
<string name="emoji_picker_category_nature">"Zvieratá a príroda"</string>
<string name="emoji_picker_category_objects">"Predmety"</string>
<string name="emoji_picker_category_people">"Smajlíky a ľudia"</string>
<string name="emoji_picker_category_places">"Cestovanie a miesta"</string>
<string name="emoji_picker_category_recent">"Nedávne emotikony"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Ťuknutím zmeníte kvalitu nahratého videa"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Súbor sa nepodarilo nahrať."</string>
@@ -7,14 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Krypterad av en okänd eller raderad enhet."</string>
<string name="crypto_event_authenticity_unsigned_device">"Krypterad av en enhet som inte verifierats av ägaren."</string>
<string name="crypto_event_authenticity_unverified_identity">"Krypterad av en overifierad användare."</string>
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flaggor"</string>
<string name="emoji_picker_category_foods">"Mat &amp; dryck"</string>
<string name="emoji_picker_category_nature">"Djur &amp; natur"</string>
<string name="emoji_picker_category_objects">"Föremål"</string>
<string name="emoji_picker_category_people">"Smileys &amp; personer"</string>
<string name="emoji_picker_category_places">"Resor &amp; platser"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="screen_media_upload_preview_caption_warning">"Bildtexter kanske inte är synliga för personer som använder äldre appar."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tryck för att ändra videouppladdningskvaliteten"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunde inte laddas upp."</string>
@@ -7,14 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Bilinmeyen veya silinmiş bir cihaz tarafından şifrelenmiştir."</string>
<string name="crypto_event_authenticity_unsigned_device">"Sahibi tarafından doğrulanmamış bir cihaz tarafından şifrelenmiştir."</string>
<string name="crypto_event_authenticity_unverified_identity">"Doğrulanmamış bir kullanıcı tarafından şifrelenmiştir."</string>
<string name="emoji_picker_category_activity">"Aktiviteler"</string>
<string name="emoji_picker_category_flags">"Bayraklar"</string>
<string name="emoji_picker_category_foods">"Yiyecek ve İçecek"</string>
<string name="emoji_picker_category_nature">"Hayvanlar ve Doğa"</string>
<string name="emoji_picker_category_objects">"Nesneler"</string>
<string name="emoji_picker_category_people">"İfadeler ve İnsanlar"</string>
<string name="emoji_picker_category_places">"Seyahat ve Yerler"</string>
<string name="emoji_picker_category_symbols">"Simgeler"</string>
<string name="screen_media_upload_preview_caption_warning">"Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Medya yüklenemedi, lütfen tekrar deneyin."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Medya yüklenemedi, lütfen tekrar deneyin."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Зашифровано невідомим або видаленим пристроєм."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашифровано пристроєм, який не звірено його власником."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашифровано не звіреним користувачем."</string>
<string name="emoji_picker_category_activity">"Дії"</string>
<string name="emoji_picker_category_flags">"Прапори"</string>
<string name="emoji_picker_category_foods">"Їжа та напої"</string>
<string name="emoji_picker_category_nature">"Тварини та природа"</string>
<string name="emoji_picker_category_objects">"Об\'єкти"</string>
<string name="emoji_picker_category_people">"Смайлики та люди"</string>
<string name="emoji_picker_category_places">"Подорожі та місця"</string>
<string name="emoji_picker_category_recent">"Нещодавні емодзі"</string>
<string name="emoji_picker_category_symbols">"Символи"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Повернути зображення по горизонталі"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонталі"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригінал"</string>
@@ -6,14 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"کسی نامعلوم یا حذف شدہ آلے کے ذریعے مرموز کردہ۔"</string>
<string name="crypto_event_authenticity_unsigned_device">"کسی ایسے آلے کے ذریعے مرموز کردہ جس کی توثیق اسکے مالک سے نہیں ہوئی۔"</string>
<string name="crypto_event_authenticity_unverified_identity">"ایک غیر توثیق شدہ صارف کے ذریعے مرموز کردہ۔"</string>
<string name="emoji_picker_category_activity">"سرگرمیاں"</string>
<string name="emoji_picker_category_flags">"جھنڈے"</string>
<string name="emoji_picker_category_foods">"طعم و مشروب"</string>
<string name="emoji_picker_category_nature">"حیوانات و فطرت"</string>
<string name="emoji_picker_category_objects">"اشیاء"</string>
<string name="emoji_picker_category_people">"مسکراہٹیں و لوگ"</string>
<string name="emoji_picker_category_places">"سفر و مقامات"</string>
<string name="emoji_picker_category_symbols">"علامتیں"</string>
<string name="screen_media_upload_preview_error_failed_processing">"وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_media_upload_preview_error_failed_sending">"وسائط رفع کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_report_content_block_user">"صارف کو مسدود کریں"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Nomaʼlum yoki oʻchirib tashlangan qurilma tomonidan shifrlangan."</string>
<string name="crypto_event_authenticity_unsigned_device">"Egasi tasdiqlamagan qurilma tomonidan shifrlangan."</string>
<string name="crypto_event_authenticity_unverified_identity">"Tasdiqlanmagan foydalanuvchi tomonidan shifrlangan."</string>
<string name="emoji_picker_category_activity">"Faoliyatlar"</string>
<string name="emoji_picker_category_flags">"Bayroqlar"</string>
<string name="emoji_picker_category_foods">"Oziq-ovqat va ichimliklar"</string>
<string name="emoji_picker_category_nature">"Hayvonlar va tabiat"</string>
<string name="emoji_picker_category_objects">"Ob\'ektlar"</string>
<string name="emoji_picker_category_people">"Smayllar va odamlar"</string>
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
<string name="emoji_picker_category_recent">"Oxirgi kulgichlar"</string>
<string name="emoji_picker_category_symbols">"Belgilar"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Rasmni gorizontal ravishda aylantiring"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Gorizontal ravishda aylantirildi"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Asl nusxasi"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Được mã hóa bởi một thiết bị không xác định hoặc đã bị xóa."</string>
<string name="crypto_event_authenticity_unsigned_device">"Được mã hóa bởi một thiết bị chưa được chủ sở hữu xác minh."</string>
<string name="crypto_event_authenticity_unverified_identity">"Được mã hóa bởi một người dùng chưa được xác minh."</string>
<string name="emoji_picker_category_activity">"Hoạt động"</string>
<string name="emoji_picker_category_flags">"Cờ"</string>
<string name="emoji_picker_category_foods">"Thực phẩm và đồ uống"</string>
<string name="emoji_picker_category_nature">"Động vật và thiên nhiên"</string>
<string name="emoji_picker_category_objects">"Đồ vật"</string>
<string name="emoji_picker_category_people">"Mặt cười &amp; mọi người"</string>
<string name="emoji_picker_category_places">"Du lịch và địa danh"</string>
<string name="emoji_picker_category_recent">"Biểu tượng cảm xúc gần đây"</string>
<string name="emoji_picker_category_symbols">"Biểu tượng"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Xử lý phương tiện tải lên không thành công, vui lòng thử lại."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Không thể tải lên tệp phương tiện. Vui lòng thử lại."</string>
<string name="screen_report_content_block_user">"Chặn người dùng"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"由未知或已刪除的裝置加密。"</string>
<string name="crypto_event_authenticity_unsigned_device">"由未經其擁有者驗證的裝置加密。"</string>
<string name="crypto_event_authenticity_unverified_identity">"由未經驗證的使用者加密。"</string>
<string name="emoji_picker_category_activity">"活動"</string>
<string name="emoji_picker_category_flags">"旗幟"</string>
<string name="emoji_picker_category_foods">"食物與飲料"</string>
<string name="emoji_picker_category_nature">"動物與大自然"</string>
<string name="emoji_picker_category_objects">"物品"</string>
<string name="emoji_picker_category_people">"表情與人物"</string>
<string name="emoji_picker_category_places">"旅行與景點"</string>
<string name="emoji_picker_category_recent">"最近使用的表情符號"</string>
<string name="emoji_picker_category_symbols">"標誌"</string>
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"由未知或已删除的设备加密。"</string>
<string name="crypto_event_authenticity_unsigned_device">"由未经其所有者验证的设备加密。"</string>
<string name="crypto_event_authenticity_unverified_identity">"由未经验证的用户加密。"</string>
<string name="emoji_picker_category_activity">"节假日"</string>
<string name="emoji_picker_category_flags">"旗帜"</string>
<string name="emoji_picker_category_foods">"饮食"</string>
<string name="emoji_picker_category_nature">"动物和自然"</string>
<string name="emoji_picker_category_objects">"日常物品"</string>
<string name="emoji_picker_category_people">"表情和人物"</string>
<string name="emoji_picker_category_places">"文旅景点"</string>
<string name="emoji_picker_category_recent">"最近的 Emoji"</string>
<string name="emoji_picker_category_symbols">"符号"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平翻转图像"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"水平翻转"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"原始"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Encrypted by an unknown or deleted device."</string>
<string name="crypto_event_authenticity_unsigned_device">"Encrypted by a device not verified by its owner."</string>
<string name="crypto_event_authenticity_unverified_identity">"Encrypted by an unverified user."</string>
<string name="emoji_picker_category_activity">"Activities"</string>
<string name="emoji_picker_category_flags">"Flags"</string>
<string name="emoji_picker_category_foods">"Food &amp; Drink"</string>
<string name="emoji_picker_category_nature">"Animals &amp; Nature"</string>
<string name="emoji_picker_category_objects">"Objects"</string>
<string name="emoji_picker_category_people">"Smileys &amp; People"</string>
<string name="emoji_picker_category_places">"Travel &amp; Places"</string>
<string name="emoji_picker_category_recent">"Recent emojis"</string>
<string name="emoji_picker_category_symbols">"Symbols"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Flip image horizontally"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Flipped horizontally"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
@@ -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
@@ -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<ComponentActivity>.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<ComponentActivity>.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,
)
}
@@ -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
@@ -11,11 +11,11 @@ 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.recentemojis.EmptyGetRecentEmojis
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
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
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -25,8 +25,8 @@ class CustomReactionPresenterTest {
val warmUpRule = WarmUpRule()
private val presenter = CustomReactionPresenter(
emojibaseProvider = FakeEmojibaseProvider(),
getRecentEmojis = { Result.success(persistentListOf()) },
emojiPickerPresenterFactory = fakeEmojiPickerPresenterFactory(),
getRecentEmojis = EmptyGetRecentEmojis,
)
@Test
@@ -38,8 +38,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)
@@ -59,8 +57,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)
@@ -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<Pair<EmojibaseCategory, ImmutableList<Emoji>>> = persistentListOf(emojiCategory()),
recentEmojis: ImmutableList<String> = persistentListOf(),
) = EmojiPickerPresenter(
emojibaseStore = EmojibaseStore(categories.toMap().toImmutableMap()),
recentEmojis = recentEmojis,
coroutineDispatchers = testCoroutineDispatchers(),
)
private fun emojiCategory(
category: EmojibaseCategory = EmojibaseCategory.Activity,
emojis: ImmutableList<Emoji> = 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<EmojiPickerState>.() -> 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()
}
}
}
@@ -81,6 +81,7 @@ dependencies {
implementation(projects.features.deactivation.api)
implementation(projects.features.home.api)
implementation(projects.features.invite.api)
implementation(projects.libraries.emoji.api)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.appnavstate.api)
@@ -98,6 +99,7 @@ dependencies {
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.emoji.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
@@ -24,6 +24,7 @@ import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
@@ -33,6 +34,7 @@ class PreferencesRootNode(
@Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter,
private val directLogoutView: DirectLogoutView,
private val emojiPickerRenderer: EmojiPickerRenderer,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToAddAccount()
@@ -75,6 +77,7 @@ class PreferencesRootNode(
val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView(
state = state,
emojiPickerRenderer = emojiPickerRenderer,
modifier = modifier,
onBackClick = this::navigateUp,
onAddAccountClick = callback::navigateToAddAccount,
@@ -41,6 +41,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
@@ -49,6 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PreferencesRootView(
state: PreferencesRootState,
emojiPickerRenderer: EmojiPickerRenderer,
onBackClick: () -> Unit,
onAddAccountClick: () -> Unit,
onSecureBackupClick: () -> Unit,
@@ -90,7 +93,11 @@ fun PreferencesRootView(
)
}
if (state.userStatusState != null) {
UserStatusSection(state.userStatusState, showTopDivider = !state.isMultiAccountEnabled)
UserStatusSection(
userStatusState = state.userStatusState,
emojiPickerRenderer = emojiPickerRenderer,
showTopDivider = !state.isMultiAccountEnabled,
)
}
// 'Account' section
ManageAccountSection(
@@ -135,6 +142,7 @@ fun PreferencesRootView(
@Composable
private fun ColumnScope.UserStatusSection(
userStatusState: UserStatusState,
emojiPickerRenderer: EmojiPickerRenderer,
showTopDivider: Boolean,
) {
if (showTopDivider) {
@@ -145,6 +153,7 @@ private fun ColumnScope.UserStatusSection(
}
UserStatusView(
state = userStatusState,
emojiPickerRenderer = emojiPickerRenderer,
modifier = Modifier.fillMaxWidth(),
)
HorizontalDivider(
@@ -369,6 +378,7 @@ internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootSta
private fun ContentToPreview(state: PreferencesRootState) {
PreferencesRootView(
state = state,
emojiPickerRenderer = NoOpEmojiPickerRenderer,
onBackClick = {},
onAddAccountClick = {},
onOpenAnalytics = {},
@@ -16,5 +16,7 @@ sealed interface UserStatusEvent {
data object OpenCustomInput : UserStatusEvent
data object CancelCustomInput : UserStatusEvent
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
data object OpenEmojiPicker : UserStatusEvent
data object DismissEmojiPicker : UserStatusEvent
data object ClearStatus : UserStatusEvent
}
@@ -19,36 +19,32 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.launch
@Inject
class UserStatusPresenter(
private val matrixClient: MatrixClient,
emojiPickerPresenterFactory: EmojiPickerPresenter.Factory,
) : Presenter<UserStatusState> {
private val emojiPickerPresenter = emojiPickerPresenterFactory.create(EmptyGetRecentEmojis)
@Composable
override fun present(): UserStatusState {
val userProfile by matrixClient.userProfile.collectAsState()
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
var isEmojiPickerVisible by remember { mutableStateOf(false) }
val customTextFieldState = rememberTextFieldState()
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: UserStatusEvent) {
when (event) {
// Mode transitions
UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker
UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden
UserStatusEvent.OpenCustomInput -> {
val raw = userProfile.rawStatus
if (raw != null) {
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
} else {
customTextFieldState.clearText()
}
pickerState = UserStatusPickerState.CustomInput(
emoji = raw?.emoji ?: "😀",
textFieldState = customTextFieldState,
)
}
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
is UserStatusEvent.SetStatus -> {
pickerState = UserStatusPickerState.Hidden
coroutineScope.launch { matrixClient.setUserStatus(event.status) }
@@ -57,19 +53,58 @@ class UserStatusPresenter(
pickerState = UserStatusPickerState.Hidden
coroutineScope.launch { matrixClient.clearUserStatus() }
}
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
UserStatusEvent.OpenCustomInput -> {
val raw = userProfile.rawStatus
if (raw != null) {
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
} else {
customTextFieldState.clearText()
}
isEmojiPickerVisible = false
pickerState = UserStatusPickerState.CustomInput(
emoji = raw?.emoji ?: "😀",
textFieldState = customTextFieldState,
emojiPickerSheetState = EmojiPickerSheetState.Hidden,
)
}
// Custom-input mutations
is UserStatusEvent.UpdateCustomEmoji -> {
isEmojiPickerVisible = false
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
pickerState = current.copy(emoji = event.emoji)
}
// Emoji-picker visibility (only meaningful within CustomInput mode)
UserStatusEvent.OpenEmojiPicker -> isEmojiPickerVisible = true
UserStatusEvent.DismissEmojiPicker -> isEmojiPickerVisible = false
}
}
// 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)
current.copy(emojiPickerSheetState = emojiPickerState)
}
else -> current
}
return UserStatusState(
displayedStatus = userProfile.displayedStatus,
rawStatus = userProfile.rawStatus,
pickerState = pickerState,
pickerState = effectivePickerState,
eventSink = ::handleEvent,
)
}
@Composable
private fun presentEmojiPickerSheetState(isVisible: Boolean): EmojiPickerSheetState {
if (!isVisible) return EmojiPickerSheetState.Hidden
val emojiPickerState = emojiPickerPresenter.present()
return if (emojiPickerState.isReady) {
EmojiPickerSheetState.Shown(emojiPickerState)
} else {
EmojiPickerSheetState.Loading
}
}
}
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Immutable
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus
@@ -28,9 +29,17 @@ sealed interface UserStatusPickerState {
data class CustomInput(
val emoji: String,
val textFieldState: TextFieldState,
val emojiPickerSheetState: EmojiPickerSheetState,
) : UserStatusPickerState
}
@Immutable
sealed interface EmojiPickerSheetState {
data object Hidden : EmojiPickerSheetState
data object Loading : EmojiPickerSheetState
data class Shown(val state: EmojiPickerState) : EmojiPickerSheetState
}
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
@@ -23,8 +23,20 @@ internal class UserStatusStateProvider : PreviewParameterProvider<UserStatusStat
rawStatus = UserStatus("🌴", "Away"),
pickerState = UserStatusPickerState.ShowingPicker,
),
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "😀", textFieldState = TextFieldState())),
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "🚀", textFieldState = TextFieldState("Working on something"))),
aUserStatusState(
pickerState = UserStatusPickerState.CustomInput(
emoji = "😀",
textFieldState = TextFieldState(),
emojiPickerSheetState = EmojiPickerSheetState.Hidden
)
),
aUserStatusState(
pickerState = UserStatusPickerState.CustomInput(
emoji = "🚀",
textFieldState = TextFieldState("Working on something"),
emojiPickerSheetState = EmojiPickerSheetState.Hidden
)
),
)
}
@@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.userstatus
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -58,13 +59,17 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentSetOf
@Composable
fun UserStatusView(
state: UserStatusState,
emojiPickerRenderer: EmojiPickerRenderer,
modifier: Modifier = Modifier,
) {
when (val pickerState = state.pickerState) {
@@ -103,8 +108,18 @@ fun UserStatusView(
)
},
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
onEmojiClick = { state.eventSink(UserStatusEvent.OpenEmojiPicker) },
modifier = modifier,
)
when (val sheet = pickerState.emojiPickerSheetState) {
EmojiPickerSheetState.Hidden, EmojiPickerSheetState.Loading -> Unit
is EmojiPickerSheetState.Shown -> EmojiPickerBottomSheet(
pickerState = sheet.state,
emojiPickerRenderer = emojiPickerRenderer,
onSelectEmoji = { emoji -> state.eventSink(UserStatusEvent.UpdateCustomEmoji(emoji.unicode)) },
onDismiss = { state.eventSink(UserStatusEvent.DismissEmojiPicker) },
)
}
}
}
}
@@ -231,6 +246,7 @@ private fun CustomStatusInputRow(
rawStatus: UserStatus?,
onConfirm: () -> Unit,
onCancel: () -> Unit,
onEmojiClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } }
@@ -297,7 +313,7 @@ private fun CustomStatusInputRow(
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.clickable { },
.clickable(onClick = onEmojiClick),
) {
Box(contentAlignment = Alignment.Center) {
EmojiText(emoji)
@@ -308,8 +324,39 @@ private fun CustomStatusInputRow(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EmojiPickerBottomSheet(
pickerState: io.element.android.libraries.emoji.api.picker.EmojiPickerState,
emojiPickerRenderer: EmojiPickerRenderer,
onSelectEmoji: (io.element.android.emojibasebindings.Emoji) -> Unit,
onDismiss: () -> Unit,
) {
val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
scrollable = false,
) {
emojiPickerRenderer.Render(
state = pickerState,
onSelectEmoji = { emoji ->
sheetState.hide(coroutineScope) {
onSelectEmoji(emoji)
}
},
selectedEmojis = persistentSetOf(),
modifier = Modifier.fillMaxSize(),
)
}
}
@PreviewsDayNight
@Composable
internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview {
UserStatusView(state = state)
UserStatusView(
state = state,
emojiPickerRenderer = NoOpEmojiPickerRenderer,
)
}
@@ -19,6 +19,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -466,6 +467,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
setContent {
PreferencesRootView(
state = state,
emojiPickerRenderer = NoOpEmojiPickerRenderer,
onBackClick = onBackClick,
onAddAccountClick = onAddAccountClick,
onSecureBackupClick = onSecureBackupClick,
@@ -8,6 +8,7 @@
package io.element.android.features.preferences.impl.userstatus
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus
import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -17,7 +18,10 @@ import org.junit.Test
class UserStatusPresenterTest {
private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) =
UserStatusPresenter(matrixClient = client)
UserStatusPresenter(
matrixClient = client,
emojiPickerPresenterFactory = fakeEmojiPickerPresenterFactory(),
)
@Test
fun `initial state - no status set`() = runTest {
@@ -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 {
@@ -19,5 +18,5 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.matrix.emojibase.bindings)
api(libs.matrix.emojibase.bindings)
}
@@ -0,0 +1,17 @@
/*
* 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
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
fun interface EmojiPickerPresenter : Presenter<EmojiPickerState> {
fun interface Factory {
fun create(getRecentEmojis: GetRecentEmojis): EmojiPickerPresenter
}
}
@@ -0,0 +1,44 @@
/*
* 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<String>,
modifier: Modifier = Modifier,
contentDescription: @Composable (Emoji, Boolean) -> String = { emoji, _ -> emoji.unicode },
)
}
object NoOpEmojiPickerRenderer : EmojiPickerRenderer {
@Composable
override fun Render(
state: EmojiPickerState,
onSelectEmoji: (Emoji) -> Unit,
selectedEmojis: ImmutableSet<String>,
modifier: Modifier,
contentDescription: @Composable (Emoji, Boolean) -> String,
) = Unit
}
@@ -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
}
@@ -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<Unit>
@@ -6,9 +6,10 @@
* 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
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<ImmutableList<String>>
}
/**
* 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<ImmutableList<String>> = Result.success(persistentListOf())
}
@@ -1,37 +1,40 @@
/*
* 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.matrix.test)
testImplementation(projects.tests.testutils)
testCommonDependencies(libs)
}
@@ -0,0 +1,36 @@
/*
* 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
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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
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()
}
@@ -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.recentemojis.api
package io.element.android.libraries.emoji.impl
import io.element.android.emojibasebindings.EmojibaseStore
interface EmojibaseProvider {
val emojibaseStore: EmojibaseStore
suspend fun getStore(): EmojibaseStore
}
@@ -0,0 +1,144 @@
/*
* 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.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
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
@AssistedInject
class DefaultEmojiPickerPresenter(
private val emojibaseProvider: EmojibaseProvider,
@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()
var isSearchActive by remember { mutableStateOf(false) }
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(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()) {
// `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()
}
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<EmojiCategory>,
val allEmojis: ImmutableList<Emoji>,
) {
companion object {
val Empty = EmojiPickerData(persistentListOf(), persistentListOf())
}
}
@@ -0,0 +1,41 @@
/*
* 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 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
@ContributesBinding(SessionScope::class)
class DefaultEmojiPickerRenderer : EmojiPickerRenderer {
@Composable
override fun Render(
state: EmojiPickerState,
onSelectEmoji: (Emoji) -> Unit,
selectedEmojis: ImmutableSet<String>,
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}")
}
}
}
@@ -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<EmojiCategory>,
val allEmojis: ImmutableList<Emoji>,
val searchQuery: TextFieldState,
val isSearchActive: Boolean,
val searchResults: SearchBarResultState<ImmutableList<Emoji>>,
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<Emoji>,
@@ -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<EmojiPickerState> {
override val values: Sequence<EmojiPickerState>
internal class DefaultEmojiPickerStateProvider : PreviewParameterProvider<DefaultEmojiPickerState> {
override val values: Sequence<DefaultEmojiPickerState>
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<EmojiPickerState> {
)
}
private fun recentEmojisCategory() = EmojiCategory(
titleId = R.string.emoji_picker_category_recent,
icon = IconSource.Resource(CompoundDrawables.ic_compound_history),
emojis = emojiList(),
)
private fun emojiList(): ImmutableList<Emoji> = persistentListOf(
Emoji(
"0x00",
@@ -56,29 +47,46 @@ private fun emojiList(): ImmutableList<Emoji> = persistentListOf(
"crying face",
persistentListOf("crying"),
persistentListOf("smile, crying"),
"\uD83E\uDD72",
"🥲",
null
)
)
internal fun anEmojiPickerState(
categories: ImmutableList<EmojiCategory> = (listOf(recentEmojisCategory()) + EmojibaseCategory.entries.map {
internal fun aDefaultEmojiPickerState(
categories: ImmutableList<EmojiCategory> = EmojibaseCategory.entries.map {
EmojiCategory(
titleId = it.title,
icon = IconSource.Vector(it.icon),
emojis = emojiList(),
)
}).toImmutableList(),
allEmojis: ImmutableList<Emoji> = categories.flatMap { it.emojis }.toImmutableList(),
}.toImmutableList(),
searchQuery: String = "",
isSearchActive: Boolean = false,
searchResults: SearchBarResultState<ImmutableList<Emoji>> = 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<EmojibaseCategory, ImmutableList<Emoji>>,
): EmojiPickerState = aDefaultEmojiPickerState(
categories = emojis.map { (category, list) ->
EmojiCategory(
titleId = category.title,
icon = IconSource.Vector(category.icon),
emojis = list,
)
}.toImmutableList(),
)
@@ -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,
@@ -77,6 +71,7 @@ fun EmojiItem(
hasSelectedSkin: Boolean,
modifier: Modifier = Modifier,
emojiSize: TextUnit = 20.sp,
contentDescription: @Composable (emoji: Emoji, isSelected: Boolean) -> String = { emoji, _ -> emoji.unicode },
) {
val backgroundColor = when {
isSelected -> ElementTheme.colors.bgActionPrimaryRest
@@ -89,10 +84,7 @@ 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
@@ -210,7 +202,7 @@ fun EmojiItem(
}
)
.clearAndSetSemantics {
contentDescription = description
this.contentDescription = description
},
contentAlignment = Alignment.Center
) {
@@ -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
}
@@ -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,14 +35,13 @@ 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
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.emoji.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
@@ -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<String>,
modifier: Modifier = Modifier,
contentDescription: @Composable (emoji: Emoji, isSelected: Boolean) -> String = { emoji, _ -> emoji.unicode },
) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = { state.categories.size })
@@ -68,7 +68,7 @@ fun EmojiPicker(
active = state.isSearchActive,
onActiveChange = { state.eventSink(EmojiPickerEvent.ToggleSearchActive(it)) },
windowInsets = WindowInsets(0, 0, 0, 0),
placeHolderTitle = stringResource(CommonStrings.emoji_picker_search_placeholder),
placeHolderTitle = stringResource(R.string.emoji_picker_search_placeholder),
) { emojis ->
EmojiResults(
emojis = emojis,
@@ -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<String>,
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(),
)
@@ -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
@@ -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<String>,
@@ -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)
@@ -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.recentemojis.GetRecentEmojis
import io.element.android.libraries.emoji.impl.EmojibaseProvider
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
@@ -25,7 +25,7 @@ class DefaultGetRecentEmojis(
private val emojibaseProvider: EmojibaseProvider,
) : GetRecentEmojis {
override suspend operator fun invoke(): Result<ImmutableList<String>> = withContext(dispatchers.io) {
val allEmojis = emojibaseProvider.emojibaseStore.allEmojis
val allEmojis = emojibaseProvider.getStore().allEmojis
client.getRecentEmojis()
.map { emojis ->
// Remove any possible duplicates
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Актыўнасці"</string>
<string name="emoji_picker_category_flags">"Сцягі"</string>
<string name="emoji_picker_category_foods">"Ежа &amp; Напоі"</string>
<string name="emoji_picker_category_nature">"Жывёлы &amp; Прырода"</string>
<string name="emoji_picker_category_objects">"Аб\'екты"</string>
<string name="emoji_picker_category_people">"Усмешкі &amp; Удзельнікі"</string>
<string name="emoji_picker_category_places">"Падарожжы &amp; Месцы"</string>
<string name="emoji_picker_category_symbols">"Сімвалы"</string>
</resources>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Дейности"</string>
<string name="emoji_picker_category_flags">"Знамена"</string>
<string name="emoji_picker_category_foods">"Храна &amp; Напитки"</string>
<string name="emoji_picker_category_nature">"Животни &amp; Природа"</string>
<string name="emoji_picker_category_objects">"Обекти"</string>
<string name="emoji_picker_category_people">"Усмивки &amp; Хора"</string>
<string name="emoji_picker_category_places">"Пътуване &amp; Места"</string>
<string name="emoji_picker_category_symbols">"Символи"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Activitats"</string>
<string name="emoji_picker_category_flags">"Banderes"</string>
<string name="emoji_picker_category_foods">"Menjar &amp; begudes"</string>
<string name="emoji_picker_category_nature">"Animals i naturalesa"</string>
<string name="emoji_picker_category_objects">"Objectes"</string>
<string name="emoji_picker_category_people">"Cares i persones"</string>
<string name="emoji_picker_category_places">"Viatges i llocs"</string>
<string name="emoji_picker_category_recent">"Emoticones recents"</string>
<string name="emoji_picker_category_symbols">"Símbols"</string>
<string name="emoji_picker_search_placeholder">"Cerca emoticones"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Aktivity"</string>
<string name="emoji_picker_category_flags">"Vlajky"</string>
<string name="emoji_picker_category_foods">"Jídlo a nápoje"</string>
<string name="emoji_picker_category_nature">"Zvířata a příroda"</string>
<string name="emoji_picker_category_objects">"Předměty"</string>
<string name="emoji_picker_category_people">"Smajlíci a lidé"</string>
<string name="emoji_picker_category_places">"Cestování a místa"</string>
<string name="emoji_picker_category_recent">"Nedávné emotikony"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="emoji_picker_search_placeholder">"Hledat emotikony"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Gweithgareddau"</string>
<string name="emoji_picker_category_flags">"Baneri"</string>
<string name="emoji_picker_category_foods">"Bwyd a Diod"</string>
<string name="emoji_picker_category_nature">"Anifeiliaid a Natur"</string>
<string name="emoji_picker_category_objects">"Gwrthrychau"</string>
<string name="emoji_picker_category_people">"Wynebau Hapus a Phobl"</string>
<string name="emoji_picker_category_places">"Teithio a Llefydd"</string>
<string name="emoji_picker_category_recent">"Emojis diweddar"</string>
<string name="emoji_picker_category_symbols">"Symbolau"</string>
<string name="emoji_picker_search_placeholder">"Chwilio emojis"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flag"</string>
<string name="emoji_picker_category_foods">"Mad og drikke"</string>
<string name="emoji_picker_category_nature">"Dyr og natur"</string>
<string name="emoji_picker_category_objects">"Objekter"</string>
<string name="emoji_picker_category_people">"Smileys og personer"</string>
<string name="emoji_picker_category_places">"Rejser og steder"</string>
<string name="emoji_picker_category_recent">"Seneste emojis"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="emoji_picker_search_placeholder">"Søg emojis"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
<string name="emoji_picker_category_flags">"Flaggen"</string>
<string name="emoji_picker_category_foods">"Essen &amp; Trinken"</string>
<string name="emoji_picker_category_nature">"Tiere &amp; Natur"</string>
<string name="emoji_picker_category_objects">"Objekte"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Menschen"</string>
<string name="emoji_picker_category_places">"Reisen &amp; Orte"</string>
<string name="emoji_picker_category_recent">"Zuletzt verwendete Emojis"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="emoji_picker_search_placeholder">"Emojis suchen"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Δραστηριότητες"</string>
<string name="emoji_picker_category_flags">"Σημαίες"</string>
<string name="emoji_picker_category_foods">"Φαγητό &amp; Ποτό"</string>
<string name="emoji_picker_category_nature">"Ζώα &amp; Φύση"</string>
<string name="emoji_picker_category_objects">"Αντικείμενα"</string>
<string name="emoji_picker_category_people">"Φατσούλες &amp; Άνθρωποι"</string>
<string name="emoji_picker_category_places">"Ταξίδια &amp; Μέρη"</string>
<string name="emoji_picker_category_recent">"Πρόσφατα emoji"</string>
<string name="emoji_picker_category_symbols">"Σύμβολα"</string>
<string name="emoji_picker_search_placeholder">"Αναζήτηση emoji"</string>
</resources>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Actividades"</string>
<string name="emoji_picker_category_flags">"Banderas"</string>
<string name="emoji_picker_category_foods">"Comida y bebida"</string>
<string name="emoji_picker_category_nature">"Animales y naturaleza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Emojis y personas"</string>
<string name="emoji_picker_category_places">"Viajes y lugares"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Tegevused"</string>
<string name="emoji_picker_category_flags">"Lipud"</string>
<string name="emoji_picker_category_foods">"Toit ja jook"</string>
<string name="emoji_picker_category_nature">"Loomad ja loodus"</string>
<string name="emoji_picker_category_objects">"Esemed"</string>
<string name="emoji_picker_category_people">"Emotikonid ja inimesed"</string>
<string name="emoji_picker_category_places">"Reisimine ja kohad"</string>
<string name="emoji_picker_category_recent">"Hiljutised emojid"</string>
<string name="emoji_picker_category_symbols">"Sümbolid"</string>
<string name="emoji_picker_search_placeholder">"Otsi emojisid"</string>
</resources>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Jarduerak"</string>
<string name="emoji_picker_category_flags">"Banderak"</string>
<string name="emoji_picker_category_foods">"Jana eta edana"</string>
<string name="emoji_picker_category_nature">"Animaliak eta natura"</string>
<string name="emoji_picker_category_objects">"Objektuak"</string>
<string name="emoji_picker_category_people">"Irribartxoak eta jendea"</string>
<string name="emoji_picker_category_places">"Bidaiak eta tokiak"</string>
<string name="emoji_picker_category_symbols">"Ikurrak"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"فعّالیت‌ها"</string>
<string name="emoji_picker_category_flags">"پرچم‌ها"</string>
<string name="emoji_picker_category_foods">"غذا و نوشیدنی"</string>
<string name="emoji_picker_category_nature">"حیوانات و طبعیت"</string>
<string name="emoji_picker_category_objects">"اشیا"</string>
<string name="emoji_picker_category_people">"شکلک‌ها و افراد"</string>
<string name="emoji_picker_category_places">"سفر و مکان‌ها"</string>
<string name="emoji_picker_category_recent">"شکلک‌های اخیر"</string>
<string name="emoji_picker_category_symbols">"نمادها"</string>
<string name="emoji_picker_search_placeholder">"حست‌وجوی شکلک‌ها"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More