mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-17 12:40:44 +01:00
extracts the swipeable bubble container to the design system
This commit is contained in:
parent
fc9a864ed8
commit
9ba6d70daa
@ -104,6 +104,12 @@ sealed class RoomEvent {
|
|||||||
abstract val utcTimestamp: Long
|
abstract val utcTimestamp: Long
|
||||||
abstract val author: RoomMember
|
abstract val author: RoomMember
|
||||||
abstract val meta: MessageMeta
|
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(
|
data class Encrypted(
|
||||||
override val eventId: EventId,
|
override val eventId: EventId,
|
||||||
@ -112,10 +118,8 @@ sealed class RoomEvent {
|
|||||||
override val meta: MessageMeta,
|
override val meta: MessageMeta,
|
||||||
) : RoomEvent() {
|
) : RoomEvent() {
|
||||||
|
|
||||||
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
|
override val edited: Boolean = false
|
||||||
val instant = Instant.ofEpochMilli(utcTimestamp)
|
|
||||||
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Message(
|
data class Message(
|
||||||
@ -124,15 +128,9 @@ sealed class RoomEvent {
|
|||||||
val content: String,
|
val content: String,
|
||||||
override val author: RoomMember,
|
override val author: RoomMember,
|
||||||
override val meta: MessageMeta,
|
override val meta: MessageMeta,
|
||||||
val edited: Boolean = false,
|
override val edited: Boolean = false,
|
||||||
val redacted: Boolean = false,
|
val redacted: Boolean = false,
|
||||||
) : RoomEvent() {
|
) : 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 Reply(
|
data class Reply(
|
||||||
val message: RoomEvent,
|
val message: RoomEvent,
|
||||||
@ -143,13 +141,9 @@ sealed class RoomEvent {
|
|||||||
override val utcTimestamp: Long = message.utcTimestamp
|
override val utcTimestamp: Long = message.utcTimestamp
|
||||||
override val author: RoomMember = message.author
|
override val author: RoomMember = message.author
|
||||||
override val meta: MessageMeta = message.meta
|
override val meta: MessageMeta = message.meta
|
||||||
|
override val edited: Boolean = message.edited
|
||||||
|
|
||||||
val replyingToSelf = replyingTo.author == message.author
|
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(
|
data class Image(
|
||||||
@ -158,14 +152,9 @@ sealed class RoomEvent {
|
|||||||
val imageMeta: ImageMeta,
|
val imageMeta: ImageMeta,
|
||||||
override val author: RoomMember,
|
override val author: RoomMember,
|
||||||
override val meta: MessageMeta,
|
override val meta: MessageMeta,
|
||||||
val edited: Boolean = false,
|
override val edited: Boolean = false,
|
||||||
) : RoomEvent() {
|
) : 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(
|
data class ImageMeta(
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
|
@ -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)
|
@ -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
|
||||||
|
}
|
@ -3,15 +3,11 @@ package app.dapk.st.messenger
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
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.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@ -56,7 +52,6 @@ import app.dapk.st.navigator.Navigator
|
|||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessengerScreen(
|
internal fun MessengerScreen(
|
||||||
@ -193,58 +188,20 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions
|
|||||||
) { index, item ->
|
) { index, item ->
|
||||||
val previousEvent = if (index != 0) state.events[index - 1] else null
|
val previousEvent = if (index != 0) state.events[index - 1] else null
|
||||||
val wasPreviousMessageSameSender = previousEvent?.author?.id == item.author.id
|
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) {
|
when (item) {
|
||||||
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
is RoomEvent.Image -> MessageImage(this, item)
|
||||||
is RoomEvent.Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
is RoomEvent.Message -> TextBubbleContent(this, event, item.content, status = status)
|
||||||
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
is RoomEvent.Reply -> ReplyBubbleContent(this, item)
|
||||||
is RoomEvent.Encrypted -> EncryptedBubbleContent(it as BubbleContent<RoomEvent.Encrypted>)
|
is RoomEvent.Encrypted -> EncryptedBubbleContent(this, item)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,15 +209,15 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
|
private fun MessageImage(bubble: BubbleMeta, event: RoomEvent.Image) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Box(modifier = Modifier.padding(start = 6.dp)) {
|
Box(modifier = Modifier.padding(start = 6.dp)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.clip(content.shape)
|
.clip(bubble.shape)
|
||||||
.background(content.background)
|
.background(bubble.background)
|
||||||
.height(IntrinsicSize.Max),
|
.height(IntrinsicSize.Max),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -269,23 +226,23 @@ private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
|
|||||||
.width(IntrinsicSize.Max)
|
.width(IntrinsicSize.Max)
|
||||||
.defaultMinSize(minWidth = 50.dp)
|
.defaultMinSize(minWidth = 50.dp)
|
||||||
) {
|
) {
|
||||||
if (content.isNotSelf) {
|
if (bubble.isNotSelf()) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
text = content.message.author.displayName ?: content.message.author.id.value,
|
text = event.author.displayName ?: event.author.id.value,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = content.textColor()
|
color = bubble.textColor()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier.size(content.message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
|
modifier = Modifier.size(event.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
|
||||||
painter = rememberAsyncImagePainter(
|
painter = rememberAsyncImagePainter(
|
||||||
model = ImageRequest.Builder(context)
|
model = ImageRequest.Builder(context)
|
||||||
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
||||||
.memoryCacheKey(content.message.imageMeta.url)
|
.memoryCacheKey(event.imageMeta.url)
|
||||||
.data(content.message)
|
.data(event)
|
||||||
.build()
|
.build()
|
||||||
),
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@ -293,15 +250,15 @@ private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
|
|||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
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(
|
Text(
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
text = "${editedPrefix ?: ""}${content.message.time}",
|
text = "${editedPrefix ?: ""}${event.time}",
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
color = content.textColor(),
|
color = bubble.textColor(),
|
||||||
modifier = Modifier.wrapContentSize()
|
modifier = Modifier.wrapContentSize()
|
||||||
)
|
)
|
||||||
SendStatus(content.message)
|
SendStatus(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,82 +286,19 @@ private fun Int.scalerFor(max: Float): Float {
|
|||||||
return max / this
|
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
|
@Composable
|
||||||
private fun Bubble(
|
private fun BubbleMeta.textColor(): Color {
|
||||||
message: RoomEvent,
|
return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble
|
||||||
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
|
@Composable
|
||||||
private fun BubbleContent<*>.textColor(): Color {
|
private fun EncryptedBubbleContent(bubble: BubbleMeta, event: RoomEvent.Encrypted) {
|
||||||
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 = Modifier.padding(start = 6.dp)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.clip(content.shape)
|
.clip(bubble.shape)
|
||||||
.background(content.background)
|
.background(bubble.background)
|
||||||
.height(IntrinsicSize.Max),
|
.height(IntrinsicSize.Max),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -413,66 +307,17 @@ private fun TextBubbleContent(content: BubbleContent<RoomEvent.Message>) {
|
|||||||
.width(IntrinsicSize.Max)
|
.width(IntrinsicSize.Max)
|
||||||
.defaultMinSize(minWidth = 50.dp)
|
.defaultMinSize(minWidth = 50.dp)
|
||||||
) {
|
) {
|
||||||
if (content.isNotSelf) {
|
if (bubble.isNotSelf()) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
text = content.message.author.displayName ?: content.message.author.id.value,
|
text = event.author.displayName ?: event.author.id.value,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = content.textColor()
|
color = bubble.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<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(
|
||||||
text = "Encrypted message",
|
text = "Encrypted message",
|
||||||
color = content.textColor(),
|
color = bubble.textColor(),
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@ -482,12 +327,12 @@ private fun EncryptedBubbleContent(content: BubbleContent<RoomEvent.Encrypted>)
|
|||||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
text = "${content.message.time}",
|
text = event.time,
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
color = content.textColor(),
|
color = bubble.textColor(),
|
||||||
modifier = Modifier.wrapContentSize()
|
modifier = Modifier.wrapContentSize()
|
||||||
)
|
)
|
||||||
SendStatus(content.message)
|
SendStatus(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -495,13 +340,13 @@ private fun EncryptedBubbleContent(content: BubbleContent<RoomEvent.Encrypted>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
private fun ReplyBubbleContent(bubble: BubbleMeta, event: RoomEvent.Reply) {
|
||||||
Box(modifier = Modifier.padding(start = 6.dp)) {
|
Box(modifier = Modifier.padding(start = 6.dp)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.clip(content.shape)
|
.clip(bubble.shape)
|
||||||
.background(content.background)
|
.background(bubble.background)
|
||||||
.height(IntrinsicSize.Max),
|
.height(IntrinsicSize.Max),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -515,26 +360,26 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.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
|
alpha = 0.2f
|
||||||
), RoundedCornerShape(12.dp)
|
), RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
val replyName = if (!content.isNotSelf && content.message.replyingToSelf) "You" else content.message.replyingTo.author.displayName
|
val replyName = if (!bubble.isNotSelf() && event.replyingToSelf) "You" else event.replyingTo.author.displayName
|
||||||
?: content.message.replyingTo.author.id.value
|
?: event.replyingTo.author.id.value
|
||||||
Text(
|
Text(
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
text = replyName,
|
text = replyName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = content.textColor()
|
color = bubble.textColor()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
when (val replyingTo = content.message.replyingTo) {
|
when (val replyingTo = event.replyingTo) {
|
||||||
is RoomEvent.Message -> {
|
is RoomEvent.Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = replyingTo.content,
|
text = replyingTo.content,
|
||||||
color = content.textColor().copy(alpha = 0.8f),
|
color = bubble.textColor().copy(alpha = 0.8f),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@ -564,7 +409,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
is RoomEvent.Encrypted -> {
|
is RoomEvent.Encrypted -> {
|
||||||
Text(
|
Text(
|
||||||
text = "Encrypted message",
|
text = "Encrypted message",
|
||||||
color = content.textColor().copy(alpha = 0.8f),
|
color = bubble.textColor().copy(alpha = 0.8f),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@ -575,19 +420,19 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
if (content.isNotSelf) {
|
if (bubble.isNotSelf()) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 11.sp,
|
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,
|
maxLines = 1,
|
||||||
color = content.textColor()
|
color = bubble.textColor()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (val message = content.message.message) {
|
when (val message = event.message) {
|
||||||
is RoomEvent.Message -> {
|
is RoomEvent.Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = message.content,
|
text = message.content,
|
||||||
color = content.textColor(),
|
color = bubble.textColor(),
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@ -617,7 +462,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
is RoomEvent.Encrypted -> {
|
is RoomEvent.Encrypted -> {
|
||||||
Text(
|
Text(
|
||||||
text = "Encrypted message",
|
text = "Encrypted message",
|
||||||
color = content.textColor(),
|
color = bubble.textColor(),
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@ -629,12 +474,12 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
text = content.message.time,
|
text = event.time,
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
color = content.textColor(),
|
color = bubble.textColor(),
|
||||||
modifier = Modifier.wrapContentSize()
|
modifier = Modifier.wrapContentSize()
|
||||||
)
|
)
|
||||||
SendStatus(content.message.message)
|
SendStatus(event.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -643,7 +488,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.SendStatus(message: RoomEvent) {
|
private fun SendStatus(message: RoomEvent) {
|
||||||
when (val meta = message.meta) {
|
when (val meta = message.meta) {
|
||||||
MessageMeta.FromServer -> {
|
MessageMeta.FromServer -> {
|
||||||
// last message is self
|
// last message is self
|
||||||
|
Loading…
x
Reference in New Issue
Block a user