Integrate emojibase
- Integrate emojibase datasource - Use element category translations - Use Material emoji category logos
This commit is contained in:
parent
a77b59824c
commit
ce4c12ce74
14 changed files with 130 additions and 63 deletions
|
|
@ -220,7 +220,7 @@ dependencies {
|
|||
implementation(libs.network.okhttp.logging)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
implementation(libs.vanniktech.emoji)
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
implementation(libs.dagger)
|
||||
kapt(libs.dagger.compiler)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import io.element.android.x.di.AppComponent
|
|||
import io.element.android.x.di.DaggerAppComponent
|
||||
import io.element.android.x.info.logApplicationInfo
|
||||
import io.element.android.x.initializer.CrashInitializer
|
||||
import io.element.android.x.initializer.EmojiInitializer
|
||||
import io.element.android.x.initializer.TracingInitializer
|
||||
|
||||
class ElementXApplication : Application(), DaggerComponentOwner {
|
||||
|
|
@ -39,7 +38,6 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
|||
AppInitializer.getInstance(this).apply {
|
||||
initializeComponent(CrashInitializer::class.java)
|
||||
initializeComponent(TracingInitializer::class.java)
|
||||
initializeComponent(EmojiInitializer::class.java)
|
||||
}
|
||||
logApplicationInfo()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ dependencies {
|
|||
implementation(libs.accompanist.systemui)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(libs.telephoto.zoomableimage)
|
||||
implementation(libs.vanniktech.emoji)
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.messagecomposer.aMessageComposerState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
|
|
@ -67,6 +69,7 @@ fun aMessagesState() = MessagesState(
|
|||
actionListState = anActionListState(),
|
||||
customReactionState = CustomReactionState(
|
||||
selectedEventId = null,
|
||||
emojiProvider = Async.Uninitialized,
|
||||
eventSink = {},
|
||||
),
|
||||
reactionSummaryState = ReactionSummaryState(
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ fun MessagesView(
|
|||
}
|
||||
|
||||
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(
|
||||
|
|
@ -187,7 +188,8 @@ fun MessagesView(
|
|||
state = state.actionListState,
|
||||
onActionSelected = ::onActionSelected,
|
||||
onCustomReactionClicked = { event ->
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
|
||||
if (event.eventId == null) return@ActionListView
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId))
|
||||
},
|
||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||
)
|
||||
|
|
@ -197,7 +199,7 @@ fun MessagesView(
|
|||
onEmojiSelected = { emoji ->
|
||||
state.customReactionState.selectedEventId?.let { eventId ->
|
||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vanniktech.emoji.Emoji
|
||||
import io.element.android.features.messages.impl.timeline.components.EmojiPicker
|
||||
import io.element.android.emojibasebindings.Emoji
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
|
||||
|
|
@ -38,18 +37,19 @@ fun CustomReactionBottomSheet(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
fun onDismiss() {
|
||||
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
}
|
||||
|
||||
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||
sheetState.hide(coroutineScope) {
|
||||
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
onEmojiSelected(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
val isVisible = state.selectedEventId != null
|
||||
if (isVisible) {
|
||||
val emojiProvider = state.emojiProvider.dataOrNull()
|
||||
val selectedEventId = state.selectedEventId
|
||||
if (emojiProvider != null && selectedEventId != null) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = ::onDismiss,
|
||||
sheetState = sheetState,
|
||||
|
|
@ -57,6 +57,7 @@ fun CustomReactionBottomSheet(
|
|||
) {
|
||||
EmojiPicker(
|
||||
onEmojiSelected = ::onEmojiSelectedDismiss,
|
||||
emojiProvider = emojiProvider,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ package io.element.android.features.messages.impl.timeline.components.customreac
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface CustomReactionEvents {
|
||||
data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents
|
||||
data class ShowCustomReactionSheet(val eventId: EventId) : CustomReactionEvents
|
||||
object DismissCustomReactionSheet : CustomReactionEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,15 @@ 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.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.matrix.api.core.EventId
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
|
||||
|
|
@ -30,13 +36,31 @@ class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionSt
|
|||
@Composable
|
||||
override fun present(): CustomReactionState {
|
||||
var selectedEventId by remember { mutableStateOf<EventId?>(null) }
|
||||
|
||||
fun handleEvents(event: CustomReactionEvents) {
|
||||
when (event) {
|
||||
is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId
|
||||
var emojiState: Async<EmojibaseStore> by remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@
|
|||
|
||||
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
|
||||
|
||||
data class CustomReactionState(
|
||||
val selectedEventId: EventId?,
|
||||
val emojiProvider: Async<EmojibaseStore>,
|
||||
val eventSink: (CustomReactionEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.clickable
|
||||
|
|
@ -39,10 +39,14 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
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.unit.dp
|
||||
import com.vanniktech.emoji.Emoji
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider
|
||||
import io.element.android.emojibasebindings.Emoji
|
||||
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.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
|
@ -54,23 +58,22 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun EmojiPicker(
|
||||
onEmojiSelected: (Emoji) -> Unit,
|
||||
emojiProvider: EmojibaseStore,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val emojiProvider = remember { GoogleEmojiProvider() }
|
||||
val categories = remember { emojiProvider.categories }
|
||||
val pagerState = rememberPagerState()
|
||||
Column(modifier) {
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
EmojibaseCategory.values().forEachIndexed { index, category ->
|
||||
Tab(
|
||||
text = {
|
||||
Icon(
|
||||
resourceId = emojiProvider.getIcon(category),
|
||||
contentDescription = category.categoryNames["en"]
|
||||
imageVector = category.icon,
|
||||
contentDescription = stringResource(id = category.title)
|
||||
)
|
||||
},
|
||||
selected = pagerState.currentPage == index,
|
||||
|
|
@ -82,18 +85,19 @@ fun EmojiPicker(
|
|||
}
|
||||
|
||||
HorizontalPager(
|
||||
pageCount = categories.size,
|
||||
pageCount = EmojibaseCategory.values().size,
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { index ->
|
||||
val category = categories[index]
|
||||
val category = EmojibaseCategory.values()[index]
|
||||
val emojis = categories[category] ?: listOf()
|
||||
LazyVerticalGrid(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
columns = GridCells.Adaptive(minSize = 40.dp),
|
||||
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
items(category.emojis, key = { it.unicode }) { item ->
|
||||
items(emojis, key = { it.unicode }) { item ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
|
|
@ -132,6 +136,7 @@ internal fun EmojiPickerDarkPreview() {
|
|||
private fun ContentToPreview() {
|
||||
EmojiPicker(
|
||||
onEmojiSelected = {},
|
||||
emojiProvider = EmojibaseDatasource().load(LocalContext.current),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -38,10 +38,10 @@ class CustomReactionPresenterTests {
|
|||
val initialState = awaitItem()
|
||||
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)
|
||||
|
||||
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||
initialState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
assertThat(awaitItem().selectedEventId).isNull()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,6 @@ sqlite = "androidx.sqlite:sqlite:2.3.1"
|
|||
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
||||
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" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.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"
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
|
||||
|
||||
# Emojibase
|
||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.0.5"
|
||||
|
||||
# Di
|
||||
inject = "javax.inject:javax.inject:1"
|
||||
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_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" }
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
# 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue