feat: basic support for replies

This commit is contained in:
Infi 2023-02-05 02:33:20 +01:00
parent e5560a068d
commit e1abc4fff5
6 changed files with 206 additions and 90 deletions

View File

@ -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')
} }

View File

@ -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,
) )
} }
} }

View File

@ -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
)
}
}
}

View File

@ -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)
)
} }
} }
} }

View File

@ -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)
) )
} }

View File

@ -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
} }