feat: message tail algorithm
This commit is contained in:
parent
3f2ba36622
commit
d56149fe3f
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue