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:
@@ -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()
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user