parent
0d2b037a6f
commit
1b63b04fde
|
|
@ -8,6 +8,8 @@ import androidx.compose.ui.graphics.Brush
|
|||
import androidx.compose.ui.graphics.Brush.Companion.linearGradient
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.core.graphics.toColorInt
|
||||
import chat.revolt.api.internals.colour.CSSColours
|
||||
|
||||
fun Brush.Companion.solidColor(colour: Color) = SolidColor(colour)
|
||||
|
||||
|
|
@ -202,16 +204,12 @@ object BrushCompat {
|
|||
@Composable
|
||||
private fun parseColourName(colour: String): Color {
|
||||
return try {
|
||||
val additionalWebColour = ADDITIONAL_WEB_COLOURS[colour]
|
||||
if (additionalWebColour != null) {
|
||||
Log.d(
|
||||
"BrushCompat",
|
||||
"Parsed additional web colour $colour to $additionalWebColour"
|
||||
)
|
||||
return additionalWebColour
|
||||
val cssColour = CSSColours[colour]
|
||||
if (cssColour != null) {
|
||||
return cssColour
|
||||
}
|
||||
|
||||
Color(android.graphics.Color.parseColor(colour))
|
||||
Color(colour.toColorInt())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.d(
|
||||
"BrushCompat",
|
||||
|
|
@ -257,6 +255,246 @@ object BrushCompat {
|
|||
}
|
||||
|
||||
|
||||
else -> {
|
||||
return Brush.solidColor(parseColourName(colour))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [BrushCompat] but does not require `@Composable` scope.
|
||||
* Instead you must initialise it with the colours you want to use.
|
||||
*/
|
||||
class InstancedBrushCompat(
|
||||
val defaultColour: Color,
|
||||
val primaryColour: Color,
|
||||
val onBackgroundColour: Color,
|
||||
val backgroundColour: Color,
|
||||
val errorColour: Color
|
||||
) {
|
||||
private fun parseLinearGradient(gradient: String): Brush {
|
||||
val stops = mutableListOf<Pair<Float, Color>>()
|
||||
|
||||
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(") -> {
|
||||
parseVarToColour(
|
||||
colourPart.substringAfter("var(").substringBeforeLast(")")
|
||||
)
|
||||
}
|
||||
|
||||
else -> parseFunctionColour(colourPart) ?: parseColourName(colourPart)
|
||||
}
|
||||
|
||||
val stop = if (splitPart.size == 2) {
|
||||
splitPart[1].removeSuffix("%").toFloat() / 100f
|
||||
} else {
|
||||
index.toFloat() / (parts.size - 1)
|
||||
}
|
||||
|
||||
stops.add(stop to colour)
|
||||
}
|
||||
}
|
||||
|
||||
return linearGradient(
|
||||
colorStops = stops.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseRadialGradient(gradient: String): Brush {
|
||||
val stops = mutableListOf<Pair<Float, Color>>()
|
||||
|
||||
val parts = mutableListOf<String>()
|
||||
var startIndex = 0
|
||||
var openParenthesesCount = 0
|
||||
|
||||
// Split the gradient string into individual components
|
||||
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)
|
||||
}
|
||||
|
||||
// Parse color stops
|
||||
parts.drop(1).forEachIndexed { index, part ->
|
||||
val splitPart = part.split(" ")
|
||||
val colorPart = splitPart[0]
|
||||
val color = when {
|
||||
colorPart.startsWith("var(") -> {
|
||||
parseVarToColour(
|
||||
colorPart.substringAfter("var(").substringBeforeLast(")")
|
||||
)
|
||||
}
|
||||
|
||||
else -> parseFunctionColour(colorPart) ?: parseColourName(colorPart)
|
||||
}
|
||||
|
||||
val stop = if (splitPart.size == 2) {
|
||||
splitPart[1].removeSuffix("%").toFloat() / 100f
|
||||
} else {
|
||||
index.toFloat() / (parts.size - 2)
|
||||
}
|
||||
|
||||
stops.add(stop to color)
|
||||
}
|
||||
|
||||
return Brush.radialGradient(
|
||||
colorStops = stops.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
fun parseFunctionColour(colourString: String): Color? {
|
||||
val cleanedString = colourString.trim()
|
||||
|
||||
return try {
|
||||
if (cleanedString.startsWith("rgb(")) {
|
||||
parseRGBColour(cleanedString)
|
||||
} else if (cleanedString.startsWith("rgba(")) {
|
||||
parseRGBAColour(cleanedString)
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid colour format: $colourString")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRGBColour(rgbString: String): Color {
|
||||
val colourParts = rgbString.removePrefix("rgb(")
|
||||
.removeSuffix(")")
|
||||
.split(",")
|
||||
.map { it.trim().toInt() }
|
||||
|
||||
val red = colourParts[0] / 255.0f
|
||||
val green = colourParts[1] / 255.0f
|
||||
val blue = colourParts[2] / 255.0f
|
||||
|
||||
return Color(red, green, blue)
|
||||
}
|
||||
|
||||
private fun parseRGBAColour(rgbaString: String): Color {
|
||||
val colourParts = rgbaString.removePrefix("rgba(")
|
||||
.removeSuffix(")")
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
|
||||
val red = colourParts[0].toInt() / 255.0f
|
||||
val green = colourParts[1].toInt() / 255.0f
|
||||
val blue = colourParts[2].toInt() / 255.0f
|
||||
val alpha = colourParts[3].removeSuffix("%").toFloat() / 100.0f
|
||||
|
||||
return Color(red, green, blue, alpha)
|
||||
}
|
||||
|
||||
private fun parseVarToColour(varName: String): Color {
|
||||
return when (varName) {
|
||||
"--accent" -> primaryColour
|
||||
"--foreground" -> onBackgroundColour
|
||||
"--background" -> backgroundColour
|
||||
"--error" -> errorColour
|
||||
else -> defaultColour
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseVar(varName: String): Brush {
|
||||
return SolidColor(parseVarToColour(varName))
|
||||
}
|
||||
|
||||
private fun parseColourName(colour: String): Color {
|
||||
return try {
|
||||
val cssColour = CSSColours[colour]
|
||||
if (cssColour != null) {
|
||||
return cssColour
|
||||
}
|
||||
|
||||
Color(colour.toColorInt())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.d(
|
||||
"BrushCompat",
|
||||
"Failed to parse colour $colour, falling back to LocalContentColor.current"
|
||||
)
|
||||
defaultColour
|
||||
}
|
||||
}
|
||||
|
||||
fun parseColour(colour: String): Brush {
|
||||
if (colour.isEmpty()) {
|
||||
return Brush.solidColor(Color.Unspecified)
|
||||
}
|
||||
|
||||
when {
|
||||
colour.startsWith("var(") -> {
|
||||
Log.d(
|
||||
"BrushCompat",
|
||||
"Parsing variable $colour"
|
||||
)
|
||||
return parseVar(
|
||||
colour.substringAfter("var(").substringBeforeLast(")")
|
||||
)
|
||||
}
|
||||
|
||||
colour.startsWith("linear-gradient(") || colour.startsWith("repeating-linear-gradient(") -> {
|
||||
return parseLinearGradient(
|
||||
colour
|
||||
.substringAfter("repeating-")
|
||||
.substringAfter("linear-gradient(")
|
||||
.substringBeforeLast(")")
|
||||
)
|
||||
}
|
||||
|
||||
colour.startsWith("radial-gradient(") || colour.startsWith("repeating-radial-gradient(") -> {
|
||||
return parseRadialGradient(
|
||||
colour
|
||||
.substringAfter("repeating-")
|
||||
.substringAfter("radial-gradient(")
|
||||
.substringBeforeLast(")")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
else -> {
|
||||
return Brush.solidColor(parseColourName(colour))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package chat.revolt.api.internals
|
||||
|
||||
enum class MessageFlag(val value: Int) {
|
||||
// Message will not send push / desktop notifications
|
||||
SuppressNotifications(1 shl 0),
|
||||
|
||||
// Message will mention all users who can see the channel
|
||||
MentionsEveryone(1 shl 1),
|
||||
|
||||
// Message will mention all users who are online and can see the channel.
|
||||
// This cannot be true if MentionsEveryone is true
|
||||
MentionsOnline(1 shl 2)
|
||||
}
|
||||
|
||||
operator fun Int.plus(other: MessageFlag): Int {
|
||||
return this or other.value
|
||||
}
|
||||
|
||||
fun Int.hasMessageFlag(flag: MessageFlag): Boolean {
|
||||
return this and flag.value == flag.value
|
||||
}
|
||||
|
||||
infix fun Int?.hasMessageFlag(flag: MessageFlag): Boolean {
|
||||
return this != null && this.hasMessageFlag(flag)
|
||||
}
|
||||
|
|
@ -1,22 +1,23 @@
|
|||
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 androidx.core.graphics.toColorInt
|
||||
import chat.revolt.api.internals.colour.CSSColours
|
||||
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()
|
||||
val cssColour = CSSColours[colour]
|
||||
if (cssColour != null) {
|
||||
return cssColour.toArgb()
|
||||
}
|
||||
|
||||
return Color.parseColor(colour)
|
||||
return colour.toColorInt()
|
||||
}
|
||||
|
||||
private fun tryParseVariable(tv: TextView, varName: String): Int {
|
||||
|
|
@ -34,7 +35,7 @@ object TextViewCompat {
|
|||
"--background" -> SurfaceColors.SURFACE_0.getColor(tv.context)
|
||||
|
||||
"--error" -> MaterialColors.getColor(tv, com.google.android.material.R.attr.colorError)
|
||||
|
||||
|
||||
else -> tv.currentTextColor
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
package chat.revolt.api.internals.colour
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val CSSColours = mapOf(
|
||||
// CSS Color Module Level 4 Editor’s Draft, 5 June 2025
|
||||
// https://drafts.csswg.org/css-color/
|
||||
"aliceblue" to Color(0xFFF0F8FF),
|
||||
"antiquewhite" to Color(0xFFFAEBD7),
|
||||
"aqua" to Color(0xFF00FFFF),
|
||||
"aquamarine" to Color(0xFF7FFFD4),
|
||||
"azure" to Color(0xFFF0FFFF),
|
||||
"beige" to Color(0xFFF5F5DC),
|
||||
"bisque" to Color(0xFFFFE4C4),
|
||||
"black" to Color(0xFF000000),
|
||||
"blanchedalmond" to Color(0xFFFFEBCD),
|
||||
"blue" to Color(0xFF0000FF),
|
||||
"blueviolet" to Color(0xFF8A2BE2),
|
||||
"brown" to Color(0xFFA52A2A),
|
||||
"burlywood" to Color(0xFFDEB887),
|
||||
"cadetblue" to Color(0xFF5F9EA0),
|
||||
"chartreuse" to Color(0xFF7FFF00),
|
||||
"chocolate" to Color(0xFFD2691E),
|
||||
"coral" to Color(0xFFFF7F50),
|
||||
"cornflowerblue" to Color(0xFF6495ED),
|
||||
"cornsilk" to Color(0xFFFFF8DC),
|
||||
"crimson" to Color(0xFFDC143C),
|
||||
"cyan" to Color(0xFF00FFFF),
|
||||
"darkblue" to Color(0xFF00008B),
|
||||
"darkcyan" to Color(0xFF008B8B),
|
||||
"darkgoldenrod" to Color(0xFFB8860B),
|
||||
"darkgray" to Color(0xFFA9A9A9),
|
||||
"darkgreen" to Color(0xFF006400),
|
||||
"darkgrey" to Color(0xFFA9A9A9),
|
||||
"darkkhaki" to Color(0xFFBDB76B),
|
||||
"darkmagenta" to Color(0xFF8B008B),
|
||||
"darkolivegreen" to Color(0xFF556B2F),
|
||||
"darkorange" to Color(0xFFFF8C00),
|
||||
"darkorchid" to Color(0xFF9932CC),
|
||||
"darkred" to Color(0xFF8B0000),
|
||||
"darksalmon" to Color(0xFFE9967A),
|
||||
"darkseagreen" to Color(0xFF8FBC8F),
|
||||
"darkslateblue" to Color(0xFF483D8B),
|
||||
"darkslategray" to Color(0xFF2F4F4F),
|
||||
"darkslategrey" to Color(0xFF2F4F4F),
|
||||
"darkturquoise" to Color(0xFF00CED1),
|
||||
"darkviolet" to Color(0xFF9400D3),
|
||||
"deeppink" to Color(0xFFFF1493),
|
||||
"deepskyblue" to Color(0xFF00BFFF),
|
||||
"dimgray" to Color(0xFF696969),
|
||||
"dimgrey" to Color(0xFF696969),
|
||||
"dodgerblue" to Color(0xFF1E90FF),
|
||||
"firebrick" to Color(0xFFB22222),
|
||||
"floralwhite" to Color(0xFFFFFAF0),
|
||||
"forestgreen" to Color(0xFF228B22),
|
||||
"fuchsia" to Color(0xFFFF00FF),
|
||||
"gainsboro" to Color(0xFFDCDCDC),
|
||||
"ghostwhite" to Color(0xFFF8F8FF),
|
||||
"gold" to Color(0xFFFFD700),
|
||||
"goldenrod" to Color(0xFFDAA520),
|
||||
"gray" to Color(0xFF808080),
|
||||
"green" to Color(0xFF008000),
|
||||
"greenyellow" to Color(0xFFADFF2F),
|
||||
"grey" to Color(0xFF808080),
|
||||
"honeydew" to Color(0xFFF0FFF0),
|
||||
"hotpink" to Color(0xFFFF69B4),
|
||||
"indianred" to Color(0xFFCD5C5C),
|
||||
"indigo" to Color(0xFF4B0082),
|
||||
"ivory" to Color(0xFFFFFFF0),
|
||||
"khaki" to Color(0xFFF0E68C),
|
||||
"lavender" to Color(0xFFE6E6FA),
|
||||
"lavenderblush" to Color(0xFFFFF0F5),
|
||||
"lawngreen" to Color(0xFF7CFC00),
|
||||
"lemonchiffon" to Color(0xFFFFFACD),
|
||||
"lightblue" to Color(0xFFADD8E6),
|
||||
"lightcoral" to Color(0xFFF08080),
|
||||
"lightcyan" to Color(0xFFE0FFFF),
|
||||
"lightgoldenrodyellow" to Color(0xFFFAFAD2),
|
||||
"lightgray" to Color(0xFFD3D3D3),
|
||||
"lightgreen" to Color(0xFF90EE90),
|
||||
"lightgrey" to Color(0xFFD3D3D3),
|
||||
"lightpink" to Color(0xFFFFB6C1),
|
||||
"lightsalmon" to Color(0xFFFFA07A),
|
||||
"lightseagreen" to Color(0xFF20B2AA),
|
||||
"lightskyblue" to Color(0xFF87CEFA),
|
||||
"lightslategray" to Color(0xFF778899),
|
||||
"lightslategrey" to Color(0xFF778899),
|
||||
"lightsteelblue" to Color(0xFFB0C4DE),
|
||||
"lightyellow" to Color(0xFFFFFFE0),
|
||||
"lime" to Color(0xFF00FF00),
|
||||
"limegreen" to Color(0xFF32CD32),
|
||||
"linen" to Color(0xFFFAF0E6),
|
||||
"magenta" to Color(0xFFFF00FF),
|
||||
"maroon" to Color(0xFF800000),
|
||||
"mediumaquamarine" to Color(0xFF66CDAA),
|
||||
"mediumblue" to Color(0xFF0000CD),
|
||||
"mediumorchid" to Color(0xFFBA55D3),
|
||||
"mediumpurple" to Color(0xFF9370DB),
|
||||
"mediumseagreen" to Color(0xFF3CB371),
|
||||
"mediumslateblue" to Color(0xFF7B68EE),
|
||||
"mediumspringgreen" to Color(0xFF00FA9A),
|
||||
"mediumturquoise" to Color(0xFF48D1CC),
|
||||
"mediumvioletred" to Color(0xFFC71585),
|
||||
"midnightblue" to Color(0xFF191970),
|
||||
"mintcream" to Color(0xFFF5FFFA),
|
||||
"mistyrose" to Color(0xFFFFE4E1),
|
||||
"moccasin" to Color(0xFFFFE4B5),
|
||||
"navajowhite" to Color(0xFFFFDEAD),
|
||||
"navy" to Color(0xFF000080),
|
||||
"oldlace" to Color(0xFFFDF5E6),
|
||||
"olive" to Color(0xFF808000),
|
||||
"olivedrab" to Color(0xFF6B8E23),
|
||||
"orange" to Color(0xFFFFA500),
|
||||
"orangered" to Color(0xFFFF4500),
|
||||
"orchid" to Color(0xFFDA70D6),
|
||||
"palegoldenrod" to Color(0xFFEEE8AA),
|
||||
"palegreen" to Color(0xFF98FB98),
|
||||
"paleturquoise" to Color(0xFFAFEEEE),
|
||||
"palevioletred" to Color(0xFFDB7093),
|
||||
"papayawhip" to Color(0xFFFFEFD5),
|
||||
"peachpuff" to Color(0xFFFFDAB9),
|
||||
"peru" to Color(0xFFCD853F),
|
||||
"pink" to Color(0xFFFFC0CB),
|
||||
"plum" to Color(0xFFDDA0DD),
|
||||
"powderblue" to Color(0xFFB0E0E6),
|
||||
"purple" to Color(0xFF800080),
|
||||
"rebeccapurple" to Color(0xFF663399),
|
||||
"red" to Color(0xFFFF0000),
|
||||
"rosybrown" to Color(0xFFBC8F8F),
|
||||
"royalblue" to Color(0xFF4169E1),
|
||||
"saddlebrown" to Color(0xFF8B4513),
|
||||
"salmon" to Color(0xFFFA8072),
|
||||
"sandybrown" to Color(0xFFF4A460),
|
||||
"seagreen" to Color(0xFF2E8B57),
|
||||
"seashell" to Color(0xFFFFF5EE),
|
||||
"sienna" to Color(0xFFA0522D),
|
||||
"silver" to Color(0xFFC0C0C0),
|
||||
"skyblue" to Color(0xFF87CEEB),
|
||||
"slateblue" to Color(0xFF6A5ACD),
|
||||
"slategray" to Color(0xFF708090),
|
||||
"slategrey" to Color(0xFF708090),
|
||||
"snow" to Color(0xFFFFFAFA),
|
||||
"springgreen" to Color(0xFF00FF7F),
|
||||
"steelblue" to Color(0xFF4682B4),
|
||||
"tan" to Color(0xFFD2B48C),
|
||||
"teal" to Color(0xFF008080),
|
||||
"thistle" to Color(0xFFD8BFD8),
|
||||
"tomato" to Color(0xFFFF6347),
|
||||
"turquoise" to Color(0xFF40E0D0),
|
||||
"violet" to Color(0xFFEE82EE),
|
||||
"wheat" to Color(0xFFF5DEB3),
|
||||
"white" to Color(0xFFFFFFFF),
|
||||
"whitesmoke" to Color(0xFFF5F5F5),
|
||||
"yellow" to Color(0xFFFFFF00),
|
||||
"yellowgreen" to Color(0xFF9ACD32),
|
||||
"transparent" to Color.Transparent,
|
||||
"inherit" to Color.Unspecified,
|
||||
"currentColor" to Color.Unspecified,
|
||||
"initial" to Color.Unspecified,
|
||||
"unset" to Color.Unspecified
|
||||
)
|
||||
|
|
@ -26,6 +26,8 @@ import chat.revolt.api.realtime.frames.receivable.ServerDeleteFrame
|
|||
import chat.revolt.api.realtime.frames.receivable.ServerMemberJoinFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.ServerMemberLeaveFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.ServerMemberUpdateFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.ServerRoleDeleteFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.ServerRoleUpdateFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.ServerUpdateFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.UserRelationshipFrame
|
||||
import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame
|
||||
|
|
@ -36,6 +38,7 @@ import chat.revolt.api.realtime.frames.sendable.PingFrame
|
|||
import chat.revolt.api.routes.server.fetchMember
|
||||
import chat.revolt.api.schemas.Channel
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
import chat.revolt.api.schemas.Role
|
||||
import chat.revolt.api.settings.LoadedSettings
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
import chat.revolt.c2dm.ChannelRegistrator
|
||||
|
|
@ -674,7 +677,6 @@ object RealtimeSocket {
|
|||
}
|
||||
|
||||
"ServerMemberUpdate" -> {
|
||||
Log.d("RealtimeSocket", "Received server member update frame. Raw: $rawFrame")
|
||||
val serverMemberUpdateFrame =
|
||||
RevoltJson.decodeFromString(ServerMemberUpdateFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
|
|
@ -730,6 +732,77 @@ object RealtimeSocket {
|
|||
)
|
||||
}
|
||||
|
||||
"ServerRoleUpdate" -> {
|
||||
val serverRoleUpdateFrame =
|
||||
RevoltJson.decodeFromString(ServerRoleUpdateFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received server role update frame for ${serverRoleUpdateFrame.id}."
|
||||
)
|
||||
|
||||
val server = RevoltAPI.serverCache[serverRoleUpdateFrame.id]
|
||||
if (server == null) {
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Server ${serverRoleUpdateFrame.id} not found in cache. Ignoring role update."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val existingRole = server.roles?.get(serverRoleUpdateFrame.roleId)
|
||||
if (existingRole == null) {
|
||||
// New role.
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"New role ${serverRoleUpdateFrame.roleId} in server ${serverRoleUpdateFrame.id}. Adding to cache."
|
||||
)
|
||||
val newRole = Role().mergeWithPartial(serverRoleUpdateFrame.data)
|
||||
val newServer = server.copy(
|
||||
roles = server.roles?.plus(
|
||||
Pair(serverRoleUpdateFrame.roleId, newRole)
|
||||
) ?: mapOf(serverRoleUpdateFrame.roleId to newRole)
|
||||
)
|
||||
RevoltAPI.serverCache[serverRoleUpdateFrame.id] = newServer
|
||||
} else {
|
||||
// True role update.
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Updating existing role ${serverRoleUpdateFrame.roleId} in server ${serverRoleUpdateFrame.id}."
|
||||
)
|
||||
val updatedRole = existingRole.mergeWithPartial(serverRoleUpdateFrame.data)
|
||||
val newServer = server.copy(
|
||||
roles = server.roles.plus(
|
||||
Pair(serverRoleUpdateFrame.roleId, updatedRole)
|
||||
)
|
||||
)
|
||||
RevoltAPI.serverCache[serverRoleUpdateFrame.id] = newServer
|
||||
}
|
||||
}
|
||||
|
||||
"ServerRoleDelete" -> {
|
||||
val serverRoleDeleteFrame =
|
||||
RevoltJson.decodeFromString(ServerRoleDeleteFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received server role delete frame for ${serverRoleDeleteFrame.id} and role ${serverRoleDeleteFrame.roleId}."
|
||||
)
|
||||
|
||||
val server = RevoltAPI.serverCache[serverRoleDeleteFrame.id]
|
||||
if (server == null) {
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Server ${serverRoleDeleteFrame.id} not found in cache. Ignoring role delete."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newRoles = server.roles?.toMutableMap() ?: mutableMapOf()
|
||||
newRoles.remove(serverRoleDeleteFrame.roleId)
|
||||
|
||||
RevoltAPI.serverCache[serverRoleDeleteFrame.id] =
|
||||
server.copy(roles = newRoles)
|
||||
}
|
||||
|
||||
"Authenticated" -> {
|
||||
SyncedSettings.fetch()
|
||||
LoadedSettings.hydrateWithSettings(SyncedSettings)
|
||||
|
|
|
|||
|
|
@ -75,7 +75,17 @@ data class Role(
|
|||
val colour: String? = null,
|
||||
val hoist: Boolean? = null,
|
||||
val rank: Double? = null
|
||||
)
|
||||
) {
|
||||
fun mergeWithPartial(other: Role): Role {
|
||||
return Role(
|
||||
name = other.name ?: name,
|
||||
permissions = other.permissions ?: permissions,
|
||||
colour = other.colour ?: colour,
|
||||
hoist = other.hoist ?: hoist,
|
||||
rank = other.rank ?: rank
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PermissionDescription(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package chat.revolt.composables.chat
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.icu.text.DateFormat
|
||||
import android.net.Uri
|
||||
|
|
@ -37,9 +38,13 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -79,6 +84,7 @@ import chat.revolt.composables.generic.UserAvatarWidthPlaceholder
|
|||
import chat.revolt.composables.markdown.LocalMarkdownTreeConfig
|
||||
import chat.revolt.composables.markdown.RichMarkdown
|
||||
import chat.revolt.internals.text.Gigamoji
|
||||
import chat.revolt.internals.text.MessageProcessor
|
||||
import chat.revolt.markdown.jbm.JBM
|
||||
import chat.revolt.markdown.jbm.JBMRenderer
|
||||
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
|
||||
|
|
@ -173,6 +179,7 @@ fun formatLongAsTime(time: Long): String {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class, JBM::class)
|
||||
@Composable
|
||||
fun Message(
|
||||
|
|
@ -201,6 +208,17 @@ fun Message(
|
|||
|
||||
val authorIsBlocked = remember(author) { author.relationship == "Blocked" }
|
||||
|
||||
var mentionsSelfRole by remember(message) { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
val serverId =
|
||||
RevoltAPI.channelCache[message.channel]?.server ?: return@LaunchedEffect
|
||||
var selfMember = RevoltAPI.selfId?.let { RevoltAPI.members.getMember(serverId, it) }
|
||||
?: return@LaunchedEffect
|
||||
var messageRoleMentions = MessageProcessor.findMentionedRoleIDs(message.content)
|
||||
|
||||
mentionsSelfRole = selfMember.roles?.any { it in messageRoleMentions } == true
|
||||
}
|
||||
|
||||
Column(modifier.animateContentSize()) {
|
||||
if (message.tail == false) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
|
@ -244,7 +262,7 @@ fun Message(
|
|||
} else {
|
||||
Column(
|
||||
modifier = Modifier.then(
|
||||
if (message.mentions?.contains(RevoltAPI.selfId) == true) {
|
||||
if ((message.mentions?.contains(RevoltAPI.selfId) == true) || mentionsSelfRole) {
|
||||
Modifier.background(
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
|
|
@ -260,12 +278,11 @@ fun Message(
|
|||
InReplyTo(
|
||||
channelId = chId,
|
||||
messageId = reply,
|
||||
withMention = replyMessage?.author?.let {
|
||||
withMention = (replyMessage?.author?.let {
|
||||
message.mentions?.contains(
|
||||
replyMessage.author
|
||||
)
|
||||
}
|
||||
?: false
|
||||
} == true),
|
||||
) {
|
||||
// TODO Add jump to message
|
||||
if (replyMessage == null) {
|
||||
|
|
@ -511,7 +528,8 @@ fun Message(
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
message.reactions?.forEach { reaction ->
|
||||
Reaction(reaction.key, reaction.value,
|
||||
Reaction(
|
||||
reaction.key, reaction.value,
|
||||
onClick = { hasOwn ->
|
||||
scope.launch {
|
||||
if (hasOwn) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ import chat.revolt.R
|
|||
import chat.revolt.activities.RevoltTweenFloat
|
||||
import chat.revolt.activities.RevoltTweenInt
|
||||
import chat.revolt.api.REVOLT_FILES
|
||||
import chat.revolt.api.internals.BrushCompat
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
import chat.revolt.api.schemas.Member
|
||||
import chat.revolt.composables.generic.RemoteImage
|
||||
|
|
@ -128,6 +129,12 @@ sealed class AutocompleteSuggestion {
|
|||
val custom: chat.revolt.api.schemas.Emoji?,
|
||||
val query: String
|
||||
) : AutocompleteSuggestion()
|
||||
|
||||
data class Role(
|
||||
val role: chat.revolt.api.schemas.Role,
|
||||
val id: String,
|
||||
val query: String
|
||||
) : AutocompleteSuggestion()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
@ -218,7 +225,7 @@ fun MessageField(
|
|||
lastWord.startsWith('@') -> {
|
||||
if (channelId != null && serverId != null) {
|
||||
autocompleteSuggestions.addAll(
|
||||
Autocomplete.user(
|
||||
Autocomplete.userOrRole(
|
||||
channelId,
|
||||
serverId,
|
||||
lastWord.substring(1)
|
||||
|
|
@ -281,6 +288,7 @@ fun MessageField(
|
|||
is AutocompleteSuggestion.User -> item.user.id!!
|
||||
is AutocompleteSuggestion.Channel -> item.channel.id!!
|
||||
is AutocompleteSuggestion.Emoji -> item.shortcode
|
||||
is AutocompleteSuggestion.Role -> item.id
|
||||
}
|
||||
}) {
|
||||
when (val item = autocompleteSuggestions[it]) {
|
||||
|
|
@ -317,6 +325,48 @@ fun MessageField(
|
|||
)
|
||||
}
|
||||
|
||||
is AutocompleteSuggestion.Role -> {
|
||||
SuggestionChip(
|
||||
onClick = {
|
||||
textFieldState.edit {
|
||||
val lastWordStartsAt =
|
||||
textFieldState.text
|
||||
.substring(0, textFieldState.selection.max)
|
||||
.lastWordStartsAt()
|
||||
replace(
|
||||
if (lastWordStartsAt == -1) 0 else (lastWordStartsAt + 1),
|
||||
textFieldState.selection.max,
|
||||
"<%${item.id}> "
|
||||
)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = "@${item.role.name}",
|
||||
style = item.role.colour?.let {
|
||||
LocalTextStyle.current.copy(
|
||||
brush = BrushCompat.parseColour(it)
|
||||
)
|
||||
} ?: LocalTextStyle.current
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
item.role.colour?.let { BrushCompat.parseColour(it) }
|
||||
?: SolidColor(MaterialTheme.colorScheme.primaryContainer)
|
||||
)
|
||||
.size(SuggestionChipDefaults.IconSize)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
)
|
||||
}
|
||||
|
||||
is AutocompleteSuggestion.Channel -> {
|
||||
SuggestionChip(
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ object Autocomplete {
|
|||
val customResults =
|
||||
RevoltAPI.emojiCache.values.filter {
|
||||
it.name?.contains(query, ignoreCase = true) ?: false
|
||||
}.map {
|
||||
}.mapNotNull {
|
||||
if (it.name != null) {
|
||||
AutocompleteSuggestion.Emoji(
|
||||
":${it.id}:",
|
||||
|
|
@ -32,16 +32,16 @@ object Autocomplete {
|
|||
} else {
|
||||
null
|
||||
}
|
||||
}.filterNotNull().distinctBy { it.custom?.id }
|
||||
}.distinctBy { it.custom?.id }
|
||||
|
||||
return (unicodeResults + customResults)
|
||||
}
|
||||
|
||||
fun user(
|
||||
fun userOrRole(
|
||||
channelId: String,
|
||||
serverId: String? = null,
|
||||
query: String
|
||||
): List<AutocompleteSuggestion.User> {
|
||||
): List<AutocompleteSuggestion> {
|
||||
val channel = RevoltAPI.channelCache[channelId] ?: return emptyList()
|
||||
|
||||
return when (channel.channelType) {
|
||||
|
|
@ -102,6 +102,7 @@ object Autocomplete {
|
|||
if (serverId == null) return emptyList()
|
||||
if (query.length < 2) return emptyList()
|
||||
|
||||
val roles = RevoltAPI.serverCache[serverId]?.roles ?: emptyMap()
|
||||
val byNickname = RevoltAPI.members.filterNamesFor(serverId, query)
|
||||
.map { m -> m to RevoltAPI.userCache[m.id?.user] }.filter { (_, u) ->
|
||||
u != null
|
||||
|
|
@ -112,7 +113,7 @@ object Autocomplete {
|
|||
it.username?.contains(
|
||||
query,
|
||||
ignoreCase = true
|
||||
) ?: false
|
||||
) == true
|
||||
}.mapNotNull {
|
||||
it.id?.let { _ ->
|
||||
RevoltAPI.members.getMember(
|
||||
|
|
@ -126,15 +127,32 @@ object Autocomplete {
|
|||
member!! to user
|
||||
}
|
||||
|
||||
val all = (byNickname + byUsername).distinctBy { it.first.id }
|
||||
val allUsers = (byNickname + byUsername).distinctBy { it.first.id }
|
||||
val rolesByName =
|
||||
roles.filter { it.value.name?.contains(query, ignoreCase = true) == true }
|
||||
.map { it.value to it.key }
|
||||
|
||||
all.map {
|
||||
|
||||
(allUsers.map {
|
||||
AutocompleteSuggestion.User(
|
||||
it.second,
|
||||
it.first,
|
||||
query
|
||||
)
|
||||
}
|
||||
} + rolesByName.map { (role, roleId) ->
|
||||
AutocompleteSuggestion.Role(
|
||||
role,
|
||||
roleId,
|
||||
query
|
||||
)
|
||||
})
|
||||
.sortedBy {
|
||||
when (it) {
|
||||
is AutocompleteSuggestion.User -> it.user.username
|
||||
is AutocompleteSuggestion.Role -> it.role.name
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> emptyList()
|
||||
|
|
@ -148,7 +166,7 @@ object Autocomplete {
|
|||
val server = RevoltAPI.serverCache[serverId] ?: return emptyList()
|
||||
val channels = server.channels?.mapNotNull { RevoltAPI.channelCache[it] } ?: emptyList()
|
||||
|
||||
return channels.filter { it.name?.contains(query, ignoreCase = true) ?: false }.map {
|
||||
return channels.filter { it.name?.contains(query, ignoreCase = true) == true }.map {
|
||||
AutocompleteSuggestion.Channel(
|
||||
it,
|
||||
query
|
||||
|
|
|
|||
|
|
@ -60,4 +60,11 @@ object MessageProcessor {
|
|||
|
||||
return returnable
|
||||
}
|
||||
|
||||
private val roleRegex = "<%([0-9A-HJKMNP-TV-Z]{26})>".toRegex()
|
||||
fun findMentionedRoleIDs(content: String?): List<String> {
|
||||
if (content.isNullOrEmpty()) return emptyList()
|
||||
return roleRegex.findAll(content).map { it.groupValues[1] }.toList().distinct()
|
||||
.filter { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,11 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -46,6 +48,7 @@ import androidx.compose.ui.geometry.Offset
|
|||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
|
@ -72,6 +75,8 @@ import chat.revolt.R
|
|||
import chat.revolt.activities.InviteActivity
|
||||
import chat.revolt.api.REVOLT_FILES
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.internals.BrushCompat
|
||||
import chat.revolt.api.internals.InstancedBrushCompat
|
||||
import chat.revolt.api.internals.isUlid
|
||||
import chat.revolt.api.routes.custom.fetchEmoji
|
||||
import chat.revolt.api.schemas.isInviteUri
|
||||
|
|
@ -102,10 +107,12 @@ enum class JBMAnnotations(val tag: String, val clickable: Boolean) {
|
|||
URL("URL", true),
|
||||
UserMention("UserMention", true),
|
||||
ChannelMention("ChannelMention", true),
|
||||
RoleMention("RoleMention", false),
|
||||
CustomEmote("CustomEmote", true),
|
||||
Timestamp("Timestamp", false),
|
||||
Checkbox("Checkbox", false),
|
||||
UserAvatar("UserAvatar", true),
|
||||
RoleChip("RoleChip", false),
|
||||
JBMBackgroundRoundingStart("JBMBackgroundRoundingStart", false),
|
||||
JBMBackgroundRoundingEnd("JBMBackgroundRoundingEnd", false),
|
||||
}
|
||||
|
|
@ -130,7 +137,8 @@ data class JBMarkdownTreeState(
|
|||
val colors: JBMColors = JBMColors(
|
||||
clickable = Color(0xFFFF00FF),
|
||||
clickableBackground = Color(0x2000FF00)
|
||||
)
|
||||
),
|
||||
val brushCompat: InstancedBrushCompat? = null
|
||||
)
|
||||
|
||||
val LocalJBMarkdownTreeState =
|
||||
|
|
@ -153,6 +161,13 @@ fun JBMRenderer(content: String, modifier: Modifier = Modifier) {
|
|||
colors = JBMColors(
|
||||
clickable = MaterialTheme.colorScheme.primary,
|
||||
clickableBackground = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
|
||||
),
|
||||
brushCompat = InstancedBrushCompat(
|
||||
defaultColour = LocalContentColor.current,
|
||||
primaryColour = MaterialTheme.colorScheme.primary,
|
||||
onBackgroundColour = MaterialTheme.colorScheme.onBackground,
|
||||
backgroundColour = MaterialTheme.colorScheme.background,
|
||||
errorColour = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
|
@ -244,6 +259,50 @@ private fun annotateText(
|
|||
}
|
||||
}
|
||||
|
||||
RSMElementTypes.ROLE_MENTION -> {
|
||||
val contents = node.getTextInNode(sourceText).toString()
|
||||
val roleId = contents.removeSurrounding("<%", ">")
|
||||
if (roleId == contents || !roleId.isUlid() || state.currentServer == null) {
|
||||
// Invalid role mention. Append as if it were regular text.
|
||||
for (child in node.children) {
|
||||
append(annotateText(state, child))
|
||||
}
|
||||
} else {
|
||||
val server = RevoltAPI.serverCache[state.currentServer]
|
||||
val role = server?.roles?.get(roleId)
|
||||
val isGradient = role?.colour?.contains("gradient") == true
|
||||
|
||||
pushStyle(
|
||||
SpanStyle(
|
||||
background = state.colors.clickableBackground
|
||||
)
|
||||
)
|
||||
pushStyle(
|
||||
SpanStyle(
|
||||
brush = (if (!isGradient) role?.colour?.let {
|
||||
state.brushCompat?.parseColour(
|
||||
it
|
||||
)
|
||||
} else null)
|
||||
?: SolidColor(state.colors.clickable),
|
||||
)
|
||||
)
|
||||
pushStyle(
|
||||
SpanStyle(
|
||||
background = state.colors.clickableBackground
|
||||
)
|
||||
)
|
||||
append(" ")
|
||||
appendInlineContent(JBMAnnotations.RoleChip.tag, roleId)
|
||||
append(" ")
|
||||
append(role?.name ?: "invalid-role")
|
||||
append(" ")
|
||||
pop()
|
||||
pop()
|
||||
pop()
|
||||
}
|
||||
}
|
||||
|
||||
RSMElementTypes.CUSTOM_EMOTE -> {
|
||||
val contents = node.getTextInNode(sourceText).toString()
|
||||
val emoteId = contents.removeSurrounding(":", ":")
|
||||
|
|
@ -649,6 +708,40 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
|
|||
}
|
||||
}
|
||||
),
|
||||
JBMAnnotations.RoleChip.tag to with(LocalDensity.current) {
|
||||
val placeholderBaseWidth =
|
||||
(LocalTextStyle.current.fontSize * 1.5).toPx() - (avatarPadding * 2).toPx()
|
||||
val widthTolerancePx =
|
||||
2 // Else we get a gap of about 1-2 pixels due to rounding errors
|
||||
val placeholderBaseHeight = (LocalTextStyle.current.fontSize * 1.5).toPx()
|
||||
val heightTolerancePx = 2 // Dito
|
||||
|
||||
InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = (placeholderBaseWidth - widthTolerancePx).toSp(),
|
||||
height = (placeholderBaseHeight - heightTolerancePx).toSp(),
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
),
|
||||
) { id ->
|
||||
val role = RevoltAPI.serverCache[mdState.currentServer]?.roles?.get(id)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.background(LocalJBMarkdownTreeState.current.colors.clickableBackground)
|
||||
.padding(vertical = avatarPadding)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
role?.colour?.let { BrushCompat.parseColour(it) }
|
||||
?: SolidColor(MaterialTheme.colorScheme.primaryContainer)
|
||||
)
|
||||
.size((LocalTextStyle.current.fontSize * 1.5).toDp() - (avatarPadding * 2))
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
JBMAnnotations.CustomEmote.tag to InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = LocalTextStyle.current.fontSize * 1.5,
|
||||
|
|
@ -811,10 +904,8 @@ private fun annotateHighlights(
|
|||
it.location.end
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
|
||||
return AnnotatedString(source, spanStyles = highlightStyles)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ object RSMElementTypes {
|
|||
@JvmField
|
||||
val CHANNEL_MENTION: IElementType = MarkdownElementType("CHANNEL_MENTION")
|
||||
|
||||
@JvmField
|
||||
val ROLE_MENTION: IElementType = MarkdownElementType("ROLE_MENTION")
|
||||
|
||||
@JvmField
|
||||
val CUSTOM_EMOTE: IElementType = MarkdownElementType("EMOJI")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package chat.revolt.markdown.jbm
|
|||
|
||||
import chat.revolt.markdown.jbm.sequentialparsers.ChannelMentionParser
|
||||
import chat.revolt.markdown.jbm.sequentialparsers.CustomEmoteParser
|
||||
import chat.revolt.markdown.jbm.sequentialparsers.RoleMentionParser
|
||||
import chat.revolt.markdown.jbm.sequentialparsers.UserMentionParser
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||
|
|
@ -24,6 +25,7 @@ class RSMFlavourDescriptor : GFMFlavourDescriptor() {
|
|||
return listOf(
|
||||
UserMentionParser(),
|
||||
ChannelMentionParser(),
|
||||
RoleMentionParser(),
|
||||
CustomEmoteParser(),
|
||||
AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)),
|
||||
BacktickParser(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package chat.revolt.markdown.jbm.sequentialparsers
|
||||
|
||||
import chat.revolt.markdown.jbm.RSMElementTypes
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder
|
||||
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
|
||||
import org.intellij.markdown.parser.sequentialparsers.TokensCache
|
||||
|
||||
class RoleMentionParser : SequentialParser {
|
||||
override fun parse(
|
||||
tokens: TokensCache,
|
||||
rangesToGlue: List<IntRange>
|
||||
): SequentialParser.ParsingResult {
|
||||
val result = SequentialParser.ParsingResultBuilder()
|
||||
val delegateIndices = RangesListBuilder()
|
||||
var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue)
|
||||
|
||||
while (iterator.type != null) {
|
||||
if (iterator.type == MarkdownTokenTypes.LT && iterator.charLookup(1) == '%') {
|
||||
val start = iterator.index
|
||||
while (iterator.type != MarkdownTokenTypes.GT && iterator.type != null) {
|
||||
iterator = iterator.advance()
|
||||
}
|
||||
if (iterator.type == MarkdownTokenTypes.GT) {
|
||||
result.withNode(
|
||||
SequentialParser.Node(
|
||||
start..iterator.index + 1,
|
||||
RSMElementTypes.ROLE_MENTION
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (iterator.type == MarkdownTokenTypes.LT) {
|
||||
delegateIndices.put(iterator.index)
|
||||
} else {
|
||||
delegateIndices.put(iterator.index)
|
||||
}
|
||||
iterator = iterator.advance()
|
||||
}
|
||||
|
||||
return result.withFurtherProcessing(delegateIndices.get())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue