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 <benoitm+elementbot@element.io>
This commit is contained in:
parent
5ecafeb49f
commit
a04cb2963c
10 changed files with 55 additions and 16 deletions
|
|
@ -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.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
|
||||||
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
override val values: Sequence<MessagesState>
|
override val values: Sequence<MessagesState>
|
||||||
|
|
@ -68,6 +69,7 @@ fun aMessagesState() = MessagesState(
|
||||||
customReactionState = CustomReactionState(
|
customReactionState = CustomReactionState(
|
||||||
selectedEventId = null,
|
selectedEventId = null,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
selectedEmoji = persistentSetOf(),
|
||||||
),
|
),
|
||||||
reactionSummaryState = ReactionSummaryState(
|
reactionSummaryState = ReactionSummaryState(
|
||||||
target = null,
|
target = null,
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ fun MessagesView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMoreReactionsClicked(event: TimelineItem.Event) {
|
fun onMoreReactionsClicked(event: TimelineItem.Event) {
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
|
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -187,7 +187,7 @@ fun MessagesView(
|
||||||
state = state.actionListState,
|
state = state.actionListState,
|
||||||
onActionSelected = ::onActionSelected,
|
onActionSelected = ::onActionSelected,
|
||||||
onCustomReactionClicked = { event ->
|
onCustomReactionClicked = { event ->
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
|
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
|
||||||
},
|
},
|
||||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.lazy.grid.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
|
|
@ -39,6 +41,7 @@ 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.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vanniktech.emoji.Emoji
|
import 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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EmojiPicker(
|
fun EmojiPicker(
|
||||||
onEmojiSelected: (Emoji) -> Unit,
|
onEmojiSelected: (Emoji) -> Unit,
|
||||||
|
selectedEmojis: ImmutableSet<String>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -91,12 +97,19 @@ fun EmojiPicker(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
columns = GridCells.Adaptive(minSize = 40.dp),
|
columns = GridCells.Adaptive(minSize = 40.dp),
|
||||||
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
items(category.emojis, key = { it.unicode }) { item ->
|
items(category.emojis, key = { it.unicode }) { item ->
|
||||||
|
val backgroundColor = if (selectedEmojis.contains(item.unicode)) {
|
||||||
|
ElementTheme.colors.bgActionPrimaryRest
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
|
.background(backgroundColor, CircleShape)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onClick = { onEmojiSelected(item) },
|
onClick = { onEmojiSelected(item) },
|
||||||
|
|
@ -132,6 +145,7 @@ internal fun EmojiPickerDarkPreview() {
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
EmojiPicker(
|
EmojiPicker(
|
||||||
onEmojiSelected = {},
|
onEmojiSelected = {},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
selectedEmojis = persistentSetOf("😀", "😄", "😃")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ fun CustomReactionBottomSheet(
|
||||||
) {
|
) {
|
||||||
EmojiPicker(
|
EmojiPicker(
|
||||||
onEmojiSelected = ::onEmojiSelectedDismiss,
|
onEmojiSelected = ::onEmojiSelectedDismiss,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
selectedEmojis = state.selectedEmoji,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface CustomReactionEvents {
|
sealed interface CustomReactionEvents {
|
||||||
data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents
|
data class UpdateSelectedEvent(val event: TimelineItem.Event?) : CustomReactionEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,22 +21,24 @@ 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.setValue
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
|
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): CustomReactionState {
|
override fun present(): CustomReactionState {
|
||||||
var selectedEventId by remember { mutableStateOf<EventId?>(null) }
|
var selectedEvent by remember { mutableStateOf<TimelineItem.Event?>(null) }
|
||||||
|
|
||||||
fun handleEvents(event: CustomReactionEvents) {
|
fun handleEvents(event: CustomReactionEvents) {
|
||||||
when (event) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
data class CustomReactionState(
|
data class CustomReactionState(
|
||||||
val selectedEventId: EventId?,
|
val selectedEventId: EventId?,
|
||||||
|
val selectedEmoji: ImmutableSet<String>,
|
||||||
val eventSink: (CustomReactionEvents) -> Unit,
|
val eventSink: (CustomReactionEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.CustomReactionEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
|
@ -38,11 +40,27 @@ class CustomReactionPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.selectedEventId).isNull()
|
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)
|
assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID)
|
||||||
|
|
||||||
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
|
||||||
assertThat(awaitItem().selectedEventId).isNull()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dedb3940216fd18804157f708340a535008cc753a579ced3ac4153f99bf01218
|
oid sha256:c5650ec2689d4ce0cf0785e12d5cb899c1bdfdad14c1f75106be1068df662f53
|
||||||
size 175737
|
size 188815
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19d8a7e0d477cc7db1bea85407b71516347b158777db2e3889734f2129b69de3
|
oid sha256:2e5e11d9c1e7d4b950a0d080e8104f08b26fac56332d243502430dbfdd02c60b
|
||||||
size 175762
|
size 188259
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue