1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2026-08-22 08:56:30 +00:00

improvements in rich messages

This commit is contained in:
2026-08-17 17:35:03 +06:00
parent 5b6fe8eb96
commit 8bee8982bb
11 changed files with 721 additions and 68 deletions

View File

@@ -55,14 +55,23 @@ class InputRichBlockTableRowBuilder {
private val cells = mutableListOf<RichBlockTableCell>()
fun cell(
align: String,
align: RichBlockTableCellAlign,
valign: String,
isHeader: Boolean? = null,
colspan: Int? = null,
rowspan: Int? = null,
block: RichTextBuilder.() -> Unit
) {
cells.add(RichBlockTableCell(buildRichText(block), isHeader, colspan, rowspan, align, valign))
cells.add(RichBlockTableCell.Regular(buildRichText(block), colspan, rowspan, align, valign))
}
fun headerCell(
align: RichBlockTableCellAlign,
valign: String,
colspan: Int? = null,
rowspan: Int? = null,
block: RichTextBuilder.() -> Unit
) {
cells.add(RichBlockTableCell.Header(buildRichText(block), colspan, rowspan, align, valign))
}
fun build(): List<RichBlockTableCell> = cells.toList()

View File

@@ -15,6 +15,11 @@ import dev.inmo.tgbotapi.types.valignField
import dev.inmo.tgbotapi.types.valueField
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.KSerializer
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Caption of a rich formatted block.
@@ -34,21 +39,86 @@ data class RichBlockCaption(
*
* @see <a href="https://core.telegram.org/bots/api#richblocktablecell">RichBlockTableCell</a>
*/
@Serializable
data class RichBlockTableCell(
@SerialName(textField)
val text: RichText? = null,
@SerialName(isHeaderField)
val isHeader: Boolean? = null,
@SerialName(colspanField)
val colspan: Int? = null,
@SerialName(rowspanField)
val rowspan: Int? = null,
@SerialName(alignField)
val align: String,
@SerialName(valignField)
@Serializable(RichBlockTableCell.Serializer::class)
sealed interface RichBlockTableCell {
val text: RichText?
val colspan: Int?
val rowspan: Int?
val align: RichBlockTableCellAlign
val valign: String
)
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data class Header(
@SerialName(textField)
override val text: RichText? = null,
@SerialName(colspanField)
override val colspan: Int? = null,
@SerialName(rowspanField)
override val rowspan: Int? = null,
@SerialName(alignField)
override val align: RichBlockTableCellAlign,
@SerialName(valignField)
override val valign: String
) : RichBlockTableCell
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data class Regular(
@SerialName(textField)
override val text: RichText? = null,
@SerialName(colspanField)
override val colspan: Int? = null,
@SerialName(rowspanField)
override val rowspan: Int? = null,
@SerialName(alignField)
override val align: RichBlockTableCellAlign,
@SerialName(valignField)
override val valign: String
) : RichBlockTableCell
object Serializer : KSerializer<RichBlockTableCell> {
@Serializable
private data class Surrogate(
@SerialName(textField)
val text: RichText? = null,
@SerialName(isHeaderField)
@EncodeDefault(EncodeDefault.Mode.NEVER)
val isHeader: Boolean? = null,
@SerialName(colspanField)
val colspan: Int? = null,
@SerialName(rowspanField)
val rowspan: Int? = null,
@SerialName(alignField)
val align: RichBlockTableCellAlign,
@SerialName(valignField)
val valign: String
)
override val descriptor: SerialDescriptor = Surrogate.serializer().descriptor
override fun deserialize(decoder: Decoder): RichBlockTableCell {
val surrogate = decoder.decodeSerializableValue(Surrogate.serializer())
return if (surrogate.isHeader == true) {
Header(surrogate.text, surrogate.colspan, surrogate.rowspan, surrogate.align, surrogate.valign)
} else {
Regular(surrogate.text, surrogate.colspan, surrogate.rowspan, surrogate.align, surrogate.valign)
}
}
override fun serialize(encoder: Encoder, value: RichBlockTableCell) {
val surrogate = Surrogate(
value.text,
true.takeIf { value is Header },
value.colspan,
value.rowspan,
value.align,
value.valign
)
encoder.encodeSerializableValue(Surrogate.serializer(), surrogate)
}
}
}
/**
* An item of a [RichBlockList].

View File

@@ -0,0 +1,52 @@
package dev.inmo.tgbotapi.types.rich
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/** Horizontal alignment of a [RichBlockTableCell]. */
@Serializable(RichBlockTableCellAlign.Serializer::class)
sealed interface RichBlockTableCellAlign {
val name: String
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object Left : RichBlockTableCellAlign {
override val name: String = "left"
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object Center : RichBlockTableCellAlign {
override val name: String = "center"
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object Right : RichBlockTableCellAlign {
override val name: String = "right"
}
object Serializer : KSerializer<RichBlockTableCellAlign> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
"RichBlockTableCellAlign",
PrimitiveKind.STRING
)
override fun deserialize(decoder: Decoder): RichBlockTableCellAlign = when (val name = decoder.decodeString()) {
Left.name -> Left
Center.name -> Center
Right.name -> Right
else -> throw SerializationException("Unknown RichBlockTableCell alignment: $name")
}
override fun serialize(encoder: Encoder, value: RichBlockTableCellAlign) {
encoder.encodeString(value.name)
}
}
}

View File

@@ -411,10 +411,9 @@ data class RichBlockTable(
fun renderRow(row: List<RichBlockTableCell>): String =
row.joinToString(separator = " | ", prefix = "| ", postfix = " |") { it.text?.markdown ?: "" }
fun alignment(cell: RichBlockTableCell): String = when (cell.align) {
"left" -> ":---"
"center" -> ":--:"
"right" -> "---:"
else -> "---"
RichBlockTableCellAlign.Left -> ":---"
RichBlockTableCellAlign.Center -> ":--:"
RichBlockTableCellAlign.Right -> "---:"
}
val header = cells.first()
val lines = mutableListOf(
@@ -433,11 +432,11 @@ data class RichBlockTable(
val captionPart = caption?.let { "<caption>${it.html}</caption>" } ?: ""
val rows = cells.joinToString(separator = "") { row ->
val renderedCells = row.joinToString(separator = "") { cell ->
val tag = if (cell.isHeader == true) "th" else "td"
val tag = if (cell is RichBlockTableCell.Header) "th" else "td"
val cellAttributes = buildString {
cell.colspan?.let { append(" colspan=\"$it\"") }
cell.rowspan?.let { append(" rowspan=\"$it\"") }
append(" align=\"${cell.align}\"")
append(" align=\"${cell.align.name}\"")
append(" valign=\"${cell.valign}\"")
}
"<$tag$cellAttributes>${cell.text?.html ?: ""}</$tag>"

View File

@@ -84,12 +84,12 @@ class InputRichBlocksDslTest {
val blocks = buildInputRichBlocks {
table(isBordered = true, isStriped = false, caption = RichTextPlain("caption")) {
row {
cell(align = "left", valign = "top", isHeader = true, colspan = 2) {
headerCell(align = RichBlockTableCellAlign.Left, valign = "top", colspan = 2) {
bold("heading")
}
}
row {
cell(align = "center", valign = "middle", rowspan = 2) {
cell(align = RichBlockTableCellAlign.Center, valign = "middle", rowspan = 2) {
plain("value")
}
}
@@ -101,19 +101,18 @@ class InputRichBlocksDslTest {
InputRichBlockTable(
cells = listOf(
listOf(
RichBlockTableCell(
RichBlockTableCell.Header(
text = RichTextBold(RichTextPlain("heading")),
isHeader = true,
colspan = 2,
align = "left",
align = RichBlockTableCellAlign.Left,
valign = "top"
)
),
listOf(
RichBlockTableCell(
RichBlockTableCell.Regular(
text = RichTextPlain("value"),
rowspan = 2,
align = "center",
align = RichBlockTableCellAlign.Center,
valign = "middle"
)
)

View File

@@ -61,7 +61,7 @@ class InputRichMessageSerializationTest {
fun encodesTableReusingReceivedCellType() {
val table = InputRichBlockTable(
cells = listOf(
listOf(RichBlockTableCell(RichTextPlain("h1"), isHeader = true, align = "left", valign = "top"))
listOf(RichBlockTableCell.Header(RichTextPlain("h1"), align = RichBlockTableCellAlign.Left, valign = "top"))
)
)
val message = InputRichMessageBlocks(blocks = listOf(table))
@@ -72,6 +72,42 @@ class InputRichMessageSerializationTest {
assertEquals(true, cell["is_header"]?.jsonPrimitive?.boolean)
}
@Test
fun serializesTableCellVariantsWithSharedFlatShape() {
val header: RichBlockTableCell = RichBlockTableCell.Header(RichTextPlain("h"), align = RichBlockTableCellAlign.Left, valign = "top")
val regular: RichBlockTableCell = RichBlockTableCell.Regular(RichTextPlain("v"), align = RichBlockTableCellAlign.Right, valign = "bottom")
val headerElement = json.encodeToJsonElement(RichBlockTableCell.Serializer, header).jsonObject
val regularElement = json.encodeToJsonElement(RichBlockTableCell.Serializer, regular).jsonObject
assertEquals(true, headerElement["is_header"]?.jsonPrimitive?.boolean)
assertTrue("is_header" !in regularElement)
assertEquals(header, json.decodeFromJsonElement(RichBlockTableCell.Serializer, headerElement))
assertEquals(regular, json.decodeFromJsonElement(RichBlockTableCell.Serializer, regularElement))
assertTrue(json.decodeFromString(RichBlockTableCell.Serializer, "{\"align\":\"left\",\"valign\":\"top\",\"is_header\":false}") is RichBlockTableCell.Regular)
}
@Test
fun serializesEveryTableCellAlignmentAsItsName() {
val alignments = listOf(
RichBlockTableCellAlign.Left,
RichBlockTableCellAlign.Center,
RichBlockTableCellAlign.Right
)
alignments.forEach { alignment ->
val encoded = json.encodeToString(RichBlockTableCellAlign.Serializer, alignment)
assertEquals("\"${alignment.name}\"", encoded)
assertEquals(alignment, json.decodeFromString(RichBlockTableCellAlign.Serializer, encoded))
}
}
@Test
fun rejectsUnknownTableCellAlignment() {
assertFailsWith<SerializationException> {
json.decodeFromString(RichBlockTableCellAlign.Serializer, "\"justify\"")
}
}
@Test
fun encodesPhotoBlockMedia() {
val photoBlock = InputRichBlockPhoto(TelegramMediaPhoto(FileId("photo_file_id")))

View File

@@ -108,12 +108,12 @@ class RichBlockFormattingTest {
val block = RichBlockTable(
listOf(
listOf(
RichBlockTableCell(text = RichTextPlain("H1"), isHeader = true, align = "left", valign = "top"),
RichBlockTableCell(text = RichTextPlain("H2"), isHeader = true, align = "center", valign = "top")
RichBlockTableCell.Header(text = RichTextPlain("H1"), align = RichBlockTableCellAlign.Left, valign = "top"),
RichBlockTableCell.Header(text = RichTextPlain("H2"), align = RichBlockTableCellAlign.Center, valign = "top")
),
listOf(
RichBlockTableCell(text = RichTextPlain("a"), align = "left", valign = "top"),
RichBlockTableCell(text = RichTextPlain("b"), align = "center", valign = "top")
RichBlockTableCell.Regular(text = RichTextPlain("a"), align = RichBlockTableCellAlign.Left, valign = "top"),
RichBlockTableCell.Regular(text = RichTextPlain("b"), align = RichBlockTableCellAlign.Center, valign = "top")
)
)
)