diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 0eb55c7d8c..b231114945 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(libs.mavericks.compose) implementation(libs.timber) implementation(libs.datetime) + implementation(libs.accompanist.flowlayout) testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt index 581a9b85e3..a135bbacbd 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateMapper.kt @@ -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.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.MessagesItemReactionState import io.element.android.x.features.messages.model.MessagesTimelineItemState import io.element.android.x.features.messages.model.content.* import io.element.android.x.matrix.MatrixClient @@ -51,6 +53,7 @@ class MessageTimelineItemStateMapper( val senderAvatarData = loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl) + return MessagesTimelineItemState.MessageEvent( id = currentTimelineItem.event.eventId() ?: "", senderId = currentSender, @@ -58,10 +61,18 @@ class MessageTimelineItemStateMapper( senderAvatar = senderAvatarData, content = currentTimelineItem.computeContent(), 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 { val content = event.content() content.asUnableToDecrypt()?.let { encryptedMessage -> diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt index 6f72ced13a..c62ec09528 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.draw.clip 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.StableCharSequence 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.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.components.* 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.MessagesViewState @@ -213,25 +212,25 @@ fun MessageEventRow( messageEvent: MessagesTimelineItemState.MessageEvent, modifier: Modifier = Modifier ) { - val contentAlignment = if (messageEvent.isMine) { - Alignment.CenterEnd + val (parentAlignment, contentAlignment) = if (messageEvent.isMine) { + Pair(Alignment.CenterEnd, End) } else { - Alignment.CenterStart + Pair(Alignment.CenterStart, Start) } Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - contentAlignment = contentAlignment + contentAlignment = parentAlignment ) { Row( modifier = modifier - .widthIn(max = 300.dp) + .widthIn(max = 300.dp), ) { if (!messageEvent.isMine) { Spacer(modifier = Modifier.width(16.dp)) } - Column { + Column(horizontalAlignment = contentAlignment) { if (messageEvent.showSenderInformation) { MessageSenderInformation( messageEvent.safeSenderName, @@ -245,7 +244,7 @@ fun MessageEventRow( modifier = Modifier .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) { is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView( 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) { Spacer(modifier = Modifier.width(16.dp)) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesReactionsView.kt b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesReactionsView.kt new file mode 100644 index 0000000000..8fb2330842 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/components/MessagesReactionsView.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemReactionState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemReactionState.kt new file mode 100644 index 0000000000..9e22dda284 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemReactionState.kt @@ -0,0 +1,15 @@ +package io.element.android.x.features.messages.model + +import androidx.compose.runtime.Stable + +@Stable +data class MessagesItemReactionState( + val reactions: List +) + +@Stable +data class AggregatedReaction( + val key: String, + val count: String, + val isHighlighted: Boolean = false +) \ No newline at end of file diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt index 0b57e2a23e..d352b73e0b 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt @@ -17,6 +17,7 @@ sealed interface MessagesTimelineItemState { val sentTime: String = "", val isMine: Boolean = false, val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None, + val reactionsState: MessagesItemReactionState ) : MessagesTimelineItemState { val showSenderInformation = groupPosition.isNew() && !isMine diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 955a773c86..895010886b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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_pager = { module = "com.google.accompanist:accompanist-pager", 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_junit = { module = "junit:junit", version.ref = "test_junit" }