Use shared recent emoji reactions from account data (#5402)
* Use shared recent emoji reactions from account data - Add `AddRecentEmoji` and `GetRecentEmojis` use cases to avoid injecting the whole `MatrixClient` for just one of these operations. - Update the UI and logic of the emoji picker and message context menu to include the recent emojis. - Add `CoroutineDispatchers.Default` with the defaults coroutines to use in the app for ease of use. * Instead of replacing suggested emojis, concatenate recent ones removing duplicates * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
4dc65d9c08
commit
f1cd80ede8
45 changed files with 572 additions and 208 deletions
|
|
@ -33,7 +33,6 @@ import io.element.android.x.BuildConfig
|
||||||
import io.element.android.x.R
|
import io.element.android.x.R
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -107,11 +106,7 @@ object AppModule {
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||||
return CoroutineDispatchers(
|
return CoroutineDispatchers.Default
|
||||||
io = Dispatchers.IO,
|
|
||||||
computation = Dispatchers.Default,
|
|
||||||
main = Dispatchers.Main,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.core.extensions.flatMap
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
@ -70,6 +71,7 @@ import io.element.android.libraries.matrix.api.core.toThreadId
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.recentemojis.AddRecentEmoji
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
|
|
@ -121,6 +123,7 @@ class MessagesPresenter(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val addRecentEmoji: AddRecentEmoji,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -398,6 +401,7 @@ class MessagesPresenter(
|
||||||
) = launch(dispatchers.io) {
|
) = launch(dispatchers.io) {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
toggleReaction(emoji, eventOrTransactionId)
|
toggleReaction(emoji, eventOrTransactionId)
|
||||||
|
.flatMap { added -> if (added) addRecentEmoji(emoji) else Result.success(Unit) }
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
|
||||||
|
|
@ -178,9 +179,11 @@ fun aReactionSummaryState(
|
||||||
|
|
||||||
fun aCustomReactionState(
|
fun aCustomReactionState(
|
||||||
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
||||||
|
recentEmojis: ImmutableList<String> = persistentListOf(),
|
||||||
eventSink: (CustomReactionEvents) -> Unit = {},
|
eventSink: (CustomReactionEvents) -> Unit = {},
|
||||||
) = CustomReactionState(
|
) = CustomReactionState(
|
||||||
target = target,
|
target = target,
|
||||||
|
recentEmojis = recentEmojis,
|
||||||
selectedEmoji = persistentSetOf(),
|
selectedEmoji = persistentSetOf(),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
|
@ -73,6 +74,7 @@ class DefaultActionListPresenter(
|
||||||
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
|
||||||
private val dateFormatter: DateFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val getRecentEmojis: GetRecentEmojis,
|
||||||
) : ActionListPresenter {
|
) : ActionListPresenter {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(RoomScope::class)
|
@ContributesBinding(RoomScope::class)
|
||||||
|
|
@ -153,14 +155,15 @@ class DefaultActionListPresenter(
|
||||||
),
|
),
|
||||||
displayEmojiReactions = displayEmojiReactions,
|
displayEmojiReactions = displayEmojiReactions,
|
||||||
verifiedUserSendFailure = verifiedUserSendFailure,
|
verifiedUserSendFailure = verifiedUserSendFailure,
|
||||||
actions = actions.toImmutableList()
|
actions = actions.toImmutableList(),
|
||||||
|
recentEmojis = getRecentEmojis().getOrNull()?.toImmutableList() ?: persistentListOf()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
target.value = ActionListState.Target.None
|
target.value = ActionListState.Target.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildActions(
|
private fun buildActions(
|
||||||
timelineItem: TimelineItem.Event,
|
timelineItem: TimelineItem.Event,
|
||||||
usersEventPermissions: UserEventPermissions,
|
usersEventPermissions: UserEventPermissions,
|
||||||
isDeveloperModeEnabled: Boolean,
|
isDeveloperModeEnabled: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ data class ActionListState(
|
||||||
val event: TimelineItem.Event,
|
val event: TimelineItem.Event,
|
||||||
val sentTimeFull: String,
|
val sentTimeFull: String,
|
||||||
val displayEmojiReactions: Boolean,
|
val displayEmojiReactions: Boolean,
|
||||||
|
val recentEmojis: ImmutableList<String>,
|
||||||
val verifiedUserSendFailure: VerifiedUserSendFailure,
|
val verifiedUserSendFailure: VerifiedUserSendFailure,
|
||||||
val actions: ImmutableList<TimelineItemAction>,
|
val actions: ImmutableList<TimelineItemAction>,
|
||||||
) : Target
|
) : Target
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
|
|
@ -41,6 +42,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -56,6 +58,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
copyAction = TimelineItemAction.CopyCaption,
|
copyAction = TimelineItemAction.CopyCaption,
|
||||||
),
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -70,6 +73,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
copyAction = TimelineItemAction.CopyCaption,
|
copyAction = TimelineItemAction.CopyCaption,
|
||||||
),
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -84,6 +88,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
copyAction = null,
|
copyAction = null,
|
||||||
),
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -98,6 +103,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
copyAction = TimelineItemAction.CopyCaption,
|
copyAction = TimelineItemAction.CopyCaption,
|
||||||
),
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -112,6 +118,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemActionList(
|
actions = aTimelineItemActionList(
|
||||||
copyAction = null,
|
copyAction = null,
|
||||||
),
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -124,6 +131,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -148,6 +157,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayEmojiReactions = false,
|
displayEmojiReactions = false,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemPollActionList(),
|
actions = aTimelineItemPollActionList(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -160,6 +170,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
anActionListState(
|
anActionListState(
|
||||||
|
|
@ -169,6 +180,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -20,9 +21,11 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -35,6 +38,10 @@ 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.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -90,6 +97,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName
|
||||||
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
|
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -218,6 +227,7 @@ private fun ActionListViewContent(
|
||||||
if (target.displayEmojiReactions) {
|
if (target.displayEmojiReactions) {
|
||||||
item {
|
item {
|
||||||
EmojiReactionsRow(
|
EmojiReactionsRow(
|
||||||
|
recentEmojis = target.recentEmojis,
|
||||||
highlightedEmojis = target.event.reactionsState.highlightedKeys,
|
highlightedEmojis = target.event.reactionsState.highlightedKeys,
|
||||||
onEmojiReactionClick = onEmojiReactionClick,
|
onEmojiReactionClick = onEmojiReactionClick,
|
||||||
onCustomReactionClick = onCustomReactionClick,
|
onCustomReactionClick = onCustomReactionClick,
|
||||||
|
|
@ -335,43 +345,67 @@ private fun MessageSummary(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val emojiRippleRadius = 24.dp
|
private val emojiRippleRadius = 24.dp
|
||||||
|
private val suggestedEmojis = persistentListOf("👍️", "👎️", "🔥", "❤️", "👏")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmojiReactionsRow(
|
private fun EmojiReactionsRow(
|
||||||
|
recentEmojis: ImmutableList<String>,
|
||||||
highlightedEmojis: ImmutableList<String>,
|
highlightedEmojis: ImmutableList<String>,
|
||||||
onEmojiReactionClick: (String) -> Unit,
|
onEmojiReactionClick: (String) -> Unit,
|
||||||
onCustomReactionClick: () -> Unit,
|
onCustomReactionClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
modifier = modifier.padding(end = 16.dp, top = 16.dp, bottom = 16.dp),
|
||||||
modifier = modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
// TODO use most recently used emojis here when available from the Rust SDK
|
val backgroundColor = ElementTheme.colors.bgCanvasDefault
|
||||||
val defaultEmojis = sequenceOf(
|
|
||||||
"👍️",
|
val emojis = remember(recentEmojis) {
|
||||||
"👎️",
|
(suggestedEmojis + recentEmojis.filter { it !in suggestedEmojis })
|
||||||
"🔥",
|
.take(100)
|
||||||
"❤️",
|
.toImmutableList()
|
||||||
"👏"
|
|
||||||
)
|
|
||||||
for (emoji in defaultEmojis) {
|
|
||||||
val isHighlighted = highlightedEmojis.contains(emoji)
|
|
||||||
EmojiButton(
|
|
||||||
modifier = Modifier
|
|
||||||
// Make it appear after the more useful actions for the accessibility service
|
|
||||||
.semantics {
|
|
||||||
traversalIndex = 1f
|
|
||||||
},
|
|
||||||
emoji = emoji,
|
|
||||||
isHighlighted = isHighlighted,
|
|
||||||
onClick = onEmojiReactionClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Box(
|
|
||||||
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp),
|
.weight(1f, fill = true)
|
||||||
contentAlignment = Alignment.Center,
|
.drawWithContent {
|
||||||
|
val gradientWidth = 24.dp.toPx()
|
||||||
|
val width = size.width
|
||||||
|
drawContent()
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
0.0f to Color.Transparent,
|
||||||
|
1.0f to backgroundColor,
|
||||||
|
startX = width - gradientWidth,
|
||||||
|
endX = width,
|
||||||
|
),
|
||||||
|
topLeft = Offset(width - gradientWidth, 0f),
|
||||||
|
size = Size(gradientWidth, size.height)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(emojis) { emoji ->
|
||||||
|
val isHighlighted = highlightedEmojis.contains(emoji)
|
||||||
|
EmojiButton(
|
||||||
|
modifier = Modifier
|
||||||
|
// Make it appear after the more useful actions for the accessibility service
|
||||||
|
.semantics {
|
||||||
|
traversalIndex = 1f
|
||||||
|
},
|
||||||
|
emoji = emoji,
|
||||||
|
isHighlighted = isHighlighted,
|
||||||
|
onClick = onEmojiReactionClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(end = 10.dp).requiredSize(48.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.ReactionAdd(),
|
imageVector = CompoundIcons.ReactionAdd(),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPicker
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPicker
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPickerPresenter
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPickerPresenter
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
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.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
|
|
@ -50,7 +51,13 @@ fun CustomReactionBottomSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
val presenter = remember { EmojiPickerPresenter(target.emojibaseStore) }
|
val presenter = remember {
|
||||||
|
EmojiPickerPresenter(
|
||||||
|
emojibaseStore = target.emojibaseStore,
|
||||||
|
recentEmojis = state.recentEmojis,
|
||||||
|
coroutineDispatchers = CoroutineDispatchers.Default,
|
||||||
|
)
|
||||||
|
}
|
||||||
EmojiPicker(
|
EmojiPicker(
|
||||||
onSelectEmoji = ::onEmojiSelectedDismiss,
|
onSelectEmoji = ::onEmojiSelectedDismiss,
|
||||||
state = presenter.present(),
|
state = presenter.present(),
|
||||||
|
|
|
||||||
|
|
@ -9,29 +9,39 @@ package io.element.android.features.messages.impl.timeline.components.customreac
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
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.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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 io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class CustomReactionPresenter(
|
class CustomReactionPresenter(
|
||||||
private val emojibaseProvider: EmojibaseProvider
|
private val emojibaseProvider: EmojibaseProvider,
|
||||||
|
private val getRecentEmojis: GetRecentEmojis,
|
||||||
) : Presenter<CustomReactionState> {
|
) : Presenter<CustomReactionState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): CustomReactionState {
|
override fun present(): CustomReactionState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
var recentEmojis by remember { mutableStateOf<ImmutableList<String>>(persistentListOf()) }
|
||||||
|
|
||||||
val target: MutableState<CustomReactionState.Target> = remember {
|
val target: MutableState<CustomReactionState.Target> = remember {
|
||||||
mutableStateOf(CustomReactionState.Target.None)
|
mutableStateOf(CustomReactionState.Target.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
fun handleShowCustomReactionSheet(event: TimelineItem.Event) {
|
fun handleShowCustomReactionSheet(event: TimelineItem.Event) {
|
||||||
target.value = CustomReactionState.Target.Loading(event)
|
target.value = CustomReactionState.Target.Loading(event)
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
|
recentEmojis = getRecentEmojis().getOrNull().orEmpty().toImmutableList()
|
||||||
target.value = CustomReactionState.Target.Success(
|
target.value = CustomReactionState.Target.Success(
|
||||||
event = event,
|
event = event,
|
||||||
emojibaseStore = emojibaseProvider.emojibaseStore
|
emojibaseStore = emojibaseProvider.emojibaseStore
|
||||||
|
|
@ -56,9 +66,11 @@ class CustomReactionPresenter(
|
||||||
?.mapNotNull { if (it.isHighlighted) it.key else null }
|
?.mapNotNull { if (it.isHighlighted) it.key else null }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.toImmutableSet()
|
.toImmutableSet()
|
||||||
|
|
||||||
return CustomReactionState(
|
return CustomReactionState(
|
||||||
target = target.value,
|
target = target.value,
|
||||||
selectedEmoji = selectedEmoji,
|
selectedEmoji = selectedEmoji,
|
||||||
|
recentEmojis = recentEmojis,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ package io.element.android.features.messages.impl.timeline.components.customreac
|
||||||
|
|
||||||
import io.element.android.emojibasebindings.EmojibaseStore
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
data class CustomReactionState(
|
data class CustomReactionState(
|
||||||
val target: Target,
|
val target: Target,
|
||||||
val selectedEmoji: ImmutableSet<String>,
|
val selectedEmoji: ImmutableSet<String>,
|
||||||
|
val recentEmojis: ImmutableList<String>,
|
||||||
val eventSink: (CustomReactionEvents) -> Unit,
|
val eventSink: (CustomReactionEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
sealed interface Target {
|
sealed interface Target {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2025 New Vector Ltd.
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
|
@ -30,16 +30,16 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojiItem
|
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojiItem
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.icon
|
import io.element.android.features.messages.impl.timeline.components.customreaction.icon
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.title
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.text.toSp
|
import io.element.android.libraries.designsystem.text.toSp
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -53,9 +53,7 @@ fun EmojiPicker(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val categories = state.categories
|
val pagerState = rememberPagerState(pageCount = { state.categories.size })
|
||||||
val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.entries.size })
|
|
||||||
|
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
SearchBar(
|
SearchBar(
|
||||||
modifier = Modifier.padding(bottom = 10.dp),
|
modifier = Modifier.padding(bottom = 10.dp),
|
||||||
|
|
@ -66,36 +64,31 @@ fun EmojiPicker(
|
||||||
onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) },
|
onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) },
|
||||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
placeHolderTitle = stringResource(CommonStrings.emoji_picker_search_placeholder),
|
placeHolderTitle = stringResource(CommonStrings.emoji_picker_search_placeholder),
|
||||||
) { results ->
|
) { emojis ->
|
||||||
val emojis = results
|
EmojiResults(
|
||||||
LazyVerticalGrid(
|
emojis = emojis,
|
||||||
modifier = Modifier.fillMaxSize(),
|
isEmojiSelected = { selectedEmojis.contains(it.unicode) },
|
||||||
columns = GridCells.Adaptive(minSize = 48.dp),
|
onSelectEmoji = onSelectEmoji,
|
||||||
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
)
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
|
||||||
) {
|
|
||||||
items(emojis, key = { it.unicode }) { item ->
|
|
||||||
SelectableEmojiItem(
|
|
||||||
item = item,
|
|
||||||
selectedEmojis = selectedEmojis,
|
|
||||||
onSelectEmoji = onSelectEmoji,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
SecondaryTabRow(
|
SecondaryTabRow(
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
) {
|
) {
|
||||||
EmojibaseCategory.entries.forEachIndexed { index, category ->
|
state.categories.forEachIndexed { index, category ->
|
||||||
Tab(
|
Tab(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
when (category.icon) {
|
||||||
imageVector = category.icon,
|
is IconSource.Resource -> Icon(
|
||||||
contentDescription = stringResource(id = category.title)
|
resourceId = category.icon.id,
|
||||||
)
|
contentDescription = stringResource(id = category.titleId)
|
||||||
|
)
|
||||||
|
is IconSource.Vector -> Icon(
|
||||||
|
imageVector = category.icon.vector,
|
||||||
|
contentDescription = stringResource(id = category.titleId)
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
selected = pagerState.currentPage == index,
|
selected = pagerState.currentPage == index,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -109,41 +102,40 @@ fun EmojiPicker(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) { index ->
|
) { index ->
|
||||||
val category = EmojibaseCategory.entries[index]
|
val emojis = state.categories[index].emojis
|
||||||
val emojis = categories[category] ?: listOf()
|
EmojiResults(
|
||||||
LazyVerticalGrid(
|
emojis = emojis,
|
||||||
modifier = Modifier.fillMaxSize(),
|
isEmojiSelected = { selectedEmojis.contains(it.unicode) },
|
||||||
columns = GridCells.Adaptive(minSize = 48.dp),
|
onSelectEmoji = onSelectEmoji,
|
||||||
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
)
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
|
||||||
) {
|
|
||||||
items(emojis, key = { it.unicode }) { item ->
|
|
||||||
SelectableEmojiItem(
|
|
||||||
item = item,
|
|
||||||
selectedEmojis = selectedEmojis,
|
|
||||||
onSelectEmoji = onSelectEmoji,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectableEmojiItem(
|
private fun EmojiResults(
|
||||||
item: Emoji,
|
emojis: ImmutableList<Emoji>,
|
||||||
selectedEmojis: ImmutableSet<String>,
|
isEmojiSelected: (Emoji) -> Boolean,
|
||||||
onSelectEmoji: (Emoji) -> Unit,
|
onSelectEmoji: (Emoji) -> Unit,
|
||||||
) {
|
) {
|
||||||
EmojiItem(
|
LazyVerticalGrid(
|
||||||
modifier = Modifier.aspectRatio(1f),
|
modifier = Modifier.fillMaxSize(),
|
||||||
item = item,
|
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||||
isSelected = selectedEmojis.contains(item.unicode),
|
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
||||||
onSelectEmoji = onSelectEmoji,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
emojiSize = 32.dp.toSp(),
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
)
|
) {
|
||||||
|
items(emojis, key = { it.unicode }) { item ->
|
||||||
|
EmojiItem(
|
||||||
|
modifier = Modifier.aspectRatio(1f),
|
||||||
|
item = item,
|
||||||
|
isSelected = isEmojiSelected(item),
|
||||||
|
onSelectEmoji = onSelectEmoji,
|
||||||
|
emojiSize = 32.dp.toSp(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|
|
||||||
|
|
@ -14,26 +14,57 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.emojibasebindings.EmojibaseStore
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
import io.element.android.features.messages.impl.R
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.icon
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.title
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class EmojiPickerPresenter(
|
class EmojiPickerPresenter(
|
||||||
private val emojibaseStore: EmojibaseStore,
|
private val emojibaseStore: EmojibaseStore,
|
||||||
|
private val recentEmojis: ImmutableList<String>,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : Presenter<EmojiPickerState> {
|
) : Presenter<EmojiPickerState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): EmojiPickerState {
|
override fun present(): EmojiPickerState {
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(SearchBarResultState.Initial()) }
|
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(SearchBarResultState.Initial()) }
|
||||||
val categories = remember { emojibaseStore.categories }
|
|
||||||
|
val recentEmojiIcon = CompoundIcons.History()
|
||||||
|
val categories = remember {
|
||||||
|
val providedCategories = emojibaseStore.categories.map { (category, emojis) ->
|
||||||
|
EmojiCategory(
|
||||||
|
titleId = category.title,
|
||||||
|
icon = IconSource.Vector(category.icon),
|
||||||
|
emojis = emojis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (recentEmojis.isNotEmpty()) {
|
||||||
|
val recentEmojis = recentEmojis.mapNotNull { recentEmoji ->
|
||||||
|
emojibaseStore.allEmojis.find { it.unicode == recentEmoji }
|
||||||
|
}.toImmutableList()
|
||||||
|
val recentCategory =
|
||||||
|
EmojiCategory(
|
||||||
|
titleId = R.string.emoji_picker_category_recent,
|
||||||
|
icon = IconSource.Vector(recentEmojiIcon),
|
||||||
|
emojis = recentEmojis
|
||||||
|
)
|
||||||
|
(listOf(recentCategory) + providedCategories).toImmutableList()
|
||||||
|
} else {
|
||||||
|
providedCategories.toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
emojiResults = if (searchQuery.isEmpty()) {
|
emojiResults = if (searchQuery.isEmpty()) {
|
||||||
|
|
@ -43,7 +74,7 @@ class EmojiPickerPresenter(
|
||||||
delay(100.milliseconds)
|
delay(100.milliseconds)
|
||||||
|
|
||||||
val lowercaseQuery = searchQuery.lowercase()
|
val lowercaseQuery = searchQuery.lowercase()
|
||||||
val results = withContext(Dispatchers.Default) {
|
val results = withContext(coroutineDispatchers.computation) {
|
||||||
emojibaseStore.allEmojis
|
emojibaseStore.allEmojis
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { emoji ->
|
.filter { emoji ->
|
||||||
|
|
@ -71,6 +102,7 @@ class EmojiPickerPresenter(
|
||||||
|
|
||||||
return EmojiPickerState(
|
return EmojiPickerState(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
allEmojis = emojibaseStore.allEmojis,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
searchResults = emojiResults,
|
searchResults = emojiResults,
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,26 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
|
||||||
|
|
||||||
data class EmojiPickerState(
|
data class EmojiPickerState(
|
||||||
val categories: ImmutableMap<EmojibaseCategory, ImmutableList<Emoji>>,
|
val categories: ImmutableList<EmojiCategory>,
|
||||||
|
val allEmojis: ImmutableList<Emoji>,
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val searchResults: SearchBarResultState<ImmutableList<Emoji>>,
|
val searchResults: SearchBarResultState<ImmutableList<Emoji>>,
|
||||||
val eventSink: (EmojiPickerEvents) -> Unit,
|
val eventSink: (EmojiPickerEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a category of emojis with a title id, icon, and the list of associated emojis.
|
||||||
|
*/
|
||||||
|
data class EmojiCategory(
|
||||||
|
@StringRes val titleId: Int,
|
||||||
|
val icon: IconSource,
|
||||||
|
val emojis: ImmutableList<Emoji>,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,15 @@ package io.element.android.features.messages.impl.timeline.components.customreac
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
|
import io.element.android.features.messages.impl.R
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.icon
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.title
|
||||||
|
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class EmojiPickerStateProvider : PreviewParameterProvider<EmojiPickerState> {
|
class EmojiPickerStateProvider : PreviewParameterProvider<EmojiPickerState> {
|
||||||
override val values: Sequence<EmojiPickerState>
|
override val values: Sequence<EmojiPickerState>
|
||||||
|
|
@ -25,57 +29,52 @@ class EmojiPickerStateProvider : PreviewParameterProvider<EmojiPickerState> {
|
||||||
anEmojiPickerState(
|
anEmojiPickerState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "smile",
|
searchQuery = "smile",
|
||||||
searchResults = SearchBarResultState.Results(
|
searchResults = SearchBarResultState.Results(emojiList())
|
||||||
persistentListOf(
|
|
||||||
Emoji(
|
|
||||||
"0x00",
|
|
||||||
"grinning face",
|
|
||||||
persistentListOf("grinning"),
|
|
||||||
persistentListOf("smile, grin"),
|
|
||||||
"😀",
|
|
||||||
null
|
|
||||||
),
|
|
||||||
Emoji(
|
|
||||||
"0x01",
|
|
||||||
"crying face",
|
|
||||||
persistentListOf("crying"),
|
|
||||||
persistentListOf("smile, crying"),
|
|
||||||
"\uD83E\uDD72",
|
|
||||||
null
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun recentEmojisCategory() = EmojiCategory(
|
||||||
|
titleId = R.string.emoji_picker_category_recent,
|
||||||
|
icon = IconSource.Resource(CompoundDrawables.ic_compound_history),
|
||||||
|
emojis = emojiList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun emojiList(): ImmutableList<Emoji> = persistentListOf(
|
||||||
|
Emoji(
|
||||||
|
"0x00",
|
||||||
|
"grinning face",
|
||||||
|
persistentListOf("grinning"),
|
||||||
|
persistentListOf("smile, grin"),
|
||||||
|
"😀",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
Emoji(
|
||||||
|
"0x01",
|
||||||
|
"crying face",
|
||||||
|
persistentListOf("crying"),
|
||||||
|
persistentListOf("smile, crying"),
|
||||||
|
"\uD83E\uDD72",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
internal fun anEmojiPickerState(
|
internal fun anEmojiPickerState(
|
||||||
categories: ImmutableMap<EmojibaseCategory, ImmutableList<Emoji>> = EmojibaseCategory.entries.associateWith {
|
categories: ImmutableList<EmojiCategory> = (listOf(recentEmojisCategory()) + EmojibaseCategory.entries.map {
|
||||||
persistentListOf(
|
EmojiCategory(
|
||||||
Emoji(
|
titleId = it.title,
|
||||||
"0x00",
|
icon = IconSource.Vector(it.icon),
|
||||||
"grinning face",
|
emojis = emojiList(),
|
||||||
persistentListOf("grinning"),
|
|
||||||
persistentListOf("smile, grin"),
|
|
||||||
"😀",
|
|
||||||
null
|
|
||||||
),
|
|
||||||
Emoji(
|
|
||||||
"0x01",
|
|
||||||
"crying face",
|
|
||||||
persistentListOf("crying"),
|
|
||||||
persistentListOf("smile, crying"),
|
|
||||||
"\uD83E\uDD72",
|
|
||||||
null
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}.toImmutableMap(),
|
}).toImmutableList(),
|
||||||
|
allEmojis: ImmutableList<Emoji> = categories.flatMap { it.emojis }.toImmutableList(),
|
||||||
searchQuery: String = "",
|
searchQuery: String = "",
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
searchResults: SearchBarResultState<ImmutableList<Emoji>> = SearchBarResultState.Initial(),
|
searchResults: SearchBarResultState<ImmutableList<Emoji>> = SearchBarResultState.Initial(),
|
||||||
eventSink: (EmojiPickerEvents) -> Unit = {},
|
eventSink: (EmojiPickerEvents) -> Unit = {},
|
||||||
) = EmojiPickerState(
|
) = EmojiPickerState(
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
allEmojis = allEmojis,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
searchResults = searchResults,
|
searchResults = searchResults,
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import io.element.android.libraries.matrix.api.core.toThreadId
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.recentemojis.AddRecentEmoji
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
|
@ -75,6 +76,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
|
@ -1269,6 +1271,7 @@ class MessagesPresenterTest {
|
||||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
|
||||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||||
|
addRecentEmoji: AddRecentEmoji = AddRecentEmoji(FakeMatrixClient(), testCoroutineDispatchers()),
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
|
|
@ -1297,6 +1300,7 @@ class MessagesPresenterTest {
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
addRecentEmoji = addRecentEmoji,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,7 @@ class MessagesViewTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = persistentListOf(TimelineItemAction.Edit),
|
actions = persistentListOf(TimelineItemAction.Edit),
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -462,6 +463,7 @@ class MessagesViewTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(TimelineItemAction.Edit),
|
actions = persistentListOf(TimelineItemAction.Edit),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
customReactionState = aCustomReactionState(
|
customReactionState = aCustomReactionState(
|
||||||
|
|
@ -491,6 +493,7 @@ class MessagesViewTest {
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
verifiedUserSendFailure = aChangedIdentitySendFailure(),
|
||||||
actions = persistentListOf(),
|
actions = persistentListOf(),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
timelineState = aTimelineState(eventSink = eventsRecorder)
|
timelineState = aTimelineState(eventSink = eventsRecorder)
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ class ActionListPresenterTest {
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -135,7 +136,8 @@ class ActionListPresenterTest {
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -182,7 +184,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -228,7 +231,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -274,7 +278,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -322,7 +327,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -370,7 +376,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -417,7 +424,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -463,7 +471,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -509,7 +518,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -552,7 +562,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -599,7 +610,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -650,7 +662,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.RemoveCaption,
|
TimelineItemAction.RemoveCaption,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -699,7 +712,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyCaption,
|
TimelineItemAction.CopyCaption,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -739,7 +753,8 @@ class ActionListPresenterTest {
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -812,7 +827,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -858,7 +874,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -911,7 +928,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.Clear)
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
|
@ -1004,7 +1022,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.CopyText,
|
TimelineItemAction.CopyText,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1048,7 +1067,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1091,7 +1111,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1133,7 +1154,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1178,7 +1200,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1215,7 +1238,8 @@ class ActionListPresenterTest {
|
||||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.ViewSource
|
TimelineItemAction.ViewSource
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1292,7 +1316,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1345,7 +1370,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1399,7 +1425,8 @@ class ActionListPresenterTest {
|
||||||
TimelineItemAction.CopyLink,
|
TimelineItemAction.CopyLink,
|
||||||
TimelineItemAction.Pin,
|
TimelineItemAction.Pin,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1450,7 +1477,8 @@ class ActionListPresenterTest {
|
||||||
// Can't reply in thread for local events
|
// Can't reply in thread for local events
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
),
|
||||||
|
recentEmojis = persistentListOf(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1472,5 +1500,6 @@ private fun createActionListPresenter(
|
||||||
dateFormatter = FakeDateFormatter(),
|
dateFormatter = FakeDateFormatter(),
|
||||||
timelineMode = timelineMode,
|
timelineMode = timelineMode,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
getRecentEmojis = { Result.success(persistentListOf()) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ class CustomReactionPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
private val presenter = CustomReactionPresenter(
|
||||||
|
emojibaseProvider = FakeEmojibaseProvider(),
|
||||||
|
getRecentEmojis = { Result.success(emptyList()) },
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle selecting and de-selecting an event`() = runTest {
|
fun `present - handle selecting and de-selecting an event`() = runTest {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.customreaction.picker
|
||||||
|
|
||||||
|
import androidx.compose.runtime.InternalComposeApi
|
||||||
|
import androidx.compose.runtime.currentComposer
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.emojibasebindings.Emoji
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
|
import io.element.android.emojibasebindings.EmojibaseStore
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class EmojiPickerPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `UpdateSearchQuery loads new results`() = runTest {
|
||||||
|
testPresenter {
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.searchQuery).isEmpty()
|
||||||
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||||
|
|
||||||
|
initialState.eventSink(EmojiPickerEvents.UpdateSearchQuery("smile"))
|
||||||
|
assertThat(awaitItem().searchQuery).isEqualTo("smile")
|
||||||
|
|
||||||
|
val stateWithResults = awaitItem()
|
||||||
|
assertThat(stateWithResults.searchQuery).isEqualTo("smile")
|
||||||
|
assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ToggleSearchActive toggles the search state`() = runTest {
|
||||||
|
testPresenter {
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
|
|
||||||
|
initialState.eventSink(EmojiPickerEvents.ToggleSearchActive(true))
|
||||||
|
assertThat(awaitItem().isSearchActive).isTrue()
|
||||||
|
|
||||||
|
initialState.eventSink(EmojiPickerEvents.ToggleSearchActive(false))
|
||||||
|
assertThat(awaitItem().isSearchActive).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `recent emojis are automatically added to the categories if present`() = runTest {
|
||||||
|
val providedCategories = persistentListOf(emojiCategory(EmojibaseCategory.Activity))
|
||||||
|
val presenter = createPresenter(
|
||||||
|
categories = providedCategories,
|
||||||
|
recentEmojis = persistentListOf("😊"),
|
||||||
|
)
|
||||||
|
testPresenter(presenter) {
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(providedCategories.size).isNotEqualTo(initialState.categories.size)
|
||||||
|
assertThat(initialState.categories.size).isEqualTo(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createPresenter(
|
||||||
|
categories: ImmutableList<Pair<EmojibaseCategory, ImmutableList<Emoji>>> = persistentListOf(emojiCategory()),
|
||||||
|
recentEmojis: ImmutableList<String> = persistentListOf(),
|
||||||
|
) = EmojiPickerPresenter(
|
||||||
|
emojibaseStore = EmojibaseStore(categories.toMap().toPersistentMap()),
|
||||||
|
recentEmojis = recentEmojis,
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun emojiCategory(
|
||||||
|
category: EmojibaseCategory = EmojibaseCategory.Activity,
|
||||||
|
emojis: ImmutableList<Emoji> = persistentListOf(
|
||||||
|
Emoji("1F3C3", "Smile", persistentListOf("smile"), persistentListOf("smile"), "😊", skins = null)
|
||||||
|
)
|
||||||
|
) = category to emojis
|
||||||
|
|
||||||
|
@OptIn(InternalComposeApi::class)
|
||||||
|
private suspend fun TestScope.testPresenter(
|
||||||
|
presenter: EmojiPickerPresenter = createPresenter(),
|
||||||
|
testBlock: suspend TurbineTestContext<EmojiPickerState>.() -> Unit,
|
||||||
|
) {
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
// These are needed to load the history icon in the presenter
|
||||||
|
currentComposer.startProviders(arrayOf(
|
||||||
|
LocalContext provides InstrumentationRegistry.getInstrumentation().context,
|
||||||
|
LocalConfiguration provides InstrumentationRegistry.getInstrumentation().context.resources.configuration,
|
||||||
|
))
|
||||||
|
val state = presenter.present()
|
||||||
|
currentComposer.endProviders()
|
||||||
|
state
|
||||||
|
}.test {
|
||||||
|
testBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,18 @@
|
||||||
package io.element.android.libraries.core.coroutine
|
package io.element.android.libraries.core.coroutine
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
data class CoroutineDispatchers(
|
data class CoroutineDispatchers(
|
||||||
val io: CoroutineDispatcher,
|
val io: CoroutineDispatcher,
|
||||||
val computation: CoroutineDispatcher,
|
val computation: CoroutineDispatcher,
|
||||||
val main: CoroutineDispatcher,
|
val main: CoroutineDispatcher,
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
val Default = CoroutineDispatchers(
|
||||||
|
io = Dispatchers.IO,
|
||||||
|
computation = Dispatchers.Default,
|
||||||
|
main = Dispatchers.Main,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,16 @@ interface MatrixClient {
|
||||||
* Returns the maximum file upload size allowed by the Matrix server.
|
* Returns the maximum file upload size allowed by the Matrix server.
|
||||||
*/
|
*/
|
||||||
suspend fun getMaxFileUploadSize(): Result<Long>
|
suspend fun getMaxFileUploadSize(): Result<Long>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of shared recent emoji reactions for this account.
|
||||||
|
*/
|
||||||
|
suspend fun getRecentEmojis(): Result<List<String>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an emoji to the list of recent emoji reactions for this account.
|
||||||
|
*/
|
||||||
|
suspend fun addRecentEmoji(emoji: String): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.recentemojis
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class AddRecentEmoji(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
|
||||||
|
client.addRecentEmoji(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.recentemojis
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
fun interface GetRecentEmojis {
|
||||||
|
suspend operator fun invoke(): Result<List<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
@Inject
|
||||||
|
class DefaultGetRecentEmojis(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
) : GetRecentEmojis {
|
||||||
|
override suspend operator fun invoke(): Result<List<String>> = withContext(dispatchers.io) {
|
||||||
|
client.getRecentEmojis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -708,6 +708,18 @@ class RustMatrixClient(
|
||||||
runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() }
|
runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun addRecentEmoji(emoji: String): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
innerClient.addRecentEmoji(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRecentEmojis(): Result<List<String>> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
innerClient.getRecentEmojis().map { it.emoji }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun File.getCacheSize(
|
private suspend fun File.getCacheSize(
|
||||||
includeCryptoDb: Boolean = false,
|
includeCryptoDb: Boolean = false,
|
||||||
): Long = withContext(sessionDispatcher) {
|
): Long = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ class FakeMatrixClient(
|
||||||
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
|
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
|
||||||
private val getMaxUploadSizeResult: () -> Result<Long> = { lambdaError() },
|
private val getMaxUploadSizeResult: () -> Result<Long> = { lambdaError() },
|
||||||
private val getJoinedRoomIdsResult: () -> Result<Set<RoomId>> = { Result.success(emptySet()) },
|
private val getJoinedRoomIdsResult: () -> Result<Set<RoomId>> = { Result.success(emptySet()) },
|
||||||
|
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
||||||
|
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
var setDisplayNameCalled: Boolean = false
|
var setDisplayNameCalled: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
@ -349,4 +351,12 @@ class FakeMatrixClient(
|
||||||
override suspend fun getMaxFileUploadSize(): Result<Long> {
|
override suspend fun getMaxFileUploadSize(): Result<Long> {
|
||||||
return getMaxUploadSizeResult()
|
return getMaxUploadSizeResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun addRecentEmoji(emoji: String): Result<Unit> {
|
||||||
|
return addRecentEmojiLambda(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRecentEmojis(): Result<List<String>> {
|
||||||
|
return getRecentEmojisLambda()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e85eb3d968eca031af366d557a1afff7aa6f427f9c5761c4568b807562248fb9
|
oid sha256:cefbe57afc5b78b598d004fa9f6cc9d42e00127ffb7900a2f5346eeee0b1531c
|
||||||
size 48880
|
size 49183
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:29276b02a32bfb0eacc9fc4853711f0d43b4e9e4dc927d7066ed0d0b1ee12680
|
oid sha256:03fbd054c197e783d2079cf4b3953f1a4f15dc7c38889dc826cb5de3b7c1f4d0
|
||||||
size 50357
|
size 50597
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:494e17d6cb91effd9351b9e331ffb4d8c4da927b28bfa8e740fdb7d865bc7fe3
|
oid sha256:34c5fd10902041682c6ab2c3022ea44977035f7de6ea9844d94876b921327ced
|
||||||
size 43609
|
size 43960
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3ec6dd722d802baa463784587d4d8844edb9a38e623fffe2fc347970e437cac6
|
oid sha256:531e376ef91fdb4f535a951222f5e25ecacc7ded6d6693e30551ec68a16dcd33
|
||||||
size 46583
|
size 46911
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:204de297913cef7d13569bb8b51135c6a296ea6eec058dc1ce31ebd547137887
|
oid sha256:c18f38af96a7f2d0e6dd30827d9ef5cae25d7c864df81ba763044f782f3bb7c7
|
||||||
size 44947
|
size 45233
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:66bb9985aee2e4988f3beaf58e7979b0812876570d5905a31406069546aaf0e9
|
oid sha256:96472d116b4c961b4dc44e7a5e8fcf9a7751ed7662e8c4a169722ff53a53ceed
|
||||||
size 42010
|
size 42380
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7729240a99b1fbf3caba5c480806f88f441d4c17f796808d6f50979ae76ecbe9
|
oid sha256:055df03c64585976d1453c0c3b5b0460cefaf1aaa93a47d17b98cc80fba11f29
|
||||||
size 45125
|
size 45525
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0f6bec6647ac0678ef3420e222ea2373c4d618f7eee32347548c0fe35a7620b2
|
oid sha256:21a5e088364ec86448df788e0d54c27dcba3c6f696a86e7f97289da0792ede75
|
||||||
size 43261
|
size 43598
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6a97331fddcbc4c89743e15bd79cc6e753b59be0f33f41de2d8cc184b3e1f3ec
|
oid sha256:e356e49f62609b0c96b294a51af0c3dba861906d0cf18492460b2d199dcd1003
|
||||||
size 45084
|
size 45368
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:48ca8baddefc71d98aa4060d730f7785e654ed078f5c9aa21459df5b730b3acc
|
oid sha256:a883e703a5f8c9a794ca274e52b493c490815f39c84c7c7b9c9cef348ecfb766
|
||||||
size 47969
|
size 48368
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c0d19fe576a9cb4895ccae8c3f862239220b9542aa0c4c102cf5a99a0184ad94
|
oid sha256:c44806a243336438401a21e265c8d513fff7e4f2cbf68a0c745f5918c89d639a
|
||||||
size 49392
|
size 49828
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d81fdfb3340f8d8d63a0529eb2f4d035ebd4789aed0c98272bdbcb9c4340c7c9
|
oid sha256:9725a74d334baddaf3946675b871abcc24f20e3518fe572f6eaffdcd5e0bf98e
|
||||||
size 42544
|
size 43028
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e882786b51e7692f52f1938cf1a29581ec299e4bcb7a3253ba69009140a29233
|
oid sha256:78379d56dda45b77f6bcfbf32e12e78f72e88c760d7a0911bb9e8c9f244c39b8
|
||||||
size 45837
|
size 46151
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fbf6bda60c2f68bfb41405919d669b7aec8db408d49593ab0520b5b9afec8627
|
oid sha256:b8213b5f1c9620cc8edaaa9971906f09c8b466d42ce253a9f52d30419af2e732
|
||||||
size 44217
|
size 44722
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d68f839ba6d6aa3d9bd9fde6d71a68d95130011aebaf43c87eaf8ff826f48a4d
|
oid sha256:5c94f65df581c57f83346e9c8d697a50eb95cffa5ac3fc4198017a41b9d72537
|
||||||
size 40689
|
size 41215
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3c139a1a328cef4752219fd11df4c79b19c47501df26278811cb7775269988c4
|
oid sha256:9fc2b4034704979785701cb42fe2b967256a6151ac90934cecd5b8b1922f780b
|
||||||
size 44501
|
size 44987
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f17461d5b74d2c1c493e58236793978a5e96bf5bf15b2ab0179dd0b39b9e626a
|
oid sha256:d14b2d18fb60fcabb3e657001c97d95f55b82e87cf8efa34c8a5fb2ab237a359
|
||||||
size 41948
|
size 42385
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f5a920e128caf9d5609d9030e5d37cf951011bd51009c988d84873c993fff95d
|
oid sha256:5d081025bedebd0993968f3522a6e4266ecd20526284c37794a7180305d07c7c
|
||||||
size 44331
|
size 44806
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:af699468bfa61662538fd3b187ebb1490f6a8e692c40bd13fc44beeb26744c93
|
oid sha256:65a59efc8cfd19f4dddf0ed9d39c4cab634ebc720c2da7737f382dd2e325ad37
|
||||||
size 21790
|
size 22517
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d79e3b8f699dab27d180942bc8f68f0193148e0e04602c03ec7592e4b73278f0
|
oid sha256:18b5a57b061028e9b0d265f725e5c2b08439589c778c49f054822ab3baab68bb
|
||||||
size 20876
|
size 21551
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue