Timeline: quick implementation of reactions (without interactions)
This commit is contained in:
parent
3e2b13fb04
commit
9c044c7d1d
7 changed files with 102 additions and 12 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue