feat: message tail algorithm

This commit is contained in:
Infi 2023-01-31 00:31:05 +01:00
parent 3f2ba36622
commit d56149fe3f
6 changed files with 122 additions and 24 deletions

View File

@ -49,6 +49,9 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
lintOptions {
abortOnError false
}
namespace 'chat.revolt'
}
@ -56,8 +59,9 @@ dependencies {
// Android/Kotlin Core
implementation 'androidx.core:core-ktx:1.9.0'
// JSON Serialization
// Kotlinx - various first-party extensions for Kotlin
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
// Compose BOM
implementation platform("androidx.compose:compose-bom:$compose_bom_version")

View File

@ -18,7 +18,10 @@ data class Message(
val edited: String? = null,
val embeds: List<Embed>? = null,
val mentions: List<String>? = null,
val masquerade: Masquerade? = null,
val system: SystemInfo? = null,
val type: String? = null, // this is _only_ used for websocket events!
val tail: Boolean? = null, // this is used to determine if the message is the last in a message group
) {
fun getAuthor(): User? {
return author?.let { RevoltAPI.userCache[it] }
@ -78,4 +81,17 @@ data class Image(
@Serializable
data class Special(
val type: String? = null
)
@Serializable
data class Masquerade(
val name: String? = null,
val avatar: String? = null,
val colour: String? = null
)
@Serializable
data class SystemInfo(
val type: String? = null,
val id: String? = null
)

View File

@ -1,9 +1,12 @@
package chat.revolt.components.chat
import android.net.Uri
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
@ -13,17 +16,21 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
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.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.ULID
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
import chat.revolt.markdown.Renderer
import chat.revolt.api.schemas.Message as MessageSchema
@ -46,42 +53,66 @@ fun formatLongAsTime(time: Long): String {
return format.format(date)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Message(
message: MessageSchema
) {
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
Row(
modifier = Modifier
.padding(8.dp)
.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()
) {
UserAvatar(
username = author.username ?: "",
userId = author.id!!,
avatar = author.avatar
)
if (message.tail == false) {
UserAvatar(
username = author.username ?: "",
userId = author.id!!,
avatar = author.avatar
)
} else {
UserAvatarWidthPlaceholder()
}
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = author.username ?: "",
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
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))
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
)
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 {

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@ -84,4 +85,12 @@ fun UserAvatar(
PresenceBadge(presence)
}
}
}
@Composable
fun UserAvatarWidthPlaceholder() {
Box(
modifier = Modifier
.width(40.dp)
)
}

View File

@ -37,6 +37,7 @@ import chat.revolt.R
import chat.revolt.RevoltTweenFloat
import chat.revolt.RevoltTweenInt
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.ULID
import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
@ -56,7 +57,9 @@ import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.ChannelIcon
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() {
@ -186,7 +189,7 @@ class ChannelScreenViewModel : ViewModel() {
messages.add(message)
}
}
setRenderableMessages(renderableMessages + messages)
regroupMessages(renderableMessages + messages)
}
}
@ -211,7 +214,7 @@ class ChannelScreenViewModel : ViewModel() {
messages.add(message)
}
}
setRenderableMessages(renderableMessages + messages)
regroupMessages(renderableMessages + messages)
}
}
@ -276,6 +279,39 @@ class ChannelScreenViewModel : ViewModel() {
} ?: it
}
}
private fun regroupMessages(newMessages: List<MessageSchema> = renderableMessages) {
val groupedMessages = arrayListOf<MessageSchema>()
// Verbatim implementation of https://wiki.rvlt.gg/index.php/Text_Channel_(UI)#Message_Grouping_Algorithm
// The exception is the date variable being pushed into cache, we don't need that here.
// Keep in mind: Recomposing UI is incredibly cheap in Jetpack Compose.
newMessages.forEach { message ->
var tail = true
val next = newMessages.getOrNull(newMessages.indexOf(message) + 1)
if (next != null) {
val dateA = Instant.fromEpochMilliseconds(ULID.asTimestamp(message.id!!))
val dateB = Instant.fromEpochMilliseconds(ULID.asTimestamp(next.id!!))
if (
message.author != next.author ||
dateB - dateA >= 7.minutes ||
message.masquerade != next.masquerade ||
message.system != null || next.system != null ||
message.replies != null || next.replies != null
) {
tail = false
}
} else {
tail = false
}
groupedMessages.add(message.copy(tail = tail))
}
setRenderableMessages(groupedMessages)
}
}
@Composable

View File

@ -1,5 +1,6 @@
package chat.revolt.ui.theme
import android.annotation.SuppressLint
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
@ -59,6 +60,7 @@ enum class Theme {
Amoled,
}
@SuppressLint("NewApi")
@Composable
fun RevoltTheme(
requestedTheme: Theme,