Merge pull request #213 from ouchadam/tech/bubble-components

Tech/bubble components
This commit is contained in:
Adam Brown 2022-10-22 11:52:21 +01:00 committed by GitHub
commit 7e8b440e77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 438 additions and 469 deletions

View File

@ -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?,

View File

@ -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.AlignedDraggableContainer(
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)

View File

@ -0,0 +1,255 @@
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.foundation.shape.RoundedCornerShape
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.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
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 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
private fun TextBubble(bubble: BubbleMeta, model: BubbleModel.Text, status: @Composable () -> Unit) {
Bubble(bubble) {
if (bubble.isNotSelf()) {
AuthorName(model.event, bubble)
}
TextContent(bubble, text = model.content)
Footer(model.event, bubble, status)
}
}
@Composable
private fun EncryptedBubble(bubble: BubbleMeta, model: BubbleModel.Encrypted, status: @Composable () -> Unit) {
TextBubble(bubble, BubbleModel.Text(content = "Encrypted message", model.event), status)
}
@Composable
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)
) {
val replyName = if (!bubble.isNotSelf() && model.isReplyingToSelf()) "You" else model.replyingTo.event.authorName
Text(
fontSize = 11.sp,
text = replyName,
maxLines = 1,
color = bubble.textColor()
)
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 BubbleModel.Image.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 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(
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: BubbleModel.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
}

View File

@ -1,17 +1,12 @@
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
@ -28,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
@ -56,7 +49,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,457 +185,43 @@ 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) {
when (item) {
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
is RoomEvent.Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
is RoomEvent.Encrypted -> EncryptedBubbleContent(it as BubbleContent<RoomEvent.Encrypted>)
}
}
}
}
}
private data class BubbleContent<T : RoomEvent>(
val shape: RoundedCornerShape,
val background: Color,
val isNotSelf: Boolean,
val message: T
)
@Composable
private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
message: T,
self: UserId,
wasPreviousMessageSameSender: Boolean,
replyActions: ReplyActions,
content: @Composable (BubbleContent<T>) -> 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))
}
}
}
}
}
@Composable
private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
val context = LocalContext.current
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)
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) },
) {
if (content.isNotSelf) {
Text(
fontSize = 11.sp,
text = content.message.author.displayName ?: content.message.author.id.value,
maxLines = 1,
color = content.textColor()
)
}
Spacer(modifier = Modifier.height(4.dp))
Image(
modifier = Modifier.size(content.message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.fetcherFactory(LocalDecyptingFetcherFactory.current)
.memoryCacheKey(content.message.imageMeta.url)
.data(content.message)
.build()
),
contentDescription = null,
)
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
Text(
fontSize = 9.sp,
text = "${editedPrefix ?: ""}${content.message.time}",
textAlign = TextAlign.End,
color = content.textColor(),
modifier = Modifier.wrapContentSize()
)
SendStatus(content.message)
}
}
}
}
}
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)
)
DpSize(
width = (width * scaler).toDp(),
height = (height * scaler).toDp(),
)
}
}
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()
}
}
@Composable
private fun BubbleContent<*>.textColor(): Color {
return if (this.isNotSelf) SmallTalkTheme.extendedColors.onOthersBubble else SmallTalkTheme.extendedColors.onSelfBubble
}
@Composable
private fun TextBubbleContent(content: BubbleContent<RoomEvent.Message>) {
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()
)
}
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)
}
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)
}
}
}
}
@Composable
private fun EncryptedBubbleContent(content: BubbleContent<RoomEvent.Encrypted>) {
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()
)
}
Text(
text = "Encrypted message",
color = content.textColor(),
fontSize = 15.sp,
modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
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)
}
Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text(
fontSize = 9.sp,
text = "${content.message.time}",
textAlign = TextAlign.End,
color = content.textColor(),
modifier = Modifier.wrapContentSize()
)
SendStatus(content.message)
}
}
}
is RoomEvent.Reply -> {
BubbleModel.Reply(this.replyingTo.toModel(event), this.message.toModel(event))
}
}
@Composable
private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
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)
) {
val context = LocalContext.current
Column(
Modifier
.fillMaxWidth()
.background(
if (content.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
Text(
fontSize = 11.sp,
text = replyName,
maxLines = 1,
color = content.textColor()
)
Spacer(modifier = Modifier.height(2.dp))
when (val replyingTo = content.message.replyingTo) {
is RoomEvent.Message -> {
Text(
text = replyingTo.content,
color = content.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 = content.textColor().copy(alpha = 0.8f),
fontSize = 14.sp,
modifier = Modifier.wrapContentSize(),
textAlign = TextAlign.Start,
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
if (content.isNotSelf) {
Text(
fontSize = 11.sp,
text = content.message.message.author.displayName ?: content.message.message.author.id.value,
maxLines = 1,
color = content.textColor()
)
}
when (val message = content.message.message) {
is RoomEvent.Message -> {
Text(
text = message.content,
color = content.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 = 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()) {
Text(
fontSize = 9.sp,
text = content.message.time,
textAlign = TextAlign.End,
color = content.textColor(),
modifier = Modifier.wrapContentSize()
)
SendStatus(content.message.message)
}
}
}
}
}
@Composable
private fun RowScope.SendStatus(message: RoomEvent) {
private fun SendStatus(message: RoomEvent) {
when (val meta = message.meta) {
MessageMeta.FromServer -> {
// last message is self