Integrate emojibase

- Integrate emojibase datasource
- Use element category translations
- Use Material emoji category logos
This commit is contained in:
David Langley 2023-08-28 16:45:42 +01:00
parent 37a4d49494
commit 685d684359
14 changed files with 130 additions and 63 deletions

View file

@ -220,7 +220,7 @@ dependencies {
implementation(libs.network.okhttp.logging) implementation(libs.network.okhttp.logging)
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(libs.vanniktech.emoji) implementation(libs.matrix.emojibase.bindings)
implementation(libs.dagger) implementation(libs.dagger)
kapt(libs.dagger.compiler) kapt(libs.dagger.compiler)

View file

@ -23,7 +23,6 @@ import io.element.android.x.di.AppComponent
import io.element.android.x.di.DaggerAppComponent import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.EmojiInitializer
import io.element.android.x.initializer.TracingInitializer import io.element.android.x.initializer.TracingInitializer
class ElementXApplication : Application(), DaggerComponentOwner { class ElementXApplication : Application(), DaggerComponentOwner {
@ -39,7 +38,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
AppInitializer.getInstance(this).apply { AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java) initializeComponent(CrashInitializer::class.java)
initializeComponent(TracingInitializer::class.java) initializeComponent(TracingInitializer::class.java)
initializeComponent(EmojiInitializer::class.java)
} }
logApplicationInfo() logApplicationInfo()
} }

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.initializer
import androidx.startup.Initializer
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider
class EmojiInitializer : Initializer<Unit> {
override fun create(context: android.content.Context) {
EmojiManager.install(GoogleEmojiProvider())
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
}

View file

@ -60,7 +60,7 @@ dependencies {
implementation(libs.accompanist.systemui) implementation(libs.accompanist.systemui)
implementation(libs.vanniktech.blurhash) implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.zoomableimage) implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.emoji) implementation(libs.matrix.emojibase.bindings)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)

View file

@ -16,7 +16,9 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.emojibasebindings.EmojibaseDatasource
import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineItemList
@ -67,6 +69,7 @@ fun aMessagesState() = MessagesState(
actionListState = anActionListState(), actionListState = anActionListState(),
customReactionState = CustomReactionState( customReactionState = CustomReactionState(
selectedEventId = null, selectedEventId = null,
emojiProvider = Async.Uninitialized,
eventSink = {}, eventSink = {},
), ),
reactionSummaryState = ReactionSummaryState( reactionSummaryState = ReactionSummaryState(

View file

@ -135,7 +135,8 @@ fun MessagesView(
} }
fun onMoreReactionsClicked(event: TimelineItem.Event) { fun onMoreReactionsClicked(event: TimelineItem.Event) {
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) if (event.eventId == null) return
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId))
} }
Scaffold( Scaffold(
@ -187,7 +188,8 @@ fun MessagesView(
state = state.actionListState, state = state.actionListState,
onActionSelected = ::onActionSelected, onActionSelected = ::onActionSelected,
onCustomReactionClicked = { event -> onCustomReactionClicked = { event ->
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) if (event.eventId == null) return@ActionListView
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId))
}, },
onEmojiReactionClicked = ::onEmojiReactionClicked, onEmojiReactionClicked = ::onEmojiReactionClicked,
) )
@ -197,7 +199,7 @@ fun MessagesView(
onEmojiSelected = { emoji -> onEmojiSelected = { emoji ->
state.customReactionState.selectedEventId?.let { eventId -> state.customReactionState.selectedEventId?.let { eventId ->
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId)) state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
} }
} }
) )

View file

