From 08a0f710d77e6827707be72d30f8b469ca4392d2 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 11 Aug 2023 16:47:06 +0100 Subject: [PATCH] Show selected reactions on the emoji picker. (#1014) * Show selected reactions on the emoji picker. * Unused import * Update screenshots * Use ImmutableSet * Fix lint issues. --------- Co-authored-by: ElementBot --- .../messages/impl/MessagesStateProvider.kt | 2 ++ .../features/messages/impl/MessagesView.kt | 4 ++-- .../impl/timeline/components/EmojiPicker.kt | 18 +++++++++++++++-- .../CustomReactionBottomSheet.kt | 3 ++- .../customreaction/CustomReactionEvents.kt | 4 ++-- .../customreaction/CustomReactionPresenter.kt | 10 ++++++---- .../customreaction/CustomReactionState.kt | 2 ++ .../CustomReactionPresenterTests.kt | 20 ++++++++++++++++++- ...ckerDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...kerLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 55 insertions(+), 16 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 7da67d468c..9b3f5073a1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.collections.immutable.persistentSetOf open class MessagesStateProvider : PreviewParameterProvider { override val values: Sequence @@ -68,6 +69,7 @@ fun aMessagesState() = MessagesState( customReactionState = CustomReactionState( selectedEventId = null, eventSink = {}, + selectedEmoji = persistentSetOf(), ), reactionSummaryState = ReactionSummaryState( target = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 34ac14af57..116480479f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -135,7 +135,7 @@ fun MessagesView( } fun onMoreReactionsClicked(event: TimelineItem.Event) { - state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) + state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event)) } Scaffold( @@ -187,7 +187,7 @@ fun MessagesView( state = state.actionListState, onActionSelected = ::onActionSelected, onCustomReactionClicked = { event -> - state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) + state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event)) }, onEmojiReactionClicked = ::onEmojiReactionClicked, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt index 6e121685f2..effd7f23f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -31,6 +32,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Tab import androidx.compose.material3.TabRow @@ -39,6 +41,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vanniktech.emoji.Emoji @@ -48,12 +51,15 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun EmojiPicker( onEmojiSelected: (Emoji) -> Unit, + selectedEmojis: ImmutableSet, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() @@ -91,12 +97,19 @@ fun EmojiPicker( modifier = Modifier.fillMaxSize(), columns = GridCells.Adaptive(minSize = 40.dp), contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceEvenly, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { items(category.emojis, key = { it.unicode }) { item -> + val backgroundColor = if (selectedEmojis.contains(item.unicode)) { + ElementTheme.colors.bgActionPrimaryRest + } else { + Color.Transparent + } + Box( modifier = Modifier .size(40.dp) + .background(backgroundColor, CircleShape) .clickable( enabled = true, onClick = { onEmojiSelected(item) }, @@ -132,6 +145,7 @@ internal fun EmojiPickerDarkPreview() { private fun ContentToPreview() { EmojiPicker( onEmojiSelected = {}, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + selectedEmojis = persistentSetOf("😀", "😄", "😃") ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt index 70c2a7dc10..d817ec0cd4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -57,7 +57,8 @@ fun CustomReactionBottomSheet( ) { EmojiPicker( onEmojiSelected = ::onEmojiSelectedDismiss, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + selectedEmojis = state.selectedEmoji, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt index b7c210553e..a0d69df372 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt @@ -16,8 +16,8 @@ package io.element.android.features.messages.impl.timeline.components.customreaction -import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface CustomReactionEvents { - data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents + data class UpdateSelectedEvent(val event: TimelineItem.Event?) : CustomReactionEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index 0a23d42085..f094f2dbc6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt @@ -21,22 +21,24 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.core.EventId +import kotlinx.collections.immutable.toImmutableSet import javax.inject.Inject class CustomReactionPresenter @Inject constructor() : Presenter { @Composable override fun present(): CustomReactionState { - var selectedEventId by remember { mutableStateOf(null) } + var selectedEvent by remember { mutableStateOf(null) } fun handleEvents(event: CustomReactionEvents) { when (event) { - is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId + is CustomReactionEvents.UpdateSelectedEvent -> selectedEvent = event.event } } - return CustomReactionState(selectedEventId = selectedEventId, eventSink = ::handleEvents) + val selectedEmoji = selectedEvent?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet() + return CustomReactionState(selectedEventId = selectedEvent?.eventId, selectedEmoji = selectedEmoji, eventSink = ::handleEvents) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt index 6c0c7f3599..9de1642dff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt @@ -17,8 +17,10 @@ package io.element.android.features.messages.impl.timeline.components.customreaction import io.element.android.libraries.matrix.api.core.EventId +import kotlinx.collections.immutable.ImmutableSet data class CustomReactionState( val selectedEventId: EventId?, + val selectedEmoji: ImmutableSet, val eventSink: (CustomReactionEvents) -> Unit, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt index 1c40483ffe..84628cedae 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt @@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -38,11 +40,27 @@ class CustomReactionPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedEventId).isNull() - initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(AN_EVENT_ID)) + initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID))) assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID) initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) assertThat(awaitItem().selectedEventId).isNull() } } + + @Test + fun `present - handle selected emojis`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.selectedEventId).isNull() + val reactions = aTimelineItemReactions(count = 1, isHighlighted = true) + val key = reactions.reactions.first().key + initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions))) + val stateWithSelectedEmojis = awaitItem() + assertThat(stateWithSelectedEmojis.selectedEventId).isEqualTo(AN_EVENT_ID) + assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key) + } + } } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerDarkPreview_0_null,NEXUS_5,1.0,en].png index e10b00e85b..d6abcd7838 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dedb3940216fd18804157f708340a535008cc753a579ced3ac4153f99bf01218 -size 175737 +oid sha256:c5650ec2689d4ce0cf0785e12d5cb899c1bdfdad14c1f75106be1068df662f53 +size 188815 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerLightPreview_0_null,NEXUS_5,1.0,en].png index e3804f7073..5898fc6fa7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_EmojiPickerLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19d8a7e0d477cc7db1bea85407b71516347b158777db2e3889734f2129b69de3 -size 175762 +oid sha256:2e5e11d9c1e7d4b950a0d080e8104f08b26fac56332d243502430dbfdd02c60b +size 188259