feat: basic support for replies
This commit is contained in:
parent
e5560a068d
commit
e1abc4fff5
|
|
@ -32,6 +32,7 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +114,9 @@ dependencies {
|
||||||
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
|
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
|
||||||
implementation "androidx.datastore:datastore:1.1.0-alpha01"
|
implementation "androidx.datastore:datastore:1.1.0-alpha01"
|
||||||
|
|
||||||
|
// JDK Desugaring - polyfill for new Java APIs
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
|
||||||
|
|
||||||
// Markdown
|
// Markdown
|
||||||
implementation project(':markdown')
|
implementation project(':markdown')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ data class Message(
|
||||||
edited = partial.edited ?: edited,
|
edited = partial.edited ?: edited,
|
||||||
embeds = partial.embeds ?: embeds,
|
embeds = partial.embeds ?: embeds,
|
||||||
mentions = partial.mentions ?: mentions,
|
mentions = partial.mentions ?: mentions,
|
||||||
type = partial.type ?: type
|
type = partial.type ?: type,
|
||||||
|
tail = partial.tail ?: tail,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.components.generic.UserAvatar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InReplyTo(
|
||||||
|
messageId: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
withMention: Boolean = false,
|
||||||
|
onMessageClick: (String) -> Unit = { _ -> },
|
||||||
|
) {
|
||||||
|
val message = RevoltAPI.messageCache[messageId]
|
||||||
|
val author = RevoltAPI.userCache[message?.author ?: ""]
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp)
|
||||||
|
.clickable { onMessageClick(messageId) },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(48.dp))
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
UserAvatar(
|
||||||
|
username = author?.username ?: "",
|
||||||
|
userId = author?.id ?: "",
|
||||||
|
avatar = author?.avatar,
|
||||||
|
size = 16.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (author != null) {
|
||||||
|
if (withMention) {
|
||||||
|
"@${author.username}"
|
||||||
|
} else {
|
||||||
|
author.username
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.unknown)
|
||||||
|
} ?: stringResource(id = R.string.unknown),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.9f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = message.content ?: "",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.reply_message_not_cached),
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,10 +46,16 @@ fun viewAttachmentInBrowser(ctx: android.content.Context, attachment: AutumnReso
|
||||||
|
|
||||||
|
|
||||||
fun formatLongAsTime(time: Long): String {
|
fun formatLongAsTime(time: Long): String {
|
||||||
// TODO: look into using a library like kotlinx.datetime
|
|
||||||
val date = java.util.Date(time)
|
val date = java.util.Date(time)
|
||||||
val format =
|
val format =
|
||||||
java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss", java.util.Locale.getDefault())
|
java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss", java.util.Locale.getDefault())
|
||||||
|
|
||||||
|
// EQUIVALENT CODE WITH kotlinx.datetime:
|
||||||
|
|
||||||
|
// val date = Instant.fromEpochMilliseconds(time)
|
||||||
|
// val format = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
|
||||||
|
|
||||||
|
|
||||||
return format.format(date)
|
return format.format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,93 +68,110 @@ fun Message(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
Row(
|
Column {
|
||||||
modifier = Modifier
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = {},
|
|
||||||
onLongClick = {
|
|
||||||
if (message.content != null && message.content.isNotEmpty()) {
|
|
||||||
clipboardManager.setText(AnnotatedString(message.content))
|
|
||||||
|
|
||||||
Toast
|
|
||||||
.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.copied),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(4.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
if (message.tail == false) {
|
if (message.tail == false) {
|
||||||
UserAvatar(
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
username = author.username ?: "",
|
|
||||||
userId = author.id!!,
|
|
||||||
avatar = author.avatar
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
UserAvatarWidthPlaceholder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
message.replies?.forEach { reply ->
|
||||||
|
val replyMessage = RevoltAPI.messageCache[reply] ?: return@forEach
|
||||||
|
|
||||||
|
InReplyTo(
|
||||||
|
messageId = reply,
|
||||||
|
withMention = message.mentions?.contains(replyMessage.author) == true
|
||||||
|
) {
|
||||||
|
// TODO Add jump to message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {
|
||||||
|
if (message.content != null && message.content.isNotEmpty()) {
|
||||||
|
clipboardManager.setText(AnnotatedString(message.content))
|
||||||
|
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.copied),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
if (message.tail == false) {
|
if (message.tail == false) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
UserAvatar(
|
||||||
Text(
|
username = author.username ?: "",
|
||||||
text = author.username ?: "",
|
userId = author.id!!,
|
||||||
fontWeight = FontWeight.Bold,
|
avatar = author.avatar
|
||||||
maxLines = 1,
|
)
|
||||||
overflow = TextOverflow.Ellipsis
|
} else {
|
||||||
)
|
UserAvatarWidthPlaceholder()
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
|
if (message.tail == false) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = author.username ?: "",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = formatLongAsTime(ULID.asTimestamp(message.id!!)),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.content?.let {
|
||||||
Text(
|
Text(
|
||||||
text = formatLongAsTime(ULID.asTimestamp(message.id!!)),
|
text = Renderer.annotateMarkdown(it),
|
||||||
fontSize = 12.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
message.content?.let {
|
message.attachments?.let {
|
||||||
Text(
|
if (message.attachments.isNotEmpty()) {
|
||||||
text = Renderer.annotateMarkdown(it),
|
message.attachments.forEach { attachment ->
|
||||||
)
|
if (attachment.metadata?.type == "Image") {
|
||||||
}
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/attachments/${attachment.id}/image.png",
|
||||||
message.attachments?.let {
|
modifier = Modifier
|
||||||
if (message.attachments.isNotEmpty()) {
|
.padding(top = 5.dp)
|
||||||
message.attachments.forEach { attachment ->
|
.clickable {
|
||||||
if (attachment.metadata?.type == "Image") {
|
viewAttachmentInBrowser(context, attachment)
|
||||||
RemoteImage(
|
},
|
||||||
url = "$REVOLT_FILES/attachments/${attachment.id}/image.png",
|
width = attachment.metadata.width?.toInt() ?: 0,
|
||||||
modifier = Modifier
|
height = attachment.metadata.height?.toInt() ?: 0,
|
||||||
.padding(top = 5.dp)
|
contentScale = ContentScale.Fit,
|
||||||
.clickable {
|
description = "Attached image ${attachment.filename}"
|
||||||
viewAttachmentInBrowser(context, attachment)
|
)
|
||||||
},
|
} else {
|
||||||
width = attachment.metadata.width?.toInt() ?: 0,
|
Text(
|
||||||
height = attachment.metadata.height?.toInt() ?: 0,
|
text = attachment.filename ?: "Attachment",
|
||||||
contentScale = ContentScale.Fit,
|
fontWeight = FontWeight.Medium,
|
||||||
description = "Attached image ${attachment.filename}"
|
modifier = Modifier
|
||||||
)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
} else {
|
.clickable {
|
||||||
Text(
|
viewAttachmentInBrowser(context, attachment)
|
||||||
text = attachment.filename ?: "Attachment",
|
}
|
||||||
fontWeight = FontWeight.Medium,
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
modifier = Modifier
|
.padding(8.dp)
|
||||||
.clip(MaterialTheme.shapes.medium)
|
)
|
||||||
.clickable {
|
}
|
||||||
viewAttachmentInBrowser(context, attachment)
|
|
||||||
}
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
import chat.revolt.api.REVOLT_BASE
|
import chat.revolt.api.REVOLT_BASE
|
||||||
|
|
@ -39,10 +40,10 @@ fun presenceColour(presence: Presence): Color {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PresenceBadge(presence: Presence) {
|
fun PresenceBadge(presence: Presence, size: Dp = 16.dp) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(16.dp)
|
.size(size)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.border(2.dp, MaterialTheme.colorScheme.background, CircleShape)
|
.border(2.dp, MaterialTheme.colorScheme.background, CircleShape)
|
||||||
.background(presenceColour(presence))
|
.background(presenceColour(presence))
|
||||||
|
|
@ -56,10 +57,12 @@ fun UserAvatar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
presence: Presence? = null,
|
presence: Presence? = null,
|
||||||
avatar: AutumnResource? = null,
|
avatar: AutumnResource? = null,
|
||||||
|
size: Dp = 40.dp,
|
||||||
|
presenceSize: Dp = 16.dp,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(40.dp),
|
.size(size),
|
||||||
contentAlignment = Alignment.BottomEnd
|
contentAlignment = Alignment.BottomEnd
|
||||||
) {
|
) {
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
|
|
@ -69,28 +72,30 @@ fun UserAvatar(
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.size(40.dp)
|
.size(size)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
RemoteImage(
|
RemoteImage(
|
||||||
url = "$REVOLT_BASE/users/${userId}/default_avatar",
|
url = "$REVOLT_BASE/users/${userId}/default_avatar",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(size)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape),
|
||||||
description = stringResource(id = R.string.avatar_alt, username),
|
description = stringResource(id = R.string.avatar_alt, username),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (presence != null) {
|
if (presence != null) {
|
||||||
PresenceBadge(presence)
|
PresenceBadge(presence, size = presenceSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAvatarWidthPlaceholder() {
|
fun UserAvatarWidthPlaceholder(
|
||||||
|
size: Dp = 40.dp,
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(40.dp)
|
.width(size)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,6 @@ import io.ktor.http.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
import chat.revolt.api.schemas.Message as MessageSchema
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
|
||||||
class ChannelScreenViewModel : ViewModel() {
|
class ChannelScreenViewModel : ViewModel() {
|
||||||
|
|
@ -294,12 +293,14 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
val dateA = Instant.fromEpochMilliseconds(ULID.asTimestamp(message.id!!))
|
val dateA = Instant.fromEpochMilliseconds(ULID.asTimestamp(message.id!!))
|
||||||
val dateB = Instant.fromEpochMilliseconds(ULID.asTimestamp(next.id!!))
|
val dateB = Instant.fromEpochMilliseconds(ULID.asTimestamp(next.id!!))
|
||||||
|
|
||||||
|
val minuteDifference = (dateA - dateB).inWholeMinutes
|
||||||
|
|
||||||
if (
|
if (
|
||||||
message.author != next.author ||
|
message.author != next.author ||
|
||||||
dateB - dateA >= 7.minutes ||
|
minuteDifference >= 7 ||
|
||||||
message.masquerade != next.masquerade ||
|
message.masquerade != next.masquerade ||
|
||||||
message.system != null || next.system != null ||
|
message.system != null || next.system != null ||
|
||||||
message.replies != null || next.replies != null
|
message.replies != null
|
||||||
) {
|
) {
|
||||||
tail = false
|
tail = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue