Skip to content

Commit 3db170d

Browse files
authored
Merge pull request #1766 from DimensionDev/bugfix/xqt_note_tweet
fix xqt note tweet parsing
2 parents 8105161 + c9198e3 commit 3db170d

File tree

2 files changed

+208
-3
lines changed
  • shared/src
    • commonMain/kotlin/dev/dimension/flare/ui/model/mapper
    • commonTest/kotlin/dev/dimension/flare/ui/model/mapper

2 files changed

+208
-3
lines changed

shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/XQT.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.dimension.flare.ui.model.mapper
33
import com.fleeksoft.ksoup.nodes.Element
44
import com.fleeksoft.ksoup.nodes.Node
55
import com.fleeksoft.ksoup.nodes.TextNode
6+
import de.cketti.codepoints.codePointCount
67
import de.cketti.codepoints.deluxe.codePointSequence
78
import dev.dimension.flare.common.decodeJson
89
import dev.dimension.flare.common.encodeJson
@@ -1330,7 +1331,7 @@ internal fun Tweet.renderContent(accountKey: MicroBlogKey): UiRichText {
13301331
result.richtext?.richtextTags?.forEach { tag ->
13311332
val str =
13321333
codePoints
1333-
.subList(tag.fromIndex, tag.toIndex)
1334+
.subList(text.codePointCount(0, tag.fromIndex), text.codePointCount(0, tag.toIndex))
13341335
.flatMap { it.toChars().toList() }
13351336
.joinToString("")
13361337
var node: Node = TextNode(str)
@@ -1346,7 +1347,7 @@ internal fun Tweet.renderContent(accountKey: MicroBlogKey): UiRichText {
13461347
) {
13471348
node = Element("i").apply { appendChild(node) }
13481349
}
1349-
add(Entity(tag.fromIndex, tag.toIndex, node))
1350+
add(Entity(text.codePointCount(0, tag.fromIndex), text.codePointCount(0, tag.toIndex), node))
13501351
}
13511352
result.media?.inlineMedia?.forEachIndexed { index, inlineMedia ->
13521353
val media = legacy?.entities?.media?.firstOrNull { it.idStr == inlineMedia.mediaId }
@@ -1369,7 +1370,8 @@ internal fun Tweet.renderContent(accountKey: MicroBlogKey): UiRichText {
13691370
},
13701371
)
13711372
}
1372-
add(Entity(inlineMedia.index, inlineMedia.index, node))
1373+
val mediaIndexCodePoint = text.codePointCount(0, inlineMedia.index)
1374+
add(Entity(mediaIndexCodePoint, mediaIndexCodePoint, node))
13731375
}
13741376
}
13751377
}.sortedBy { it.start }

