From 9ba6d70daa47681e366e9f41fe15f89deb789069 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 18 Oct 2022 22:08:24 +0100 Subject: [PATCH 01/16] extracts the swipeable bubble container to the design system --- .../main/kotlin/app/dapk/st/engine/Models.kt | 35 +-- .../st/design/components/AlignedContainer.kt | 147 ++++++++++ .../app/dapk/st/design/components/Bubble.kt | 76 +++++ .../app/dapk/st/messenger/MessengerScreen.kt | 271 ++++-------------- 4 files changed, 293 insertions(+), 236 deletions(-) create mode 100644 design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt create mode 100644 design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index b823b03..5a29bff 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -104,6 +104,12 @@ sealed class RoomEvent { abstract val utcTimestamp: Long abstract val author: RoomMember abstract val meta: MessageMeta + abstract val edited: Boolean + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } data class Encrypted( override val eventId: EventId, @@ -112,10 +118,8 @@ sealed class RoomEvent { override val meta: MessageMeta, ) : RoomEvent() { - val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } + override val edited: Boolean = false + } data class Message( @@ -124,15 +128,9 @@ sealed class RoomEvent { val content: String, override val author: RoomMember, override val meta: MessageMeta, - val edited: Boolean = false, + override val edited: Boolean = false, val redacted: Boolean = false, - ) : RoomEvent() { - - val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } - } + ) : RoomEvent() data class Reply( val message: RoomEvent, @@ -143,13 +141,9 @@ sealed class RoomEvent { override val utcTimestamp: Long = message.utcTimestamp override val author: RoomMember = message.author override val meta: MessageMeta = message.meta + override val edited: Boolean = message.edited val replyingToSelf = replyingTo.author == message.author - - val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } } data class Image( @@ -158,14 +152,9 @@ sealed class RoomEvent { val imageMeta: ImageMeta, override val author: RoomMember, override val meta: MessageMeta, - val edited: Boolean = false, + override val edited: Boolean = false, ) : RoomEvent() { - val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } - data class ImageMeta( val width: Int?, val height: Int?, diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt new file mode 100644 index 0000000..5eea262 --- /dev/null +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt @@ -0,0 +1,147 @@ +package app.dapk.st.design.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +private val selfBackgroundShape = RoundedCornerShape(12.dp, 0.dp, 12.dp, 12.dp) +private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp) + +data class BubbleMeta( + val shape: RoundedCornerShape, + val background: Color, + val isSelf: Boolean, +) + +fun BubbleMeta.isNotSelf() = !this.isSelf + +@Composable +fun LazyItemScope.AlignedContainer( + avatar: Avatar, + isSelf: Boolean, + wasPreviousMessageSameSender: Boolean, + onReply: () -> Unit, + content: @Composable BubbleMeta.() -> Unit +) { + val rowWithMeta = @Composable { + DraggableRow( + avatar = avatar, + isSelf = isSelf, + wasPreviousMessageSameSender = wasPreviousMessageSameSender, + onReply = { onReply() } + ) { + content( + when (isSelf) { + true -> BubbleMeta(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, isSelf = true) + false -> BubbleMeta(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, isSelf = false) + } + ) + } + } + + when (isSelf) { + true -> SelfContainer(rowWithMeta) + false -> OtherContainer(rowWithMeta) + } +} + +@Composable +private fun LazyItemScope.OtherContainer(content: @Composable () -> Unit) { + Box(modifier = Modifier.Companion.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) { + content() + } +} + +@Composable +private fun LazyItemScope.SelfContainer(content: @Composable () -> Unit) { + Box(modifier = Modifier.Companion.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) { + Box(modifier = Modifier.Companion.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) { + content() + } + } +} + +@Composable +private fun DraggableRow( + isSelf: Boolean, + wasPreviousMessageSameSender: Boolean, + onReply: () -> Unit, + avatar: Avatar, + content: @Composable () -> Unit +) { + + val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp + val localDensity = LocalDensity.current + + val coroutineScope = rememberCoroutineScope() + val offsetX = remember { Animatable(0f) } + + Row( + Modifier.padding(horizontal = 12.dp) + .offset { IntOffset(offsetX.value.roundToInt(), 0) } + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { + if ((offsetX.value + it) > 0) { + coroutineScope.launch { offsetX.snapTo(offsetX.value + it) } + } + }, + onDragStopped = { + with(localDensity) { + if (offsetX.value > (screenWidthDp.toPx() * 0.15)) { + onReply() + } + } + + coroutineScope.launch { + offsetX.animateTo(targetValue = 0f) + } + } + ) + ) { + when (isSelf) { + true -> { + // do nothing + } + + false -> SenderAvatar(wasPreviousMessageSameSender, avatar) + } + content() + } +} + +@Composable +private fun SenderAvatar(wasPreviousMessageSameSender: Boolean, avatar: Avatar) { + val displayImageSize = 32.dp + when { + wasPreviousMessageSameSender -> { + Spacer(modifier = Modifier.width(displayImageSize)) + } + + avatar.url == null -> { + MissingAvatarIcon(avatar.name, displayImageSize) + } + + else -> { + MessengerUrlIcon(avatar.url, displayImageSize) + } + } +} + +data class Avatar(val url: String?, val name: String) \ No newline at end of file diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt new file mode 100644 index 0000000..667cbbc --- /dev/null +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt @@ -0,0 +1,76 @@ +package app.dapk.st.design.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +data class Event(val authorName: String, val edited: Boolean, val time: String) + +@Composable +fun Bubble(bubble: BubbleMeta, content: @Composable () -> Unit) { + Box(modifier = Modifier.padding(start = 6.dp)) { + Box( + Modifier + .padding(4.dp) + .clip(bubble.shape) + .background(bubble.background) + .height(IntrinsicSize.Max), + ) { + content() + } + } +} + +@Composable +fun TextBubbleContent(bubble: BubbleMeta, event: Event, textContent: String, status: @Composable () -> Unit) { + Bubble(bubble) { + Column( + Modifier + .padding(8.dp) + .width(IntrinsicSize.Max) + .defaultMinSize(minWidth = 50.dp) + ) { + if (bubble.isNotSelf()) { + Text( + fontSize = 11.sp, + text = event.authorName, + maxLines = 1, + color = bubble.textColor() + ) + } + Text( + text = textContent, + color = bubble.textColor(), + fontSize = 15.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + + Spacer(modifier = Modifier.height(2.dp)) + Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val editedPrefix = if (event.edited) "(edited) " else null + Text( + fontSize = 9.sp, + text = "${editedPrefix ?: ""}${event.time}", + textAlign = TextAlign.End, + color = bubble.textColor(), + modifier = Modifier.wrapContentSize() + ) + status() + } + } + } +} + +@Composable +private fun BubbleMeta.textColor(): Color { + return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble +} \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 8662e70..d5476fe 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -3,15 +3,11 @@ package app.dapk.st.messenger import android.content.res.Configuration import androidx.activity.result.ActivityResultLauncher import androidx.compose.animation.* -import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape @@ -56,7 +52,6 @@ import app.dapk.st.navigator.Navigator import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import kotlinx.coroutines.launch -import kotlin.math.roundToInt @Composable internal fun MessengerScreen( @@ -193,58 +188,20 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions ) { index, item -> val previousEvent = if (index != 0) state.events[index - 1] else null val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id - AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) { + + AlignedContainer( + avatar = Avatar(item.author.avatarUrl?.value, item.author.displayName ?: item.author.id.value), + isSelf = self == item.author.id, + wasPreviousMessageSameSender = wasPreviousMessageSameSender, + onReply = { replyActions.onReply(item) }, + ) { + val event = Event(item.author.displayName ?: item.author.id.value, item.edited, item.time) + val status = @Composable { SendStatus(item) } when (item) { - is RoomEvent.Image -> MessageImage(it as BubbleContent) - is RoomEvent.Message -> TextBubbleContent(it as BubbleContent) - is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent) - is RoomEvent.Encrypted -> EncryptedBubbleContent(it as BubbleContent) - } - } - } - } -} - -private data class BubbleContent( - val shape: RoundedCornerShape, - val background: Color, - val isNotSelf: Boolean, - val message: T -) - -@Composable -private fun LazyItemScope.AlignedBubble( - message: T, - self: UserId, - wasPreviousMessageSameSender: Boolean, - replyActions: ReplyActions, - content: @Composable (BubbleContent) -> Unit -) { - when (message.author.id == self) { - true -> { - Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.TopEnd) { - Box(modifier = Modifier.fillParentMaxWidth(0.85f), contentAlignment = Alignment.TopEnd) { - Bubble( - message = message, - isNotSelf = false, - wasPreviousMessageSameSender = wasPreviousMessageSameSender, - replyActions = replyActions, - ) { - content(BubbleContent(selfBackgroundShape, SmallTalkTheme.extendedColors.selfBubble, false, message)) - } - } - } - } - - false -> { - Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) { - Bubble( - message = message, - isNotSelf = true, - wasPreviousMessageSameSender = wasPreviousMessageSameSender, - replyActions = replyActions, - ) { - content(BubbleContent(othersBackgroundShape, SmallTalkTheme.extendedColors.othersBubble, true, message)) + is RoomEvent.Image -> MessageImage(this, item) + is RoomEvent.Message -> TextBubbleContent(this, event, item.content, status = status) + is RoomEvent.Reply -> ReplyBubbleContent(this, item) + is RoomEvent.Encrypted -> EncryptedBubbleContent(this, item) } } } @@ -252,15 +209,15 @@ private fun LazyItemScope.AlignedBubble( } @Composable -private fun MessageImage(content: BubbleContent) { +private fun MessageImage(bubble: BubbleMeta, event: RoomEvent.Image) { val context = LocalContext.current Box(modifier = Modifier.padding(start = 6.dp)) { Box( Modifier .padding(4.dp) - .clip(content.shape) - .background(content.background) + .clip(bubble.shape) + .background(bubble.background) .height(IntrinsicSize.Max), ) { Column( @@ -269,23 +226,23 @@ private fun MessageImage(content: BubbleContent) { .width(IntrinsicSize.Max) .defaultMinSize(minWidth = 50.dp) ) { - if (content.isNotSelf) { + if (bubble.isNotSelf()) { Text( fontSize = 11.sp, - text = content.message.author.displayName ?: content.message.author.id.value, + text = event.author.displayName ?: event.author.id.value, maxLines = 1, - color = content.textColor() + color = bubble.textColor() ) } Spacer(modifier = Modifier.height(4.dp)) Image( - modifier = Modifier.size(content.message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)), + modifier = Modifier.size(event.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)), painter = rememberAsyncImagePainter( model = ImageRequest.Builder(context) .fetcherFactory(LocalDecyptingFetcherFactory.current) - .memoryCacheKey(content.message.imageMeta.url) - .data(content.message) + .memoryCacheKey(event.imageMeta.url) + .data(event) .build() ), contentDescription = null, @@ -293,15 +250,15 @@ private fun MessageImage(content: BubbleContent) { Spacer(modifier = Modifier.height(4.dp)) Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val editedPrefix = if (content.message.edited) "(edited) " else null + val editedPrefix = if (event.edited) "(edited) " else null Text( fontSize = 9.sp, - text = "${editedPrefix ?: ""}${content.message.time}", + text = "${editedPrefix ?: ""}${event.time}", textAlign = TextAlign.End, - color = content.textColor(), + color = bubble.textColor(), modifier = Modifier.wrapContentSize() ) - SendStatus(content.message) + SendStatus(event) } } } @@ -329,82 +286,19 @@ private fun Int.scalerFor(max: Float): Float { return max / this } -private val selfBackgroundShape = RoundedCornerShape(12.dp, 0.dp, 12.dp, 12.dp) -private val othersBackgroundShape = RoundedCornerShape(0.dp, 12.dp, 12.dp, 12.dp) - @Composable -private fun Bubble( - message: RoomEvent, - isNotSelf: Boolean, - wasPreviousMessageSameSender: Boolean, - replyActions: ReplyActions, - content: @Composable () -> Unit -) { - - val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp - val localDensity = LocalDensity.current - - val coroutineScope = rememberCoroutineScope() - val offsetX = remember { Animatable(0f) } - - Row( - Modifier.padding(horizontal = 12.dp) - .offset { IntOffset(offsetX.value.roundToInt(), 0) } - .draggable( - orientation = Orientation.Horizontal, - state = rememberDraggableState { - if ((offsetX.value + it) > 0) { - coroutineScope.launch { offsetX.snapTo(offsetX.value + it) } - } - }, - onDragStopped = { - with(localDensity) { - if (offsetX.value > (screenWidthDp.toPx() * 0.15)) { - replyActions.onReply(message) - } - } - - coroutineScope.launch { - offsetX.animateTo(targetValue = 0f) - } - } - ) - ) { - when { - isNotSelf -> { - val displayImageSize = 32.dp - when { - wasPreviousMessageSameSender -> { - Spacer(modifier = Modifier.width(displayImageSize)) - } - - message.author.avatarUrl == null -> { - MissingAvatarIcon(message.author.displayName ?: message.author.id.value, displayImageSize) - } - - else -> { - MessengerUrlIcon(message.author.avatarUrl!!.value, displayImageSize) - } - } - } - } - content() - } +private fun BubbleMeta.textColor(): Color { + return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble } @Composable -private fun BubbleContent<*>.textColor(): Color { - return if (this.isNotSelf) SmallTalkTheme.extendedColors.onOthersBubble else SmallTalkTheme.extendedColors.onSelfBubble -} - -@Composable -private fun TextBubbleContent(content: BubbleContent) { +private fun EncryptedBubbleContent(bubble: BubbleMeta, event: RoomEvent.Encrypted) { Box(modifier = Modifier.padding(start = 6.dp)) { Box( Modifier .padding(4.dp) - .clip(content.shape) - .background(content.background) + .clip(bubble.shape) + .background(bubble.background) .height(IntrinsicSize.Max), ) { Column( @@ -413,66 +307,17 @@ private fun TextBubbleContent(content: BubbleContent) { .width(IntrinsicSize.Max) .defaultMinSize(minWidth = 50.dp) ) { - if (content.isNotSelf) { + if (bubble.isNotSelf()) { Text( fontSize = 11.sp, - text = content.message.author.displayName ?: content.message.author.id.value, + text = event.author.displayName ?: event.author.id.value, maxLines = 1, - color = content.textColor() - ) - } - Text( - text = content.message.content, - color = content.textColor(), - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - - Spacer(modifier = Modifier.height(2.dp)) - Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val editedPrefix = if (content.message.edited) "(edited) " else null - Text( - fontSize = 9.sp, - text = "${editedPrefix ?: ""}${content.message.time}", - textAlign = TextAlign.End, - color = content.textColor(), - modifier = Modifier.wrapContentSize() - ) - SendStatus(content.message) - } - } - } - } -} - -@Composable -private fun EncryptedBubbleContent(content: BubbleContent) { - Box(modifier = Modifier.padding(start = 6.dp)) { - Box( - Modifier - .padding(4.dp) - .clip(content.shape) - .background(content.background) - .height(IntrinsicSize.Max), - ) { - Column( - Modifier - .padding(8.dp) - .width(IntrinsicSize.Max) - .defaultMinSize(minWidth = 50.dp) - ) { - if (content.isNotSelf) { - Text( - fontSize = 11.sp, - text = content.message.author.displayName ?: content.message.author.id.value, - maxLines = 1, - color = content.textColor() + color = bubble.textColor() ) } Text( text = "Encrypted message", - color = content.textColor(), + color = bubble.textColor(), fontSize = 15.sp, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, @@ -482,12 +327,12 @@ private fun EncryptedBubbleContent(content: BubbleContent) Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Text( fontSize = 9.sp, - text = "${content.message.time}", + text = event.time, textAlign = TextAlign.End, - color = content.textColor(), + color = bubble.textColor(), modifier = Modifier.wrapContentSize() ) - SendStatus(content.message) + SendStatus(event) } } } @@ -495,13 +340,13 @@ private fun EncryptedBubbleContent(content: BubbleContent) } @Composable -private fun ReplyBubbleContent(content: BubbleContent) { +private fun ReplyBubbleContent(bubble: BubbleMeta, event: RoomEvent.Reply) { Box(modifier = Modifier.padding(start = 6.dp)) { Box( Modifier .padding(4.dp) - .clip(content.shape) - .background(content.background) + .clip(bubble.shape) + .background(bubble.background) .height(IntrinsicSize.Max), ) { Column( @@ -515,26 +360,26 @@ private fun ReplyBubbleContent(content: BubbleContent) { Modifier .fillMaxWidth() .background( - if (content.isNotSelf) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy( + if (bubble.isNotSelf()) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy( alpha = 0.2f ), RoundedCornerShape(12.dp) ) .padding(8.dp) ) { - val replyName = if (!content.isNotSelf && content.message.replyingToSelf) "You" else content.message.replyingTo.author.displayName - ?: content.message.replyingTo.author.id.value + val replyName = if (!bubble.isNotSelf() && event.replyingToSelf) "You" else event.replyingTo.author.displayName + ?: event.replyingTo.author.id.value Text( fontSize = 11.sp, text = replyName, maxLines = 1, - color = content.textColor() + color = bubble.textColor() ) Spacer(modifier = Modifier.height(2.dp)) - when (val replyingTo = content.message.replyingTo) { + when (val replyingTo = event.replyingTo) { is RoomEvent.Message -> { Text( text = replyingTo.content, - color = content.textColor().copy(alpha = 0.8f), + color = bubble.textColor().copy(alpha = 0.8f), fontSize = 14.sp, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, @@ -564,7 +409,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { is RoomEvent.Encrypted -> { Text( text = "Encrypted message", - color = content.textColor().copy(alpha = 0.8f), + color = bubble.textColor().copy(alpha = 0.8f), fontSize = 14.sp, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, @@ -575,19 +420,19 @@ private fun ReplyBubbleContent(content: BubbleContent) { Spacer(modifier = Modifier.height(12.dp)) - if (content.isNotSelf) { + if (bubble.isNotSelf()) { Text( fontSize = 11.sp, - text = content.message.message.author.displayName ?: content.message.message.author.id.value, + text = event.message.author.displayName ?: event.message.author.id.value, maxLines = 1, - color = content.textColor() + color = bubble.textColor() ) } - when (val message = content.message.message) { + when (val message = event.message) { is RoomEvent.Message -> { Text( text = message.content, - color = content.textColor(), + color = bubble.textColor(), fontSize = 15.sp, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, @@ -617,7 +462,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { is RoomEvent.Encrypted -> { Text( text = "Encrypted message", - color = content.textColor(), + color = bubble.textColor(), fontSize = 15.sp, modifier = Modifier.wrapContentSize(), textAlign = TextAlign.Start, @@ -629,12 +474,12 @@ private fun ReplyBubbleContent(content: BubbleContent) { Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Text( fontSize = 9.sp, - text = content.message.time, + text = event.time, textAlign = TextAlign.End, - color = content.textColor(), + color = bubble.textColor(), modifier = Modifier.wrapContentSize() ) - SendStatus(content.message.message) + SendStatus(event.message) } } } @@ -643,7 +488,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { @Composable -private fun RowScope.SendStatus(message: RoomEvent) { +private fun SendStatus(message: RoomEvent) { when (val meta = message.meta) { MessageMeta.FromServer -> { // last message is self From d17ee34d78878a12443d662c79707fec61ac01db Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 18 Oct 2022 22:25:52 +0100 Subject: [PATCH 02/16] porting image bubble to design system --- .../app/dapk/st/design/components/Bubble.kt | 127 ++++++++++++++---- .../app/dapk/st/messenger/MessengerScreen.kt | 118 ++-------------- 2 files changed, 111 insertions(+), 134 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt index 667cbbc..0a61c84 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt @@ -1,5 +1,7 @@ package app.dapk.st.design.components +import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Text @@ -8,11 +10,18 @@ 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.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest data class Event(val authorName: String, val edited: Boolean, val time: String) +data class ImageContent(val width: Int?, val height: Int?, val url: String) @Composable fun Bubble(bubble: BubbleMeta, content: @Composable () -> Unit) { @@ -30,7 +39,7 @@ fun Bubble(bubble: BubbleMeta, content: @Composable () -> Unit) { } @Composable -fun TextBubbleContent(bubble: BubbleMeta, event: Event, textContent: String, status: @Composable () -> Unit) { +fun TextBubble(bubble: BubbleMeta, event: Event, textContent: String, status: @Composable () -> Unit) { Bubble(bubble) { Column( Modifier @@ -39,37 +48,101 @@ fun TextBubbleContent(bubble: BubbleMeta, event: Event, textContent: String, sta .defaultMinSize(minWidth = 50.dp) ) { if (bubble.isNotSelf()) { - Text( - fontSize = 11.sp, - text = event.authorName, - maxLines = 1, - color = bubble.textColor() - ) - } - Text( - text = textContent, - color = bubble.textColor(), - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - - Spacer(modifier = Modifier.height(2.dp)) - Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val editedPrefix = if (event.edited) "(edited) " else null - Text( - fontSize = 9.sp, - text = "${editedPrefix ?: ""}${event.time}", - textAlign = TextAlign.End, - color = bubble.textColor(), - modifier = Modifier.wrapContentSize() - ) - status() + AuthorName(event, bubble) } + TextContent(bubble, text = textContent) + Footer(event, bubble, status) } } } +@Composable +fun EncryptedBubble(bubble: BubbleMeta, event: Event, status: @Composable () -> Unit) { + TextBubble(bubble, event, textContent = "Encrypted message", status) +} + +@Composable +fun ImageBubble(bubble: BubbleMeta, event: Event, imageContent: ImageContent, status: @Composable () -> Unit, imageRequest: ImageRequest) { + Bubble(bubble) { + Column( + Modifier + .padding(8.dp) + .width(IntrinsicSize.Max) + .defaultMinSize(minWidth = 50.dp) + ) { + if (bubble.isNotSelf()) { + AuthorName(event, bubble) + } + + Spacer(modifier = Modifier.height(4.dp)) + Image( + modifier = Modifier.size(imageContent.scale(LocalDensity.current, LocalConfiguration.current)), + painter = rememberAsyncImagePainter(model = imageRequest), + contentDescription = null, + ) + Footer(event, bubble, status) + } + } +} + +private fun ImageContent.scale(density: Density, configuration: Configuration): DpSize { + val height = this@scale.height ?: 250 + val width = this@scale.width ?: 250 + return with(density) { + val scaler = minOf( + height.scalerFor(configuration.screenHeightDp.dp.toPx() * 0.5f), + width.scalerFor(configuration.screenWidthDp.dp.toPx() * 0.6f) + ) + + DpSize( + width = (width * scaler).toDp(), + height = (height * scaler).toDp(), + ) + } +} + + +private fun Int.scalerFor(max: Float): Float { + return max / this +} + + +@Composable +private fun Footer(event: Event, bubble: BubbleMeta, status: @Composable () -> Unit) { + Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 2.dp)) { + val editedPrefix = if (event.edited) "(edited) " else null + Text( + fontSize = 9.sp, + text = "${editedPrefix ?: ""}${event.time}", + textAlign = TextAlign.End, + color = bubble.textColor(), + modifier = Modifier.wrapContentSize() + ) + status() + } +} + +@Composable +private fun TextContent(bubble: BubbleMeta, text: String) { + Text( + text = text, + color = bubble.textColor(), + fontSize = 15.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) +} + +@Composable +private fun AuthorName(event: Event, bubble: BubbleMeta) { + Text( + fontSize = 11.sp, + text = event.authorName, + maxLines = 1, + color = bubble.textColor() + ) +} + @Composable private fun BubbleMeta.textColor(): Color { return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index d5476fe..8d0ae15 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -198,67 +198,19 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions val event = Event(item.author.displayName ?: item.author.id.value, item.edited, item.time) val status = @Composable { SendStatus(item) } when (item) { - is RoomEvent.Image -> MessageImage(this, item) - is RoomEvent.Message -> TextBubbleContent(this, event, item.content, status = status) - is RoomEvent.Reply -> ReplyBubbleContent(this, item) - is RoomEvent.Encrypted -> EncryptedBubbleContent(this, item) - } - } - } - } -} - -@Composable -private fun MessageImage(bubble: BubbleMeta, event: RoomEvent.Image) { - val context = LocalContext.current - - Box(modifier = Modifier.padding(start = 6.dp)) { - Box( - Modifier - .padding(4.dp) - .clip(bubble.shape) - .background(bubble.background) - .height(IntrinsicSize.Max), - ) { - Column( - Modifier - .padding(8.dp) - .width(IntrinsicSize.Max) - .defaultMinSize(minWidth = 50.dp) - ) { - if (bubble.isNotSelf()) { - Text( - fontSize = 11.sp, - text = event.author.displayName ?: event.author.id.value, - maxLines = 1, - color = bubble.textColor() - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - Image( - modifier = Modifier.size(event.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)), - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(context) + is RoomEvent.Image -> { + val context = LocalContext.current + val imageRequest = ImageRequest.Builder(context) .fetcherFactory(LocalDecyptingFetcherFactory.current) - .memoryCacheKey(event.imageMeta.url) - .data(event) + .memoryCacheKey(item.imageMeta.url) + .data(item) .build() - ), - contentDescription = null, - ) - Spacer(modifier = Modifier.height(4.dp)) + ImageBubble(this, event, ImageContent(item.imageMeta.width, item.imageMeta.height, item.imageMeta.url), status, imageRequest) + } - Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val editedPrefix = if (event.edited) "(edited) " else null - Text( - fontSize = 9.sp, - text = "${editedPrefix ?: ""}${event.time}", - textAlign = TextAlign.End, - color = bubble.textColor(), - modifier = Modifier.wrapContentSize() - ) - SendStatus(event) + is RoomEvent.Message -> TextBubble(this, event, item.content, status = status) + is RoomEvent.Reply -> ReplyBubble(this, item) + is RoomEvent.Encrypted -> EncryptedBubble(this, event, status) } } } @@ -292,55 +244,7 @@ private fun BubbleMeta.textColor(): Color { } @Composable -private fun EncryptedBubbleContent(bubble: BubbleMeta, event: RoomEvent.Encrypted) { - Box(modifier = Modifier.padding(start = 6.dp)) { - Box( - Modifier - .padding(4.dp) - .clip(bubble.shape) - .background(bubble.background) - .height(IntrinsicSize.Max), - ) { - Column( - Modifier - .padding(8.dp) - .width(IntrinsicSize.Max) - .defaultMinSize(minWidth = 50.dp) - ) { - if (bubble.isNotSelf()) { - Text( - fontSize = 11.sp, - text = event.author.displayName ?: event.author.id.value, - maxLines = 1, - color = bubble.textColor() - ) - } - Text( - text = "Encrypted message", - color = bubble.textColor(), - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - - Spacer(modifier = Modifier.height(2.dp)) - Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Text( - fontSize = 9.sp, - text = event.time, - textAlign = TextAlign.End, - color = bubble.textColor(), - modifier = Modifier.wrapContentSize() - ) - SendStatus(event) - } - } - } - } -} - -@Composable -private fun ReplyBubbleContent(bubble: BubbleMeta, event: RoomEvent.Reply) { +private fun ReplyBubble(bubble: BubbleMeta, event: RoomEvent.Reply) { Box(modifier = Modifier.padding(start = 6.dp)) { Box( Modifier From 6acbdaf40f656fa6e616a3c7db21ef5a24f99237 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 05:36:24 +0000 Subject: [PATCH 03/16] Bump kluent from 1.70 to 1.71 Bumps [kluent](https://github.com/MarkusAmshove/Kluent) from 1.70 to 1.71. - [Release notes](https://github.com/MarkusAmshove/Kluent/releases) - [Changelog](https://github.com/MarkusAmshove/Kluent/blob/master/CHANGELOG.md) - [Commits](https://github.com/MarkusAmshove/Kluent/compare/v1.70...v1.71) --- updated-dependencies: - dependency-name: org.amshove.kluent:kluent dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 183c92f..6e77fe3 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -144,7 +144,7 @@ ext.Dependencies.with { accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.25.1" junit = "junit:junit:4.13.2" - kluent = "org.amshove.kluent:kluent:1.70" + kluent = "org.amshove.kluent:kluent:1.71" mockk = 'io.mockk:mockk:1.13.2' matrixOlm = "org.matrix.android:olm-sdk:3.2.12" From e024860a77db60b864a32feb8399c06c096413cd Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 11:46:33 +0100 Subject: [PATCH 04/16] extract reply bubble view to the components module --- .../st/design/components/AlignedContainer.kt | 2 +- .../app/dapk/st/design/components/Bubble.kt | 194 ++++++++++++---- .../app/dapk/st/messenger/MessengerScreen.kt | 209 ++---------------- 3 files changed, 170 insertions(+), 235 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt index 5eea262..bad52b4 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/AlignedContainer.kt @@ -32,7 +32,7 @@ data class BubbleMeta( fun BubbleMeta.isNotSelf() = !this.isSelf @Composable -fun LazyItemScope.AlignedContainer( +fun LazyItemScope.AlignedDraggableContainer( avatar: Avatar, isSelf: Boolean, wasPreviousMessageSameSender: Boolean, diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt index 0a61c84..3533232 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -20,72 +21,156 @@ import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest -data class Event(val authorName: String, val edited: Boolean, val time: String) -data class ImageContent(val width: Int?, val height: Int?, val url: String) +sealed interface BubbleModel { + val event: Event + + data class Text(val content: String, override val event: Event) : BubbleModel + data class Encrypted(override val event: Event) : BubbleModel + data class Image(val imageContent: ImageContent, val imageRequest: ImageRequest, override val event: Event) : BubbleModel { + data class ImageContent(val width: Int?, val height: Int?, val url: String) + } + + data class Reply(val replyingTo: BubbleModel, val reply: BubbleModel) : BubbleModel { + override val event = reply.event + } + + data class Event(val authorId: String, val authorName: String, val edited: Boolean, val time: String) + +} + +private fun BubbleModel.Reply.isReplyingToSelf() = this.replyingTo.event.authorId == this.reply.event.authorId + @Composable -fun Bubble(bubble: BubbleMeta, content: @Composable () -> Unit) { - Box(modifier = Modifier.padding(start = 6.dp)) { - Box( - Modifier - .padding(4.dp) - .clip(bubble.shape) - .background(bubble.background) - .height(IntrinsicSize.Max), - ) { - content() - } +fun MessageBubble(bubble: BubbleMeta, model: BubbleModel, status: @Composable () -> Unit) { + when (model) { + is BubbleModel.Text -> TextBubble(bubble, model, status) + is BubbleModel.Encrypted -> EncryptedBubble(bubble, model, status) + is BubbleModel.Image -> ImageBubble(bubble, model, status) + is BubbleModel.Reply -> ReplyBubble(bubble, model, status) } } @Composable -fun TextBubble(bubble: BubbleMeta, event: Event, textContent: String, status: @Composable () -> Unit) { +private fun TextBubble(bubble: BubbleMeta, model: BubbleModel.Text, status: @Composable () -> Unit) { Bubble(bubble) { - Column( - Modifier - .padding(8.dp) - .width(IntrinsicSize.Max) - .defaultMinSize(minWidth = 50.dp) - ) { - if (bubble.isNotSelf()) { - AuthorName(event, bubble) - } - TextContent(bubble, text = textContent) - Footer(event, bubble, status) + if (bubble.isNotSelf()) { + AuthorName(model.event, bubble) } + TextContent(bubble, text = model.content) + Footer(model.event, bubble, status) } } @Composable -fun EncryptedBubble(bubble: BubbleMeta, event: Event, status: @Composable () -> Unit) { - TextBubble(bubble, event, textContent = "Encrypted message", status) +private fun EncryptedBubble(bubble: BubbleMeta, model: BubbleModel.Encrypted, status: @Composable () -> Unit) { + TextBubble(bubble, BubbleModel.Text(content = "Encrypted message", model.event), status) } @Composable -fun ImageBubble(bubble: BubbleMeta, event: Event, imageContent: ImageContent, status: @Composable () -> Unit, imageRequest: ImageRequest) { +private fun ImageBubble(bubble: BubbleMeta, model: BubbleModel.Image, status: @Composable () -> Unit) { + Bubble(bubble) { + if (bubble.isNotSelf()) { + AuthorName(model.event, bubble) + } + + Spacer(modifier = Modifier.height(4.dp)) + Image( + modifier = Modifier.size(model.imageContent.scale(LocalDensity.current, LocalConfiguration.current)), + painter = rememberAsyncImagePainter(model = model.imageRequest), + contentDescription = null, + ) + Footer(model.event, bubble, status) + } +} + +@Composable +private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @Composable () -> Unit) { Bubble(bubble) { Column( Modifier + .fillMaxWidth() + .background( + if (bubble.isNotSelf()) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy( + alpha = 0.2f + ), RoundedCornerShape(12.dp) + ) .padding(8.dp) - .width(IntrinsicSize.Max) - .defaultMinSize(minWidth = 50.dp) ) { - if (bubble.isNotSelf()) { - AuthorName(event, bubble) - } - - Spacer(modifier = Modifier.height(4.dp)) - Image( - modifier = Modifier.size(imageContent.scale(LocalDensity.current, LocalConfiguration.current)), - painter = rememberAsyncImagePainter(model = imageRequest), - contentDescription = null, + val replyName = if (!bubble.isNotSelf() && model.isReplyingToSelf()) "You" else model.replyingTo.event.authorName + Text( + fontSize = 11.sp, + text = replyName, + maxLines = 1, + color = bubble.textColor() ) - Footer(event, bubble, status) + Spacer(modifier = Modifier.height(2.dp)) + + when (val replyingTo = model.replyingTo) { + is BubbleModel.Text -> { + Text( + text = replyingTo.content, + color = bubble.textColor().copy(alpha = 0.8f), + fontSize = 14.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } + + is BubbleModel.Encrypted -> { + Text( + text = "Encrypted message", + color = bubble.textColor().copy(alpha = 0.8f), + fontSize = 14.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } + + is BubbleModel.Image -> { + Spacer(modifier = Modifier.height(4.dp)) + Image( + modifier = Modifier.size(replyingTo.imageContent.scale(LocalDensity.current, LocalConfiguration.current)), + painter = rememberAsyncImagePainter(replyingTo.imageRequest), + contentDescription = null, + ) + Spacer(modifier = Modifier.height(4.dp)) + } + + is BubbleModel.Reply -> { + // TODO - a reply to a reply + } + } } + + Spacer(modifier = Modifier.height(12.dp)) + + if (bubble.isNotSelf()) { + AuthorName(model.event, bubble) + } + + when (val message = model.reply) { + is BubbleModel.Text -> TextContent(bubble, message.content) + is BubbleModel.Encrypted -> TextContent(bubble, "Encrypted message") + is BubbleModel.Image -> { + Spacer(modifier = Modifier.height(4.dp)) + Image( + modifier = Modifier.size(message.imageContent.scale(LocalDensity.current, LocalConfiguration.current)), + painter = rememberAsyncImagePainter(model = message.imageRequest), + contentDescription = null, + ) + } + + is BubbleModel.Reply -> { + // TODO - a reply to a reply + } + } + + Footer(model.event, bubble, status) } } -private fun ImageContent.scale(density: Density, configuration: Configuration): DpSize { +private fun BubbleModel.Image.ImageContent.scale(density: Density, configuration: Configuration): DpSize { val height = this@scale.height ?: 250 val width = this@scale.width ?: 250 return with(density) { @@ -101,14 +186,35 @@ private fun ImageContent.scale(density: Density, configuration: Configuration): } } - private fun Int.scalerFor(max: Float): Float { return max / this } @Composable -private fun Footer(event: Event, bubble: BubbleMeta, status: @Composable () -> Unit) { +private fun Bubble(bubble: BubbleMeta, content: @Composable () -> Unit) { + Box(modifier = Modifier.padding(start = 6.dp)) { + Box( + Modifier + .padding(4.dp) + .clip(bubble.shape) + .background(bubble.background) + .height(IntrinsicSize.Max), + ) { + Column( + Modifier + .padding(8.dp) + .width(IntrinsicSize.Max) + .defaultMinSize(minWidth = 50.dp) + ) { + content() + } + } + } +} + +@Composable +private fun Footer(event: BubbleModel.Event, bubble: BubbleMeta, status: @Composable () -> Unit) { Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 2.dp)) { val editedPrefix = if (event.edited) "(edited) " else null Text( @@ -134,7 +240,7 @@ private fun TextContent(bubble: BubbleMeta, text: String) { } @Composable -private fun AuthorName(event: Event, bubble: BubbleMeta) { +private fun AuthorName(event: BubbleModel.Event, bubble: BubbleMeta) { Text( fontSize = 11.sp, text = event.authorName, diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 8d0ae15..a1fa188 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -1,6 +1,5 @@ package app.dapk.st.messenger -import android.content.res.Configuration import androidx.activity.result.ActivityResultLauncher import androidx.compose.animation.* import androidx.compose.animation.core.tween @@ -24,10 +23,8 @@ import androidx.compose.runtime.* 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.graphics.SolidColor import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.input.KeyboardCapitalization @@ -189,208 +186,40 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions val previousEvent = if (index != 0) state.events[index - 1] else null val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id - AlignedContainer( + AlignedDraggableContainer( avatar = Avatar(item.author.avatarUrl?.value, item.author.displayName ?: item.author.id.value), isSelf = self == item.author.id, wasPreviousMessageSameSender = wasPreviousMessageSameSender, onReply = { replyActions.onReply(item) }, ) { - val event = Event(item.author.displayName ?: item.author.id.value, item.edited, item.time) + val event = BubbleModel.Event(item.author.id.value, item.author.displayName ?: item.author.id.value, item.edited, item.time) val status = @Composable { SendStatus(item) } - when (item) { - is RoomEvent.Image -> { - val context = LocalContext.current - val imageRequest = ImageRequest.Builder(context) - .fetcherFactory(LocalDecyptingFetcherFactory.current) - .memoryCacheKey(item.imageMeta.url) - .data(item) - .build() - ImageBubble(this, event, ImageContent(item.imageMeta.width, item.imageMeta.height, item.imageMeta.url), status, imageRequest) - } - - is RoomEvent.Message -> TextBubble(this, event, item.content, status = status) - is RoomEvent.Reply -> ReplyBubble(this, item) - is RoomEvent.Encrypted -> EncryptedBubble(this, event, status) - } + MessageBubble(this, item.toModel(event), status) } } } } -private fun RoomEvent.Image.ImageMeta.scale(density: Density, configuration: Configuration): DpSize { - val height = this@scale.height ?: 250 - val width = this@scale.width ?: 250 - return with(density) { - val scaler = minOf( - height.scalerFor(configuration.screenHeightDp.dp.toPx() * 0.5f), - width.scalerFor(configuration.screenWidthDp.dp.toPx() * 0.6f) - ) +@Composable +private fun RoomEvent.toModel(event: BubbleModel.Event): BubbleModel = when (this) { + is RoomEvent.Message -> BubbleModel.Text(this.content, event) + is RoomEvent.Encrypted -> BubbleModel.Encrypted(event) + is RoomEvent.Image -> { + val context = LocalContext.current + val imageRequest = ImageRequest.Builder(context) + .fetcherFactory(LocalDecyptingFetcherFactory.current) + .memoryCacheKey(this.imageMeta.url) + .data(this) + .build() + val imageContent = BubbleModel.Image.ImageContent(this.imageMeta.width, this.imageMeta.height, this.imageMeta.url) + BubbleModel.Image(imageContent, imageRequest, event) + } - DpSize( - width = (width * scaler).toDp(), - height = (height * scaler).toDp(), - ) + is RoomEvent.Reply -> { + BubbleModel.Reply(this.replyingTo.toModel(event), this.message.toModel(event)) } } - -private fun Int.scalerFor(max: Float): Float { - return max / this -} - -@Composable -private fun BubbleMeta.textColor(): Color { - return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble -} - -@Composable -private fun ReplyBubble(bubble: BubbleMeta, event: RoomEvent.Reply) { - Box(modifier = Modifier.padding(start = 6.dp)) { - Box( - Modifier - .padding(4.dp) - .clip(bubble.shape) - .background(bubble.background) - .height(IntrinsicSize.Max), - ) { - Column( - Modifier - .padding(8.dp) - .width(IntrinsicSize.Max) - .defaultMinSize(minWidth = 50.dp) - ) { - val context = LocalContext.current - Column( - Modifier - .fillMaxWidth() - .background( - if (bubble.isNotSelf()) SmallTalkTheme.extendedColors.onOthersBubble.copy(alpha = 0.1f) else SmallTalkTheme.extendedColors.onSelfBubble.copy( - alpha = 0.2f - ), RoundedCornerShape(12.dp) - ) - .padding(8.dp) - ) { - val replyName = if (!bubble.isNotSelf() && event.replyingToSelf) "You" else event.replyingTo.author.displayName - ?: event.replyingTo.author.id.value - Text( - fontSize = 11.sp, - text = replyName, - maxLines = 1, - color = bubble.textColor() - ) - Spacer(modifier = Modifier.height(2.dp)) - when (val replyingTo = event.replyingTo) { - is RoomEvent.Message -> { - Text( - text = replyingTo.content, - color = bubble.textColor().copy(alpha = 0.8f), - fontSize = 14.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - } - - is RoomEvent.Image -> { - Spacer(modifier = Modifier.height(4.dp)) - Image( - modifier = Modifier.size(replyingTo.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)), - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(context) - .fetcherFactory(LocalDecyptingFetcherFactory.current) - .memoryCacheKey(replyingTo.imageMeta.url) - .data(replyingTo) - .build() - ), - contentDescription = null, - ) - Spacer(modifier = Modifier.height(4.dp)) - } - - is RoomEvent.Reply -> { - // TODO - a reply to a reply - } - - is RoomEvent.Encrypted -> { - Text( - text = "Encrypted message", - color = bubble.textColor().copy(alpha = 0.8f), - fontSize = 14.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - } - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - if (bubble.isNotSelf()) { - Text( - fontSize = 11.sp, - text = event.message.author.displayName ?: event.message.author.id.value, - maxLines = 1, - color = bubble.textColor() - ) - } - when (val message = event.message) { - is RoomEvent.Message -> { - Text( - text = message.content, - color = bubble.textColor(), - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - } - - is RoomEvent.Image -> { - Spacer(modifier = Modifier.height(4.dp)) - Image( - modifier = Modifier.size(message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)), - painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(context) - .data(message) - .memoryCacheKey(message.imageMeta.url) - .fetcherFactory(LocalDecyptingFetcherFactory.current) - .build() - ), - contentDescription = null, - ) - Spacer(modifier = Modifier.height(4.dp)) - } - - is RoomEvent.Reply -> { - // TODO - a reply to a reply - } - - is RoomEvent.Encrypted -> { - Text( - text = "Encrypted message", - color = bubble.textColor(), - fontSize = 15.sp, - modifier = Modifier.wrapContentSize(), - textAlign = TextAlign.Start, - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Text( - fontSize = 9.sp, - text = event.time, - textAlign = TextAlign.End, - color = bubble.textColor(), - modifier = Modifier.wrapContentSize() - ) - SendStatus(event.message) - } - } - } - } -} - - @Composable private fun SendStatus(message: RoomEvent) { when (val meta = message.meta) { From 05363641211be2db3fcb34bc52f4caa922d5f6ef Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 11:52:16 +0100 Subject: [PATCH 05/16] extract member cache sizes to vars --- .../app/dapk/st/matrix/room/internal/RoomMembers.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt index 9c7a03b..d9004d0 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt @@ -31,6 +31,7 @@ class RoomMembers(private val memberStore: MemberStore, private val membersCache missingIds.isNotEmpty() -> { (memberStore.query(roomId, missingIds).also { membersCache.insert(roomId, it) } + cachedMembers) } + else -> cachedMembers } } @@ -44,14 +45,17 @@ class RoomMembers(private val memberStore: MemberStore, private val membersCache } } +private const val ROOMS_TO_CACHE_MEMBERS_FOR_SIZE = 12 +private const val MEMBERS_TO_CACHE_PER_ROOM = 25 + class RoomMembersCache { - private val cache = LRUCache>(maxSize = 12) + private val cache = LRUCache>(maxSize = ROOMS_TO_CACHE_MEMBERS_FOR_SIZE) fun room(roomId: RoomId) = cache.get(roomId) fun insert(roomId: RoomId, members: List) { - val map = cache.getOrPut(roomId) { LRUCache(maxSize = 25) } + val map = cache.getOrPut(roomId) { LRUCache(maxSize = MEMBERS_TO_CACHE_PER_ROOM) } members.forEach { map.put(it.id, it) } } } From be002709584351f9041944d217ee2b338d2dc4be Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 12:01:29 +0100 Subject: [PATCH 06/16] updating setup-java to latest --- .github/workflows/assemble.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml index ed0cc2a..26935f8 100644 --- a/.github/workflows/assemble.yml +++ b/.github/workflows/assemble.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'adopt' java-version: '11' From 6ca4ecce682bc8b808136c88e138bc041027cdc8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 12:09:51 +0100 Subject: [PATCH 07/16] using a fallback user when we're unable to find the author fixes potential crash when failing to lookup a user by id --- .../st/matrix/sync/internal/sync/RoomEventFactory.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt index 4ad293b..2abbd45 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt @@ -1,14 +1,14 @@ package app.dapk.st.matrix.sync.internal.sync -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserCredentials -import app.dapk.st.matrix.common.convertMxUrToUrl +import app.dapk.st.matrix.common.* import app.dapk.st.matrix.sync.MessageMeta import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomMembersService import app.dapk.st.matrix.sync.find import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent +private val UNKNOWN_AUTHOR = RoomMember(id = UserId("unknown"), displayName = null, avatarUrl = null) + internal class RoomEventFactory( private val roomMembersService: RoomMembersService ) { @@ -21,7 +21,7 @@ internal class RoomEventFactory( ) = RoomEvent.Message( eventId = this.id, content = content, - author = roomMembersService.find(roomId, this.senderId)!!, + author = roomMembersService.find(roomId, this.senderId) ?: UNKNOWN_AUTHOR, utcTimestamp = utcTimestamp, meta = MessageMeta.FromServer, edited = edited, @@ -36,7 +36,7 @@ internal class RoomEventFactory( ) = RoomEvent.Image( eventId = this.id, imageMeta = imageMeta, - author = roomMembersService.find(roomId, this.senderId)!!, + author = roomMembersService.find(roomId, this.senderId) ?: UNKNOWN_AUTHOR, utcTimestamp = utcTimestamp, meta = MessageMeta.FromServer, edited = edited, From 1ac9ce1ec0e7fc58e90a75566585587b2df739c5 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 12:16:31 +0100 Subject: [PATCH 08/16] extracting the firebase version to the dependencies file and updating to latest --- dependencies.gradle | 2 ++ domains/firebase/crashlytics/build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 6e77fe3..dae5794 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -114,6 +114,8 @@ ext.Dependencies.with { kotlinCompilerExtensionVersion = "1.3.2" firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1" + firebaseBom = "com.google.firebase:firebase-bom:31.0.1" + jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5" } diff --git a/domains/firebase/crashlytics/build.gradle b/domains/firebase/crashlytics/build.gradle index f502422..cbb5990 100644 --- a/domains/firebase/crashlytics/build.gradle +++ b/domains/firebase/crashlytics/build.gradle @@ -2,6 +2,6 @@ applyAndroidLibraryModule(project) dependencies { implementation project(':core') - implementation platform('com.google.firebase:firebase-bom:29.0.3') + implementation platform(Dependencies.google.firebaseBom) implementation 'com.google.firebase:firebase-crashlytics' } From 80c1847a481bc58e1d22f46cb3035be54db81a5f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 12:34:34 +0100 Subject: [PATCH 09/16] reusing mockk version and upgrading crashlytics gradle plugin --- build.gradle | 4 ++-- dependencies.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index cc4e835..997f289 100644 --- a/build.gradle +++ b/build.gradle @@ -132,7 +132,7 @@ ext.kotlinTest = { dependencies -> dependencies.testImplementation Dependencies.mavenCentral.kluent dependencies.testImplementation Dependencies.mavenCentral.kotlinTest dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.7.20" - dependencies.testImplementation 'io.mockk:mockk:1.13.2' + dependencies.testImplementation Dependencies.mavenCentral.mockk dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' @@ -140,7 +140,7 @@ ext.kotlinTest = { dependencies -> } ext.kotlinFixtures = { dependencies -> - dependencies.testFixturesImplementation 'io.mockk:mockk:1.13.1' + dependencies.testFixturesImplementation Dependencies.mavenCentral.mockk dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore } diff --git a/dependencies.gradle b/dependencies.gradle index dae5794..c3417d1 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -113,7 +113,7 @@ ext.Dependencies.with { androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" kotlinCompilerExtensionVersion = "1.3.2" - firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1" + firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.2" firebaseBom = "com.google.firebase:firebase-bom:31.0.1" jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5" From e87e97fa82cdee3c36225c809e51ffc456fa43de Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 12:38:15 +0100 Subject: [PATCH 10/16] upgrading material3 library --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index c3417d1..b847b66 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -108,7 +108,7 @@ ext.Dependencies.with { androidxComposeUi = "androidx.compose.ui:ui:${composeVer}" androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}" - androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-beta03" + androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-rc01" androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" kotlinCompilerExtensionVersion = "1.3.2" From 400e1812ddf695aa623d2a0d7a3f585a5d160718 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 12:41:27 +0100 Subject: [PATCH 11/16] upgrading activity-compose to latest --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index b847b66..6b00507 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -110,7 +110,7 @@ ext.Dependencies.with { androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}" androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-rc01" androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" - androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" + androidxActivityCompose = "androidx.activity:activity-compose:1.6.0" kotlinCompilerExtensionVersion = "1.3.2" firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.2" From 4a912c70faceede9d39451178eb1deff1b4cb6c6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 14:02:13 +0100 Subject: [PATCH 12/16] reusing image request build base instance to avoid redundant creations --- .../kotlin/app/dapk/st/messenger/MessengerActivity.kt | 9 ++++----- .../main/kotlin/app/dapk/st/messenger/MessengerScreen.kt | 4 +--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt index 6849d35..045e6dc 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable -import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider @@ -16,9 +15,10 @@ import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.matrix.common.RoomId import app.dapk.st.messenger.gallery.GetImageFromGallery import app.dapk.st.navigator.MessageAttachment +import coil.request.ImageRequest import kotlinx.parcelize.Parcelize -val LocalDecyptingFetcherFactory = staticCompositionLocalOf { throw IllegalAccessError() } +val LocalImageRequestFactory = staticCompositionLocalOf { throw IllegalAccessError() } class MessengerActivity : DapkActivity() { @@ -50,7 +50,7 @@ class MessengerActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val payload = readPayload() - val factory = module.decryptingFetcherFactory(RoomId(payload.roomId)) + val factory = ImageRequest.Builder(applicationContext).fetcherFactory(module.decryptingFetcherFactory(RoomId(payload.roomId))) val galleryLauncher = registerForActivityResult(GetImageFromGallery()) { it?.let { uri -> @@ -65,10 +65,9 @@ class MessengerActivity : DapkActivity() { } } - setContent { Surface(Modifier.fillMaxSize()) { - CompositionLocalProvider(LocalDecyptingFetcherFactory provides factory) { + CompositionLocalProvider(LocalImageRequestFactory provides factory) { MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator, galleryLauncher) } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index a1fa188..06bb911 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -205,9 +205,7 @@ private fun RoomEvent.toModel(event: BubbleModel.Event): BubbleModel = when (thi is RoomEvent.Message -> BubbleModel.Text(this.content, event) is RoomEvent.Encrypted -> BubbleModel.Encrypted(event) is RoomEvent.Image -> { - val context = LocalContext.current - val imageRequest = ImageRequest.Builder(context) - .fetcherFactory(LocalDecyptingFetcherFactory.current) + val imageRequest = LocalImageRequestFactory.current .memoryCacheKey(this.imageMeta.url) .data(this) .build() From f695b319ebb547a5dd11233de1d3ba6d3fa8d536 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 16:16:03 +0100 Subject: [PATCH 13/16] handling bubble long clicks as copy text events --- .../kotlin/app/dapk/st/graph/AppModule.kt | 1 + .../app/dapk/st/design/components/Bubble.kt | 36 ++++++++++--------- .../app/dapk/st/messenger/CopyToClipboard.kt | 22 ++++++++++++ .../app/dapk/st/messenger/LocalIdFactory.kt | 7 ---- .../app/dapk/st/messenger/MessengerModule.kt | 5 +++ .../app/dapk/st/messenger/MessengerScreen.kt | 33 ++++++++++------- .../app/dapk/st/messenger/MessengerState.kt | 1 + .../dapk/st/messenger/MessengerViewModel.kt | 33 +++++++++++++++++ .../st/messenger/MessengerViewModelTest.kt | 10 ++++++ 9 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 features/messenger/src/main/kotlin/app/dapk/st/messenger/CopyToClipboard.kt delete mode 100644 features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalIdFactory.kt diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index a77fc82..47caa4c 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -160,6 +160,7 @@ internal class FeatureModules internal constructor( chatEngineModule.engine, context, storeModule.value.messageStore(), + deviceMeta, ) } val homeModule by unsafeLazy { HomeModule(chatEngineModule.engine, storeModule.value, buildMeta) } diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt index 3533232..0a59935 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Bubble.kt @@ -1,8 +1,10 @@ package app.dapk.st.design.components import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -40,20 +42,20 @@ sealed interface BubbleModel { private fun BubbleModel.Reply.isReplyingToSelf() = this.replyingTo.event.authorId == this.reply.event.authorId - @Composable -fun MessageBubble(bubble: BubbleMeta, model: BubbleModel, status: @Composable () -> Unit) { +fun MessageBubble(bubble: BubbleMeta, model: BubbleModel, status: @Composable () -> Unit, onLongClick: (BubbleModel) -> Unit) { + val itemisedLongClick = { onLongClick.invoke(model) } when (model) { - is BubbleModel.Text -> TextBubble(bubble, model, status) - is BubbleModel.Encrypted -> EncryptedBubble(bubble, model, status) - is BubbleModel.Image -> ImageBubble(bubble, model, status) - is BubbleModel.Reply -> ReplyBubble(bubble, model, status) + is BubbleModel.Text -> TextBubble(bubble, model, status, itemisedLongClick) + is BubbleModel.Encrypted -> EncryptedBubble(bubble, model, status, itemisedLongClick) + is BubbleModel.Image -> ImageBubble(bubble, model, status, itemisedLongClick) + is BubbleModel.Reply -> ReplyBubble(bubble, model, status, itemisedLongClick) } } @Composable -private fun TextBubble(bubble: BubbleMeta, model: BubbleModel.Text, status: @Composable () -> Unit) { - Bubble(bubble) { +private fun TextBubble(bubble: BubbleMeta, model: BubbleModel.Text, status: @Composable () -> Unit, onLongClick: () -> Unit) { + Bubble(bubble, onLongClick) { if (bubble.isNotSelf()) { AuthorName(model.event, bubble) } @@ -63,13 +65,13 @@ private fun TextBubble(bubble: BubbleMeta, model: BubbleModel.Text, status: @Com } @Composable -private fun EncryptedBubble(bubble: BubbleMeta, model: BubbleModel.Encrypted, status: @Composable () -> Unit) { - TextBubble(bubble, BubbleModel.Text(content = "Encrypted message", model.event), status) +private fun EncryptedBubble(bubble: BubbleMeta, model: BubbleModel.Encrypted, status: @Composable () -> Unit, onLongClick: () -> Unit) { + TextBubble(bubble, BubbleModel.Text(content = "Encrypted message", model.event), status, onLongClick) } @Composable -private fun ImageBubble(bubble: BubbleMeta, model: BubbleModel.Image, status: @Composable () -> Unit) { - Bubble(bubble) { +private fun ImageBubble(bubble: BubbleMeta, model: BubbleModel.Image, status: @Composable () -> Unit, onLongClick: () -> Unit) { + Bubble(bubble, onLongClick) { if (bubble.isNotSelf()) { AuthorName(model.event, bubble) } @@ -85,8 +87,8 @@ private fun ImageBubble(bubble: BubbleMeta, model: BubbleModel.Image, status: @C } @Composable -private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @Composable () -> Unit) { - Bubble(bubble) { +private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @Composable () -> Unit, onLongClick: () -> Unit) { + Bubble(bubble, onLongClick) { Column( Modifier .fillMaxWidth() @@ -191,15 +193,17 @@ private fun Int.scalerFor(max: Float): Float { } +@OptIn(ExperimentalFoundationApi::class) @Composable -private fun Bubble(bubble: BubbleMeta, content: @Composable () -> Unit) { +private fun Bubble(bubble: BubbleMeta, onLongClick: () -> Unit, content: @Composable () -> Unit) { Box(modifier = Modifier.padding(start = 6.dp)) { Box( Modifier .padding(4.dp) .clip(bubble.shape) .background(bubble.background) - .height(IntrinsicSize.Max), + .height(IntrinsicSize.Max) + .combinedClickable(onLongClick = onLongClick, onClick = {}), ) { Column( Modifier diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/CopyToClipboard.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/CopyToClipboard.kt new file mode 100644 index 0000000..a389b3c --- /dev/null +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/CopyToClipboard.kt @@ -0,0 +1,22 @@ +package app.dapk.st.messenger + +import android.content.ClipData +import android.content.ClipboardManager + +class CopyToClipboard(private val clipboard: ClipboardManager) { + + fun copy(copyable: Copyable) { + + clipboard.addPrimaryClipChangedListener { } + + when (copyable) { + is Copyable.Text -> { + clipboard.setPrimaryClip(ClipData.newPlainText("", copyable.value)) + } + } + } + + sealed interface Copyable { + data class Text(val value: String) : Copyable + } +} diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalIdFactory.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalIdFactory.kt deleted file mode 100644 index ecb1777..0000000 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalIdFactory.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.dapk.st.messenger - -import java.util.* - -internal class LocalIdFactory { - fun create() = "local.${UUID.randomUUID()}" -} \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt index e46f37a..bcb5ad4 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt @@ -1,6 +1,8 @@ package app.dapk.st.messenger +import android.content.ClipboardManager import android.content.Context +import app.dapk.st.core.DeviceMeta import app.dapk.st.core.ProvidableModule import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine @@ -10,12 +12,15 @@ class MessengerModule( private val chatEngine: ChatEngine, private val context: Context, private val messageOptionsStore: MessageOptionsStore, + private val deviceMeta: DeviceMeta, ) : ProvidableModule { internal fun messengerViewModel(): MessengerViewModel { return MessengerViewModel( chatEngine, messageOptionsStore, + CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager), + deviceMeta, ) } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 06bb911..6fe4ce1 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -1,5 +1,6 @@ package app.dapk.st.messenger +import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.compose.animation.* import androidx.compose.animation.core.tween @@ -7,6 +8,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape @@ -71,9 +73,10 @@ internal fun MessengerScreen( else -> null } - val replyActions = ReplyActions( + val messageActions = MessageActions( onReply = { viewModel.post(MessengerAction.ComposerEnterReplyMode(it)) }, - onDismiss = { viewModel.post(MessengerAction.ComposerExitReplyMode) } + onDismiss = { viewModel.post(MessengerAction.ComposerExitReplyMode) }, + onLongClick = { viewModel.post(MessengerAction.CopyToClipboard(it)) } ) Column { @@ -84,13 +87,13 @@ internal fun MessengerScreen( }) when (state.composerState) { is ComposerState.Text -> { - Room(state.roomState, replyActions, onRetry = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) }) + Room(state.roomState, messageActions, onRetry = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) }) TextComposer( state.composerState, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, onSend = { viewModel.post(MessengerAction.ComposerSendText) }, onAttach = { viewModel.startAttachment() }, - replyActions = replyActions, + messageActions = messageActions, ) } @@ -107,6 +110,7 @@ internal fun MessengerScreen( @Composable private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLauncher) { + val context = LocalContext.current StartObserving { this@ObserveEvents.events.launch { when (it) { @@ -115,17 +119,21 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun galleryLauncher.launch(ImageGalleryActivityPayload(it.roomState.roomOverview.roomName ?: "")) } } + + is MessengerEvent.Toast -> { + Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show() + } } } } } @Composable -private fun ColumnScope.Room(roomStateLce: Lce, replyActions: ReplyActions, onRetry: () -> Unit) { +private fun ColumnScope.Room(roomStateLce: Lce, messageActions: MessageActions, onRetry: () -> Unit) { when (val state = roomStateLce) { is Lce.Loading -> CenteredLoading() is Lce.Content -> { - RoomContent(state.value.self, state.value.roomState, replyActions) + RoomContent(state.value.self, state.value.roomState, messageActions) val eventBarHeight = 14.dp val typing = state.value.typing when { @@ -159,7 +167,7 @@ private fun ColumnScope.Room(roomStateLce: Lce, replyActions: Re } @Composable -private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions: ReplyActions) { +private fun ColumnScope.RoomContent(self: UserId, state: RoomState, messageActions: MessageActions) { val listState: LazyListState = rememberLazyListState( initialFirstVisibleItemIndex = 0 ) @@ -190,11 +198,11 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions avatar = Avatar(item.author.avatarUrl?.value, item.author.displayName ?: item.author.id.value), isSelf = self == item.author.id, wasPreviousMessageSameSender = wasPreviousMessageSameSender, - onReply = { replyActions.onReply(item) }, + onReply = { messageActions.onReply(item) }, ) { val event = BubbleModel.Event(item.author.id.value, item.author.displayName ?: item.author.id.value, item.edited, item.time) val status = @Composable { SendStatus(item) } - MessageBubble(this, item.toModel(event), status) + MessageBubble(this, item.toModel(event), status, onLongClick = messageActions.onLongClick) } } } @@ -260,7 +268,7 @@ private fun SendStatus(message: RoomEvent) { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, replyActions: ReplyActions) { +private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit, messageActions: MessageActions) { Row( Modifier .fillMaxWidth() @@ -289,7 +297,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un ) { if (it is RoomEvent.Message) { Box(Modifier.padding(12.dp)) { - Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { + Box(Modifier.padding(8.dp).clickable { messageActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { Icon( modifier = Modifier.size(16.dp), imageVector = Icons.Filled.Close, @@ -420,7 +428,8 @@ private fun AttachmentComposer(state: ComposerState.Attachments, onSend: () -> U } } -class ReplyActions( +class MessageActions( val onReply: (RoomEvent) -> Unit, val onDismiss: () -> Unit, + val onLongClick: (BubbleModel) -> Unit, ) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt index ce4d829..fb50fc9 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt @@ -14,6 +14,7 @@ data class MessengerScreenState( sealed interface MessengerEvent { object SelectImageAttachment : MessengerEvent + data class Toast(val message: String) : MessengerEvent } sealed interface ComposerState { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index 6c87dc0..66de6e1 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -1,8 +1,11 @@ package app.dapk.st.messenger +import android.os.Build import androidx.lifecycle.viewModelScope +import app.dapk.st.core.DeviceMeta import app.dapk.st.core.Lce import app.dapk.st.core.extensions.takeIfContent +import app.dapk.st.design.components.BubbleModel import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.RoomEvent @@ -20,6 +23,8 @@ import kotlinx.coroutines.launch internal class MessengerViewModel( private val chatEngine: ChatEngine, private val messageOptionsStore: MessageOptionsStore, + private val copyToClipboard: CopyToClipboard, + private val deviceMeta: DeviceMeta, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( initialState = MessengerScreenState( @@ -65,6 +70,21 @@ internal class MessengerViewModel( } ) } + + is MessengerAction.CopyToClipboard -> { + viewModelScope.launch { + when (val result = action.model.findCopyableContent()) { + is CopyableResult.Content -> { + copyToClipboard.copy(result.value) + if (deviceMeta.apiVersion <= Build.VERSION_CODES.S_V2) { + _events.emit(MessengerEvent.Toast("Copied to clipboard")) + } + } + + CopyableResult.NothingToCopy -> _events.emit(MessengerEvent.Toast("Nothing to copy")) + } + } + } } } @@ -137,10 +157,23 @@ internal class MessengerViewModel( } +private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) { + is BubbleModel.Encrypted -> CopyableResult.NothingToCopy + is BubbleModel.Image -> CopyableResult.NothingToCopy + is BubbleModel.Reply -> this.reply.findCopyableContent() + is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content)) +} + +private sealed interface CopyableResult { + object NothingToCopy : CopyableResult + data class Content(val value: CopyToClipboard.Copyable) : CopyableResult +} + sealed interface MessengerAction { data class ComposerTextUpdate(val newValue: String) : MessengerAction data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction object ComposerExitReplyMode : MessengerAction + data class CopyToClipboard(val model: BubbleModel) : MessengerAction data class ComposerImageUpdate(val newValue: MessageAttachment) : MessengerAction object ComposerSendText : MessengerAction object ComposerClear : MessengerAction diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt index fdaaadc..ee605bf 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt @@ -1,6 +1,7 @@ package app.dapk.st.messenger import ViewModelTest +import app.dapk.st.core.DeviceMeta import app.dapk.st.core.Lce import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.engine.MessengerState @@ -12,6 +13,7 @@ import app.dapk.st.matrix.common.UserId import fake.FakeChatEngine import fake.FakeMessageOptionsStore import fixture.* +import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.junit.Test @@ -27,10 +29,14 @@ class MessengerViewModelTest { private val fakeMessageOptionsStore = FakeMessageOptionsStore() private val fakeChatEngine = FakeChatEngine() + private val fakeCopyToClipboard = FakeCopyToClipboard() + private val deviceMeta = DeviceMeta(26) private val viewModel = MessengerViewModel( fakeChatEngine, fakeMessageOptionsStore.instance, + fakeCopyToClipboard.instance, + deviceMeta, factory = runViewModelTest.testMutableStateFactory(), ) @@ -110,3 +116,7 @@ fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, m roomState = Lce.Content(roomState), composerState = ComposerState.Text(value = messageContent ?: "", reply = null) ) + +class FakeCopyToClipboard { + val instance = mockk() +} From 4bd75f1bd89e48a75e60c00eef920e1e44aab5a7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 19:17:35 +0100 Subject: [PATCH 14/16] allowing the one time keys count to be optional - for dendrite the field is provided in later syncs --- .../app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt | 2 +- .../app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt index 2bc14c2..39b8a90 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt @@ -13,7 +13,7 @@ internal data class ApiSyncResponse( @SerialName("account_data") val accountData: ApiAccountData? = null, @SerialName("rooms") val rooms: ApiSyncRooms? = null, @SerialName("to_device") val toDevice: ToDevice? = null, - @SerialName("device_one_time_keys_count") val oneTimeKeysCount: Map, + @SerialName("device_one_time_keys_count") val oneTimeKeysCount: Map? = null, @SerialName("next_batch") val nextBatch: SyncToken, @SerialName("prev_batch") val prevBatch: SyncToken? = null, ) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt index f08dd1d..ad02e65 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/SyncSideEffects.kt @@ -28,7 +28,7 @@ internal class SyncSideEffects( notifyDevicesUpdated.notifyChanges(it, requestToken) } - oneTimeKeyProducer.onServerKeyCount(response.oneTimeKeysCount["signed_curve25519"] ?: ServerKeyCount(0)) + oneTimeKeyProducer.onServerKeyCount(response.oneTimeKeysCount?.get("signed_curve25519") ?: ServerKeyCount(0)) val decryptedToDeviceEvents = decryptedToDeviceEvents(response) val roomKeys = handleRoomKeyShares(decryptedToDeviceEvents) From 1f3c62e9c358448821eb664e7b08a2c8d68117cb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 22 Oct 2022 20:06:31 +0100 Subject: [PATCH 15/16] allow state events to be missing/null - dendrite can omit the state field from the sync response --- .../dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt | 2 +- .../st/matrix/sync/internal/sync/RoomOverviewProcessor.kt | 2 +- .../app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt | 2 +- .../app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt index 39b8a90..b4f417c 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt @@ -53,7 +53,7 @@ internal data class ApiInviteEvents( @Serializable internal data class ApiSyncRoom( @SerialName("timeline") val timeline: ApiSyncRoomTimeline, - @SerialName("state") val state: ApiSyncRoomState, + @SerialName("state") val state: ApiSyncRoomState? = null, @SerialName("account_data") val accountData: ApiAccountData? = null, @SerialName("ephemeral") val ephemeral: ApiEphemeral? = null, @SerialName("summary") val summary: ApiRoomSummary? = null, diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt index f4a9149..0218d81 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomOverviewProcessor.kt @@ -13,7 +13,7 @@ internal class RoomOverviewProcessor( ) { suspend fun process(roomToProcess: RoomToProcess, previousState: RoomOverview?, lastMessage: LastMessage?): RoomOverview? { - val combinedEvents = roomToProcess.apiSyncRoom.state.stateEvents + roomToProcess.apiSyncRoom.timeline.apiTimelineEvents + val combinedEvents = (roomToProcess.apiSyncRoom.state?.stateEvents.orEmpty()) + roomToProcess.apiSyncRoom.timeline.apiTimelineEvents val isEncrypted = combinedEvents.any { it is ApiTimelineEvent.Encryption } val readMarker = roomToProcess.apiSyncRoom.accountData?.events?.filterIsInstance()?.firstOrNull()?.content?.eventId return when (previousState) { diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt index eca9c77..9691686 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt @@ -50,7 +50,7 @@ internal class RoomProcessor( } private fun ApiSyncRoom.collectMembers(userCredentials: UserCredentials): List { - return (this.state.stateEvents + this.timeline.apiTimelineEvents) + return (this.state?.stateEvents.orEmpty() + this.timeline.apiTimelineEvents) .filterIsInstance() .mapNotNull { when { diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt index 9bba338..a606895 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt @@ -70,7 +70,7 @@ internal class SyncReducer( } private fun findRoomsLeft(response: ApiSyncResponse, userCredentials: UserCredentials) = response.rooms?.leave?.filter { - it.value.state.stateEvents.filterIsInstance().any { + it.value.state?.stateEvents.orEmpty().filterIsInstance().any { it.content.membership.isLeave() && it.senderId == userCredentials.userId } }?.map { it.key } ?: emptyList() @@ -91,7 +91,7 @@ internal class SyncReducer( } private fun Map.keepRoomsWithChanges() = this.filter { - it.value.state.stateEvents.isNotEmpty() || + it.value.state?.stateEvents.orEmpty().isNotEmpty() || it.value.timeline.apiTimelineEvents.isNotEmpty() || it.value.accountData?.events?.isNotEmpty() == true || it.value.ephemeral?.events?.isNotEmpty() == true From 173591e6bb7878489d049b1dbed1545e383622fd Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 24 Oct 2022 20:11:03 +0100 Subject: [PATCH 16/16] updating version for release --- version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.json b/version.json index 06414af..2fa8509 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "code": 22, - "name": "17/10/2022-V1" + "code": 23, + "name": "24/10/2022-V1" } \ No newline at end of file