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}'
|
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
namespace 'chat.revolt'
|
namespace 'chat.revolt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,8 +59,9 @@ dependencies {
|
||||||
// Android/Kotlin Core
|
// Android/Kotlin Core
|
||||||
implementation 'androidx.core:core-ktx:1.9.0'
|
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-serialization-json:1.4.1"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
|
||||||
|
|
||||||
// Compose BOM
|
// Compose BOM
|
||||||
implementation platform("androidx.compose:compose-bom:$compose_bom_version")
|
implementation platform("androidx.compose:compose-bom:$compose_bom_version")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ data class Message(
|
||||||
val edited: String? = null,
|
val edited: String? = null,
|
||||||
val embeds: List<Embed>? = null,
|
val embeds: List<Embed>? = null,
|
||||||
val mentions: List<String>? = 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 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? {
|
fun getAuthor(): User? {
|
||||||
return author?.let { RevoltAPI.userCache[it] }
|
return author?.let { RevoltAPI.userCache[it] }
|
||||||
|
|
@ -79,3 +82,16 @@ data class Image(
|
||||||
data class Special(
|
data class Special(
|
||||||
val type: String? = null
|
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
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -13,17 +16,21 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.revolt.R
|
||||||
import chat.revolt.api.REVOLT_FILES
|
import chat.revolt.api.REVOLT_FILES
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
import chat.revolt.api.schemas.AutumnResource
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
import chat.revolt.components.generic.RemoteImage
|
import chat.revolt.components.generic.RemoteImage
|
||||||
import chat.revolt.components.generic.UserAvatar
|
import chat.revolt.components.generic.UserAvatar
|
||||||
|
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
|
||||||
import chat.revolt.markdown.Renderer
|
import chat.revolt.markdown.Renderer
|
||||||
import chat.revolt.api.schemas.Message as MessageSchema
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
|
||||||
|
|
@ -46,42 +53,66 @@ fun formatLongAsTime(time: Long): String {
|
||||||
return format.format(date)
|
return format.format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Message(
|
fun Message(
|
||||||
message: MessageSchema
|
message: MessageSchema
|
||||||
) {
|
) {
|
||||||
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
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()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
UserAvatar(
|
if (message.tail == false) {
|
||||||
username = author.username ?: "",
|
UserAvatar(
|
||||||
userId = author.id!!,
|
username = author.username ?: "",
|
||||||
avatar = author.avatar
|
userId = author.id!!,
|
||||||
)
|
avatar = author.avatar
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
UserAvatarWidthPlaceholder()
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
if (message.tail == false) {
|
||||||
Text(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
text = author.username ?: "",
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
text = author.username ?: "",
|
||||||
maxLines = 1,
|
fontWeight = FontWeight.Bold,
|
||||||
overflow = TextOverflow.Ellipsis
|
maxLines = 1,
|
||||||
)
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = formatLongAsTime(ULID.asTimestamp(message.id!!)),
|
text = formatLongAsTime(ULID.asTimestamp(message.id!!)),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message.content?.let {
|
message.content?.let {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -85,3 +86,11 @@ fun UserAvatar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserAvatarWidthPlaceholder() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(40.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ import chat.revolt.R
|
||||||
import chat.revolt.RevoltTweenFloat
|
import chat.revolt.RevoltTweenFloat
|
||||||
import chat.revolt.RevoltTweenInt
|
import chat.revolt.RevoltTweenInt
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.internals.ULID
|
||||||
import chat.revolt.api.realtime.RealtimeSocket
|
import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
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 chat.revolt.components.screens.chat.ChannelIcon
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
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() {
|
||||||
|
|
@ -186,7 +189,7 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
messages.add(message)
|
messages.add(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setRenderableMessages(renderableMessages + messages)
|
regroupMessages(renderableMessages + messages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +214,7 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
messages.add(message)
|
messages.add(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setRenderableMessages(renderableMessages + messages)
|
regroupMessages(renderableMessages + messages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,6 +279,39 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
} ?: it
|
} ?: 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
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package chat.revolt.ui.theme
|
package chat.revolt.ui.theme
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
|
@ -59,6 +60,7 @@ enum class Theme {
|
||||||
Amoled,
|
Amoled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
@Composable
|
@Composable
|
||||||
fun RevoltTheme(
|
fun RevoltTheme(
|
||||||
requestedTheme: Theme,
|
requestedTheme: Theme,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue