From d3a7ca834b8a85d512f68d517e3f566e0b32cafb Mon Sep 17 00:00:00 2001 From: Zed Date: Thu, 11 Jul 2019 19:22:23 +0200 Subject: [PATCH 1/6] Implement card fetching and parsing --- src/api.nim | 43 ++++++++++++++++++++++++++++++++++++++---- src/formatters.nim | 4 ++++ src/parser.nim | 29 ++++++++++++++++++++++++++++ src/parserutils.nim | 20 +++++++++++++++++++- src/search.nim | 10 +++++----- src/types.nim | 18 ++++++++++++++++-- src/views/timeline.nim | 2 +- 7 files changed, 113 insertions(+), 13 deletions(-) diff --git a/src/api.nim b/src/api.nim index 4f36dd8..cbd481f 100644 --- a/src/api.nim +++ b/src/api.nim @@ -163,6 +163,37 @@ proc getConversationPolls*(convo: Conversation) {.async.} = futs.add convo.replies.map(getPolls) await all(futs) +proc getCard*(tweet: Tweet) {.async.} = + if tweet.card.isNone(): return + + let headers = newHttpHeaders({ + "Accept": cardAccept, + "Referer": $(base / getLink(tweet)), + "User-Agent": agent, + "Authority": "twitter.com", + "Accept-Language": lang, + }) + + let url = base / get(tweet.card).query + let html = await fetchHtml(url, headers) + if html == nil: return + + parseCard(get(tweet.card), html) + # echo tweet.card.get() + +proc getCards*(thread: Thread) {.async.} = + if thread == nil: return + var cards = thread.tweets.filterIt(it.card.isSome) + await all(cards.map(getCard)) + +proc getConversationCards*(convo: Conversation) {.async.} = + var futs: seq[Future[void]] + futs.add getCard(convo.tweet) + futs.add getCards(convo.before) + futs.add getCards(convo.after) + futs.add convo.replies.map(getCards) + await all(futs) + proc getPhotoRail*(username: string): Future[seq[GalleryPhoto]] {.async.} = let headers = newHttpHeaders({ "Accept": jsonAccept, @@ -234,9 +265,12 @@ proc getTweet*(username, id: string): Future[Conversation] {.async.} = result = parseConversation(html) - let vidsFut = getConversationVideos(result) - let pollFut = getConversationPolls(result) - await all(vidsFut, pollFut) + let + vidsFut = getConversationVideos(result) + pollFut = getConversationPolls(result) + cardFut = getConversationCards(result) + + await all(vidsFut, pollFut, cardFut) proc finishTimeline(json: JsonNode; query: Option[Query]; after: string): Future[Timeline] {.async.} = if json == nil: return Timeline() @@ -257,8 +291,9 @@ proc finishTimeline(json: JsonNode; query: Option[Query]; after: string): Future thread = parseThread(html) vidsFut = getVideos(thread) pollFut = getPolls(thread) + cardFut = getCards(thread) - await all(vidsFut, pollFut) + await all(vidsFut, pollFut, cardFut) result.tweets = thread.tweets proc getTimeline*(username, after: string): Future[Timeline] {.async.} = diff --git a/src/formatters.nim b/src/formatters.nim index 56b366f..a21f4c1 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -81,3 +81,7 @@ proc getTime*(tweet: Tweet): string = proc getLink*(tweet: Tweet | Quote): string = &"/{tweet.profile.username}/status/{tweet.id}" + +proc getUrls*(text: string): seq[string] = + # temporary + text.findAll(urlRegex).mapIt(text[it.group(0)[0]]) diff --git a/src/parser.nim b/src/parser.nim index 5deb817..a1289ae 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -178,3 +178,32 @@ proc parsePhotoRail*(node: XmlNode): seq[GalleryPhoto] = tweetId: img.attr("data-tweet-id"), color: img.attr("background-color").replace("style: ", "") ) + +proc parseCard*(card: var Card; node: XmlNode) = + let cardKind = node.select("head > meta[name*=card_name]").attr("content") + + if "summary_large_image" in cardKind: + card.kind = summaryLarge + elif "summary" in cardKind: + card.kind = summary + elif "live_event" in cardKind: + card.kind = liveEvent + elif "player" in cardKind: + card.kind = player + elif "promo_website" in cardKind: + card.kind = promoWebsite + + card.title = node.selectText("h2.TwitterCard-title") + card.text = node.selectText("p.tcu-resetMargin") + card.dest = node.selectText("span.SummaryCard-destination") + + let image = node.select(".tcu-imageWrapper > img") + if image != nil: + # workaround for issue 11713 + card.image = image.attr("data-src").replace("gname", "g&name") + else: + echo card.id + + if card.kind == liveEvent: + card.text = card.title + card.title = node.selectText(".TwitterCard-attribution--category") diff --git a/src/parserutils.nim b/src/parserutils.nim index 35a086c..d5b1c4e 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -169,8 +169,26 @@ proc getQuoteMedia*(quote: var Quote; node: XmlNode) = proc getTweetCards*(tweet: Tweet; node: XmlNode) = if node.attr("data-has-cards") == "false": return - if "poll" in node.attr("data-card2-type"): + let cardType = node.attr("data-card2-type") + + if "poll" in cardType: tweet.poll = some(Poll()) + return + + let cardDiv = node.select(".card2 > div") + if cardDiv == nil: return + + var card = Card( + id: tweet.id, + query: cardDiv.attr("data-src") + ) + + # temporary solution + let text = node.selectText(".tweet-text") + let urls = getUrls(text) + card.url = urls[0] + + tweet.card = some(card) proc getMoreReplies*(node: XmlNode): int = let text = node.innerText().strip() diff --git a/src/search.nim b/src/search.nim index 216fbe3..cc53eb2 100644 --- a/src/search.nim +++ b/src/search.nim @@ -21,7 +21,7 @@ const proc initQuery*(filters, includes, excludes, separator: string; name=""): Query = var sep = separator.strip().toUpper() Query( - queryType: custom, + kind: custom, filters: filters.split(",").filterIt(it in validFilters), includes: includes.split(",").filterIt(it in validFilters), excludes: excludes.split(",").filterIt(it in validFilters), @@ -31,7 +31,7 @@ proc initQuery*(filters, includes, excludes, separator: string; name=""): Query proc getMediaQuery*(name: string): Query = Query( - queryType: media, + kind: media, filters: @["twimg", "native_video"], fromUser: name, sep: "OR" @@ -39,7 +39,7 @@ proc getMediaQuery*(name: string): Query = proc getReplyQuery*(name: string): Query = Query( - queryType: replies, + kind: replies, includes: @["nativeretweets"], fromUser: name ) @@ -61,8 +61,8 @@ proc genQueryParam*(query: Query): string = return strip(param & filters.join(&" {query.sep} ")) proc genQueryUrl*(query: Query): string = - result = &"/{query.queryType}?" - if query.queryType != custom: return + result = &"/{query.kind}?" + if query.kind != custom: return var params: seq[string] if query.filters.len > 0: diff --git a/src/types.nim b/src/types.nim index 4aac3b5..1f6f573 100644 --- a/src/types.nim +++ b/src/types.nim @@ -31,11 +31,11 @@ db("cache.db", "", "", ""): .}: Time type - QueryType* = enum + QueryKind* = enum replies, media, custom = "search" Query* = object - queryType*: QueryType + kind*: QueryKind filters*: seq[string] includes*: seq[string] excludes*: seq[string] @@ -70,6 +70,19 @@ type status*: string leader*: int + CardKind* = enum + summary, summaryLarge, liveEvent, player, promoWebsite + + Card* = object + kind*: CardKind + id*: string + query*: string + url*: string + title*: string + dest*: string + text*: string + image*: string + Quote* = object id*: string profile*: Profile @@ -104,6 +117,7 @@ type stats*: TweetStats retweet*: Option[Retweet] quote*: Option[Quote] + card*: Option[Card] gif*: Option[Gif] video*: Option[Video] photos*: seq[string] diff --git a/src/views/timeline.nim b/src/views/timeline.nim index cb1d32e..bacd326 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -14,7 +14,7 @@ proc getTabClass(timeline: Timeline; tab: string): string = if timeline.query.isNone: if tab == "tweets": classes.add "active" - elif $timeline.query.get().queryType == tab: + elif $timeline.query.get().kind == tab: classes.add "active" return classes.join(" ") From 829cac9cbdbb02c090c8ebb72b11c370727da028 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 15 Jul 2019 03:44:33 +0200 Subject: [PATCH 2/6] Force showing sensitive card thumbnails --- src/api.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api.nim b/src/api.nim index cbd481f..93d3b06 100644 --- a/src/api.nim +++ b/src/api.nim @@ -174,8 +174,8 @@ proc getCard*(tweet: Tweet) {.async.} = "Accept-Language": lang, }) - let url = base / get(tweet.card).query - let html = await fetchHtml(url, headers) + let query = get(tweet.card).query.replace("sensitive=true", "sensitive=false") + let html = await fetchHtml(base / query, headers) if html == nil: return parseCard(get(tweet.card), html) From 0da076ddcf932e1f7f14a5c8c09cea5567c4c3a3 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 15 Jul 2019 13:40:59 +0200 Subject: [PATCH 3/6] Fix card link parsing edge cases --- src/formatters.nim | 6 ------ src/parser.nim | 7 +++++-- src/parserutils.nim | 10 +++++----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/formatters.nim b/src/formatters.nim index a21f4c1..5d0de04 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -10,7 +10,6 @@ const emailRegex = re"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)" usernameRegex = re"(^|[^A-z0-9_?])@([A-z0-9_]+)" picRegex = re"pic.twitter.com/[^ ]+" - cardRegex = re"(https?://)?cards.twitter.com/[^ ]+" ellipsisRegex = re" ?…" nbsp = $Rune(0x000A0) @@ -60,7 +59,6 @@ proc linkifyText*(text: string): string = proc stripTwitterUrls*(text: string): string = result = text result = result.replace(picRegex, "") - result = result.replace(cardRegex, "") result = result.replace(ellipsisRegex, "") proc getUserpic*(userpic: string; style=""): string = @@ -81,7 +79,3 @@ proc getTime*(tweet: Tweet): string = proc getLink*(tweet: Tweet | Quote): string = &"/{tweet.profile.username}/status/{tweet.id}" - -proc getUrls*(text: string): seq[string] = - # temporary - text.findAll(urlRegex).mapIt(text[it.group(0)[0]]) diff --git a/src/parser.nim b/src/parser.nim index a1289ae..07dd458 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -75,7 +75,7 @@ proc parseTweet*(node: XmlNode): Tweet = ) result.getTweetMedia(tweet) - result.getTweetCards(tweet) + result.getTweetCard(tweet) let by = tweet.selectText(".js-retweet-text > a > b") if by.len > 0: @@ -197,7 +197,10 @@ proc parseCard*(card: var Card; node: XmlNode) = card.text = node.selectText("p.tcu-resetMargin") card.dest = node.selectText("span.SummaryCard-destination") - let image = node.select(".tcu-imageWrapper > img") + if card.url.len == 0: + card.url = node.select("a").attr("href") + + let image = node.select(".tcu-imageWrapper img") if image != nil: # workaround for issue 11713 card.image = image.attr("data-src").replace("gname", "g&name") diff --git a/src/parserutils.nim b/src/parserutils.nim index d5b1c4e..25e0b5d 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -167,7 +167,7 @@ proc getQuoteMedia*(quote: var Quote; node: XmlNode) = elif gifBadge != nil: quote.badge = "GIF" -proc getTweetCards*(tweet: Tweet; node: XmlNode) = +proc getTweetCard*(tweet: Tweet; node: XmlNode) = if node.attr("data-has-cards") == "false": return let cardType = node.attr("data-card2-type") @@ -183,10 +183,10 @@ proc getTweetCards*(tweet: Tweet; node: XmlNode) = query: cardDiv.attr("data-src") ) - # temporary solution - let text = node.selectText(".tweet-text") - let urls = getUrls(text) - card.url = urls[0] + let cardUrl = cardDiv.attr("data-card-url") + for n in node.selectAll(".tweet-text a"): + if n.attr("href") == cardUrl: + card.url = n.attr("data-expanded-url") tweet.card = some(card) From 9d1682012d32b8788d30e9fe9e4e865b22632139 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 15 Jul 2019 13:41:27 +0200 Subject: [PATCH 4/6] Render cards --- public/style.css | 126 +++++++++++++++++++++++++++++++++++++++++++- src/api.nim | 1 - src/views/tweet.nim | 23 ++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/public/style.css b/public/style.css index d3c5799..472ed7d 100644 --- a/public/style.css +++ b/public/style.css @@ -419,7 +419,6 @@ video { .profile-banner-color { width: 100%; padding-bottom: 25%; - margin-bottom: 8px; } .profile-card { @@ -882,6 +881,131 @@ video { margin: 0; } +.card { + margin: 5px 0; +} + +.card-container { + border-radius: 10px; + border-width: 1px; + border-style: solid; + border-color: #404040; + background-color: #121212; + overflow: hidden; + color: inherit; + display: flex; + text-decoration: none !important; +} + +.card-container:hover { + border-color: #808080; +} + +.large .card-container { + display: block; +} + +.card-content { + padding: 0.5em; +} + +.card-title { + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; + font-size: 1.15em; +} + +.card-description { + margin: 0.3em 0; +} + +.card-destination { + color: hsla(240,1%,73%,.9); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +} + +.card-image-container { + width: 98px; + flex-shrink: 0; + position: relative; + overflow: hidden; +} + +.large .card-image-container { + width: unset; +} + +.card-image-container:before { + content: ""; + display: block; + padding-top: 100%; +} + +.large .card-image-container:before { + display: none; +} + +.card-image { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: white; +} + +.large .card-image { + position: unset; + border-style: solid; + border-color: #404040; + border-width: 0; + border-bottom-width: 1px; +} + +.card-image img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; +} + +.card-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + opacity: 0.8; +} + +.card-overlay-circle { + border-radius: 50%; + background-color: #404040; + width: 40px; + height: 40px; + align-items: center; + display: flex; + border-width: 5px; + border-color: #d8574d; + border-style: solid; +} + +.card-overlay-triangle { + width: 0; + height: 0; + border-style: solid; + border-width: 12px 0 12px 17px; + border-color: transparent transparent transparent #d8574d; + margin-left: 14px; +} + .poll-meter { overflow: hidden; position: relative; diff --git a/src/api.nim b/src/api.nim index 93d3b06..7ec3626 100644 --- a/src/api.nim +++ b/src/api.nim @@ -179,7 +179,6 @@ proc getCard*(tweet: Tweet) {.async.} = if html == nil: return parseCard(get(tweet.card), html) - # echo tweet.card.get() proc getCards*(thread: Thread) {.async.} = if thread == nil: return diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 3ffd50e..7ec7051 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -77,6 +77,27 @@ proc renderPoll(poll: Poll): VNode = span(class="poll-info"): text $poll.votes & " votes • " & poll.status +proc renderCard(card: Card): VNode = + const largeCards = {summaryLarge, liveEvent, promoWebsite} + let large = if card.kind in largeCards: " large" else: "" + + buildHtml(tdiv(class=("card" & large))): + a(class="card-container", href=card.url): + if card.image.len > 0: + tdiv(class="card-image-container"): + tdiv(class="card-image"): + img(src=card.image.getSigUrl("pic")) + if card.kind == player: + tdiv(class="card-overlay"): + tdiv(class="card-overlay-circle"): + span(class="card-overlay-triangle") + + tdiv(class="card-content-container"): + tdiv(class="card-content"): + h2(class="card-title"): text card.title + p(class="card-description"): text card.text + span(class="card-destination"): text card.dest + proc renderStats(stats: TweetStats): VNode = buildHtml(tdiv(class="tweet-stats")): span(class="tweet-stat"): text "💬 " & $stats.replies @@ -168,6 +189,8 @@ proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNod renderGif(tweet.gif.get()) elif tweet.poll.isSome: renderPoll(tweet.poll.get()) + elif tweet.card.isSome: + renderCard(tweet.card.get()) renderStats(tweet.stats) From d12f14135ec0fd6a8236c764e9a138dcfd81f677 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 15 Jul 2019 14:15:22 +0200 Subject: [PATCH 5/6] Update readme --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 22aee5e..ae2b254 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,15 @@ is on implementing missing features. - Search (images/videos, hashtags, etc.) - Custom timeline filter -- Media-only/gallery view - Nitter link previews - Server configuration -- Caching (waiting for [moigagoo/norm#19](https://github.com/moigagoo/norm/pull/19)) -- Twitter "Cards" (link previews) +- More caching (waiting for [moigagoo/norm#19](https://github.com/moigagoo/norm/pull/19)) - Simple account system with customizable feed -- Emoji support (WIP, needs font) - Video support with hls.js - Json API endpoints - Themes - Nitter logo +- Emoji support (WIP, uses native font for now) ## Why? From 27cf4cdf64c86f0a3d42d904a05cfce559d2a706 Mon Sep 17 00:00:00 2001 From: Zed Date: Mon, 15 Jul 2019 16:03:01 +0200 Subject: [PATCH 6/6] Support promo_video_website cards --- public/style.css | 7 ++++++- src/api.nim | 6 +++++- src/parser.nim | 17 +---------------- src/parserutils.nim | 14 +++++++++++--- src/types.nim | 10 ++++++++-- src/views/tweet.nim | 29 +++++++++++++++++------------ 6 files changed, 48 insertions(+), 35 deletions(-) diff --git a/public/style.css b/public/style.css index 472ed7d..df421de 100644 --- a/public/style.css +++ b/public/style.css @@ -237,7 +237,7 @@ nav { .gallery-row .attachment:last-child, .gallery-row .attachments:last-child, .video-container { margin: 0; - max-height: 500px; + max-height: 530px; } .attachments .attachment { @@ -901,6 +901,11 @@ video { border-color: #808080; } +.card-container .attachments { + margin: 0; + border-radius: 0; +} + .large .card-container { display: block; } diff --git a/src/api.nim b/src/api.nim index 7ec3626..9106414 100644 --- a/src/api.nim +++ b/src/api.nim @@ -106,7 +106,11 @@ proc getVideo*(tweet: Tweet; token: string) {.async.} = await getVideo(tweet, guestToken) return - tweet.video = some(parseVideo(json)) + if tweet.card.isNone: + tweet.video = some(parseVideo(json)) + else: + get(tweet.card).video = some(parseVideo(json)) + tweet.video = none(Video) tokenUses.inc proc getVideos*(thread: Thread; token="") {.async.} = diff --git a/src/parser.nim b/src/parser.nim index 07dd458..db8f654 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -180,19 +180,6 @@ proc parsePhotoRail*(node: XmlNode): seq[GalleryPhoto] = ) proc parseCard*(card: var Card; node: XmlNode) = - let cardKind = node.select("head > meta[name*=card_name]").attr("content") - - if "summary_large_image" in cardKind: - card.kind = summaryLarge - elif "summary" in cardKind: - card.kind = summary - elif "live_event" in cardKind: - card.kind = liveEvent - elif "player" in cardKind: - card.kind = player - elif "promo_website" in cardKind: - card.kind = promoWebsite - card.title = node.selectText("h2.TwitterCard-title") card.text = node.selectText("p.tcu-resetMargin") card.dest = node.selectText("span.SummaryCard-destination") @@ -203,9 +190,7 @@ proc parseCard*(card: var Card; node: XmlNode) = let image = node.select(".tcu-imageWrapper img") if image != nil: # workaround for issue 11713 - card.image = image.attr("data-src").replace("gname", "g&name") - else: - echo card.id + card.image = some(image.attr("data-src").replace("gname", "g&name")) if card.kind == liveEvent: card.text = card.title diff --git a/src/parserutils.nim b/src/parserutils.nim index 25e0b5d..a75400e 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -1,4 +1,4 @@ -import xmltree, htmlparser, strtabs, strformat, times +import xmltree, htmlparser, strtabs, strformat, strutils, times import regex import types, formatters, api @@ -169,13 +169,16 @@ proc getQuoteMedia*(quote: var Quote; node: XmlNode) = proc getTweetCard*(tweet: Tweet; node: XmlNode) = if node.attr("data-has-cards") == "false": return - let cardType = node.attr("data-card2-type") + var cardType = node.attr("data-card2-type") + + if ":" in cardType: + cardType = cardType.split(":")[^1] if "poll" in cardType: tweet.poll = some(Poll()) return - let cardDiv = node.select(".card2 > div") + let cardDiv = node.select(".card2 > .js-macaw-cards-iframe-container") if cardDiv == nil: return var card = Card( @@ -183,6 +186,11 @@ proc getTweetCard*(tweet: Tweet; node: XmlNode) = query: cardDiv.attr("data-src") ) + try: + card.kind = parseEnum[CardKind](cardType) + except ValueError: + card.kind = summary + let cardUrl = cardDiv.attr("data-card-url") for n in node.selectAll(".tweet-text a"): if n.attr("href") == cardUrl: diff --git a/src/types.nim b/src/types.nim index 1f6f573..9cde0c7 100644 --- a/src/types.nim +++ b/src/types.nim @@ -71,7 +71,12 @@ type leader*: int CardKind* = enum - summary, summaryLarge, liveEvent, player, promoWebsite + summary = "summary" + summaryLarge = "summary_large_image" + promoWebsite = "promo_website" + promoVideo = "promo_video_website" + player = "player" + liveEvent = "live_event" Card* = object kind*: CardKind @@ -81,7 +86,8 @@ type title*: string dest*: string text*: string - image*: string + image*: Option[string] + video*: Option[Video] Quote* = object id*: string diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 7ec7051..7805a66 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -77,20 +77,25 @@ proc renderPoll(poll: Poll): VNode = span(class="poll-info"): text $poll.votes & " votes • " & poll.status +proc renderCardImage(card: Card): VNode = + buildHtml(tdiv(class="card-image-container")): + tdiv(class="card-image"): + img(src=get(card.image).getSigUrl("pic")) + if card.kind == player: + tdiv(class="card-overlay"): + tdiv(class="card-overlay-circle"): + span(class="card-overlay-triangle") + proc renderCard(card: Card): VNode = - const largeCards = {summaryLarge, liveEvent, promoWebsite} + const largeCards = {summaryLarge, liveEvent, promoWebsite, promoVideo} let large = if card.kind in largeCards: " large" else: "" buildHtml(tdiv(class=("card" & large))): a(class="card-container", href=card.url): - if card.image.len > 0: - tdiv(class="card-image-container"): - tdiv(class="card-image"): - img(src=card.image.getSigUrl("pic")) - if card.kind == player: - tdiv(class="card-overlay"): - tdiv(class="card-overlay-circle"): - span(class="card-overlay-triangle") + if card.image.isSome: + renderCardImage(card) + elif card.video.isSome: + renderVideo(get(card.video)) tdiv(class="card-content-container"): tdiv(class="card-content"): @@ -181,7 +186,9 @@ proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNod if tweet.quote.isSome: renderQuote(tweet.quote.get()) - if tweet.photos.len > 0: + if tweet.card.isSome: + renderCard(tweet.card.get()) + elif tweet.photos.len > 0: renderAlbum(tweet) elif tweet.video.isSome: renderVideo(tweet.video.get()) @@ -189,8 +196,6 @@ proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNod renderGif(tweet.gif.get()) elif tweet.poll.isSome: renderPoll(tweet.poll.get()) - elif tweet.card.isSome: - renderCard(tweet.card.get()) renderStats(tweet.stats)