Skip to content

Commit a8c4d5d

Browse files
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 <[email protected]>
1 parent c412d6c commit a8c4d5d

File tree

45 files changed

+572
-208
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+572
-208
lines changed

app/src/main/kotlin/io/element/android/x/di/AppModule.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import io.element.android.x.BuildConfig
3333
import io.element.android.x.R
3434
import kotlinx.coroutines.CoroutineName
3535
import kotlinx.coroutines.CoroutineScope
36-
import kotlinx.coroutines.Dispatchers
3736
import kotlinx.coroutines.MainScope
3837
import kotlinx.coroutines.plus
3938
import java.io.File
@@ -107,11 +106,7 @@ object AppModule {
107106
@Provides
108107
@SingleIn(AppScope::class)
109108
fun providesCoroutineDispatchers(): CoroutineDispatchers {
110-
return CoroutineDispatchers(
111-
io = Dispatchers.IO,
112-
computation = Dispatchers.Default,
113-
main = Dispatchers.Main,
114-
)
109+
return CoroutineDispatchers.Default
115110
}
116111

117112
@Provides

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
5757
import io.element.android.libraries.architecture.AsyncData
5858
import io.element.android.libraries.architecture.Presenter
5959
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
60+
import io.element.android.libraries.core.extensions.flatMap
6061
import io.element.android.libraries.core.extensions.runCatchingExceptions
6162
import io.element.android.libraries.core.meta.BuildMeta
6263
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -70,6 +71,7 @@ import io.element.android.libraries.matrix.api.core.toThreadId
7071
import io.element.android.libraries.matrix.api.encryption.EncryptionService
7172
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
7273
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
74+
import io.element.android.libraries.matrix.api.recentemojis.AddRecentEmoji
7375
import io.element.android.libraries.matrix.api.room.JoinedRoom
7476
import io.element.android.libraries.matrix.api.room.MessageEventType
7577
import io.element.android.libraries.matrix.api.room.RoomInfo
@@ -121,6 +123,7 @@ class MessagesPresenter(
121123
private val analyticsService: AnalyticsService,
122124
private val encryptionService: EncryptionService,
123125
private val featureFlagService: FeatureFlagService,
126+
private val addRecentEmoji: AddRecentEmoji,
124127
) : Presenter<MessagesState> {
125128
@AssistedFactory
126129
interface Factory {
@@ -398,6 +401,7 @@ class MessagesPresenter(
398401
) = launch(dispatchers.io) {
399402
timelineController.invokeOnCurrentTimeline {
400403
toggleReaction(emoji, eventOrTransactionId)
404+
.flatMap { added -> if (added) addRecentEmoji(emoji) else Result.success(Unit) }
401405
.onFailure { Timber.e(it) }
402406
}
403407
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
4848
import io.element.android.libraries.matrix.api.timeline.Timeline
4949
import io.element.android.libraries.textcomposer.model.MessageComposerMode
5050
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
51+
import kotlinx.collections.immutable.ImmutableList
5152
import kotlinx.collections.immutable.persistentListOf
5253
import kotlinx.collections.immutable.persistentSetOf
5354

@@ -178,9 +179,11 @@ fun aReactionSummaryState(
178179

179180
fun aCustomReactionState(
180181
target: CustomReactionState.Target = CustomReactionState.Target.None,
182+
recentEmojis: ImmutableList<String> = persistentListOf(),
181183
eventSink: (CustomReactionEvents) -> Unit = {},
182184
) = CustomReactionState(
183185
target = target,
186+
recentEmojis = recentEmojis,
184187
selectedEmoji = persistentSetOf(),
185188
eventSink = eventSink,
186189
)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import io.element.android.libraries.di.RoomScope
4343
import io.element.android.libraries.featureflag.api.FeatureFlagService
4444
import io.element.android.libraries.featureflag.api.FeatureFlags
4545
import io.element.android.libraries.matrix.api.core.EventId
46+
import io.element.android.libraries.matrix.api.recentemojis.GetRecentEmojis
4647
import io.element.android.libraries.matrix.api.room.BaseRoom
4748
import io.element.android.libraries.matrix.api.timeline.Timeline
4849
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@@ -73,6 +74,7 @@ class DefaultActionListPresenter(
7374
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
7475
private val dateFormatter: DateFormatter,
7576
private val featureFlagService: FeatureFlagService,
77+
private val getRecentEmojis: GetRecentEmojis,
7678
) : ActionListPresenter {
7779
@AssistedFactory
7880
@ContributesBinding(RoomScope::class)
@@ -153,14 +155,15 @@ class DefaultActionListPresenter(
153155
),
154156
displayEmojiReactions = displayEmojiReactions,
155157
verifiedUserSendFailure = verifiedUserSendFailure,
156-
actions = actions.toImmutableList()
158+
actions = actions.toImmutableList(),
159+
recentEmojis = getRecentEmojis().getOrNull()?.toImmutableList() ?: persistentListOf()
157160
)
158161
} else {
159162
target.value = ActionListState.Target.None
160163
}
161164
}
162165

163-
private suspend fun buildActions(
166+
private fun buildActions(
164167
timelineItem: TimelineItem.Event,
165168
usersEventPermissions: UserEventPermissions,
166169
isDeveloperModeEnabled: Boolean,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ data class ActionListState(
2626
val event: TimelineItem.Event,
2727
val sentTimeFull: String,
2828
val displayEmojiReactions: Boolean,
29+
val recentEmojis: ImmutableList<String>,
2930
val verifiedUserSendFailure: VerifiedUserSendFailure,
3031
val actions: ImmutableList<TimelineItemAction>,
3132
) : Target

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
2323
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
2424
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
2525
import kotlinx.collections.immutable.ImmutableList
26+
import kotlinx.collections.immutable.persistentListOf
2627
import kotlinx.collections.immutable.toPersistentList
2728

2829
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
@@ -41,6 +42,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
4142
displayEmojiReactions = true,
4243
verifiedUserSendFailure = VerifiedUserSendFailure.None,
4344
actions = aTimelineItemActionList(),
45+
recentEmojis = persistentListOf(),
4446
)
4547
),
4648
anActionListState(
@@ -56,6 +58,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
5658
actions = aTimelineItemActionList(
5759
copyAction = TimelineItemAction.CopyCaption,
5860
),
61+
recentEmojis = persistentListOf(),
5962
)
6063
),
6164
anActionListState(
@@ -70,6 +73,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
7073
actions = aTimelineItemActionList(
7174
copyAction = TimelineItemAction.CopyCaption,
7275
),
76+
recentEmojis = persistentListOf(),
7377
)
7478
),
7579
anActionListState(
@@ -84,6 +88,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
8488
actions = aTimelineItemActionList(
8589
copyAction = null,
8690
),
91+
recentEmojis = persistentListOf(),
8792
)
8893
),
8994
anActionListState(
@@ -98,6 +103,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
98103
actions = aTimelineItemActionList(
99104
copyAction = TimelineItemAction.CopyCaption,
100105
),
106+
recentEmojis = persistentListOf(),
101107
)
102108
),
103109
anActionListState(
@@ -112,6 +118,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
112118
actions = aTimelineItemActionList(
113119
copyAction = null,
114120
),
121+
recentEmojis = persistentListOf(),
115122
)
116123
),
117124
anActionListState(
@@ -124,6 +131,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
124131
displayEmojiReactions = true,
125132
verifiedUserSendFailure = VerifiedUserSendFailure.None,
126133
actions = aTimelineItemActionList(),
134+
recentEmojis = persistentListOf(),
127135
)
128136
),
129137
anActionListState(
@@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
136144
displayEmojiReactions = false,
137145
verifiedUserSendFailure = VerifiedUserSendFailure.None,
138146
actions = aTimelineItemActionList(),
147+
recentEmojis = persistentListOf(),
139148
),
140149
),
141150
anActionListState(
@@ -148,6 +157,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
148157
displayEmojiReactions = false,
149158
verifiedUserSendFailure = VerifiedUserSendFailure.None,
150159
actions = aTimelineItemPollActionList(),
160+
recentEmojis = persistentListOf(),
151161
),
152162
),
153163
anActionListState(
@@ -160,6 +170,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
160170
displayEmojiReactions = true,
161171
verifiedUserSendFailure = VerifiedUserSendFailure.None,
162172
actions = aTimelineItemActionList(),
173+
recentEmojis = persistentListOf(),
163174
)
164175
),
165176
anActionListState(
@@ -169,6 +180,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
169180
displayEmojiReactions = true,
170181
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
171182
actions = aTimelineItemActionList(),
183+
recentEmojis = persistentListOf(),
172184
)
173185
),
174186
)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
1313
import androidx.compose.foundation.layout.Arrangement
1414
import androidx.compose.foundation.layout.Box
1515
import androidx.compose.foundation.layout.Column
16+
import androidx.compose.foundation.layout.PaddingValues
1617
import androidx.compose.foundation.layout.Row
1718
import androidx.compose.foundation.layout.Spacer
1819
import androidx.compose.foundation.layout.fillMaxWidth
1920
import androidx.compose.foundation.layout.height
2021
import androidx.compose.foundation.layout.imePadding
2122
import androidx.compose.foundation.layout.navigationBarsPadding
2223
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.foundation.layout.requiredSize
2325
import androidx.compose.foundation.layout.size
2426
import androidx.compose.foundation.layout.width
2527
import androidx.compose.foundation.lazy.LazyColumn
28+
import androidx.compose.foundation.lazy.LazyRow
2629
import androidx.compose.foundation.lazy.items
2730
import androidx.compose.foundation.shape.CircleShape
2831
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -35,6 +38,10 @@ import androidx.compose.runtime.remember
3538
import androidx.compose.runtime.rememberCoroutineScope
3639
import androidx.compose.ui.Alignment
3740
import androidx.compose.ui.Modifier
41+
import androidx.compose.ui.draw.drawWithContent
42+
import androidx.compose.ui.geometry.Offset
43+
import androidx.compose.ui.geometry.Size
44+
import androidx.compose.ui.graphics.Brush
3845
import androidx.compose.ui.graphics.Color
3946
import androidx.compose.ui.platform.LocalContext
4047
import androidx.compose.ui.res.stringResource
@@ -90,6 +97,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName
9097
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
9198
import io.element.android.libraries.ui.strings.CommonStrings
9299
import kotlinx.collections.immutable.ImmutableList
100+
import kotlinx.collections.immutable.persistentListOf
101+
import kotlinx.collections.immutable.toImmutableList
93102

