feat(markdown): support timestamps

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-07-03 16:38:22 +02:00
parent 10a40f75e7
commit 33fd3c6bd4
4 changed files with 96 additions and 0 deletions

View File

@ -29,6 +29,7 @@ import chat.revolt.internals.markdown.CustomEmoteRule
import chat.revolt.internals.markdown.MarkdownContext import chat.revolt.internals.markdown.MarkdownContext
import chat.revolt.internals.markdown.MarkdownParser import chat.revolt.internals.markdown.MarkdownParser
import chat.revolt.internals.markdown.MarkdownState import chat.revolt.internals.markdown.MarkdownState
import chat.revolt.internals.markdown.TimestampRule
import chat.revolt.internals.markdown.UserMentionRule import chat.revolt.internals.markdown.UserMentionRule
import chat.revolt.internals.markdown.createCodeRule import chat.revolt.internals.markdown.createCodeRule
import chat.revolt.internals.markdown.createInlineCodeRule import chat.revolt.internals.markdown.createInlineCodeRule
@ -61,6 +62,7 @@ fun UIMarkdown(
UserMentionRule(), UserMentionRule(),
ChannelMentionRule(), ChannelMentionRule(),
CustomEmoteRule(), CustomEmoteRule(),
TimestampRule(),
) )
.addRules( .addRules(
createCodeRule(context, codeBlockColor.toArgb()), createCodeRule(context, codeBlockColor.toArgb()),

View File

@ -1,7 +1,15 @@
package chat.revolt.internals.markdown package chat.revolt.internals.markdown
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.format.DateUtils
import android.util.Log
import com.discord.simpleast.core.node.Node import com.discord.simpleast.core.node.Node
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
class UserMentionNode(private val userId: String) : Node<MarkdownContext>() { class UserMentionNode(private val userId: String) : Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) { override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
@ -30,3 +38,67 @@ class CustomEmoteNode(private val emoteId: String) : Node<MarkdownContext>() {
) )
} }
} }
class TimestampNode(private val timestamp: Long, private val modifier: String? = null) :
Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
val normalisedModifier = modifier.orEmpty().removePrefix(":")
val instant = Instant.fromEpochSeconds(timestamp)
val javaInstant = instant.toJavaInstant()
try {
if (timestamp < 0) {
builder.append("<invalid timestamp>")
return
}
val outString = when (normalisedModifier) {
// 22:22
"t" -> DateTimeFormatter.ofPattern("HH:mm")
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
.format(javaInstant)
// 22:22:22
"T" -> DateTimeFormatter.ofPattern("HH:mm:ss")
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
.format(javaInstant)
// 22 September 2022
"D" -> DateTimeFormatter.ofPattern("dd MMMM yyyy")
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
.format(javaInstant)
// 22 September 2022 22:22
"f" -> DateTimeFormatter.ofPattern("dd MMMM yyyy HH:mm")
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
.format(javaInstant)
// Thursday, 22 September 2022 22:22
"F" -> DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy HH:mm")
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
.format(javaInstant)
// 9 months ago
"R" -> DateUtils.getRelativeTimeSpanString(
timestamp * 1000,
Clock.System.now().toEpochMilliseconds(),
DateUtils.MINUTE_IN_MILLIS
)
// Fallback. Shouldn't happen
else -> timestamp.toString()
}
builder.append(outString)
} catch (e: Exception) {
Log.e("TimestampNode", "Failed to parse timestamp", e)
builder.append("<invalid timestamp>")
}
}
}

View File

@ -47,6 +47,26 @@ class CustomEmoteRule<S> :
} }
} }
class TimestampRule<S> :
Rule<MarkdownContext, TimestampNode, S>(Pattern.compile("^<t:([0-9]+?)(:[tTDfFR])?>")) {
override fun parse(
matcher: Matcher,
parser: Parser<MarkdownContext, in TimestampNode, S>,
state: S
): ParseSpec<MarkdownContext, S> {
return ParseSpec.createTerminal(
TimestampNode(
try {
matcher.group(1)!!.toLong()
} catch (e: NumberFormatException) {
-1
},
matcher.group(2)
), state
)
}
}
fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, S> { fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, S> {
return CodeRules.createInlineCodeRule( return CodeRules.createInlineCodeRule(
{ listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) }, { listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },

View File

@ -70,6 +70,7 @@ import chat.revolt.internals.markdown.CustomEmoteRule
import chat.revolt.internals.markdown.MarkdownContext import chat.revolt.internals.markdown.MarkdownContext
import chat.revolt.internals.markdown.MarkdownParser import chat.revolt.internals.markdown.MarkdownParser
import chat.revolt.internals.markdown.MarkdownState import chat.revolt.internals.markdown.MarkdownState
import chat.revolt.internals.markdown.TimestampRule
import chat.revolt.internals.markdown.UserMentionRule import chat.revolt.internals.markdown.UserMentionRule
import chat.revolt.internals.markdown.createCodeRule import chat.revolt.internals.markdown.createCodeRule
import chat.revolt.internals.markdown.createInlineCodeRule import chat.revolt.internals.markdown.createInlineCodeRule
@ -256,6 +257,7 @@ fun ChannelScreen(
UserMentionRule(), UserMentionRule(),
ChannelMentionRule(), ChannelMentionRule(),
CustomEmoteRule(), CustomEmoteRule(),
TimestampRule(),
) )
.addRules( .addRules(
createCodeRule(context, codeBlockColor.toArgb()), createCodeRule(context, codeBlockColor.toArgb()),