@ -22,8 +22,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vanniktech.emoji.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.features.messages.impl.timeline.components.EmojiPicker
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.designsystem.theme.components.hide
@ -38,18 +37,19 @@ fun CustomReactionBottomSheet(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
fun onDismiss() { fun onDismiss() {
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
} }
fun onEmojiSelectedDismiss(emoji: Emoji) { fun onEmojiSelectedDismiss(emoji: Emoji) {
sheetState.hide(coroutineScope) { sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
onEmojiSelected(emoji) onEmojiSelected(emoji)
} }
} }
val isVisible = state.selectedEventId != null val emojiProvider = state.emojiProvider.dataOrNull()
if (isVisible) { val selectedEventId = state.selectedEventId
if (emojiProvider != null && selectedEventId != null) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = ::onDismiss, onDismissRequest = ::onDismiss,
sheetState = sheetState, sheetState = sheetState,
@ -57,6 +57,7 @@ fun CustomReactionBottomSheet(
) { ) {
EmojiPicker( EmojiPicker(
onEmojiSelected = ::onEmojiSelectedDismiss, onEmojiSelected = ::onEmojiSelectedDismiss,
emojiProvider = emojiProvider,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }

View file

@ -19,5 +19,6 @@ package io.element.android.features.messages.impl.timeline.components.customreac
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
sealed interface CustomReactionEvents { sealed interface CustomReactionEvents {
data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents data class ShowCustomReactionSheet(val eventId: EventId) : CustomReactionEvents
object DismissCustomReactionSheet : CustomReactionEvents
} }

View file

@ -20,9 +20,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import io.element.android.emojibasebindings.EmojibaseDatasource
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> { class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
@ -30,13 +36,31 @@ class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionSt
@Composable @Composable
override fun present(): CustomReactionState { override fun present(): CustomReactionState {
var selectedEventId by remember { mutableStateOf<EventId?>(null) } var selectedEventId by remember { mutableStateOf<EventId?>(null) }
var emojiState: Async<EmojibaseStore> by remember {
fun handleEvents(event: CustomReactionEvents) { mutableStateOf(Async.Uninitialized)
when (event) { }
is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId val localCoroutineScope = rememberCoroutineScope()
val context = LocalContext.current
fun handleShowCustomReactionSheet(eventId: EventId) {
selectedEventId = eventId
emojiState = Async.Loading()
localCoroutineScope.launch {
emojiState = Async.Success(EmojibaseDatasource().load(context))
} }
} }
return CustomReactionState(selectedEventId = selectedEventId, eventSink = ::handleEvents) fun handleDismissCustomReactionSheet() {
selectedEventId = null
emojiState = Async.Uninitialized
}
fun handleEvents(event: CustomReactionEvents) {
when (event) {
is CustomReactionEvents.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.eventId)
is CustomReactionEvents.DismissCustomReactionSheet -> handleDismissCustomReactionSheet()
}
}
return CustomReactionState(selectedEventId = selectedEventId, emojiProvider = emojiState, eventSink = ::handleEvents)
} }
} }

View file

@ -16,9 +16,12 @@
package io.element.android.features.messages.impl.timeline.components.customreaction package io.element.android.features.messages.impl.timeline.components.customreaction
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
data class CustomReactionState( data class CustomReactionState(
val selectedEventId: EventId?, val selectedEventId: EventId?,
val emojiProvider: Async<EmojibaseStore>,
val eventSink: (CustomReactionEvents) -> Unit, val eventSink: (CustomReactionEvents) -> Unit,
) )

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.messages.impl.timeline.components package io.element.android.features.messages.impl.timeline.components.customreaction
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -39,10 +39,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vanniktech.emoji.Emoji import io.element.android.emojibasebindings.Emoji
import com.vanniktech.emoji.google.GoogleEmojiProvider import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.emojibasebindings.EmojibaseDatasource
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
@ -54,23 +58,22 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun EmojiPicker( fun EmojiPicker(
onEmojiSelected: (Emoji) -> Unit, onEmojiSelected: (Emoji) -> Unit,
emojiProvider: EmojibaseStore,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val emojiProvider = remember { GoogleEmojiProvider() }
val categories = remember { emojiProvider.categories } val categories = remember { emojiProvider.categories }
val pagerState = rememberPagerState() val pagerState = rememberPagerState()
Column(modifier) { Column(modifier) {
TabRow( TabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
) { ) {
categories.forEachIndexed { index, category -> EmojibaseCategory.values().forEachIndexed { index, category ->
Tab( Tab(
text = { text = {
Icon( Icon(
resourceId = emojiProvider.getIcon(category), imageVector = category.icon,
contentDescription = category.categoryNames["en"] contentDescription = stringResource(id = category.title)
) )
}, },
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
@ -82,18 +85,19 @@ fun EmojiPicker(
} }
HorizontalPager( HorizontalPager(
pageCount = categories.size, pageCount = EmojibaseCategory.values().size,
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { index -> ) { index ->
val category = categories[index] val category = EmojibaseCategory.values()[index]
val emojis = categories[category] ?: listOf()
LazyVerticalGrid( LazyVerticalGrid(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(minSize = 40.dp), columns = GridCells.Adaptive(minSize = 40.dp),
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp), contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
items(category.emojis, key = { it.unicode }) { item -> items(emojis, key = { it.unicode }) { item ->
Box( Box(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@ -132,6 +136,7 @@ internal fun EmojiPickerDarkPreview() {
private fun ContentToPreview() { private fun ContentToPreview() {
EmojiPicker( EmojiPicker(
onEmojiSelected = {}, onEmojiSelected = {},
emojiProvider = EmojibaseDatasource().load(LocalContext.current),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.customreaction
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.ui.strings.R
@get:StringRes
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
}
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
}

View file

@ -38,10 +38,10 @@ class CustomReactionPresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.selectedEventId).isNull() assertThat(initialState.selectedEventId).isNull()
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(AN_EVENT_ID)) initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(AN_EVENT_ID))
assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID) assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID)
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) initialState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
assertThat(awaitItem().selectedEventId).isNull() assertThat(awaitItem().selectedEventId).isNull()
} }
} }

View file

@ -154,7 +154,6 @@ sqlite = "androidx.sqlite:sqlite:2.3.1"
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.0" statemachine = "com.freeletics.flowredux:compose:1.2.0"
maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0"
@ -166,6 +165,9 @@ posthog = "com.posthog.android:posthog:2.0.3"
sentry = "io.sentry:sentry-android:6.28.0" sentry = "io.sentry:sentry-android:6.28.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.0.5"
# Di # Di
inject = "javax.inject:javax.inject:1" inject = "javax.inject:javax.inject:1"
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
@ -177,7 +179,6 @@ anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.r
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" } google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" }
# Miscellaneous # Miscellaneous
# Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the # Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the
# value of `composecompiler` (which is used to set composeOptions.kotlinCompilerExtensionVersion. # value of `composecompiler` (which is used to set composeOptions.kotlinCompilerExtensionVersion.