Timeline: quick implementation of reactions (without interactions)

This commit is contained in:
ganfra 2022-11-09 19:36:12 +01:00
parent 3e2b13fb04
commit 9c044c7d1d
7 changed files with 102 additions and 12 deletions

View file

@ -14,6 +14,7 @@ dependencies {
implementation(libs.mavericks.compose) implementation(libs.mavericks.compose)
implementation(libs.timber) implementation(libs.timber)
implementation(libs.datetime) implementation(libs.datetime)
implementation(libs.accompanist.flowlayout)
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")

View file

@ -2,7 +2,9 @@ package io.element.android.x.features.messages
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemGroupPosition import io.element.android.x.features.messages.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesItemReactionState
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.content.* import io.element.android.x.features.messages.model.content.*
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
@ -51,6 +53,7 @@ class MessageTimelineItemStateMapper(
val senderAvatarData = val senderAvatarData =
loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl) loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl)
return MessagesTimelineItemState.MessageEvent( return MessagesTimelineItemState.MessageEvent(
id = currentTimelineItem.event.eventId() ?: "", id = currentTimelineItem.event.eventId() ?: "",
senderId = currentSender, senderId = currentSender,
@ -58,10 +61,18 @@ class MessageTimelineItemStateMapper(
senderAvatar = senderAvatarData, senderAvatar = senderAvatarData,
content = currentTimelineItem.computeContent(), content = currentTimelineItem.computeContent(),
isMine = currentTimelineItem.event.isOwn(), isMine = currentTimelineItem.event.isOwn(),
groupPosition = groupPosition groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState()
) )
} }
private fun MatrixTimelineItem.Event.computeReactionsState(): MessagesItemReactionState {
val aggregatedReactions = event.reactions().map {
AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false)
}
return MessagesItemReactionState(aggregatedReactions)
}
private fun MatrixTimelineItem.Event.computeContent(): MessagesTimelineItemContent { private fun MatrixTimelineItem.Event.computeContent(): MessagesTimelineItemContent {
val content = event.content() val content = event.content()
content.asUnableToDecrypt()?.let { encryptedMessage -> content.asUnableToDecrypt()?.let { encryptedMessage ->

View file

@ -21,6 +21,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.End
import androidx.compose.ui.Alignment.Companion.Start
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -36,10 +38,7 @@ import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.data.LogCompositions import io.element.android.x.core.data.LogCompositions
import io.element.android.x.core.data.StableCharSequence import io.element.android.x.core.data.StableCharSequence
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.components.MessagesTimelineItemEncryptedView import io.element.android.x.features.messages.components.*
import io.element.android.x.features.messages.components.MessagesTimelineItemRedactedView
import io.element.android.x.features.messages.components.MessagesTimelineItemTextView
import io.element.android.x.features.messages.components.MessagesTimelineItemUnknownView
import io.element.android.x.features.messages.model.MessagesItemGroupPosition import io.element.android.x.features.messages.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState import io.element.android.x.features.messages.model.MessagesViewState
@ -213,25 +212,25 @@ fun MessageEventRow(
messageEvent: MessagesTimelineItemState.MessageEvent, messageEvent: MessagesTimelineItemState.MessageEvent,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val contentAlignment = if (messageEvent.isMine) { val (parentAlignment, contentAlignment) = if (messageEvent.isMine) {
Alignment.CenterEnd Pair(Alignment.CenterEnd, End)
} else { } else {
Alignment.CenterStart Pair(Alignment.CenterStart, Start)
} }
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .wrapContentHeight(),
contentAlignment = contentAlignment contentAlignment = parentAlignment
) { ) {
Row( Row(
modifier = modifier modifier = modifier
.widthIn(max = 300.dp) .widthIn(max = 300.dp),
) { ) {
if (!messageEvent.isMine) { if (!messageEvent.isMine) {
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
} }
Column { Column(horizontalAlignment = contentAlignment) {
if (messageEvent.showSenderInformation) { if (messageEvent.showSenderInformation) {
MessageSenderInformation( MessageSenderInformation(
messageEvent.safeSenderName, messageEvent.safeSenderName,
@ -245,7 +244,7 @@ fun MessageEventRow(
modifier = Modifier modifier = Modifier
.zIndex(-1f) .zIndex(-1f)
) { ) {
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
when (messageEvent.content) { when (messageEvent.content) {
is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView( is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView(
content = messageEvent.content, content = messageEvent.content,
@ -265,6 +264,13 @@ fun MessageEventRow(
) )
} }
} }
MessagesReactionsView(
reactionsState = messageEvent.reactionsState,
modifier = Modifier
.zIndex(1f)
.offset(x = if (messageEvent.isMine) 0.dp else 20.dp, y = -(16.dp))
)
} }
if (messageEvent.isMine) { if (messageEvent.isMine) {
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))

View file

@ -0,0 +1,55 @@
package io.element.android.x.features.messages.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import io.element.android.x.features.messages.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemReactionState
@Composable
fun MessagesReactionsView(
reactionsState: MessagesItemReactionState,
modifier: Modifier = Modifier,
) {
FlowRow(
modifier = modifier,
mainAxisSpacing = 2.dp,
crossAxisSpacing = 8.dp,
) {
reactionsState.reactions.forEach { reaction ->
MessagesReactionButton(reaction = reaction)
}
}
}
@Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant,
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
) {
Row(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = reaction.key, fontSize = 12.sp)
Spacer(modifier = Modifier.width(4.dp))
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
}

View file

@ -0,0 +1,15 @@
package io.element.android.x.features.messages.model
import androidx.compose.runtime.Stable
@Stable
data class MessagesItemReactionState(
val reactions: List<AggregatedReaction>
)
@Stable
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)

View file

@ -17,6 +17,7 @@ sealed interface MessagesTimelineItemState {
val sentTime: String = "", val sentTime: String = "",
val isMine: Boolean = false, val isMine: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None, val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
val reactionsState: MessagesItemReactionState
) : MessagesTimelineItemState { ) : MessagesTimelineItemState {
val showSenderInformation = groupPosition.isNew() && !isMine val showSenderInformation = groupPosition.isNew() && !isMine

View file

@ -65,6 +65,7 @@ accompanist_systemui = { module = "com.google.accompanist:accompanist-systemuico
accompanist_placeholder = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist" } accompanist_placeholder = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist" }
accompanist_pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } accompanist_pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" }
accompanist_pagerindicator = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" } accompanist_pagerindicator = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" }
accompanist_flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
# Test # Test
test_junit = { module = "junit:junit", version.ref = "test_junit" } test_junit = { module = "junit:junit", version.ref = "test_junit" }