Merge pull request #1162 from vector-im/feature/dla/emojibase_integration
Emojibase integration
This commit is contained in:
commit
ec3b6bf1ae
22 changed files with 246 additions and 74 deletions
|
|
@ -218,7 +218,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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import androidx.preference.PreferenceManager
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
|
@ -105,4 +107,10 @@ object AppModule {
|
||||||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
||||||
return SnackbarDispatcher()
|
return SnackbarDispatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
|
||||||
|
return DefaultEmojibaseProvider(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,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)
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ fun aMessagesState() = MessagesState(
|
||||||
),
|
),
|
||||||
actionListState = anActionListState(),
|
actionListState = anActionListState(),
|
||||||
customReactionState = CustomReactionState(
|
customReactionState = CustomReactionState(
|
||||||
selectedEventId = null,
|
target = CustomReactionState.Target.None,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
selectedEmoji = persistentSetOf(),
|
selectedEmoji = persistentSetOf(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ fun MessagesView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMoreReactionsClicked(event: TimelineItem.Event) {
|
fun onMoreReactionsClicked(event: TimelineItem.Event) {
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
|
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -194,18 +194,17 @@ fun MessagesView(
|
||||||
state = state.actionListState,
|
state = state.actionListState,
|
||||||
onActionSelected = ::onActionSelected,
|
onActionSelected = ::onActionSelected,
|
||||||
onCustomReactionClicked = { event ->
|
onCustomReactionClicked = { event ->
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
|
if (event.eventId == null) return@ActionListView
|
||||||
|
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||||
},
|
},
|
||||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||||
)
|
)
|
||||||
|
|
||||||
CustomReactionBottomSheet(
|
CustomReactionBottomSheet(
|
||||||
state = state.customReactionState,
|
state = state.customReactionState,
|
||||||
onEmojiSelected = { emoji ->
|
onEmojiSelected = { eventId, emoji ->
|
||||||
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)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,34 +22,35 @@ 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
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomReactionBottomSheet(
|
fun CustomReactionBottomSheet(
|
||||||
state: CustomReactionState,
|
state: CustomReactionState,
|
||||||
onEmojiSelected: (Emoji) -> Unit,
|
onEmojiSelected: (EventId, Emoji) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val target = state.target as? CustomReactionState.Target.Success
|
||||||
|
|
||||||
fun onDismiss() {
|
fun onDismiss() {
|
||||||
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
fun onEmojiSelectedDismiss(emoji: Emoji) {
|
||||||
|
if (target?.event?.eventId == null) return
|
||||||
sheetState.hide(coroutineScope) {
|
sheetState.hide(coroutineScope) {
|
||||||
state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
onEmojiSelected(emoji)
|
onEmojiSelected(target.event.eventId, emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isVisible = state.selectedEventId != null
|
if (target?.emojibaseStore != null && target.event.eventId != null) {
|
||||||
if (isVisible) {
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = ::onDismiss,
|
onDismissRequest = ::onDismiss,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
|
|
@ -57,8 +58,9 @@ fun CustomReactionBottomSheet(
|
||||||
) {
|
) {
|
||||||
EmojiPicker(
|
EmojiPicker(
|
||||||
onEmojiSelected = ::onEmojiSelectedDismiss,
|
onEmojiSelected = ::onEmojiSelectedDismiss,
|
||||||
modifier = Modifier.fillMaxSize(),
|
emojibaseStore = target.emojibaseStore,
|
||||||
selectedEmojis = state.selectedEmoji,
|
selectedEmojis = state.selectedEmoji,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,6 @@ package io.element.android.features.messages.impl.timeline.components.customreac
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface CustomReactionEvents {
|
sealed interface CustomReactionEvents {
|
||||||
data class UpdateSelectedEvent(val event: TimelineItem.Event?) : CustomReactionEvents
|
data class ShowCustomReactionSheet(val event: TimelineItem.Event) : CustomReactionEvents
|
||||||
|
object DismissCustomReactionSheet : CustomReactionEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,28 +17,53 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
|
class CustomReactionPresenter @Inject constructor(
|
||||||
|
private val emojibaseProvider: EmojibaseProvider
|
||||||
|
) : Presenter<CustomReactionState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): CustomReactionState {
|
override fun present(): CustomReactionState {
|
||||||
var selectedEvent by remember { mutableStateOf<TimelineItem.Event?>(null) }
|
val target: MutableState<CustomReactionState.Target> = remember {
|
||||||
|
mutableStateOf(CustomReactionState.Target.None)
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: CustomReactionEvents) {
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
when (event) {
|
fun handleShowCustomReactionSheet(event: TimelineItem.Event) {
|
||||||
is CustomReactionEvents.UpdateSelectedEvent -> selectedEvent = event.event
|
target.value = CustomReactionState.Target.Loading(event)
|
||||||
|
localCoroutineScope.launch {
|
||||||
|
target.value = CustomReactionState.Target.Success(
|
||||||
|
event = event,
|
||||||
|
emojibaseStore = emojibaseProvider.emojibaseStore
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedEmoji = selectedEvent?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet()
|
fun handleDismissCustomReactionSheet() {
|
||||||
return CustomReactionState(selectedEventId = selectedEvent?.eventId, selectedEmoji = selectedEmoji, eventSink = ::handleEvents)
|
target.value = CustomReactionState.Target.None
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: CustomReactionEvents) {
|
||||||
|
when (event) {
|
||||||
|
is CustomReactionEvents.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.event)
|
||||||
|
is CustomReactionEvents.DismissCustomReactionSheet -> handleDismissCustomReactionSheet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val event = (target.value as? CustomReactionState.Target.Success)?.event
|
||||||
|
val selectedEmoji = event?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet()
|
||||||
|
return CustomReactionState(
|
||||||
|
target = target.value,
|
||||||
|
selectedEmoji = selectedEmoji,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,23 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
data class CustomReactionState(
|
data class CustomReactionState(
|
||||||
val selectedEventId: EventId?,
|
val target: Target,
|
||||||
val selectedEmoji: ImmutableSet<String>,
|
val selectedEmoji: ImmutableSet<String>,
|
||||||
val eventSink: (CustomReactionEvents) -> Unit,
|
val eventSink: (CustomReactionEvents) -> Unit,
|
||||||
)
|
) {
|
||||||
|
sealed interface Target {
|
||||||
|
|
||||||
|
data object None : Target
|
||||||
|
data class Loading(val event: TimelineItem.Event) : Target
|
||||||
|
data class Success(
|
||||||
|
val event: TimelineItem.Event,
|
||||||
|
val emojibaseStore: EmojibaseStore,
|
||||||
|
) : Target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.x.initializer
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
import androidx.startup.Initializer
|
import android.content.Context
|
||||||
import com.vanniktech.emoji.EmojiManager
|
import io.element.android.emojibasebindings.EmojibaseDatasource
|
||||||
import com.vanniktech.emoji.google.GoogleEmojiProvider
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
|
||||||
class EmojiInitializer : Initializer<Unit> {
|
class DefaultEmojibaseProvider(val context: Context): EmojibaseProvider {
|
||||||
override fun create(context: android.content.Context) {
|
|
||||||
EmojiManager.install(GoogleEmojiProvider())
|
override val emojibaseStore: EmojibaseStore by lazy {
|
||||||
|
EmojibaseDatasource().load(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
|
|
||||||
}
|
}
|
||||||
|
|
@ -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.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -41,11 +41,15 @@ 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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
|
|
@ -59,24 +63,23 @@ import kotlinx.coroutines.launch
|
||||||
@Composable
|
@Composable
|
||||||
fun EmojiPicker(
|
fun EmojiPicker(
|
||||||
onEmojiSelected: (Emoji) -> Unit,
|
onEmojiSelected: (Emoji) -> Unit,
|
||||||
|
emojibaseStore: EmojibaseStore,
|
||||||
selectedEmojis: ImmutableSet<String>,
|
selectedEmojis: ImmutableSet<String>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val categories = remember { emojibaseStore.categories }
|
||||||
val emojiProvider = remember { GoogleEmojiProvider() }
|
val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.values().size })
|
||||||
val categories = remember { emojiProvider.categories }
|
|
||||||
val pagerState = rememberPagerState(pageCount = { emojiProvider.categories.size })
|
|
||||||
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,
|
||||||
|
|
@ -91,14 +94,16 @@ fun EmojiPicker(
|
||||||
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.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
items(category.emojis, key = { it.unicode }) { item ->
|
|
||||||
|
items(emojis, key = { it.unicode }) { item ->
|
||||||
val backgroundColor = if (selectedEmojis.contains(item.unicode)) {
|
val backgroundColor = if (selectedEmojis.contains(item.unicode)) {
|
||||||
ElementTheme.colors.bgActionPrimaryRest
|
ElementTheme.colors.bgActionPrimaryRest
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -144,7 +149,8 @@ internal fun EmojiPickerDarkPreview() {
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
EmojiPicker(
|
EmojiPicker(
|
||||||
onEmojiSelected = {},
|
onEmojiSelected = {},
|
||||||
|
emojibaseStore = EmojibaseDatasource().load(LocalContext.current),
|
||||||
|
selectedEmojis = persistentSetOf("😀", "😄", "😃"),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
selectedEmojis = persistentSetOf("😀", "😄", "😃")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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.CommonStrings
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val EmojibaseCategory.title: Int get() =
|
||||||
|
when(this){
|
||||||
|
EmojibaseCategory.People -> CommonStrings.emoji_picker_category_people
|
||||||
|
EmojibaseCategory.Nature -> CommonStrings.emoji_picker_category_nature
|
||||||
|
EmojibaseCategory.Foods -> CommonStrings.emoji_picker_category_foods
|
||||||
|
EmojibaseCategory.Activity -> CommonStrings.emoji_picker_category_activity
|
||||||
|
EmojibaseCategory.Places -> CommonStrings.emoji_picker_category_places
|
||||||
|
EmojibaseCategory.Objects -> CommonStrings.emoji_picker_category_objects
|
||||||
|
EmojibaseCategory.Symbols -> CommonStrings.emoji_picker_category_symbols
|
||||||
|
EmojibaseCategory.Flags -> CommonStrings.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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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 io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
|
||||||
|
interface EmojibaseProvider {
|
||||||
|
val emojibaseStore: EmojibaseStore
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
|
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
|
||||||
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
|
|
@ -603,7 +604,7 @@ class MessagesPresenterTest {
|
||||||
)
|
)
|
||||||
val buildMeta = aBuildMeta()
|
val buildMeta = aBuildMeta()
|
||||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||||
val customReactionPresenter = CustomReactionPresenter()
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
|
|
|
||||||
|
|
@ -24,27 +24,34 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class CustomReactionPresenterTests {
|
class CustomReactionPresenterTests {
|
||||||
|
|
||||||
private val presenter = CustomReactionPresenter()
|
private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle selecting and de-selecting an event`() = runTest {
|
fun `present - handle selecting and de-selecting an event`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
||||||
|
val event = aTimelineItemEvent(eventId = AN_EVENT_ID)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.selectedEventId).isNull()
|
assertThat(initialState.target).isEqualTo(CustomReactionState.Target.None)
|
||||||
|
|
||||||
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID)))
|
initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||||
assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID)
|
|
||||||
|
|
||||||
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event))
|
||||||
assertThat(awaitItem().selectedEventId).isNull()
|
|
||||||
|
val eventId = (awaitItem().target as? CustomReactionState.Target.Success)?.event?.eventId
|
||||||
|
assertThat(eventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
|
||||||
|
initialState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,13 +60,19 @@ class CustomReactionPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.selectedEventId).isNull()
|
|
||||||
val reactions = aTimelineItemReactions(count = 1, isHighlighted = true)
|
val reactions = aTimelineItemReactions(count = 1, isHighlighted = true)
|
||||||
|
val event = aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.target).isEqualTo(CustomReactionState.Target.None)
|
||||||
|
|
||||||
val key = reactions.reactions.first().key
|
val key = reactions.reactions.first().key
|
||||||
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions)))
|
initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||||
|
|
||||||
|
assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event))
|
||||||
|
|
||||||
val stateWithSelectedEmojis = awaitItem()
|
val stateWithSelectedEmojis = awaitItem()
|
||||||
assertThat(stateWithSelectedEmojis.selectedEventId).isEqualTo(AN_EVENT_ID)
|
val eventId = (stateWithSelectedEmojis.target as? CustomReactionState.Target.Success)?.event?.eventId
|
||||||
|
assertThat(eventId).isEqualTo(AN_EVENT_ID)
|
||||||
assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key)
|
assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.timeline.components.customreaction
|
||||||
|
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider
|
||||||
|
|
||||||
|
class FakeEmojibaseProvider: EmojibaseProvider {
|
||||||
|
override val emojibaseStore: EmojibaseStore
|
||||||
|
get() = EmojibaseStore(mapOf())
|
||||||
|
}
|
||||||
|
|
@ -155,7 +155,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"
|
||||||
|
|
@ -167,6 +166,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" }
|
||||||
|
|
@ -178,7 +180,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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5ea42394330d37a0aa5ba8940e023fc4454d902caf6d265adec0e11c8a34d85f
|
||||||
|
size 187744
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1fb074cd58034c90010dc437fc01d6ea77a2a22aa07bc1d4cf0c1730b543b41a
|
||||||
|
size 186707
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:cd761ca4ff5a70770869780c2575006ed8bd187ed4b3f197ffe1e4ea18d8390b
|
|
||||||
size 189559
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d8c6e3f139ca634c47c45fbeeaccf3e7e903fb8f83a88fdf8d7247455e76e4a9
|
|
||||||
size 188653
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue