From 1e9d4bae83a97207618ba302f25fc413ce17717c Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Tue, 17 Dec 2024 17:01:49 -0700 Subject: [PATCH] Community Notes support --- src/parser.nim | 8 ++++++++ src/parserutils.nim | 25 +++++++++++++++++++++++++ src/sass/tweet/_base.scss | 1 + src/sass/tweet/community_note.scss | 30 ++++++++++++++++++++++++++++++ src/types.nim | 6 ++++++ src/views/tweet.nim | 11 +++++++++++ 6 files changed, 81 insertions(+) create mode 100644 src/sass/tweet/community_note.scss diff --git a/src/parser.nim b/src/parser.nim index 95a1fbc..10d2866 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -466,6 +466,14 @@ proc parseGraphTweet(js: JsonNode; isLegacy=false): Tweet = if result.quote.isSome: result.quote = some(parseGraphTweet(js{"quoted_status_result", "result"}, isLegacy)) + with communityNote, js{"birdwatch_pivot"}: + let note = BirdwatchNote( + id: communityNote{"note", "rest_id"}.getId, + title: communityNote{"title"}.getStr, + ) + note.expandBirdwatchEntities(communityNote{"subtitle"}) + result.birdwatch = some(note) + proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = for t in js{"content", "items"}: let entryId = t{"entryId"}.getStr diff --git a/src/parserutils.nim b/src/parserutils.nim index 00ea6f4..0995fee 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -319,3 +319,28 @@ proc expandNoteTweetEntities*(tweet: Tweet; js: JsonNode) = tweet.expandTextEntities(entities, text, textSlice) tweet.text = tweet.text.multiReplace((unicodeOpen, xmlOpen), (unicodeClose, xmlClose)) + +proc expandBirdwatchEntities*(note: BirdwatchNote; js: JsonNode) = + let + entities = ? js{"entities"} + text = js{"text"}.getStr + runes = text.toRunes + + var replacements = newSeq[ReplaceSlice]() + + for ent in entities: + let + # twitter devs in their infinite wisdom making these entities of all others 1 indexed + slice = (ent{"fromIndex"}.getInt - 1) .. (ent{"toIndex"}.getInt - 1) + refType = ent{"ref", "type"}.getStr + + case refType + of "TimelineUrl": + let + url = ent{"ref", "url"}.getStr + display = $runes[slice] + + replacements.add ReplaceSlice(kind: rkUrl, slice: slice, url: url, display: display) + + note.text = runes.replacedWith(replacements, 0 .. runes.len) + diff --git a/src/sass/tweet/_base.scss b/src/sass/tweet/_base.scss index 3431a7b..6621a3f 100644 --- a/src/sass/tweet/_base.scss +++ b/src/sass/tweet/_base.scss @@ -7,6 +7,7 @@ @import 'card'; @import 'poll'; @import 'quote'; +@import 'community_note'; .tweet-body { flex: 1; diff --git a/src/sass/tweet/community_note.scss b/src/sass/tweet/community_note.scss new file mode 100644 index 0000000..9643dd9 --- /dev/null +++ b/src/sass/tweet/community_note.scss @@ -0,0 +1,30 @@ +@import '_variables'; + +.community-note { + margin-top: 10px; + border: solid 1px var(--dark_grey); + border-radius: 10px; + background-color: var(--bg_elements); + overflow: hidden; + pointer-events: all; + position: relative; + width: 100%; + + &:hover { + border-color: var(--grey); + } + + .community-note-title { + font-weight: bold; + background-color: var(--bg_overlays); + padding: 6px 8px; + margin-top: 1px; + } + + .community-note-text { + overflow: hidden; + white-space: pre-wrap; + word-wrap: break-word; + padding: 8px; + } +} diff --git a/src/types.nim b/src/types.nim index d60248d..4fdef5a 100644 --- a/src/types.nim +++ b/src/types.nim @@ -198,6 +198,11 @@ type likes*: int quotes*: int + BirdwatchNote* = ref object + id*: int64 + title*: string + text*: string + Tweet* = ref object id*: int64 threadId*: int64 @@ -223,6 +228,7 @@ type gif*: Option[Gif] video*: Option[Video] photos*: seq[string] + birdwatch*: Option[BirdwatchNote] Tweets* = seq[Tweet] diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 4081900..1603b2a 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -263,6 +263,14 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome: renderQuoteMedia(quote, prefs, path) +proc renderCommunityNote(note: BirdwatchNote; prefs: Prefs): VNode = + buildHtml(tdiv(class="community-note")): + tdiv(class="community-note-title"): + text note.title + + tdiv(class="community-note-text", dir="auto"): + verbatim replaceUrls(note.text, prefs) + proc renderLocation*(tweet: Tweet): string = let (place, url) = tweet.getLocation() if place.len == 0: return @@ -343,6 +351,9 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; if tweet.quote.isSome: renderQuote(tweet.quote.get(), prefs, path) + if mainTweet and tweet.birdwatch.isSome: + renderCommunityNote(tweet.birdwatch.get(), prefs) + if mainTweet: p(class="tweet-published"): text &"{getTime(tweet)}"