Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions java/src/org/futo/inputmethod/latin/EmojiDictionary.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,37 @@ class EmojiDictionary(locale: Locale) : Dictionary(TYPE_EMOJI, locale) {
): ArrayList<SuggestedWords.SuggestedWordInfo?>? {
if(DataStoreHelper.getSetting(SHOW_EMOJI_SUGGESTIONS) == false) return arrayListOf()

var emoji: String? = null
var candidates: List<String> = emptyList()

if(!typedWord.isEmpty()) {
if(isWordValidForShortcut((typedWord)))
emoji = PersistentEmojiState.getShortcut(mLocale, typedWord.lowercase(mLocale))
if(isWordValidForShortcut(typedWord))
candidates = PersistentEmojiState.getShortcuts(mLocale, typedWord.lowercase(mLocale))
} else if((ngramContext?.prevWordCount ?: 0) > 0 && isBatchMode == false) {
val prevWord = ngramContext?.getNthPrevWord(1)?.toString() ?: ""
if(!prevWord.isEmpty()) {
if(isWordValidForShortcut(prevWord))
emoji = PersistentEmojiState.getShortcut(mLocale, prevWord.lowercase(mLocale))
candidates = PersistentEmojiState.getShortcuts(mLocale, prevWord.lowercase(mLocale))
}
}

if(emoji != null) {
emoji = PersistentEmojiState.transformEmojiToLastSkinTone(emoji)
}
// Dedup after skin-tone: two base emojis can collapse to the same toned string.
candidates = candidates.map { PersistentEmojiState.transformEmojiToLastSkinTone(it) }.distinct()

return if(emoji != null) {
val score = Suggest.SUPPRESS_SUGGEST_THRESHOLD + 1
arrayListOf(
SuggestedWords.SuggestedWordInfo(
emoji,
"",
score,
SuggestedWords.SuggestedWordInfo.KIND_EMOJI_SUGGESTION,
null,
SuggestedWords.SuggestedWordInfo.NOT_AN_INDEX,
SuggestedWords.SuggestedWordInfo.NOT_A_CONFIDENCE
)
// Threshold filter is strict (<), so THRESHOLD+0 survives. Data layer caps at
// take(MAX_EMOJI_SUGGESTIONS).
val baseScore = Suggest.SUPPRESS_SUGGEST_THRESHOLD + 1

return ArrayList(candidates.mapIndexed { i, emoji ->
SuggestedWords.SuggestedWordInfo(
emoji,
"",
baseScore - i,
SuggestedWords.SuggestedWordInfo.KIND_EMOJI_SUGGESTION,
null,
SuggestedWords.SuggestedWordInfo.NOT_AN_INDEX,
SuggestedWords.SuggestedWordInfo.NOT_A_CONFIDENCE
)
} else {
arrayListOf()
}
})
}

override fun getSuggestions(
Expand All @@ -76,4 +75,4 @@ class EmojiDictionary(locale: Locale) : Dictionary(TYPE_EMOJI, locale) {
override fun isInDictionary(word: String?): Boolean {
return false
}
}
}
52 changes: 40 additions & 12 deletions java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import org.futo.inputmethod.latin.SuggestionBlacklist
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewListener
import org.futo.inputmethod.latin.uix.actions.FavoriteActions
import org.futo.inputmethod.latin.uix.actions.PersistentEmojiState
import org.futo.inputmethod.latin.uix.actions.MoreActionsAction
import org.futo.inputmethod.latin.uix.actions.PinnedActions
import org.futo.inputmethod.latin.uix.actions.toActionList
Expand Down Expand Up @@ -293,7 +294,7 @@ fun TextStyle.withCustomFont(orDefault: FontFamily? = null): TextStyle {

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, forcePrimary: Boolean, isEmoji: Boolean) {
fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, forcePrimary: Boolean, isEmoji: Boolean, weight: Float = 1.0f) {
val wordInfo = words.getInfoOrNull(idx)
val isVerbatim = wordInfo?.kind == KIND_TYPED
val word = wordInfo?.mWord
Expand Down Expand Up @@ -346,7 +347,7 @@ fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean,

Box(
modifier = textButtonModifier
.weight(1.0f)
.weight(weight)
.fillMaxHeight()
.combinedClickable(
enabled = word != null,
Expand Down Expand Up @@ -501,18 +502,45 @@ fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, o
}
}

// Two emoji at half-weight each so together they occupy one normal slot.
val emojiItem = @Composable { suggestion: SuggestedWordInfo ->
val idx = words.indexOf(suggestion)
SuggestionItem(
words, idx,
isPrimary = false, forcePrimary = false,
onClick = { onClick(idx) }, onLongClick = { onLongClick(idx) },
isEmoji = true, weight = 0.5f
)
}

// Renders the leftmost emoji slot(s), or a word fallback when no emoji is available.
// fallback is pre-evaluated so callers can side-effect supplementalSuggestionIndex safely.
val emojiSlots = @Composable { fallback: SuggestedWordInfo? ->
when {
layout.emojiMatches.isEmpty() ->
suggestionItem(fallback)
layout.emojiMatches.size >= PersistentEmojiState.MAX_EMOJI_SUGGESTIONS -> {
emojiItem(layout.emojiMatches[0])
SuggestionSeparator()
emojiItem(layout.emojiMatches[1])
}
else ->
suggestionItem(layout.emojiMatches[0])
}
}

when {
layout.isGestureBatch ||
(layout.emojiMatches.isEmpty() && layout.presentableSuggestions.size <= 1) ->
suggestionItem(layout.presentableSuggestions.firstOrNull())

layout.autocorrectMatch != null -> {
var supplementalSuggestionIndex = 0
if(layout.emojiMatches.isEmpty()) {
suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
} else {
suggestionItem(layout.emojiMatches[0])
}

emojiSlots(
if(layout.emojiMatches.isEmpty()) layout.sortedMatches.getOrNull(supplementalSuggestionIndex++) else null
)

SuggestionSeparator()
suggestionItem(layout.autocorrectMatch)
SuggestionSeparator()
Expand All @@ -526,11 +554,11 @@ fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, o

else -> {
var supplementalSuggestionIndex = 1
if(layout.emojiMatches.isEmpty()) {
suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
} else {
suggestionItem(layout.emojiMatches[0])
}

emojiSlots(
if(layout.emojiMatches.isEmpty()) layout.sortedMatches.getOrNull(supplementalSuggestionIndex++) else null
)

SuggestionSeparator()
suggestionItem(layout.sortedMatches.getOrNull(0))
SuggestionSeparator()
Expand Down
26 changes: 18 additions & 8 deletions java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -923,12 +923,16 @@ data class LooseShortcut(val emoji: String, val shortcut: String, val score: Int

class PersistentEmojiState : PersistentActionState {
companion object {
const val MAX_EMOJI_SUGGESTIONS = 2

var emojis: MutableState<List<EmojiItem>?> = mutableStateOf(null)
var emojiMap: HashMap<String, EmojiItem> = HashMap()

// Language name to translations
private val loadedTranslations: HashMap<String, EmojiTranslations> = hashMapOf()
private val loadedTranslatedShortcuts: HashMap<String, Map<String, String>> = hashMapOf()

// word → ranked emoji list (up to 2), per language
private val loadedTranslatedShortcuts: HashMap<String, Map<String, List<String>>> = hashMapOf()

@JvmStatic
fun getTranslationForLocale(locale: Locale): EmojiTranslations? {
Expand Down Expand Up @@ -962,8 +966,8 @@ class PersistentEmojiState : PersistentActionState {


@JvmStatic
fun getShortcut(locale: Locale, text: String): String? {
return loadedTranslatedShortcuts[locale.language]?.get(text)
fun getShortcuts(locale: Locale, text: String): List<String> {
return loadedTranslatedShortcuts[locale.language]?.get(text) ?: emptyList()
}

@JvmStatic
Expand Down Expand Up @@ -1014,13 +1018,18 @@ class PersistentEmojiState : PersistentActionState {
val ttsName = entry.value.names.last()

val names = entry.value.names.flatMap { it.split(" ") }
names.filter { wordCounts[it] == 1 && it.length > 1 }.map { it.lowercase() to entry.key } +

// score: ttsName (1) ranks above word fragments (0)
names.filter { wordCounts[it] == 1 && it.length > 1 }.map { Triple(it.lowercase(), entry.key, 0) } +
if(!ttsName.contains(' ')) {
listOf(ttsName.lowercase() to entry.key)
listOf(Triple(ttsName.lowercase(), entry.key, 1))
} else {
emptyList()
}
}.reversed().toMap()
}
.sortedByDescending { it.third }
.groupBy({ it.first }, { it.second })
.mapValues { (_, v) -> v.distinct().take(MAX_EMOJI_SUGGESTIONS) }

if(language != "en") loadedTranslatedShortcuts.put(language, aliases)
}
Expand Down Expand Up @@ -1125,10 +1134,11 @@ class PersistentEmojiState : PersistentActionState {
}
}

// Dedup by emoji before take: same emoji at multiple scores collapses both strip slots.
loadedTranslatedShortcuts["en"] = englishShortcuts
.sortedByDescending { it.score }
.distinctBy { it.shortcut }
.associate { it.shortcut to it.emoji }
.groupBy { it.shortcut }
.mapValues { (_, v) -> v.map { it.emoji }.distinct().take(MAX_EMOJI_SUGGESTIONS) }

loadedTranslations["en_gemoji"] = EmojiTranslations(englishTranslations)
}
Expand Down