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
@@ -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}")
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.annotation.StringRes
import androidx.compose.foundation.text.input.TextFieldState
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
@Immutable
internal data class DefaultEmojiPickerState(
val categories: ImmutableList<EmojiCategory>,
val searchQuery: TextFieldState,
val isSearchActive: Boolean,
val searchResults: SearchBarResultState<ImmutableList<Emoji>>,
val eventSink: (EmojiPickerEvent) -> Unit,
) : EmojiPickerState {
override val isReady = categories.isNotEmpty()
}
internal data class EmojiCategory(
@StringRes val titleId: Int,
val icon: IconSource,
val emojis: ImmutableList<Emoji>,
)
@@ -0,0 +1,92 @@
/*
* 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.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.libraries.designsystem.theme.components.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
internal class DefaultEmojiPickerStateProvider : PreviewParameterProvider<DefaultEmojiPickerState> {
override val values: Sequence<DefaultEmojiPickerState>
get() = sequenceOf(
aDefaultEmojiPickerState(),
aDefaultEmojiPickerState(isSearchActive = true),
aDefaultEmojiPickerState(isSearchActive = true, searchQuery = "smile"),
aDefaultEmojiPickerState(
isSearchActive = true,
searchQuery = "smile",
searchResults = SearchBarResultState.Results(emojiList())
),
)
}
private fun emojiList(): ImmutableList<Emoji> = persistentListOf(
Emoji(
"0x00",
"grinning face",
persistentListOf("grinning"),
persistentListOf("smile, grin"),
"😀",
null
),
Emoji(
"0x01",
"crying face",
persistentListOf("crying"),
persistentListOf("smile, crying"),
"🥲",
null
)
)
internal fun aDefaultEmojiPickerState(
categories: ImmutableList<EmojiCategory> = EmojibaseCategory.entries.map {
EmojiCategory(
titleId = it.title,
icon = IconSource.Vector(it.icon),
emojis = emojiList(),
)
}.toImmutableList(),
searchQuery: String = "",
isSearchActive: Boolean = false,
searchResults: SearchBarResultState<ImmutableList<Emoji>> = SearchBarResultState.Initial(),
eventSink: (EmojiPickerEvent) -> Unit = {},
) = DefaultEmojiPickerState(
categories = categories,
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(),
)
@@ -0,0 +1,313 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
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.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
@Composable
internal fun EmojiItem(
item: Emoji,
isSelected: Boolean,
onSelectEmoji: (Emoji) -> Unit,
onLongPress: (Emoji) -> Unit,
onDismissSkinPicker: () -> Unit,
skinPickerEmoji: Emoji?,
selectedSkinUnicodes: ImmutableSet<String>,
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
hasSelectedSkin -> ElementTheme.colors.bgActionTertiarySelected
else -> Color.Transparent
}
val density = LocalDensity.current
var itemSize by remember { mutableStateOf(IntSize.Zero) }
var itemOffsetPercent by remember { mutableFloatStateOf(0f) }
var hoveredIndex by remember { mutableIntStateOf(-1) }
var dismissed by remember { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
val description = contentDescription(item, isSelected)
val hasSkinTones = !item.skins.isNullOrEmpty()
Box(
modifier = modifier
.onPlaced { coordinates ->
val position = coordinates.positionInParent()
val parentBounds = coordinates.parentLayoutCoordinates?.size ?: return@onPlaced
// Calculate the offset of the item inside its parent as a percentage, to be able to set the initial offset
// for the selection inside the skin tone picker relative to its size.
itemOffsetPercent = if (parentBounds.width > 0) {
position.x / parentBounds.width
} else {
0f
}
}
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.onSizeChanged { itemSize = it }
.background(backgroundColor, CircleShape)
.indication(interactionSource, ripple())
.pointerInput(item) {
// Always detect long press and tap gestures
detectTapGestures(
onPress = { pressOffset ->
val press = PressInteraction.Press(pressOffset)
interactionSource.emit(press)
if (tryAwaitRelease()) {
interactionSource.emit(PressInteraction.Release(press))
if (hasSkinTones) {
onSelectEmoji(item)
}
} else {
interactionSource.emit(PressInteraction.Cancel(press))
}
},
onTap = { onSelectEmoji(item) },
)
}
.then(
// Only detect drag after long press for those items which have a skin tone picker
if (hasSkinTones) {
Modifier.pointerInput(item) {
val slotWidthPx = with(density) { SkinToneSlotSize.toPx() }
val spacingPx = with(density) { SkinToneSlotSpacing.toPx() }
val paddingPx = with(density) { SkinTonePadding.toPx() }
var startOffset = Offset.Zero
detectDragGesturesAfterLongPress(
onDragStart = { position ->
startOffset = position
dismissed = false
onLongPress(item)
hoveredIndex = -1
},
onDrag = { change, _ ->
if (!dismissed) {
change.consume()
// It's a valid drag event if it's within ~2 items height above, so, on top of the current one or the skin tone selector,
// or 1 item below, which should be far enough to not be triggered by mistake
val isValidDrag = if (change.position.y < startOffset.y) {
startOffset.y - change.position.y <= itemSize.height * 2f
} else {
change.position.y - startOffset.y <= itemSize.height
}
if (!isValidDrag) {
dismissed = true
hoveredIndex = -1
onDismissSkinPicker()
} else {
val skinItemsCount = item.skins!!.size
// Original + variants
val totalSlots = 1 + skinItemsCount
// Calculate the whole size of the skin tone picker
val pickerWidthPx = 2 * paddingPx + totalSlots * slotWidthPx + (totalSlots - 1) * spacingPx
// Calculate the initial offset inside the picker, given the relative position of the item inside its parent
val initialOffset = pickerWidthPx * itemOffsetPercent
val xInPicker = initialOffset + change.position.x
// If it's a valid offset, calculate the hovered index, otherwise, set it to -1 to indicate that no item is hovered
val index = if (xInPicker in 0f..pickerWidthPx) {
(xInPicker / slotWidthPx).toInt().coerceIn(0, totalSlots - 1)
} else {
-1
}
hoveredIndex = index
}
}
},
onDragEnd = {
if (!dismissed) {
val idx = hoveredIndex
val skinCount = item.skins?.size ?: 0
if (idx in 0..skinCount) {
if (idx == 0) {
onSelectEmoji(item)
} else {
val skin = item.skins?.getOrNull(idx - 1)
if (skin != null) {
onSelectEmoji(item.copy(unicode = skin.unicode))
}
}
} else if (idx < 0) {
onSelectEmoji(item)
}
}
dismissed = false
hoveredIndex = -1
onDismissSkinPicker()
},
onDragCancel = {
dismissed = false
hoveredIndex = -1
},
)
}
} else {
Modifier
}
)
.clearAndSetSemantics {
this.contentDescription = description
},
contentAlignment = Alignment.Center
) {
Text(
text = item.unicode,
style = LocalTextStyle.current.copy(fontSize = emojiSize),
)
if (hasSkinTones) {
val pickerIndicatorColor = ElementTheme.colors.borderInteractivePrimary
Canvas(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(8.dp),
) {
val path = Path().apply {
moveTo(size.width, size.height)
lineTo(size.width, 0f)
lineTo(0f, size.height)
close()
}
drawPath(path, color = pickerIndicatorColor, style = Fill)
}
}
if (skinPickerEmoji == item) {
val pickerHeight = SkinTonePadding * 2 + SkinToneSlotSize
val popupOffsetPx = with(density) { -pickerHeight.roundToPx() }
Popup(
onDismissRequest = { onDismissSkinPicker() },
alignment = Alignment.BottomCenter,
offset = IntOffset(0, popupOffsetPx),
) {
SkinTonePicker(
emoji = item,
onSelect = { selectedEmoji ->
onSelectEmoji(selectedEmoji)
onDismissSkinPicker()
},
hoveredIndex = hoveredIndex,
selectedUnicodes = selectedSkinUnicodes,
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun EmojiItemPreview() = ElementPreview {
Row(
modifier = Modifier.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
for (isSelected in listOf(false, true)) {
for (skinList in listOf(persistentListOf(), aSkinList())) {
val hasSelectedSkinValues = if (skinList.isNotEmpty()) listOf(false, true) else listOf(false)
for (hasSelectedSkin in hasSelectedSkinValues) {
EmojiItem(
item = Emoji(
hexcode = "",
label = "",
tags = null,
shortcodes = persistentListOf(),
unicode = "👍",
skins = skinList,
),
isSelected = isSelected,
onSelectEmoji = {},
hasSelectedSkin = hasSelectedSkin,
onLongPress = {},
onDismissSkinPicker = {},
skinPickerEmoji = null,
selectedSkinUnicodes = persistentSetOf(),
)
}
}
}
}
}
@PreviewsDayNight
@Composable
internal fun EmojiItemWithPopupPreview() = ElementPreview {
Row(
modifier = Modifier
.padding(top = 100.dp, bottom = 4.dp, start = 200.dp, end = 200.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
val item = Emoji(
hexcode = "",
label = "",
tags = null,
shortcodes = persistentListOf(),
unicode = "👍",
skins = aSkinList(),
)
EmojiItem(
item = item,
isSelected = false,
onSelectEmoji = {},
hasSelectedSkin = false,
onLongPress = {},
onDismissSkinPicker = {},
skinPickerEmoji = item,
selectedSkinUnicodes = persistentSetOf("👍🏾"),
)
}
}
@@ -0,0 +1,13 @@
/*
* 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
internal sealed interface EmojiPickerEvent {
data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvent
}
@@ -0,0 +1,176 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.emojibasebindings.Emoji
import io.element.android.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.emoji.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun EmojiPickerView(
state: DefaultEmojiPickerState,
onSelectEmoji: (Emoji) -> Unit,
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 })
var skinPickerEmoji by remember { mutableStateOf<Emoji?>(null) }
Column(modifier) {
SearchBar(
modifier = Modifier.padding(bottom = 10.dp),
queryState = state.searchQuery,
resultState = state.searchResults,
active = state.isSearchActive,
onActiveChange = { state.eventSink(EmojiPickerEvent.ToggleSearchActive(it)) },
windowInsets = WindowInsets(0, 0, 0, 0),
placeHolderTitle = stringResource(R.string.emoji_picker_search_placeholder),
) { emojis ->
EmojiResults(
emojis = emojis,
isEmojiSelected = { selectedEmojis.contains(it.unicode) },
onSelectEmoji = onSelectEmoji,
onLongPress = { skinPickerEmoji = it },
skinPickerEmoji = skinPickerEmoji,
onDismissSkinPicker = { skinPickerEmoji = null },
selectedEmojis = selectedEmojis,
contentDescription = contentDescription,
)
}
if (!state.isSearchActive) {
SecondaryTabRow(
selectedTabIndex = pagerState.currentPage,
) {
state.categories.forEachIndexed { index, category ->
Tab(
icon = {
when (category.icon) {
is IconSource.Resource -> Icon(
resourceId = category.icon.id,
contentDescription = stringResource(id = category.titleId)
)
is IconSource.Vector -> Icon(
imageVector = category.icon.vector,
contentDescription = stringResource(id = category.titleId)
)
}
},
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
}
)
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxWidth(),
) { index ->
val emojis = state.categories[index].emojis
EmojiResults(
emojis = emojis,
isEmojiSelected = { selectedEmojis.contains(it.unicode) },
onSelectEmoji = onSelectEmoji,
onLongPress = { skinPickerEmoji = it },
skinPickerEmoji = skinPickerEmoji,
onDismissSkinPicker = { skinPickerEmoji = null },
selectedEmojis = selectedEmojis,
contentDescription = contentDescription,
)
}
}
}
}
@Composable
private fun EmojiResults(
emojis: ImmutableList<Emoji>,
isEmojiSelected: (Emoji) -> Boolean,
onSelectEmoji: (Emoji) -> Unit,
onLongPress: (Emoji) -> Unit,
skinPickerEmoji: Emoji?,
onDismissSkinPicker: () -> Unit,
selectedEmojis: ImmutableSet<String>,
contentDescription: @Composable (emoji: Emoji, isSelected: Boolean) -> String,
) {
LazyVerticalGrid(
modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(minSize = 48.dp),
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
items(emojis, key = { it.unicode }) { item ->
EmojiItem(
modifier = Modifier.aspectRatio(1f),
item = item,
isSelected = isEmojiSelected(item),
onSelectEmoji = onSelectEmoji,
onLongPress = onLongPress,
skinPickerEmoji = skinPickerEmoji,
onDismissSkinPicker = onDismissSkinPicker,
emojiSize = 32.dp.toSp(),
selectedSkinUnicodes = selectedEmojis,
hasSelectedSkin = item.skins?.any { skin -> skin.unicode in selectedEmojis } == true,
contentDescription = contentDescription,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun EmojiPickerViewPreview(@PreviewParameter(DefaultEmojiPickerStateProvider::class) state: DefaultEmojiPickerState) = ElementPreview {
EmojiPickerView(
state = state,
onSelectEmoji = {},
selectedEmojis = persistentSetOf("😀", "😄", "😃"),
modifier = Modifier.fillMaxWidth(),
)
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.EmojiEvents
import androidx.compose.material.icons.outlined.EmojiFlags
import androidx.compose.material.icons.outlined.EmojiFoodBeverage
import androidx.compose.material.icons.outlined.EmojiNature
import androidx.compose.material.icons.outlined.EmojiObjects
import androidx.compose.material.icons.outlined.EmojiPeople
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.libraries.emoji.impl.R
@get:StringRes
internal val EmojibaseCategory.title: Int
get() = when (this) {
EmojibaseCategory.People -> R.string.emoji_picker_category_people
EmojibaseCategory.Nature -> R.string.emoji_picker_category_nature
EmojibaseCategory.Foods -> R.string.emoji_picker_category_foods
EmojibaseCategory.Activity -> R.string.emoji_picker_category_activity
EmojibaseCategory.Places -> R.string.emoji_picker_category_places
EmojibaseCategory.Objects -> R.string.emoji_picker_category_objects
EmojibaseCategory.Symbols -> R.string.emoji_picker_category_symbols
EmojibaseCategory.Flags -> R.string.emoji_picker_category_flags
}
internal val EmojibaseCategory.icon: ImageVector
get() = when (this) {
EmojibaseCategory.People -> Icons.Outlined.EmojiPeople
EmojibaseCategory.Nature -> Icons.Outlined.EmojiNature
EmojibaseCategory.Foods -> Icons.Outlined.EmojiFoodBeverage
EmojibaseCategory.Activity -> Icons.Outlined.EmojiEvents
EmojibaseCategory.Places -> Icons.Outlined.EmojiTransportation
EmojibaseCategory.Objects -> Icons.Outlined.EmojiObjects
EmojibaseCategory.Symbols -> Icons.Outlined.EmojiSymbols
EmojibaseCategory.Flags -> Icons.Outlined.EmojiFlags
}
@@ -0,0 +1,137 @@
/*
* 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.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojiSkin
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.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
internal val SkinToneSlotSize = 48.dp
internal val SkinToneSlotSpacing = 2.dp
internal val SkinTonePadding = 4.dp
@Composable
internal fun SkinTonePicker(
emoji: Emoji,
hoveredIndex: Int,
selectedUnicodes: ImmutableSet<String>,
onSelect: (Emoji) -> Unit,
modifier: Modifier = Modifier,
) {
val skins = emoji.skins.orEmpty()
val emojiSize = 32.dp.toSp()
Surface(
modifier = modifier,
shape = RoundedCornerShape(12.dp),
color = ElementTheme.colors.bgCanvasDefault,
tonalElevation = 4.dp,
) {
Row(
modifier = Modifier.padding(SkinTonePadding),
horizontalArrangement = Arrangement.spacedBy(SkinToneSlotSpacing),
) {
SkinToneSlot(
unicode = emoji.unicode,
emojiSize = emojiSize,
isHovered = hoveredIndex == 0,
isSelected = emoji.unicode in selectedUnicodes,
onClick = { onSelect(emoji) },
)
skins.forEachIndexed { index, skin ->
SkinToneSlot(
unicode = skin.unicode,
emojiSize = emojiSize,
isHovered = hoveredIndex == index + 1,
isSelected = skin.unicode in selectedUnicodes,
onClick = { onSelect(emoji.copy(unicode = skin.unicode)) },
)
}
}
}
}
@Composable
private fun SkinToneSlot(
unicode: String,
emojiSize: TextUnit,
isHovered: Boolean,
isSelected: Boolean,
onClick: () -> Unit,
) {
val background = when {
isSelected && isHovered -> ElementTheme.colors.bgActionPrimaryPressed
isSelected -> ElementTheme.colors.bgActionPrimaryRest
isHovered -> ElementTheme.colors.bgActionPrimaryHovered
else -> Color.Transparent
}
Box(
modifier = Modifier
.size(SkinToneSlotSize)
.clip(CircleShape)
.background(background, CircleShape)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Text(
text = unicode,
style = LocalTextStyle.current.copy(fontSize = emojiSize),
)
}
}
@PreviewsDayNight
@Composable
internal fun SkinTonePickerPreview() = ElementPreview {
SkinTonePicker(
emoji = Emoji(
hexcode = "1F44D",
label = "thumbs up",
tags = null,
shortcodes = persistentListOf("+1", "thumbsup"),
unicode = "👍",
skins = aSkinList(),
),
hoveredIndex = -1,
selectedUnicodes = persistentSetOf(),
onSelect = {},
)
}
internal fun aSkinList() = persistentListOf(
EmojiSkin("1F44D-1F3FB", "thumbs up: light skin tone", "👍🏻"),
EmojiSkin("1F44D-1F3FC", "thumbs up: medium-light skin tone", "👍🏼"),
EmojiSkin("1F44D-1F3FD", "thumbs up: medium skin tone", "👍🏽"),
EmojiSkin("1F44D-1F3FE", "thumbs up: medium-dark skin tone", "👍🏾"),
EmojiSkin("1F44D-1F3FF", "thumbs up: dark skin tone", "👍🏿"),
)
@@ -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>
@@ -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">"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="emoji_picker_search_placeholder">"Etsi emojeja"</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">"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="emoji_picker_search_placeholder">"Chercher des 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">"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="emoji_picker_search_placeholder">"Pretraživanje emotikona"</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">"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="emoji_picker_search_placeholder">"Emodzsik keresése"</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">"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="emoji_picker_search_placeholder">"Cari emoji"</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">"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="emoji_picker_search_placeholder">"Cerca emoji"</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>
@@ -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">"Საჭმელ-სასმელი"</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>
</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">"최근 이모지"</string>
<string name="emoji_picker_category_symbols">"상징"</string>
<string name="emoji_picker_search_placeholder">"이모지 검색"</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">"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>
</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">"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="emoji_picker_search_placeholder">"Søk etter emojier"</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">"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>
</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">"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="emoji_picker_search_placeholder">"Wyszukaj emoji"</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">"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="emoji_picker_search_placeholder">"Pesquisar 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">"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="emoji_picker_search_placeholder">"Pesquisar 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">"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="emoji_picker_search_placeholder">"Căutați emoticoane"</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>
@@ -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">"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="emoji_picker_search_placeholder">"Hľadať emotikony"</string>
</resources>
@@ -0,0 +1,12 @@
<?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">"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="emoji_picker_search_placeholder">"Sök emojier"</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">"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>
</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>
@@ -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">"طعم و مشروب"</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>
</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">"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="emoji_picker_search_placeholder">"Emojilarni qidiring"</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">"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="emoji_picker_search_placeholder">"Tìm kiếm biểu tượng cảm xúc"</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>
@@ -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">"最近的 Emoji"</string>
<string name="emoji_picker_category_symbols">"符号"</string>
<string name="emoji_picker_search_placeholder">"搜索 Emoji"</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">"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="emoji_picker_search_placeholder">"Search emojis"</string>
</resources>
@@ -6,18 +6,18 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.recentemojis.test
package io.element.android.libraries.emoji.impl.fixtures
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.emoji.impl.EmojibaseProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentMap
class FakeEmojibaseProvider(
val emojis: Map<EmojibaseCategory, ImmutableList<Emoji>> = mapOf(),
internal class FakeEmojibaseProvider(
emojis: Map<EmojibaseCategory, ImmutableList<Emoji>> = mapOf(),
) : EmojibaseProvider {
override val emojibaseStore: EmojibaseStore
get() = EmojibaseStore(emojis.toPersistentMap())
private val store = EmojibaseStore(emojis.toPersistentMap())
override suspend fun getStore(): EmojibaseStore = store
}
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.emoji.impl.picker
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
import io.element.android.libraries.emoji.impl.fixtures.FakeEmojibaseProvider
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.robolectric.RobolectricTest
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class EmojiPickerPresenterTest : RobolectricTest() {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `updating search query loads new results`() = runTest {
createPresenter().test {
val initialState = awaitStoreLoaded()
assertThat(initialState.searchQuery.text.toString()).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
initialState.searchQuery.setTextAndPlaceCursorAtEnd("smile")
val stateAfterQuery = awaitItem() as DefaultEmojiPickerState
assertThat(stateAfterQuery.searchQuery.text.toString()).isEqualTo("smile")
val stateWithResults = awaitItem() as DefaultEmojiPickerState
assertThat(stateWithResults.searchQuery.text.toString()).isEqualTo("smile")
assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
}
}
@Test
fun `recent emojis are prepended as a category when the source returns any`() = runTest {
createPresenter(
categories = persistentListOf(emojiCategory(EmojibaseCategory.Activity)),
getRecentEmojis = GetRecentEmojis { Result.success(persistentListOf("😊")) },
).test {
val state = awaitStoreLoaded()
assertThat(state.categories.size).isEqualTo(2)
}
}
@Test
fun `ToggleSearchActive toggles the search state`() = runTest {
createPresenter().test {
val initialState = awaitStoreLoaded()
assertThat(initialState.isSearchActive).isFalse()
initialState.eventSink(EmojiPickerEvent.ToggleSearchActive(true))
assertThat((awaitItem() as DefaultEmojiPickerState).isSearchActive).isTrue()
initialState.eventSink(EmojiPickerEvent.ToggleSearchActive(false))
assertThat((awaitItem() as DefaultEmojiPickerState).isSearchActive).isFalse()
}
}
@Test
fun `presenter exposes categories from the emoji store when the recent source is empty`() = runTest {
val providedCategories = persistentListOf(
emojiCategory(EmojibaseCategory.Activity),
emojiCategory(EmojibaseCategory.People),
)
createPresenter(categories = providedCategories).test {
val state = awaitStoreLoaded()
assertThat(state.categories.size).isEqualTo(providedCategories.size)
}
}
private suspend fun TurbineTestContext<EmojiPickerState>.awaitStoreLoaded(): DefaultEmojiPickerState {
var state = awaitItem() as DefaultEmojiPickerState
while (state.categories.isEmpty()) {
state = awaitItem() as DefaultEmojiPickerState
}
return state
}
private fun TestScope.createPresenter(
categories: ImmutableList<Pair<EmojibaseCategory, ImmutableList<Emoji>>> = persistentListOf(emojiCategory()),
getRecentEmojis: GetRecentEmojis = GetRecentEmojis { Result.success(persistentListOf()) },
) = DefaultEmojiPickerPresenter(
emojibaseProvider = FakeEmojibaseProvider(emojis = categories.toMap()),
getRecentEmojis = getRecentEmojis,
coroutineDispatchers = testCoroutineDispatchers(),
)
private fun emojiCategory(
category: EmojibaseCategory = EmojibaseCategory.Activity,
emojis: ImmutableList<Emoji> = persistentListOf(
Emoji("1F3C3", "Smile", persistentListOf("smile"), persistentListOf("smile"), "😊", skins = null)
)
) = category to emojis
}
@@ -6,14 +6,14 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.recentemojis.impl
package io.element.android.libraries.emoji.impl.recentemojis
import com.google.common.truth.Truth.assertThat
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.emojibasebindings.EmojibaseCategory.People
import io.element.android.libraries.emoji.impl.fixtures.FakeEmojibaseProvider
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.recentemojis.test.FakeEmojibaseProvider
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.emoji.test"
}
dependencies {
api(projects.libraries.emoji.api)
implementation(projects.libraries.architecture)
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.emoji.test
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
fun fakeEmojiPickerPresenter(
state: EmojiPickerState = FakeEmojiPickerState(isReady = true),
): EmojiPickerPresenter = EmojiPickerPresenter { state }
fun fakeEmojiPickerPresenterFactory(
presenter: EmojiPickerPresenter = fakeEmojiPickerPresenter(),
): EmojiPickerPresenter.Factory = EmojiPickerPresenter.Factory { presenter }
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.emoji.test
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
/**
* Minimal [EmojiPickerState] for use in tests that don't render the picker (typically presenter
* tests).
*/
data class FakeEmojiPickerState(
override val isReady: Boolean,
) : EmojiPickerState
@@ -1,20 +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.libraries.recentemojis.impl
import android.content.Context
import io.element.android.emojibasebindings.EmojibaseDatasource
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
class DefaultEmojibaseProvider(val context: Context) : EmojibaseProvider {
override val emojibaseStore: EmojibaseStore by lazy {
EmojibaseDatasource().load(context)
}
}
@@ -1,26 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.recentemojis.test"
}
dependencies {
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.coroutines.test)
implementation(projects.tests.testutils)
implementation(projects.libraries.recentemojis.api)
implementation(libs.matrix.emojibase.bindings)
}
@@ -321,7 +321,6 @@ Segur que vols continuar?"</string>
<string name="dialog_unsaved_changes_description">"Hi ha canvis sense desar."</string>
<string name="dialog_unsaved_changes_description_android">"Els canvis no s\'han desat. Segur que vols tornar enrere?"</string>
<string name="dialog_unsaved_changes_title">"Desar canvis?"</string>
<string name="emoji_picker_search_placeholder">"Cerca emoticones"</string>
<string name="error_account_creation_not_possible">"El servidor utilitzat s\'ha d\'actualitzar per admetre el servei d\'autenticació de Matrix i la creació de comptes."</string>
<string name="error_failed_creating_the_permalink">"No s\'ha pogut crear l\'enllaç permanent"</string>
<string name="error_failed_loading_map">"%1$s no ha pogut carregar el mapa. Torna-ho a provar més tard."</string>
@@ -452,7 +452,6 @@ Opravdu chcete pokračovat?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Maximální povolená velikost souboru je: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Vyberte kvalitu videa, které chcete nahrát."</string>
<string name="dialog_video_quality_selector_title">"Vyberte kvalitu nahrávání videa"</string>
<string name="emoji_picker_search_placeholder">"Hledat emotikony"</string>
<string name="error_account_already_logged_in">"Na tomto zařízení jste již přihlášeni jako %1$s."</string>
<string name="error_account_creation_not_possible">"Váš domovský server je třeba upgradovat, aby podporoval službu Matrix Authentication Service a vytváření účtu."</string>
<string name="error_failed_creating_the_permalink">"Vytvoření trvalého odkazu se nezdařilo"</string>
@@ -416,7 +416,6 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Y maint ffeil mwyaf sy\'n cael ei ganiatáu yw: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Dewiswch ansawdd y fideo rydych chi am ei llwytho."</string>
<string name="dialog_video_quality_selector_title">"Dewiswch ansawdd llwytho fideo"</string>
<string name="emoji_picker_search_placeholder">"Chwilio emojis"</string>
<string name="error_account_already_logged_in">"Rydych chi eisoes wedi mewngofnodi ar y ddyfais hon fel %1$s ."</string>
<string name="error_account_creation_not_possible">"Mae angen uwchraddio eich gweinydd cartref i gefnogi Gwasanaeth Dilysu Matrix a chreu cyfrif."</string>
<string name="error_failed_creating_the_permalink">"Wedi methu creu\'r ddolen barhaol"</string>
@@ -440,7 +440,6 @@ Er du sikker på, at du vil fortsætte?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Den maksimalt tilladte filstørrelse er: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Vælg den kvalitet, du ønsker at uploade videoen i."</string>
<string name="dialog_video_quality_selector_title">"Vælg kvalitet for video-overførsel"</string>
<string name="emoji_picker_search_placeholder">"Søg emojis"</string>
<string name="error_account_already_logged_in">"Du er allerede logget ind på denne enhed som %1$s."</string>
<string name="error_account_creation_not_possible">"Din hjemmeserver skal opgraderes for at understøtte Matrix Authentication Service og kontooprettelse."</string>
<string name="error_failed_creating_the_permalink">"Oprettelse af permalink mislykkedes"</string>
@@ -433,7 +433,6 @@ Möchtest du wirklich fortfahren?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Die maximal erlaubte Dateigröße ist: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Wähle die Qualität des Videos, das du hochladen möchtest."</string>
<string name="dialog_video_quality_selector_title">"Wähle die Video-Upload-Qualität"</string>
<string name="emoji_picker_search_placeholder">"Emojis suchen"</string>
<string name="error_account_already_logged_in">"Du bist auf diesem Gerät bereits als %1$s angemeldet."</string>
<string name="error_account_creation_not_possible">"Dein Homeserver muss aktualisiert werden, um den Matrix Authentication Services und die Erstellung von Konten zu unterstützen."</string>
<string name="error_failed_creating_the_permalink">"Fehler beim Erstellen des Permalinks"</string>
@@ -434,7 +434,6 @@
<string name="dialog_video_quality_selector_subtitle_file_size">"Το μέγιστο επιτρεπόμενο μέγεθος αρχείου είναι: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Επιλέξτε την ποιότητα του βίντεο που θέλετε να ανεβάσετε."</string>
<string name="dialog_video_quality_selector_title">"Επιλέξτε την ποιότητα μεταφόρτωσης βίντεο"</string>
<string name="emoji_picker_search_placeholder">"Αναζήτηση emoji"</string>
<string name="error_account_already_logged_in">"Έχετε ήδη συνδεθεί σε αυτή τη συσκευή ως %1$s."</string>
<string name="error_account_creation_not_possible">"Ο οικιακός διακομιστής σου πρέπει να αναβαθμιστεί για να υποστηρίζει το Matrix Authentication Service και τη δημιουργία λογαριασμού."</string>
<string name="error_failed_creating_the_permalink">"Αποτυχία δημιουργίας του μόνιμου συνδέσμου"</string>
@@ -477,7 +477,6 @@ Kas sa oled kindel, et soovid jätkata?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Suurim lubatud failisuurus on: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Vali üleslaaditava video kvaliteet."</string>
<string name="dialog_video_quality_selector_title">"Vali video kvaliteet."</string>
<string name="emoji_picker_search_placeholder">"Otsi emojisid"</string>
<string name="error_account_already_logged_in">"Sa juba oled sellesse seadmesse sisseloginud kasutajana %1$s."</string>
<string name="error_account_creation_not_possible">"Selleks et koos kasutajakonto loomisega toimiks Matrix Authentication Service\'i tugi, vajab sinu koduserver uuendamist."</string>
<string name="error_failed_creating_the_permalink">"Püsilingi loomine ei õnnestumud"</string>
@@ -348,7 +348,6 @@
<string name="dialog_unsaved_changes_description">"تغییراتی ذخیره نشده دارید."</string>
<string name="dialog_unsaved_changes_description_android">"تغییراتتان ذخیره نشده‌اند. مطمئنید که می‌خواهید برگردید؟"</string>
<string name="dialog_unsaved_changes_title">"ذخیرهٔ تغییرات؟"</string>
<string name="emoji_picker_search_placeholder">"حست‌وجوی شکلک‌ها"</string>
<string name="error_failed_creating_the_permalink">"شکست در ایجاد پایاپیوند"</string>
<string name="error_failed_loading_map">"%1$s نتوانست نقشه را بارگیری کند. لطفا بعدا دوباره امتحان کنید."</string>
<string name="error_failed_loading_messages">"شکست در بار کردن پیام‌ها"</string>
@@ -477,7 +477,6 @@ Haluatko varmasti jatkaa?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Suurin sallittu tiedostokoko on: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Valitse lähetettävän videon laatu."</string>
<string name="dialog_video_quality_selector_title">"Valitse videon lähetyslaatu"</string>
<string name="emoji_picker_search_placeholder">"Etsi emojeja"</string>
<string name="error_account_already_logged_in">"Olet jo kirjautuneena tälle laitteelle käyttäjällä %1$s."</string>
<string name="error_account_creation_not_possible">"Kotipalvelimesi on päivitettävä tukemaan Matrix Authentication Serviceä ja tilin luomista."</string>
<string name="error_failed_creating_the_permalink">"Pysyvän linkin luominen epäonnistui"</string>
@@ -452,7 +452,6 @@ Raison : %1$s."</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"La taille maximale de fichier autorisée est: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Sélectionnez la qualité des vidéos que vous souhaitez envoyer."</string>
<string name="dialog_video_quality_selector_title">"Sélectionnez la qualité d’envoi des vidéos"</string>
<string name="emoji_picker_search_placeholder">"Chercher des emojis"</string>
<string name="error_account_already_logged_in">"Vous êtes déjà connecté sur cet appareil en tant que %1$s."</string>
<string name="error_account_creation_not_possible">"Votre serveur d’accueil doit être mis à jour pour prendre en charge le protocole MAS (Matrix Authentication Service) et la création de compte."</string>
<string name="error_failed_creating_the_permalink">"Échec de la création du permalien"</string>
@@ -475,7 +475,6 @@ Jeste li sigurni da želite nastaviti?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Maksimalna dopuštena veličina datoteke je: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Odaberite kvalitetu videozapisa koji želite prenijeti."</string>
<string name="dialog_video_quality_selector_title">"Odaberi kvalitetu prijenosa videozapisa"</string>
<string name="emoji_picker_search_placeholder">"Pretraživanje emotikona"</string>
<string name="error_account_already_logged_in">"Već ste prijavljeni na ovom uređaju kao %1$s."</string>
<string name="error_account_creation_not_possible">"Vaš matični poslužitelj potrebno je nadograditi kako bi podržavao uslugu Matrix Authentication Service i stvaranje računa."</string>
<string name="error_failed_creating_the_permalink">"Nije uspjelo stvaranje trajne poveznice"</string>
@@ -467,7 +467,6 @@ Biztos, hogy folytatja?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"A legnagyobb megengedett fájlméret: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Válassza ki a feltöltendő videó minőségét."</string>
<string name="dialog_video_quality_selector_title">"Feltöltött videó minőségének kiválasztása"</string>
<string name="emoji_picker_search_placeholder">"Emodzsik keresése"</string>
<string name="error_account_already_logged_in">"Már bejelentkezett erre az eszközre, mint %1$s."</string>
<string name="error_account_creation_not_possible">"A Matrix-kiszolgálót frissíteni kell a Matrix Authentication Service és a fióklétrehozás támogatásához."</string>
<string name="error_failed_creating_the_permalink">"Nem sikerült létrehozni az állandó hivatkozást"</string>
@@ -439,7 +439,6 @@ Apakah Anda yakin ingin melanjutkan?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Ukuran file maksimum yang diizinkan adalah: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Pilih kualitas video yang ingin Anda unggah."</string>
<string name="dialog_video_quality_selector_title">"Pilih kualitas unggahan video"</string>
<string name="emoji_picker_search_placeholder">"Cari emoji"</string>
<string name="error_account_already_logged_in">"Anda sudah masuk ke akun di perangkat ini sebagai%1$s ."</string>
<string name="error_account_creation_not_possible">"Homeserver Anda perlu ditingkatkan untuk mendukung Matrix Authentication Service dan pembuatan akun."</string>
<string name="error_failed_creating_the_permalink">"Gagal membuat tautan permanen"</string>
@@ -467,7 +467,6 @@ Sei sicuro di voler continuare?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"La dimensione massima consentita per il file è: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Seleziona la qualità del video che vuoi caricare."</string>
<string name="dialog_video_quality_selector_title">"Seleziona la qualità di caricamento del video"</string>
<string name="emoji_picker_search_placeholder">"Cerca emoji"</string>
<string name="error_account_already_logged_in">"Hai già effettuato l\'accesso su questo dispositivo come %1$s ."</string>
<string name="error_account_creation_not_possible">"Il tuo homeserver deve essere aggiornato per supportare il Matrix Authentication Service e la creazione di account."</string>
<string name="error_failed_creating_the_permalink">"Impossibile creare il collegamento permanente"</string>
@@ -452,7 +452,6 @@
<string name="dialog_video_quality_selector_subtitle_file_size">"アップロードできる最大サイズは %1$s です。"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"アップロードする画質を選択してください。"</string>
<string name="dialog_video_quality_selector_title">"動画のアップロード品質を選択"</string>
<string name="emoji_picker_search_placeholder">"絵文字を検索"</string>
<string name="error_account_already_logged_in">"この端末では既に %1$s としてログインしています。"</string>
<string name="error_account_creation_not_possible">"Matrix Authentication Service とアカウント作成に対応するため、このホームサーバーはアップグレードが必要です。"</string>
<string name="error_failed_creating_the_permalink">"固定リンクの作成に失敗しました"</string>
@@ -443,7 +443,6 @@
"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"업로드할 비디오의 품질을 선택하세요."</string>
<string name="dialog_video_quality_selector_title">"비디오 업로드 품질 선택"</string>
<string name="emoji_picker_search_placeholder">"이모지 검색"</string>
<string name="error_account_already_logged_in">"이 기기에서 이미 %1$s님으로 로그인되어 있습니다."</string>
<string name="error_account_creation_not_possible">"Matrix Authentication Service 및 계정 생성을 지원하려면 홈서버를 업그레이드해야 합니다."</string>
<string name="error_failed_creating_the_permalink">"퍼머링크 생성 실패"</string>
@@ -429,7 +429,6 @@ Er du sikker på at du vil fortsette?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Maksimal tillatt filstørrelse er: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Velg kvaliteten på videoen du vil laste opp."</string>
<string name="dialog_video_quality_selector_title">"Velg kvalitet for videoopplasting"</string>
<string name="emoji_picker_search_placeholder">"Søk etter emojier"</string>
<string name="error_account_already_logged_in">"Du er allerede logget inn på denne enheten som %1$s."</string>
<string name="error_account_creation_not_possible">"Hjemmeserveren din må oppgraderes for å støtte Matrix Authentication Service og kontooppretting."</string>
<string name="error_failed_creating_the_permalink">"Opprettelse av permalenken mislyktes"</string>
@@ -477,7 +477,6 @@ Czy na pewno chcesz kontynuować?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Maksymalny dozwolony rozmiar pliku to: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Wybierz jakość filmu, który chcesz przesłać."</string>
<string name="dialog_video_quality_selector_title">"Wybierz jakość przesyłania wideo"</string>
<string name="emoji_picker_search_placeholder">"Wyszukaj emoji"</string>
<string name="error_account_already_logged_in">"Jesteś już zalogowany na tym urządzeniu jako %1$s."</string>
<string name="error_account_creation_not_possible">"Twój serwer domowy wymaga aktualizacji, aby uzyskać wsparcie usługi Matrix Authentication Service i tworzenia kont."</string>
<string name="error_failed_creating_the_permalink">"Nie udało się utworzyć linku bezpośredniego"</string>
@@ -410,7 +410,6 @@ Você tem certeza de que deseja continuar?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"O tamanho máximo permitido de arquivos é: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Selecione a qualidade do video que quer enviar."</string>
<string name="dialog_video_quality_selector_title">"Selecione a qualidade de envio dos vídeos"</string>
<string name="emoji_picker_search_placeholder">"Pesquisar emojis"</string>
<string name="error_account_already_logged_in">"Você já está conectado neste dispositivo como %1$s ."</string>
<string name="error_account_creation_not_possible">"Seu servidor-casa precisa ser atualizado para oferecer suporte ao Matrix Authentication Service e à criação de contas."</string>
<string name="error_failed_creating_the_permalink">"Falha ao criar o link permanente"</string>
@@ -391,7 +391,6 @@ Tens a certeza de que queres continuar?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"O tamanho máximo de ficheiro permitido é: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Seleciona a qualidade do vídeo que pretendes carregar."</string>
<string name="dialog_video_quality_selector_title">"Seleciona a qualidade de carregamento do vídeo"</string>
<string name="emoji_picker_search_placeholder">"Pesquisar emojis"</string>
<string name="error_account_already_logged_in">"Já tens sessão iniciada como %1$s neste dispositivo."</string>
<string name="error_account_creation_not_possible">"Seu homeserver precisa ser atualizado para suportar o Matrix Authentication Service e a criação de conta."</string>
<string name="error_failed_creating_the_permalink">"Falha ao criar ligação permanente"</string>
@@ -451,7 +451,6 @@ Sunteți sigur că doriți să continuați?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Dimensiunea maximă permisă pentru fișiere este: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Selectați calitatea videoclipului pe care doriți să îl încărcați."</string>
<string name="dialog_video_quality_selector_title">"Selectați calitatea de încărcare a videoclipurilor"</string>
<string name="emoji_picker_search_placeholder">"Căutați emoticoane"</string>
<string name="error_account_already_logged_in">"Sunteți deja conectat pe acest dispozitiv ca %1$s."</string>
<string name="error_account_creation_not_possible">"Serverul dumneavoastră trebuie actualizat pentru a suporta serviciul de autentificare Matrix și crearea de conturi."</string>
<string name="error_failed_creating_the_permalink">"Crearea permalink-ului a eșuat"</string>
@@ -476,7 +476,6 @@
<string name="dialog_video_quality_selector_subtitle_file_size">"Максимально допустимый размер файла: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Выберите качество видео, которое Вы хотите загрузить."</string>
<string name="dialog_video_quality_selector_title">"Выберите качество загружаемого видео"</string>
<string name="emoji_picker_search_placeholder">"Поиск эмодзи"</string>
<string name="error_account_already_logged_in">"Вы уже вошли на данном устройстве как %1$s."</string>
<string name="error_account_creation_not_possible">"Домашний сервер необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учётных записей."</string>
<string name="error_failed_creating_the_permalink">"Не удалось создать постоянную ссылку"</string>
@@ -423,7 +423,6 @@ Naozaj chcete pokračovať?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Maximálna povolená veľkosť súboru je: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Vyberte kvalitu videa, ktoré chcete nahrať."</string>
<string name="dialog_video_quality_selector_title">"Vyberte kvalitu nahrávania videa"</string>
<string name="emoji_picker_search_placeholder">"Hľadať emotikony"</string>
<string name="error_account_already_logged_in">"Už ste prihlásený/á na tomto zariadení ako %1$s ."</string>
<string name="error_account_creation_not_possible">"Váš domovský server musí byť aktualizovaný tak, aby podporoval Matrix Authentication Service a vytvorenie účtu."</string>
<string name="error_failed_creating_the_permalink">"Nepodarilo sa vytvoriť trvalý odkaz"</string>
@@ -379,7 +379,6 @@ Anledning:%1$s."</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Den maximala tillåtna filstorleken är: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Välj kvaliteten på videon du vill ladda upp."</string>
<string name="dialog_video_quality_selector_title">"Välj videouppladdningskvalitet"</string>
<string name="emoji_picker_search_placeholder">"Sök emojier"</string>
<string name="error_account_already_logged_in">"Du är redan inloggad på den här enheten som %1$s."</string>
<string name="error_account_creation_not_possible">"Din hemserver måste uppgraderas för att stödja Matrix Authentication Service och skapande av konto."</string>
<string name="error_failed_creating_the_permalink">"Misslyckades att skapa permalänken"</string>
@@ -476,7 +476,6 @@
<string name="dialog_video_quality_selector_subtitle_file_size">"Максимально дозволений розмір файлу: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Виберіть якість відео, яке ви хочете вивантажити."</string>
<string name="dialog_video_quality_selector_title">"Виберіть якість вивантажуваного відео"</string>
<string name="emoji_picker_search_placeholder">"Пошук емоджі"</string>
<string name="error_account_already_logged_in">"Ви вже ввійшли на цьому пристрої як %1$s."</string>
<string name="error_account_creation_not_possible">"Ваш домашній сервер потрібно оновити, щоб він підтримував службу автентифікації Matrix і створення облікових записів."</string>
<string name="error_failed_creating_the_permalink">"Не вдалося створити постійне посилання"</string>
@@ -450,7 +450,6 @@ Davom etasizmi?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Ruxsat etilgan maksimal fayl hajmi: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Yuklanadigan video sifatini tanlang."</string>
<string name="dialog_video_quality_selector_title">"Video yuklash sifatini tanlang"</string>
<string name="emoji_picker_search_placeholder">"Emojilarni qidiring"</string>
<string name="error_account_already_logged_in">"Bu qurilmada allaqachon %1$s hisobiga kirgansiz."</string>
<string name="error_account_creation_not_possible">"Matrix autentifikatsiya xizmati va hisob yaratish imkoniyatini qo‘llab-quvvatlash uchun uy serveringizni yangilash talab etiladi."</string>
<string name="error_failed_creating_the_permalink">"Doimiy havola yaratilmadi"</string>
@@ -422,7 +422,6 @@ Bạn có chắc muốn tiếp tục không?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Kích thước tệp tối đa cho phép là: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Chọn chất lượng video bạn muốn tải lên."</string>
<string name="dialog_video_quality_selector_title">"Chọn chất lượng tải lên video"</string>
<string name="emoji_picker_search_placeholder">"Tìm kiếm biểu tượng cảm xúc"</string>
<string name="error_account_already_logged_in">"Bạn đã đăng nhập trên thiết bị này với tư cách là%1$s ."</string>
<string name="error_account_creation_not_possible">"Máy chủ của bạn cần được nâng cấp để hỗ trợ Dịch vụ Xác thực và tạo tài khoản."</string>
<string name="error_failed_creating_the_permalink">"Không tạo được liên kết cố định"</string>
@@ -425,7 +425,6 @@
<string name="dialog_video_quality_selector_subtitle_file_size">"最大允許的檔案大小為:%1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"選取您要上傳的視訊的品質。"</string>
<string name="dialog_video_quality_selector_title">"選取視訊上傳品質"</string>
<string name="emoji_picker_search_placeholder">"搜尋表情符號"</string>
<string name="error_account_already_logged_in">"您已在此裝置上以 %1$s 的身份登入。"</string>
<string name="error_account_creation_not_possible">"您的家伺服器需要升級才能支援 Matrix Authentication Service 與帳號建立。"</string>
<string name="error_failed_creating_the_permalink">"無法建立永久連結"</string>
@@ -466,7 +466,6 @@
<string name="dialog_video_quality_selector_subtitle_file_size">"允许的最大文件大小为:%1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"选择你要上传的视频的质量。"</string>
<string name="dialog_video_quality_selector_title">"选择视频上传质量"</string>
<string name="emoji_picker_search_placeholder">"搜索 Emoji"</string>
<string name="error_account_already_logged_in">"你已在此设备以 %1$s 的身份登录。"</string>
<string name="error_account_creation_not_possible">"你的主服务器需要升级,以支持 Matrix 认证服务和账户创建。"</string>
<string name="error_failed_creating_the_permalink">"永久链接创建失败"</string>
@@ -477,7 +477,6 @@ Are you sure you want to continue?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"The max file size allowed is: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Select the quality of the video you want to upload."</string>
<string name="dialog_video_quality_selector_title">"Select video upload quality"</string>
<string name="emoji_picker_search_placeholder">"Search emojis"</string>
<string name="error_account_already_logged_in">"You\'re already logged in on this device as %1$s."</string>
<string name="error_account_creation_not_possible">"Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."</string>
<string name="error_failed_creating_the_permalink">"Failed creating the permalink"</string>