shared/src/commonTest/kotlin/dev/dimension/flare/ui/model/mapper/XQTTest.kt

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,4 +484,207 @@ class XQTTest {
484484
assertTrue(html.contains(mediaUrl))
485485
assertTrue(html.contains("Text after media."))
486486
}
487+
488+
@Test
489+
fun renderContent_complexEntities_rendersCorrectly() {
490+
// Text: "Start 🫠 @user End"
491+
// Indices (UTF-16):
492+
// "Start " -> 0..5 (6 chars)
493+
// "🫠" -> 6..7 (2 chars, 1 code point)
494+
// " " -> 8 (1 char)
495+
// "@user" -> 9..13 (5 chars)
496+
// " " -> 14 (1 char)
497+
// "End" -> 15..17 (3 chars)
498+
val text = "Start 🫠 @user End"
499+
500+
val mediaId = "media_123"
501+
val mediaUrl = "https://example.com/image.jpg"
502+
503+
val noteTweet =
504+
NoteTweet(
505+
isExpandable = true,
506+
noteTweetResults =
507+
NoteTweetResult(
508+
result =
509+
NoteTweetResultData(
510+
id = "note_id",
511+
text = text,
512+
entitySet =
513+
Entities(
514+
userMentions =
515+
listOf(
516+
UserMention(
517+
screenName = "user",
518+
name = "User",
519+
indices = listOf(8, 13),
520+
),
521+
),
522+
media =
523+
listOf(
524+
dev.dimension.flare.data.network.xqt.model.Media(
525+
idStr = mediaId,
526+
mediaUrlHttps = mediaUrl,
527+
type = dev.dimension.flare.data.network.xqt.model.Media.Type.photo,
528+
originalInfo =
529+
dev.dimension.flare.data.network.xqt.model
530+
.MediaOriginalInfo(100, 100),
531+
displayUrl = "example.com/image",
532+
expandedUrl = mediaUrl,
533+
url = mediaUrl,
534+
indices = listOf(0, 5), // dummy
535+
sizes =
536+
dev.dimension.flare.data.network.xqt.model.MediaSizes(
537+
large =
538+
dev.dimension.flare.data.network.xqt.model.MediaSize(
539+
100,
540+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
541+
100,
542+
),
543+
medium =
544+
dev.dimension.flare.data.network.xqt.model.MediaSize(
545+
100,
546+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
547+
100,
548+
),
549+
small =
550+
dev.dimension.flare.data.network.xqt.model.MediaSize(
551+
100,
552+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
553+
100,
554+
),
555+
thumb =
556+
dev.dimension.flare.data.network.xqt.model.MediaSize(
557+
100,
558+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
559+
100,
560+
),
561+
),
562+
),
563+
),
564+
),
565+
media =
566+
dev.dimension.flare.data.network.xqt.model.NoteTweetResultMedia(
567+
inlineMedia =
568+
listOf(
569+
dev.dimension.flare.data.network.xqt.model.NoteTweetResultMediaInlineMedia(
570+
index = 14, // UTF-16 index before " " (Space before End)
571+
mediaId = mediaId,
572+
),
573+
),
574+
),
575+
richtext =
576+
NoteTweetResultRichText(
577+
richtextTags =
578+
listOf(
579+
NoteTweetResultRichTextTag(
580+
fromIndex = 0,
581+
toIndex = 5,
582+
richtextTypes = listOf(NoteTweetResultRichTextTag.RichtextTypes.bold),
583+
),
584+
NoteTweetResultRichTextTag(
585+
fromIndex = 15,
586+
toIndex = 18,
587+
richtextTypes = listOf(NoteTweetResultRichTextTag.RichtextTypes.italic),
588+
),
589+
),
590+
),
591+
),
592+
),
593+
)
594+
// Legacy needed for resolving media URL in inline media processing
595+
val legacy =
596+
TweetLegacy(
597+
idStr = "123",
598+
fullText = "Legacy text",
599+
displayTextRange = listOf(0, 10),
600+
createdAt = "Wed Oct 10 20:19:24 +0000 2018",
601+
favoriteCount = 0,
602+
favorited = false,
603+
isQuoteStatus = false,
604+
lang = "en",
605+
quoteCount = 0,
606+
replyCount = 0,
607+
retweetCount = 0,
608+
retweeted = false,
609+
entities =
610+
Entities(
611+
media =
612+
listOf(
613+
dev.dimension.flare.data.network.xqt.model.Media(
614+
idStr = mediaId,
615+
mediaUrlHttps = mediaUrl,
616+
type = dev.dimension.flare.data.network.xqt.model.Media.Type.photo,
617+
originalInfo =
618+
dev.dimension.flare.data.network.xqt.model
619+
.MediaOriginalInfo(100, 100),
620+
displayUrl = "example.com/image",
621+
expandedUrl = mediaUrl,
622+
url = mediaUrl,
623+
indices = listOf(0, 5),
624+
sizes =
625+
dev.dimension.flare.data.network.xqt.model.MediaSizes(
626+
large =
627+
dev.dimension.flare.data.network.xqt.model.MediaSize(
628+
100,
629+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
630+
100,
631+
),
632+
medium =
633+
dev.dimension.flare.data.network.xqt.model.MediaSize(
634+
100,
635+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
636+
100,
637+
),
638+
small =
639+
dev.dimension.flare.data.network.xqt.model.MediaSize(
640+
100,
641+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
642+
100,
643+
),
644+
thumb =
645+
dev.dimension.flare.data.network.xqt.model.MediaSize(
646+
100,
647+
dev.dimension.flare.data.network.xqt.model.MediaSize.Resize.crop,
648+
100,
649+
),
650+
),
651+
),
652+
),
653+
),
654+
)
655+
656+
val tweet =
657+
Tweet(
658+
restId = "123",
659+
noteTweet = noteTweet,
660+
legacy = legacy,
661+
)
662+
663+
val result = tweet.renderContent(accountKey)
664+
val html = result.html
665+
666+
// Expected Structure: "Start 🫠 <a ...>@user</a> <figure>...</figure>End"
667+
668+
// Expected Structure: "<b>Start</b> 🫠 <a ...>@user</a> <figure>...</figure><i>End</i>"
669+
670+
assertTrue(html.contains("<b>Start</b>"), "Start should be bold")
671+
assertTrue(html.contains("🫠"), "Emoji preserved")
672+
assertTrue(html.contains(">@user</a>"), "User mention linked")
673+
assertTrue(html.contains("<figure>"), "Media figure present")
674+
assertTrue(html.contains("src=\"$mediaUrl\""), "Media source correct")
675+
assertTrue(html.contains("<i>End</i>"), "End should be italic")
676+
677+
// order check
678+
val indexUser = html.indexOf(">@user</a>")
679+
val indexMedia = html.indexOf("<figure>")
680+
val indexEnd = html.indexOf("End")
681+
682+
assertTrue(indexUser != -1)
683+
assertTrue(indexMedia != -1)
684+
assertTrue(indexEnd != -1)
685+
686+
assertTrue(indexUser < indexMedia, "User mention should be before media")
687+
assertTrue(indexUser < indexMedia, "User mention should be before media")
688+
assertTrue(indexMedia < indexEnd, "Media should be before 'End'")
689+
}
487690
}

0 commit comments

Comments
 (0)