Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,7 @@
<string name="title_reactorsSheet">Reactions</string>
<string name="hint_searchEmoji">Search</string>
<string name="action_clearEmojiSearch">Clear search</string>
<string name="action_chooseSkinTone">Choose skin tone</string>
<string name="action_addReaction">Add reaction</string>

<string name="label_unknownContact">Unknown Contact</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ private fun FlowReactionPickerScreen(messageId: Long) {
ReactionPickerSheet(
searchState = state.searchFieldState,
sections = state.sections,
tones = state.tones,
loaded = state.loaded,
onSelected = { emoji ->
chatViewModel.dispatchEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.viewModelScope
import com.flipcash.libs.coroutines.DispatcherProvider
import com.getcode.libs.emojis.reactions.EmojiDrawability
import com.getcode.libs.emojis.reactions.EmojiCatalog
import com.getcode.libs.emojis.reactions.EmojiCatalogLoader
import com.getcode.libs.emojis.reactions.EmojiDrawability
import com.getcode.libs.emojis.reactions.EmojiPickerModel
import com.getcode.libs.emojis.reactions.RecentReactionsStore
import com.getcode.view.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext

/**
* The full emoji picker's search and sections. Scoped to the picker's own nav entry
Expand All @@ -26,7 +30,7 @@ import kotlinx.coroutines.flow.onEach
*/
@HiltViewModel
class EmojiPickerViewModel internal constructor(
dispatchers: DispatcherProvider,
private val dispatchers: DispatcherProvider,
private val emojiCatalogLoader: EmojiCatalogLoader,
private val recentReactionsStore: RecentReactionsStore,
// A seam for tests: Robolectric's Paint reports no emoji glyphs, which would empty every section.
Expand All @@ -49,11 +53,36 @@ class EmojiPickerViewModel internal constructor(
val sections: List<EmojiPickerModel.Section> = emptyList(),
/** False until the first load lands, which is what the sheet's spinner waits on. */
val loaded: Boolean = false,
/** What a long-press on an emoji offers; see [EmojiPickerModel.toneOptions]. */
val tones: Map<String, List<String>> = emptyMap(),
)

sealed interface Event {
/** Internal: the sections for the settled search text finished loading. */
data class SectionsLoaded(val sections: List<EmojiPickerModel.Section>) : Event
data class SectionsLoaded(
val sections: List<EmojiPickerModel.Section>,
val tones: Map<String, List<String>>,
) : Event
}

private class CatalogInputs(
val catalog: EmojiCatalog,
val undrawable: Set<String>,
val tones: Map<String, List<String>>,
)

/**
* The catalog with what this device can't draw and the tone choices, worked out once per sheet:
* the glyph probe over all 3,944 entries is the costly part, and its answer can't change while
* the sheet is open.
*/
private val catalogInputs = viewModelScope.async(dispatchers.Default, start = CoroutineStart.LAZY) {
val catalog = emojiCatalogLoader.load()
val undrawable = catalog.entries
.map { it.emoji }
.filterNot { EmojiDrawability.isDrawable(it, glyphProbe) }
.toSet()
CatalogInputs(catalog, undrawable, EmojiPickerModel.toneOptions(catalog, undrawable))
}

init {
Expand All @@ -62,30 +91,36 @@ class EmojiPickerViewModel internal constructor(
snapshotFlow { stateFlow.value.searchFieldState.text.toString() }
.distinctUntilChanged()
.debounce(150)
.mapLatest { query -> Event.SectionsLoaded(sections(query)) }
.mapLatest { query -> sections(query) }
.onEach { dispatchEvent(it) }
.launchIn(viewModelScope)
}

/**
* The sections for [query] (recents row + one section per catalog category, or a single
* search-results section — see [EmojiPickerModel.sections]). The loader caches the parsed
* catalog, so reloading it per query is cheap after the first.
* search-results section — see [EmojiPickerModel.sections]), built on the default dispatcher
* so a keystroke's filtering stays off the main thread.
*/
private suspend fun sections(query: String): List<EmojiPickerModel.Section> {
val catalog = emojiCatalogLoader.load()
val undrawable = catalog.entries
.map { it.emoji }
.filterNot { EmojiDrawability.isDrawable(it, glyphProbe) }
.toSet()
val recents = recentReactionsStore.rank(undrawable = undrawable)
return EmojiPickerModel.sections(catalog = catalog, undrawable = undrawable, recents = recents, query = query)
private suspend fun sections(query: String): Event.SectionsLoaded = withContext(dispatchers.Default) {
val inputs = catalogInputs.await()
val recents = recentReactionsStore.rank(undrawable = inputs.undrawable)
Event.SectionsLoaded(
sections = EmojiPickerModel.sections(
catalog = inputs.catalog,
undrawable = inputs.undrawable,
recents = recents,
query = query,
),
tones = inputs.tones,
)
}

companion object {
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
when (event) {
is Event.SectionsLoaded -> { state -> state.copy(sections = event.sections, loaded = true) }
is Event.SectionsLoaded -> { state ->
state.copy(sections = event.sections, tones = event.tones, loaded = true)
}
}
}
}
Expand Down
Loading
Loading