diff --git a/app/build.gradle b/app/build.gradle index 7a9f48b7..a04ca576 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,7 @@ android { } } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility 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:1.1.0-alpha01" + // JDK Desugaring - polyfill for new Java APIs + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + // Markdown implementation project(':markdown') } diff --git a/app/src/main/java/chat/revolt/api/schemas/Messages.kt b/app/src/main/java/chat/revolt/api/schemas/Messages.kt index 7c5b44f3..bed72cdd 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Messages.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -40,7 +40,8 @@ data class Message( edited = partial.edited ?: edited, embeds = partial.embeds ?: embeds, mentions = partial.mentions ?: mentions, - type = partial.type ?: type + type = partial.type ?: type, + tail = partial.tail ?: tail, ) } } diff --git a/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt b/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt new file mode 100644 index 00000000..fe956e46 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/chat/InReplyTo.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/chat/Message.kt b/app/src/main/java/chat/revolt/components/chat/Message.kt index 4a76807a..b1f9466d 100644 --- a/app/src/main/java/chat/revolt/components/chat/Message.kt +++ b/app/src/main/java/chat/revolt/components/chat/Message.kt @@ -46,10 +46,16 @@ fun viewAttachmentInBrowser(ctx: android.content.Context, attachment: AutumnReso fun formatLongAsTime(time: Long): String { - // TODO: look into using a library like kotlinx.datetime val date = java.util.Date(time) val format = 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) } @@ -62,93 +68,110 @@ fun Message( val context = LocalContext.current val clipboardManager = LocalClipboardManager.current - 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(4.dp) - .fillMaxWidth() - ) { + Column { if (message.tail == false) { - UserAvatar( - username = author.username ?: "", - userId = author.id!!, - avatar = author.avatar - ) - } else { - UserAvatarWidthPlaceholder() + Spacer(modifier = Modifier.height(10.dp)) } - 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) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = author.username ?: "", - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + UserAvatar( + username = author.username ?: "", + userId = author.id!!, + avatar = author.avatar + ) + } 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 = formatLongAsTime(ULID.asTimestamp(message.id!!)), - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), - maxLines = 1, - overflow = TextOverflow.Ellipsis + text = Renderer.annotateMarkdown(it), ) } - } - message.content?.let { - Text( - text = Renderer.annotateMarkdown(it), - ) - } - - message.attachments?.let { - if (message.attachments.isNotEmpty()) { - message.attachments.forEach { attachment -> - if (attachment.metadata?.type == "Image") { - RemoteImage( - url = "$REVOLT_FILES/attachments/${attachment.id}/image.png", - modifier = Modifier - .padding(top = 5.dp) - .clickable { - viewAttachmentInBrowser(context, attachment) - }, - width = attachment.metadata.width?.toInt() ?: 0, - height = attachment.metadata.height?.toInt() ?: 0, - contentScale = ContentScale.Fit, - description = "Attached image ${attachment.filename}" - ) - } else { - Text( - text = attachment.filename ?: "Attachment", - fontWeight = FontWeight.Medium, - modifier = Modifier - .clip(MaterialTheme.shapes.medium) - .clickable { - viewAttachmentInBrowser(context, attachment) - } - .background(MaterialTheme.colorScheme.surface) - .padding(8.dp) - ) + message.attachments?.let { + if (message.attachments.isNotEmpty()) { + message.attachments.forEach { attachment -> + if (attachment.metadata?.type == "Image") { + RemoteImage( + url = "$REVOLT_FILES/attachments/${attachment.id}/image.png", + modifier = Modifier + .padding(top = 5.dp) + .clickable { + viewAttachmentInBrowser(context, attachment) + }, + width = attachment.metadata.width?.toInt() ?: 0, + height = attachment.metadata.height?.toInt() ?: 0, + contentScale = ContentScale.Fit, + description = "Attached image ${attachment.filename}" + ) + } else { + Text( + text = attachment.filename ?: "Attachment", + fontWeight = FontWeight.Medium, + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable { + viewAttachmentInBrowser(context, attachment) + } + .background(MaterialTheme.colorScheme.surface) + .padding(8.dp) + ) + } } } } diff --git a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt index 711e6107..61a113ef 100644 --- a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt +++ b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.revolt.R import chat.revolt.api.REVOLT_BASE @@ -39,10 +40,10 @@ fun presenceColour(presence: Presence): Color { } @Composable -fun PresenceBadge(presence: Presence) { +fun PresenceBadge(presence: Presence, size: Dp = 16.dp) { Box( modifier = Modifier - .size(16.dp) + .size(size) .clip(CircleShape) .border(2.dp, MaterialTheme.colorScheme.background, CircleShape) .background(presenceColour(presence)) @@ -56,10 +57,12 @@ fun UserAvatar( modifier: Modifier = Modifier, presence: Presence? = null, avatar: AutumnResource? = null, + size: Dp = 40.dp, + presenceSize: Dp = 16.dp, ) { Box( modifier = modifier - .size(40.dp), + .size(size), contentAlignment = Alignment.BottomEnd ) { if (avatar != null) { @@ -69,28 +72,30 @@ fun UserAvatar( contentScale = ContentScale.Crop, modifier = Modifier .clip(CircleShape) - .size(40.dp) + .size(size) ) } else { RemoteImage( url = "$REVOLT_BASE/users/${userId}/default_avatar", modifier = Modifier - .size(40.dp) + .size(size) .clip(CircleShape), description = stringResource(id = R.string.avatar_alt, username), ) } if (presence != null) { - PresenceBadge(presence) + PresenceBadge(presence, size = presenceSize) } } } @Composable -fun UserAvatarWidthPlaceholder() { +fun UserAvatarWidthPlaceholder( + size: Dp = 40.dp, +) { Box( modifier = Modifier - .width(40.dp) + .width(size) ) } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt index 03f2c667..87593241 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/ChannelScreen.kt @@ -59,7 +59,6 @@ import io.ktor.http.* import kotlinx.coroutines.launch import kotlinx.datetime.Instant import java.io.File -import kotlin.time.Duration.Companion.minutes import chat.revolt.api.schemas.Message as MessageSchema class ChannelScreenViewModel : ViewModel() { @@ -294,12 +293,14 @@ class ChannelScreenViewModel : ViewModel() { val dateA = Instant.fromEpochMilliseconds(ULID.asTimestamp(message.id!!)) val dateB = Instant.fromEpochMilliseconds(ULID.asTimestamp(next.id!!)) + val minuteDifference = (dateA - dateB).inWholeMinutes + if ( message.author != next.author || - dateB - dateA >= 7.minutes || + minuteDifference >= 7 || message.masquerade != next.masquerade || message.system != null || next.system != null || - message.replies != null || next.replies != null + message.replies != null ) { tail = false }