feat: xml messageview (for future optimisations)
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
b25ea4dffc
commit
98bbc09cd2
|
|
@ -15,7 +15,7 @@ fun Brush.Companion.solidColor(colour: Color) = SolidColor(colour)
|
||||||
// not exhaustive, but covers most of the ones I've seen in the wild
|
// not exhaustive, but covers most of the ones I've seen in the wild
|
||||||
// for the sake of all of us, please just use hex codes
|
// for the sake of all of us, please just use hex codes
|
||||||
// reference: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
|
// reference: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
|
||||||
private val ADDITIONAL_WEB_COLOURS = mapOf(
|
val ADDITIONAL_WEB_COLOURS = mapOf(
|
||||||
"orange" to Color(0xFFFFA500),
|
"orange" to Color(0xFFFFA500),
|
||||||
"rebeccapurple" to Color(0xFF663399),
|
"rebeccapurple" to Color(0xFF663399),
|
||||||
"transparent" to Color.Transparent,
|
"transparent" to Color.Transparent,
|
||||||
|
|
@ -24,7 +24,7 @@ private val ADDITIONAL_WEB_COLOURS = mapOf(
|
||||||
"unset" to Color.Unspecified
|
"unset" to Color.Unspecified
|
||||||
)
|
)
|
||||||
|
|
||||||
object WebCompat {
|
object BrushCompat {
|
||||||
@Composable
|
@Composable
|
||||||
private fun parseLinearGradient(gradient: String): Brush {
|
private fun parseLinearGradient(gradient: String): Brush {
|
||||||
val stops = mutableListOf<Pair<Float, Color>>()
|
val stops = mutableListOf<Pair<Float, Color>>()
|
||||||
|
|
@ -85,8 +85,7 @@ object WebCompat {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
fun parseFunctionColour(colourString: String): Color? {
|
||||||
private fun parseFunctionColour(colourString: String): Color? {
|
|
||||||
val cleanedString = colourString.trim()
|
val cleanedString = colourString.trim()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
|
@ -151,7 +150,7 @@ object WebCompat {
|
||||||
val additionalWebColour = ADDITIONAL_WEB_COLOURS[colour]
|
val additionalWebColour = ADDITIONAL_WEB_COLOURS[colour]
|
||||||
if (additionalWebColour != null) {
|
if (additionalWebColour != null) {
|
||||||
Log.d(
|
Log.d(
|
||||||
"WebCompat",
|
"BrushCompat",
|
||||||
"Parsed additional web colour $colour to $additionalWebColour"
|
"Parsed additional web colour $colour to $additionalWebColour"
|
||||||
)
|
)
|
||||||
return additionalWebColour
|
return additionalWebColour
|
||||||
|
|
@ -160,7 +159,7 @@ object WebCompat {
|
||||||
Color(android.graphics.Color.parseColor(colour))
|
Color(android.graphics.Color.parseColor(colour))
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Log.d(
|
Log.d(
|
||||||
"WebCompat",
|
"BrushCompat",
|
||||||
"Failed to parse colour $colour, falling back to LocalContentColor.current"
|
"Failed to parse colour $colour, falling back to LocalContentColor.current"
|
||||||
)
|
)
|
||||||
LocalContentColor.current
|
LocalContentColor.current
|
||||||
|
|
@ -172,7 +171,7 @@ object WebCompat {
|
||||||
when {
|
when {
|
||||||
colour.startsWith("var(") -> {
|
colour.startsWith("var(") -> {
|
||||||
Log.d(
|
Log.d(
|
||||||
"WebCompat",
|
"BrushCompat",
|
||||||
"Parsing variable $colour"
|
"Parsing variable $colour"
|
||||||
)
|
)
|
||||||
return parseVar(
|
return parseVar(
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package chat.revolt.api.internals
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.LinearGradient
|
||||||
|
import android.graphics.Shader
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
|
|
||||||
|
object TextViewCompat {
|
||||||
|
private fun tryParseDirectColour(colour: String): Int {
|
||||||
|
val additionalWebColour = ADDITIONAL_WEB_COLOURS[colour]
|
||||||
|
if (additionalWebColour != null) {
|
||||||
|
return additionalWebColour.toArgb()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color.parseColor(colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryParseVariable(tv: TextView, varName: String): Int {
|
||||||
|
return when (varName) {
|
||||||
|
"--accent" -> MaterialColors.getColor(
|
||||||
|
tv,
|
||||||
|
com.google.android.material.R.attr.colorPrimary
|
||||||
|
)
|
||||||
|
|
||||||
|
"--foreground" -> MaterialColors.getColor(
|
||||||
|
tv,
|
||||||
|
com.google.android.material.R.attr.colorOnBackground
|
||||||
|
)
|
||||||
|
|
||||||
|
"--background" -> SurfaceColors.SURFACE_0.getColor(tv.context)
|
||||||
|
|
||||||
|
"--error" -> MaterialColors.getColor(tv, com.google.android.material.R.attr.colorError)
|
||||||
|
|
||||||
|
else -> tv.currentTextColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryParseSetLinearGradient(tv: TextView, gradient: String): Pair<Shader, Int> {
|
||||||
|
val stops = mutableListOf<Pair<Float, Int>>()
|
||||||
|
|
||||||
|
val parts = mutableListOf<String>()
|
||||||
|
var startIndex = 0
|
||||||
|
var openParenthesesCount = 0
|
||||||
|
|
||||||
|
for (i in gradient.indices) {
|
||||||
|
when (gradient[i]) {
|
||||||
|
'(' -> openParenthesesCount++
|
||||||
|
')' -> openParenthesesCount--
|
||||||
|
',' -> {
|
||||||
|
if (openParenthesesCount == 0) {
|
||||||
|
val part = gradient.substring(startIndex, i).trim()
|
||||||
|
parts.add(part)
|
||||||
|
startIndex = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastPart = gradient.substring(startIndex).trim()
|
||||||
|
if (lastPart.isNotEmpty()) {
|
||||||
|
parts.add(lastPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.forEachIndexed { index, part ->
|
||||||
|
if (part.startsWith("to") || part.endsWith("deg")) {
|
||||||
|
// we don't support any other direction / blocked on compose supporting them
|
||||||
|
// TODO could probably emulate this by swapping the values around
|
||||||
|
} else {
|
||||||
|
val splitPart = part.split(" ")
|
||||||
|
|
||||||
|
val colourPart = splitPart[0]
|
||||||
|
val colour = when {
|
||||||
|
colourPart.startsWith("var(") -> {
|
||||||
|
tryParseVariable(
|
||||||
|
tv,
|
||||||
|
colourPart.substringAfter("var(").substringBeforeLast(")")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> BrushCompat.parseFunctionColour(colourPart)?.toArgb()
|
||||||
|
?: tryParseDirectColour(colourPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
val stop = if (splitPart.size == 2) {
|
||||||
|
splitPart[1].removeSuffix("%").toFloat() / 100f
|
||||||
|
} else {
|
||||||
|
index.toFloat() / (parts.size - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
stops.add(stop to colour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val width = tv.paint.measureText(tv.text.toString())
|
||||||
|
|
||||||
|
return LinearGradient(
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
width,
|
||||||
|
0f,
|
||||||
|
stops.map { it.second }.toIntArray(),
|
||||||
|
stops.map { it.first }.toFloatArray(),
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
) to stops.first().second
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColourFromRoleColour(tv: TextView, colour: String) {
|
||||||
|
when {
|
||||||
|
colour.startsWith("var(") -> {
|
||||||
|
val varName = colour.substringAfter("var(").substringBeforeLast(")")
|
||||||
|
val parsedColour = tryParseVariable(tv, varName)
|
||||||
|
tv.setTextColor(parsedColour)
|
||||||
|
}
|
||||||
|
|
||||||
|
colour.startsWith("linear-gradient(") || colour.startsWith("repeating-linear-gradient(") -> {
|
||||||
|
val gradient = colour.substringAfter("(").substringBeforeLast(")")
|
||||||
|
val shader = tryParseSetLinearGradient(tv, gradient)
|
||||||
|
tv.paint.shader = shader.first
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
try {
|
||||||
|
val directColour = tryParseDirectColour(colour)
|
||||||
|
tv.setTextColor(directColour)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Log.d(
|
||||||
|
"TextViewCompat",
|
||||||
|
"Failed to parse colour $colour, not setting colour"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.revolt.api.internals.WebCompat
|
import chat.revolt.api.internals.BrushCompat
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
|
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
|
||||||
import chat.revolt.api.schemas.Embed
|
import chat.revolt.api.schemas.Embed
|
||||||
|
|
@ -51,7 +51,7 @@ fun RegularEmbed(
|
||||||
.width(4.dp)
|
.width(4.dp)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.background(
|
.background(
|
||||||
embed.colour?.let { WebCompat.parseColour(it) }
|
embed.colour?.let { BrushCompat.parseColour(it) }
|
||||||
?: Brush.solidColor(MaterialTheme.colorScheme.primary)
|
?: Brush.solidColor(MaterialTheme.colorScheme.primary)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import chat.revolt.api.REVOLT_FILES
|
import chat.revolt.api.REVOLT_FILES
|
||||||
import chat.revolt.api.internals.Roles
|
import chat.revolt.api.internals.Roles
|
||||||
import chat.revolt.api.internals.WebCompat
|
import chat.revolt.api.internals.BrushCompat
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
import chat.revolt.api.schemas.Member
|
import chat.revolt.api.schemas.Member
|
||||||
import chat.revolt.api.schemas.User
|
import chat.revolt.api.schemas.User
|
||||||
|
|
@ -35,7 +35,7 @@ fun MemberListItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val colour = highestColourRole?.colour?.let { WebCompat.parseColour(it) }
|
val colour = highestColourRole?.colour?.let { BrushCompat.parseColour(it) }
|
||||||
?: Brush.solidColor(LocalContentColor.current)
|
?: Brush.solidColor(LocalContentColor.current)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.internals.Roles
|
import chat.revolt.api.internals.Roles
|
||||||
import chat.revolt.api.internals.SpecialUsers
|
import chat.revolt.api.internals.SpecialUsers
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
import chat.revolt.api.internals.WebCompat
|
import chat.revolt.api.internals.BrushCompat
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
import chat.revolt.api.routes.channel.react
|
import chat.revolt.api.routes.channel.react
|
||||||
import chat.revolt.api.routes.channel.unreact
|
import chat.revolt.api.routes.channel.unreact
|
||||||
|
|
@ -86,7 +86,7 @@ import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
@Composable
|
@Composable
|
||||||
fun authorColour(message: MessageSchema): Brush {
|
fun authorColour(message: MessageSchema): Brush {
|
||||||
return if (message.masquerade?.colour != null) {
|
return if (message.masquerade?.colour != null) {
|
||||||
WebCompat.parseColour(message.masquerade.colour)
|
BrushCompat.parseColour(message.masquerade.colour)
|
||||||
} else {
|
} else {
|
||||||
val defaultColour = Brush.solidColor(LocalContentColor.current)
|
val defaultColour = Brush.solidColor(LocalContentColor.current)
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ fun authorColour(message: MessageSchema): Brush {
|
||||||
Roles.resolveHighestRole(serverId, it, withColour = true)
|
Roles.resolveHighestRole(serverId, it, withColour = true)
|
||||||
} ?: return defaultColour
|
} ?: return defaultColour
|
||||||
|
|
||||||
highestRole.colour?.let { WebCompat.parseColour(it) }
|
highestRole.colour?.let { BrushCompat.parseColour(it) }
|
||||||
?: defaultColour
|
?: defaultColour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@ fun LabsHomeScreen(navController: NavController) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Divider()
|
Divider()
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text("XML Message Column")
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
navController.navigate("mockups/xmlmessage")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.navigation.compose.rememberNavController
|
||||||
import chat.revolt.api.settings.FeatureFlags
|
import chat.revolt.api.settings.FeatureFlags
|
||||||
import chat.revolt.api.settings.LabsAccessControlVariates
|
import chat.revolt.api.settings.LabsAccessControlVariates
|
||||||
import chat.revolt.screens.labs.ui.mockups.CallScreenMockup
|
import chat.revolt.screens.labs.ui.mockups.CallScreenMockup
|
||||||
|
import chat.revolt.screens.labs.ui.mockups.XMLMessageMockup
|
||||||
|
|
||||||
annotation class LabsFeature
|
annotation class LabsFeature
|
||||||
|
|
||||||
|
|
@ -65,6 +66,10 @@ fun LabsRootScreen(topNav: NavController) {
|
||||||
composable("mockups/call") {
|
composable("mockups/call") {
|
||||||
CallScreenMockup()
|
CallScreenMockup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable("mockups/xmlmessage") {
|
||||||
|
XMLMessageMockup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package chat.revolt.screens.labs.ui.mockups
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.schemas.Message
|
||||||
|
import chat.revolt.components.chat.Message
|
||||||
|
import chat.revolt.internals.markdown.MarkdownContext
|
||||||
|
import chat.revolt.internals.markdown.MarkdownParser
|
||||||
|
import chat.revolt.internals.markdown.MarkdownState
|
||||||
|
import chat.revolt.internals.markdown.addRevoltRules
|
||||||
|
import chat.revolt.internals.markdown.createCodeRule
|
||||||
|
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||||
|
import chat.revolt.views.MessageView
|
||||||
|
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
||||||
|
import com.discord.simpleast.core.simple.SimpleRenderer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun XMLMessageMockup() {
|
||||||
|
var message by remember { mutableStateOf<Message?>(null) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val codeBlockColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
|
||||||
|
fun reroll() {
|
||||||
|
message = RevoltAPI.messageCache.values.random()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
reroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.safeDrawingPadding()
|
||||||
|
) {
|
||||||
|
message?.let {
|
||||||
|
AndroidView(
|
||||||
|
factory = {
|
||||||
|
MessageView(it, onLongPress = {
|
||||||
|
Toast.makeText(context, "Long pressed!", Toast.LENGTH_SHORT).show()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
update = {
|
||||||
|
it.fromMessage(message!!)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Message(
|
||||||
|
message = message!!.copy(tail = false),
|
||||||
|
truncate = false,
|
||||||
|
onMessageContextMenu = {
|
||||||
|
Toast.makeText(context, "Context menu!", Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
parse = {
|
||||||
|
val parser = MarkdownParser()
|
||||||
|
.addRules(
|
||||||
|
SimpleMarkdownRules.createEscapeRule()
|
||||||
|
)
|
||||||
|
.addRevoltRules(context)
|
||||||
|
.addRules(
|
||||||
|
createCodeRule(context, codeBlockColor.toArgb()),
|
||||||
|
createInlineCodeRule(
|
||||||
|
context,
|
||||||
|
codeBlockColor.toArgb()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addRules(
|
||||||
|
SimpleMarkdownRules.createSimpleMarkdownRules(
|
||||||
|
includeEscapeRule = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SimpleRenderer.render(
|
||||||
|
source = it.content ?: "",
|
||||||
|
parser = parser,
|
||||||
|
initialState = MarkdownState(0),
|
||||||
|
renderContext = MarkdownContext(
|
||||||
|
memberMap = mapOf(),
|
||||||
|
userMap = RevoltAPI.userCache.toMap(),
|
||||||
|
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||||
|
ch.value.name ?: ch.value.id
|
||||||
|
?: "#DeletedChannel"
|
||||||
|
},
|
||||||
|
emojiMap = RevoltAPI.emojiCache,
|
||||||
|
serverId = message!!.channel?.let { x -> RevoltAPI.channelCache[x] }?.server
|
||||||
|
?: "",
|
||||||
|
// check if message consists solely of one *or more* custom emotes
|
||||||
|
useLargeEmojis = it.content?.matches(
|
||||||
|
Regex("(:([0-9A-Z]{26}):)+")
|
||||||
|
) == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(onClick = { reroll() }) {
|
||||||
|
Text("Different message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ import androidx.compose.ui.unit.sp
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
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.internals.WebCompat
|
import chat.revolt.api.internals.BrushCompat
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
import chat.revolt.api.routes.user.fetchUserProfile
|
import chat.revolt.api.routes.user.fetchUserProfile
|
||||||
import chat.revolt.api.schemas.Profile
|
import chat.revolt.api.schemas.Profile
|
||||||
|
|
@ -120,7 +120,7 @@ fun UserInfoSheet(
|
||||||
role?.let {
|
role?.let {
|
||||||
RoleListEntry(
|
RoleListEntry(
|
||||||
label = role.name ?: "null",
|
label = role.name ?: "null",
|
||||||
brush = role.colour?.let { WebCompat.parseColour(it) }
|
brush = role.colour?.let { BrushCompat.parseColour(it) }
|
||||||
?: Brush.solidColor(LocalContentColor.current)
|
?: Brush.solidColor(LocalContentColor.current)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ fun UserInfoSheet(
|
||||||
role?.let {
|
role?.let {
|
||||||
RoleListEntry(
|
RoleListEntry(
|
||||||
label = role.name ?: "null",
|
label = role.name ?: "null",
|
||||||
brush = role.colour?.let { WebCompat.parseColour(it) }
|
brush = role.colour?.let { BrushCompat.parseColour(it) }
|
||||||
?: Brush.solidColor(LocalContentColor.current)
|
?: Brush.solidColor(LocalContentColor.current)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
package chat.revolt.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.icu.text.DateFormat
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.REVOLT_FILES
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.internals.Roles
|
||||||
|
import chat.revolt.api.internals.TextViewCompat
|
||||||
|
import chat.revolt.api.internals.ULID
|
||||||
|
import chat.revolt.api.schemas.Message
|
||||||
|
import chat.revolt.api.schemas.User
|
||||||
|
import chat.revolt.databinding.ViewMessageBinding
|
||||||
|
import chat.revolt.internals.markdown.MarkdownContext
|
||||||
|
import chat.revolt.internals.markdown.MarkdownParser
|
||||||
|
import chat.revolt.internals.markdown.MarkdownState
|
||||||
|
import chat.revolt.internals.markdown.addRevoltRules
|
||||||
|
import chat.revolt.internals.markdown.createCodeRule
|
||||||
|
import chat.revolt.internals.markdown.createInlineCodeRule
|
||||||
|
import com.bumptech.glide.GenericTransitionOptions
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.request.transition.DrawableCrossFadeFactory
|
||||||
|
import com.discord.simpleast.core.simple.SimpleMarkdownRules
|
||||||
|
import com.discord.simpleast.core.simple.SimpleRenderer
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
|
|
||||||
|
|
||||||
|
class MessageView(
|
||||||
|
ctx: Context
|
||||||
|
) : ConstraintLayout(ctx) {
|
||||||
|
private var binding: ViewMessageBinding
|
||||||
|
private val parser = MarkdownParser()
|
||||||
|
.addRules(
|
||||||
|
SimpleMarkdownRules.createEscapeRule()
|
||||||
|
)
|
||||||
|
.addRevoltRules(context)
|
||||||
|
.addRules(
|
||||||
|
createCodeRule(context, SurfaceColors.SURFACE_2.getColor(ctx)),
|
||||||
|
createInlineCodeRule(
|
||||||
|
context,
|
||||||
|
SurfaceColors.SURFACE_2.getColor(ctx),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addRules(
|
||||||
|
SimpleMarkdownRules.createSimpleMarkdownRules(
|
||||||
|
includeEscapeRule = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private var messageServer: String? = null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
ctx: Context,
|
||||||
|
onLongPress: (() -> Unit)? = null
|
||||||
|
) : this(ctx) {
|
||||||
|
binding.root.setOnLongClickListener {
|
||||||
|
onLongPress?.invoke()
|
||||||
|
onLongPress != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(ctx, R.layout.view_message, this)
|
||||||
|
binding = ViewMessageBinding.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAuthor(author: String) {
|
||||||
|
binding.author.text = author
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContent(content: String) {
|
||||||
|
binding.messageContent.text = SimpleRenderer.render(
|
||||||
|
source = content,
|
||||||
|
parser = parser,
|
||||||
|
initialState = MarkdownState(0),
|
||||||
|
renderContext = MarkdownContext(
|
||||||
|
memberMap = messageServer?.let { RevoltAPI.members.markdownMemberMapFor(it) }
|
||||||
|
?: mapOf(),
|
||||||
|
userMap = RevoltAPI.userCache.toMap(),
|
||||||
|
channelMap = RevoltAPI.channelCache.mapValues { ch ->
|
||||||
|
ch.value.name ?: ch.value.id
|
||||||
|
?: "#DeletedChannel"
|
||||||
|
},
|
||||||
|
emojiMap = RevoltAPI.emojiCache,
|
||||||
|
serverId = messageServer,
|
||||||
|
// check if message consists solely of one *or more* custom emotes
|
||||||
|
useLargeEmojis = content.matches(
|
||||||
|
Regex("(:([0-9A-Z]{26}):)+")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTimestamp(timestamp: String) {
|
||||||
|
binding.timestamp.text = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAvatarUrl(avatar: String?) {
|
||||||
|
if (avatar == null) {
|
||||||
|
Glide.with(this).clear(binding.avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()
|
||||||
|
|
||||||
|
Glide.with(this).load(avatar).diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
|
.transition(GenericTransitionOptions.with(factory))
|
||||||
|
.circleCrop()
|
||||||
|
.into(binding.avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatLongAsTime(time: Long): String {
|
||||||
|
val date = java.util.Date(time)
|
||||||
|
|
||||||
|
val withinLastWeek = System.currentTimeMillis() - time < 604800000
|
||||||
|
|
||||||
|
return if (withinLastWeek) {
|
||||||
|
val relativeDate = DateUtils.getRelativeTimeSpanString(
|
||||||
|
time,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
DateUtils.DAY_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_ABBREV_ALL
|
||||||
|
)
|
||||||
|
val relativeTime = DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||||
|
|
||||||
|
"$relativeDate $relativeTime"
|
||||||
|
} else {
|
||||||
|
val absoluteDate = DateFormat.getDateInstance(DateFormat.SHORT).format(date)
|
||||||
|
val absoluteTime = DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||||
|
|
||||||
|
"$absoluteDate $absoluteTime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authorName(message: Message): String {
|
||||||
|
if (message.masquerade?.name != null) {
|
||||||
|
return message.masquerade.name
|
||||||
|
}
|
||||||
|
|
||||||
|
messageServer
|
||||||
|
?: return RevoltAPI.userCache[message.author]?.let { User.resolveDefaultName(it) }
|
||||||
|
?: context.getString(R.string.unknown)
|
||||||
|
|
||||||
|
val member = messageServer?.let { sid ->
|
||||||
|
message.author?.let {
|
||||||
|
RevoltAPI.members.getMember(
|
||||||
|
sid,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: return context.getString(R.string.unknown)
|
||||||
|
|
||||||
|
return member.nickname
|
||||||
|
?: RevoltAPI.userCache[message.author]?.let { User.resolveDefaultName(it) }
|
||||||
|
?: context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAuthorColour() {
|
||||||
|
binding.author.setTextColor(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.author,
|
||||||
|
com.google.android.material.R.attr.colorOnBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.author.paint.shader = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthorColour(message: Message) {
|
||||||
|
resetAuthorColour()
|
||||||
|
|
||||||
|
if (message.masquerade?.colour != null) {
|
||||||
|
TextViewCompat.setColourFromRoleColour(binding.author, message.masquerade.colour)
|
||||||
|
} else {
|
||||||
|
val serverId = RevoltAPI.channelCache[message.channel]?.server ?: return
|
||||||
|
|
||||||
|
val highestRole = message.author?.let {
|
||||||
|
Roles.resolveHighestRole(serverId, it, withColour = true)
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
highestRole.colour?.let {
|
||||||
|
TextViewCompat.setColourFromRoleColour(binding.author, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromMessage(message: Message) {
|
||||||
|
messageServer = RevoltAPI.channelCache[message.channel]?.server
|
||||||
|
|
||||||
|
message.content?.let { setContent(it) }
|
||||||
|
message.id?.let { setTimestamp(formatLongAsTime(ULID.asTimestamp(it))) }
|
||||||
|
// dont have this
|
||||||
|
val resolvedAuthor = RevoltAPI.userCache[message.author]
|
||||||
|
// dont inline this
|
||||||
|
setAvatarUrl(resolvedAuthor?.avatar?.let { "$REVOLT_FILES/avatars/${it.id}?max_side=256" }
|
||||||
|
?: "")
|
||||||
|
setAuthor(authorName(message))
|
||||||
|
|
||||||
|
setAuthorColour(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
resetAuthorColour()
|
||||||
|
setContent("")
|
||||||
|
setTimestamp("")
|
||||||
|
setAuthor("")
|
||||||
|
setAvatarUrl(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:longClickable="true"
|
||||||
|
android:id="@+id/message_container"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:id="@+id/avatar"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
tools:src="@drawable/ic_launcher_background"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textFontWeight="700"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:id="@+id/author"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
tools:text="Jennifer"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
tools:targetApi="p" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:id="@+id/timestamp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:alpha="0.6"
|
||||||
|
tools:text="10:00 AM"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/author"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/author"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/author"
|
||||||
|
tools:targetApi="p" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:id="@+id/message_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
tools:text="Hello fdsfdsfoijsdijofsjoi oijjofsdjois fdsfds fdsfsddoijsdf"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/author"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/author"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Loading…
Reference in New Issue