Add 'more reactions' button to message (#756)

- Add 'more reactions' button to message
- Fix display of existing emoji reactions to match designs
- Refactor emoji reactions to reduce nesting of composables


---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
jonnyandrew 2023-07-05 15:38:20 +00:00 committed by GitHub
parent f0f00e40a0
commit a6825b66e1
56 changed files with 261 additions and 131 deletions

View file

@ -126,6 +126,9 @@ fun MessagesView(
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
}
fun onMoreReactionsClicked(event: TimelineItem.Event): Unit =
state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.statusBars,
@ -155,6 +158,7 @@ fun MessagesView(
}
},
onReactionClicked = ::onEmojiReactionClicked,
onMoreReactionsClicked = ::onMoreReactionsClicked,
onSendLocationClicked = onSendLocationClicked,
onSwipeToReply = { targetEvent ->
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
@ -240,6 +244,7 @@ fun MessagesViewContent(
onMessageClicked: (TimelineItem.Event) -> Unit,
onUserDataClicked: (UserId) -> Unit,
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
onMessageLongClicked: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSendLocationClicked: () -> Unit,
@ -262,6 +267,7 @@ fun MessagesViewContent(
onUserDataClicked = onUserDataClicked,
onTimestampClicked = onTimestampClicked,
onReactionClicked = onReactionClicked,
onMoreReactionsClicked = onMoreReactionsClicked,
onSwipeToReply = onSwipeToReply,
)
}

View file

@ -82,6 +82,7 @@ fun TimelineView(
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
) {
fun onReachedLoadMore() {
@ -127,6 +128,7 @@ fun TimelineView(
onUserDataClick = onUserDataClicked,
inReplyToClick = ::inReplyToClicked,
onReactionClick = onReactionClicked,
onMoreReactionsClick = onMoreReactionsClicked,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = onSwipeToReply,
)
@ -154,6 +156,7 @@ fun TimelineItemRow(
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier
@ -184,6 +187,7 @@ fun TimelineItemRow(
onUserDataClick = onUserDataClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
onMoreReactionsClick = onMoreReactionsClick,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = { onSwipeToReply(timelineItem) },
modifier = modifier,
@ -221,6 +225,7 @@ fun TimelineItemRow(
onUserDataClick = onUserDataClick,
onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick,
onMoreReactionsClick = onMoreReactionsClick,
onSwipeToReply = {},
)
}
@ -323,6 +328,7 @@ private fun ContentToPreview(content: TimelineItemEventContent) {
onUserDataClicked = {},
onMessageLongClicked = {},
onReactionClicked = { _, _ -> },
onMoreReactionsClicked = {},
onSwipeToReply = {},
)
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddReaction
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
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.Surface
import io.element.android.libraries.theme.ElementTheme
@Composable
fun MessagesMoreReactionsButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
val buttonColor = ElementTheme.colors.bgSubtleSecondary
Surface(
modifier = modifier
.background(Color.Transparent)
// Outer border, same colour as background
.border(
BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(14.dp))
)
.padding(vertical = 2.dp, horizontal = 2.dp)
// Clip click indicator inside the outer border
.clip(RoundedCornerShape(corner = CornerSize(12.dp)))
.clickable(onClick = onClick)
.background(buttonColor, RoundedCornerShape(corner = CornerSize(12.dp)))
.padding(vertical = 4.dp, horizontal = 10.dp),
color = buttonColor
) {
Icon(
imageVector = Icons.Outlined.AddReaction,
contentDescription = "Add emoji",
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
// Same size as the line height of reaction emoji text
.size(with(LocalDensity.current) { 20.sp.toDp() })
)
}
}
@Preview
@Composable
internal fun MessagesMoreReactionsButtonLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun MessagesMoreReactionsButtonDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
MessagesMoreReactionsButton(onClick = {})
}

View file

@ -17,9 +17,9 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
@ -30,6 +30,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -45,35 +46,47 @@ import io.element.android.libraries.theme.ElementTheme
@Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier, onClick: () -> Unit) {
// First Surface is to render a border with the same background color as the background
val buttonColor = if (reaction.isHighlighted) {
ElementTheme.colors.bgSubtlePrimary
} else {
ElementTheme.colors.bgSubtleSecondary
}
val borderColor = if (reaction.isHighlighted) {
ElementTheme.colors.borderInteractivePrimary
} else {
buttonColor
}
Surface(
modifier = modifier.clickable(onClick = onClick::invoke),
// TODO Should use compound.bgSubtlePrimary
color = ElementTheme.legacyColors.gray300,
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(14.dp)),
modifier = modifier
.background(Color.Transparent)
// Outer border, same colour as background
.border(
BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(14.dp))
)
.padding(vertical = 2.dp, horizontal = 2.dp)
// Clip click indicator inside the outer border
.clip(RoundedCornerShape(corner = CornerSize(12.dp)))
.clickable(onClick = onClick)
// Inner border, to highlight when selected
.border(BorderStroke(1.dp, borderColor), RoundedCornerShape(corner = CornerSize(12.dp)))
.background(buttonColor, RoundedCornerShape(corner = CornerSize(12.dp)))
.padding(vertical = 4.dp, horizontal = 10.dp),
color = buttonColor
) {
Box(modifier = Modifier.padding(2.dp)) {
val reactionModifier = if (reaction.isHighlighted) {
Modifier
// TODO Check the color, should use compound.borderInteractivePrimary
.border(BorderStroke(1.dp, Color(0xFF808994)), RoundedCornerShape(corner = CornerSize(12.dp)))
} else {
Modifier
}
Row(
modifier = reactionModifier.padding(vertical = 4.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = reaction.key, fontSize = 15.sp)
if (reaction.count > 1) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = reaction.count.toString(),
color = if (reaction.isHighlighted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
fontSize = 14.sp
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = reaction.key, fontSize = 15.sp, lineHeight = 20.sp
)
if (reaction.count > 1) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = reaction.count.toString(),
color = if (reaction.isHighlighted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
fontSize = 14.sp
)
}
}
}

View file

@ -101,6 +101,7 @@ fun TimelineItemEventRow(
inReplyToClick: (EventId) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
onSwipeToReply: () -> Unit,
modifier: Modifier = Modifier
) {
@ -110,9 +111,6 @@ fun TimelineItemEventRow(
onUserDataClick(event.senderId)
}
fun onReactionClicked(emoji: String) =
onReactionClick(emoji, event)
fun inReplyToClicked() {
val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return
inReplyToClick(inReplyToEventId)
@ -144,7 +142,8 @@ fun TimelineItemEventRow(
onTimestampClicked = onTimestampClicked,
inReplyToClicked = ::inReplyToClicked,
onUserDataClicked = ::onUserDataClicked,
onReactionClicked = ::onReactionClicked,
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
)
}
)
@ -158,7 +157,8 @@ fun TimelineItemEventRow(
onTimestampClicked = onTimestampClicked,
inReplyToClicked = ::inReplyToClicked,
onUserDataClicked = ::onUserDataClicked,
onReactionClicked = ::onReactionClicked,
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
)
}
// This is assuming that we are in a ColumnScope, but this is OK, for both Preview and real usage.
@ -180,6 +180,7 @@ private fun TimelineItemEventRowContent(
inReplyToClicked: () -> Unit,
onUserDataClicked: () -> Unit,
onReactionClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
) {
// To avoid using negative offset, we display in this Box a column with:
@ -240,6 +241,7 @@ private fun TimelineItemEventRowContent(
TimelineItemReactionsView(
reactionsState = event.reactionsState,
onReactionClicked = onReactionClicked,
onMoreReactionsClicked = { onMoreReactionsClicked(event) },
modifier = Modifier
.align(if (event.isMine) Alignment.BottomEnd else Alignment.BottomStart)
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
@ -534,6 +536,7 @@ private fun ContentToPreview() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
)
@ -551,6 +554,7 @@ private fun ContentToPreview() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
)
@ -595,6 +599,7 @@ private fun ContentToPreviewWithReply() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
)
@ -613,6 +618,7 @@ private fun ContentToPreviewWithReply() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
)
@ -668,6 +674,7 @@ private fun ContentTimestampToPreview(event: TimelineItem.Event) {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
)

View file

@ -29,8 +29,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun TimelineItemReactionsView(
reactionsState: TimelineItemReactions,
onReactionClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: () -> Unit,
modifier: Modifier = Modifier,
onReactionClicked: (emoji: String) -> Unit
) {
FlowRow(
modifier = modifier,
@ -43,6 +44,9 @@ fun TimelineItemReactionsView(
onClick = { onReactionClicked(reaction.key) }
)
}
MessagesMoreReactionsButton(
onClick = onMoreReactionsClicked
)
}
}
@ -60,6 +64,7 @@ internal fun TimelineItemReactionsViewDarkPreview() =
private fun ContentToPreview() {
TimelineItemReactionsView(
reactionsState = aTimelineItemReactions(),
onReactionClicked = { }
onReactionClicked = {},
onMoreReactionsClicked = {},
)
}