94103
@OptIn(ExperimentalMaterial3Api::class)
95104
@Composable
@@ -218,6 +227,7 @@ private fun ActionListViewContent(
218227
if (target.displayEmojiReactions) {
219228
item {
220229
EmojiReactionsRow(
230+
recentEmojis = target.recentEmojis,
221231
highlightedEmojis = target.event.reactionsState.highlightedKeys,
222232
onEmojiReactionClick = onEmojiReactionClick,
223233
onCustomReactionClick = onCustomReactionClick,
@@ -335,43 +345,67 @@ private fun MessageSummary(
335345
}
336346

337347
private val emojiRippleRadius = 24.dp
348+
private val suggestedEmojis = persistentListOf("👍️", "👎️", "🔥", "❤️", "👏")
338349

339350
@Composable
340351
private fun EmojiReactionsRow(
352+
recentEmojis: ImmutableList<String>,
341353
highlightedEmojis: ImmutableList<String>,
342354
onEmojiReactionClick: (String) -> Unit,
343355
onCustomReactionClick: () -> Unit,
344356
modifier: Modifier = Modifier,
345357
) {
346358
Row(
347-
horizontalArrangement = Arrangement.SpaceBetween,
348-
modifier = modifier.padding(horizontal = 24.dp, vertical = 16.dp)
359+
modifier = modifier.padding(end = 16.dp, top = 16.dp, bottom = 16.dp),
349360
) {
350-
// TODO use most recently used emojis here when available from the Rust SDK
351-
val defaultEmojis = sequenceOf(
352-
"👍️",
353-
"👎️",
354-
"🔥",
355-
"❤️",
356-
"👏"
357-
)
358-
for (emoji in defaultEmojis) {
359-
val isHighlighted = highlightedEmojis.contains(emoji)
360-
EmojiButton(
361-
modifier = Modifier
362-
// Make it appear after the more useful actions for the accessibility service
363-
.semantics {
364-
traversalIndex = 1f
365-
},
366-
emoji = emoji,
367-
isHighlighted = isHighlighted,
368-
onClick = onEmojiReactionClick
369-
)
361+
val backgroundColor = ElementTheme.colors.bgCanvasDefault
362+
363+
val emojis = remember(recentEmojis) {
364+
(suggestedEmojis + recentEmojis.filter { it !in suggestedEmojis })
365+
.take(100)
366+
.toImmutableList()
370367
}
371-
Box(
368+
369+
LazyRow(
372370
modifier = Modifier
373-
.size(48.dp),
374-
contentAlignment = Alignment.Center,
371+
.weight(1f, fill = true)
372+
.drawWithContent {
373+
val gradientWidth = 24.dp.toPx()
374+
val width = size.width
375+
drawContent()
376+
377+
drawRect(
378+
brush = Brush.horizontalGradient(
379+
0.0f to Color.Transparent,
380+
1.0f to backgroundColor,
381+
startX = width - gradientWidth,
382+
endX = width,
383+
),
384+
topLeft = Offset(width - gradientWidth, 0f),
385+
size = Size(gradientWidth, size.height)
386+
)
387+
},
388+
contentPadding = PaddingValues(horizontal = 16.dp),
389+
horizontalArrangement = Arrangement.spacedBy(8.dp),
390+
) {
391+
items(emojis) { emoji ->
392+
val isHighlighted = highlightedEmojis.contains(emoji)
393+
EmojiButton(
394+
modifier = Modifier
395+
// Make it appear after the more useful actions for the accessibility service
396+
.semantics {
397+
traversalIndex = 1f
398+
},
399+
emoji = emoji,
400+
isHighlighted = isHighlighted,
401+
onClick = onEmojiReactionClick
402+
)
403+
}
404+
}
405+
406+
Box(
407+
modifier = Modifier.padding(end = 10.dp).requiredSize(48.dp),
408+
contentAlignment = Alignment.CenterEnd,
375409
) {
376410
Icon(
377411
imageVector = CompoundIcons.ReactionAdd(),

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
1717
import io.element.android.emojibasebindings.Emoji
1818
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPicker
1919
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPickerPresenter
20+
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
2021
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
2122
import io.element.android.libraries.designsystem.theme.components.hide
2223
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
@@ -50,7 +51,13 @@ fun CustomReactionBottomSheet(
5051
sheetState = sheetState,
5152
modifier = modifier
5253
) {
53-
val presenter = remember { EmojiPickerPresenter(target.emojibaseStore) }
54+
val presenter = remember {
55+
EmojiPickerPresenter(
56+
emojibaseStore = target.emojibaseStore,
57+
recentEmojis = state.recentEmojis,
58+
coroutineDispatchers = CoroutineDispatchers.Default,
59+
)
60+
}
5461
EmojiPicker(
5562
onSelectEmoji = ::onEmojiSelectedDismiss,
5663
state = presenter.present(),

0 commit comments

Comments
 (0)