Support "Replying to" and "Show thread"
This commit is contained in:
parent
97166feec9
commit
a67d27e0c4
|
@ -138,6 +138,11 @@ a:hover {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.replying-to {
|
||||||
|
color: hsla(240,1%,73%,.7);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.status-el .status-content {
|
.status-el .status-content {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
|
|
|
@ -42,7 +42,9 @@ proc parseTweetProfile*(profile: XmlNode): Profile =
|
||||||
proc parseQuote*(quote: XmlNode): Quote =
|
proc parseQuote*(quote: XmlNode): Quote =
|
||||||
result = Quote(
|
result = Quote(
|
||||||
id: quote.attr("data-item-id"),
|
id: quote.attr("data-item-id"),
|
||||||
text: getQuoteText(quote)
|
text: getQuoteText(quote),
|
||||||
|
reply: parseTweetReply(quote),
|
||||||
|
hasThread: quote.select(".self-thread-context") != nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.profile = Profile(
|
result.profile = Profile(
|
||||||
|
@ -64,6 +66,8 @@ proc parseTweet*(node: XmlNode): Tweet =
|
||||||
shortTime: getShortTime(tweet),
|
shortTime: getShortTime(tweet),
|
||||||
profile: parseTweetProfile(tweet),
|
profile: parseTweetProfile(tweet),
|
||||||
stats: parseTweetStats(tweet),
|
stats: parseTweetStats(tweet),
|
||||||
|
reply: parseTweetReply(tweet),
|
||||||
|
hasThread: tweet.select(".self-thread-context") != nil,
|
||||||
pinned: "pinned" in tweet.attr("class"),
|
pinned: "pinned" in tweet.attr("class"),
|
||||||
available: true
|
available: true
|
||||||
)
|
)
|
||||||
|
|
|
@ -120,11 +120,17 @@ proc parseTweetStats*(node: XmlNode): TweetStats =
|
||||||
of "rep": result.replies = text[0]
|
of "rep": result.replies = text[0]
|
||||||
of "lik": result.likes = text[0]
|
of "lik": result.likes = text[0]
|
||||||
|
|
||||||
|
proc parseTweetReply*(node: XmlNode): seq[string] =
|
||||||
|
let reply = node.select(".ReplyingToContextBelowAuthor")
|
||||||
|
if reply == nil: return
|
||||||
|
for username in reply.selectAll("a"):
|
||||||
|
result.add username.selectText("b")
|
||||||
|
|
||||||
proc getGif(player: XmlNode): Gif =
|
proc getGif(player: XmlNode): Gif =
|
||||||
let
|
let
|
||||||
thumb = player.attr("style").replace(thumbRegex, "$1")
|
thumb = player.attr("style").replace(thumbRegex, "$1")
|
||||||
id = thumb.replace(gifRegex, "$1")
|
id = thumb.replace(gifRegex, "$1")
|
||||||
url = fmt"https://video.twimg.com/tweet_video/{id}.mp4"
|
url = &"https://video.twimg.com/tweet_video/{id}.mp4"
|
||||||
Gif(url: url, thumb: thumb)
|
Gif(url: url, thumb: thumb)
|
||||||
|
|
||||||
proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
|
proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
|
||||||
|
@ -146,15 +152,15 @@ proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
|
||||||
|
|
||||||
let media = node.select(".QuoteMedia")
|
let media = node.select(".QuoteMedia")
|
||||||
if media != nil:
|
if media != nil:
|
||||||
quote.thumb = some(media.selectAttr("img", "src"))
|
quote.thumb = media.selectAttr("img", "src")
|
||||||
|
|
||||||
let badge = node.select(".AdaptiveMedia-badgeText")
|
let badge = node.select(".AdaptiveMedia-badgeText")
|
||||||
let gifBadge = node.select(".Icon--gifBadge")
|
let gifBadge = node.select(".Icon--gifBadge")
|
||||||
|
|
||||||
if badge != nil:
|
if badge != nil:
|
||||||
quote.badge = some(badge.innerText())
|
quote.badge = badge.innerText()
|
||||||
elif gifBadge != nil:
|
elif gifBadge != nil:
|
||||||
quote.badge = some("GIF")
|
quote.badge = "GIF"
|
||||||
|
|
||||||
proc getTweetCards*(tweet: Tweet; node: XmlNode) =
|
proc getTweetCards*(tweet: Tweet; node: XmlNode) =
|
||||||
if node.attr("data-has-cards") == "false": return
|
if node.attr("data-has-cards") == "false": return
|
||||||
|
|
|
@ -58,9 +58,11 @@ type
|
||||||
id*: string
|
id*: string
|
||||||
profile*: Profile
|
profile*: Profile
|
||||||
text*: string
|
text*: string
|
||||||
|
reply*: seq[string]
|
||||||
|
hasThread*: bool
|
||||||
sensitive*: bool
|
sensitive*: bool
|
||||||
thumb*: Option[string]
|
thumb*: string
|
||||||
badge*: Option[string]
|
badge*: string
|
||||||
|
|
||||||
Retweet* = object
|
Retweet* = object
|
||||||
by*: string
|
by*: string
|
||||||
|
@ -77,8 +79,10 @@ type
|
||||||
text*: string
|
text*: string
|
||||||
time*: Time
|
time*: Time
|
||||||
shortTime*: string
|
shortTime*: string
|
||||||
available*: bool
|
reply*: seq[string]
|
||||||
pinned*: bool
|
pinned*: bool
|
||||||
|
available*: bool
|
||||||
|
hasThread*: bool
|
||||||
stats*: TweetStats
|
stats*: TweetStats
|
||||||
retweet*: Option[Retweet]
|
retweet*: Option[Retweet]
|
||||||
quote*: Option[Quote]
|
quote*: Option[Quote]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#? stdtmpl(subsChar = '$', metaChar = '#')
|
#? stdtmpl(subsChar = '$', metaChar = '#')
|
||||||
#import xmltree, strutils, times, sequtils, uri
|
#import xmltree, strutils, strformat, sequtils, times, uri
|
||||||
#import ../types, ../formatters, ../utils
|
#import ../types, ../formatters, ../utils
|
||||||
#
|
#
|
||||||
#proc renderHeading(tweet: Tweet): string =
|
#proc renderHeading(tweet: Tweet): string =
|
||||||
|
@ -29,38 +29,6 @@
|
||||||
</div>
|
</div>
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
#proc renderQuote(quote: Quote): string =
|
|
||||||
#let hasMedia = quote.thumb.isSome() or quote.sensitive
|
|
||||||
<div class="quote">
|
|
||||||
<div class="quote-container">
|
|
||||||
<a class="quote-link" href="${getLink(quote)}"></a>
|
|
||||||
#if hasMedia:
|
|
||||||
<div class="quote-media-container">
|
|
||||||
<div class="quote-media">
|
|
||||||
#if quote.thumb.isSome():
|
|
||||||
${genImg(quote.thumb.get())}
|
|
||||||
#if quote.badge.isSome():
|
|
||||||
<div class="quote-badge">
|
|
||||||
<div class="quote-badge-text">${quote.badge.get()}</div>
|
|
||||||
</div>
|
|
||||||
#end if
|
|
||||||
#elif quote.sensitive:
|
|
||||||
<div class="quote-sensitive">
|
|
||||||
<span class="icon quote-sensitive-icon">❗</span>
|
|
||||||
</div>
|
|
||||||
#end if
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
#end if
|
|
||||||
<div class="fullname-and-username">
|
|
||||||
${linkUser(quote.profile, class="fullname")}
|
|
||||||
${linkUser(quote.profile, class="username")}
|
|
||||||
</div>
|
|
||||||
<div class="quote-text">${linkifyText(quote.text)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
#end proc
|
|
||||||
#
|
|
||||||
#proc renderMediaGroup(tweet: Tweet): string =
|
#proc renderMediaGroup(tweet: Tweet): string =
|
||||||
#let groups = if tweet.photos.len > 2: tweet.photos.distribute(2) else: @[tweet.photos]
|
#let groups = if tweet.photos.len > 2: tweet.photos.distribute(2) else: @[tweet.photos]
|
||||||
#let class = if groups.len == 1 and groups[0].len == 1: "single-image" else: ""
|
#let class = if groups.len == 1 and groups[0].len == 1: "single-image" else: ""
|
||||||
|
@ -140,6 +108,53 @@
|
||||||
</div>
|
</div>
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
|
#proc renderShowThread(tweet: Tweet | Quote): string =
|
||||||
|
<a href="${getLink(tweet)}">Show this thread</a>
|
||||||
|
#end proc
|
||||||
|
#
|
||||||
|
#proc renderReply(tweet: Tweet | Quote): string =
|
||||||
|
#let usernames = tweet.reply.mapIt(&"""<a href="{it}">@{it}</a>""")
|
||||||
|
<div class="replying-to">Replying to ${usernames.join(" ")}</div>
|
||||||
|
#end proc
|
||||||
|
#
|
||||||
|
#proc renderQuote(quote: Quote): string =
|
||||||
|
#let hasMedia = quote.thumb.len > 0 or quote.sensitive
|
||||||
|
<div class="quote">
|
||||||
|
<div class="quote-container">
|
||||||
|
<a class="quote-link" href="${getLink(quote)}"></a>
|
||||||
|
#if hasMedia:
|
||||||
|
<div class="quote-media-container">
|
||||||
|
<div class="quote-media">
|
||||||
|
#if quote.thumb.len > 0:
|
||||||
|
${genImg(quote.thumb)}
|
||||||
|
#if quote.badge.len > 0:
|
||||||
|
<div class="quote-badge">
|
||||||
|
<div class="quote-badge-text">${quote.badge}</div>
|
||||||
|
</div>
|
||||||
|
#end if
|
||||||
|
#elif quote.sensitive:
|
||||||
|
<div class="quote-sensitive">
|
||||||
|
<span class="icon quote-sensitive-icon">❗</span>
|
||||||
|
</div>
|
||||||
|
#end if
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
#end if
|
||||||
|
<div class="fullname-and-username">
|
||||||
|
${linkUser(quote.profile, class="fullname")}
|
||||||
|
${linkUser(quote.profile, class="username")}
|
||||||
|
</div>
|
||||||
|
#if quote.reply.len > 0:
|
||||||
|
${renderReply(quote)}
|
||||||
|
#end if
|
||||||
|
<div class="quote-text">${linkifyText(quote.text)}</div>
|
||||||
|
#if quote.hasThread:
|
||||||
|
${renderShowThread(quote)}
|
||||||
|
#end if
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
#end proc
|
||||||
|
#
|
||||||
#proc renderTweet*(tweet: Tweet; class=""; last=false): string =
|
#proc renderTweet*(tweet: Tweet; class=""; last=false): string =
|
||||||
#var divClass = if last: "thread-last " & class else: class
|
#var divClass = if last: "thread-last " & class else: class
|
||||||
#if divClass.len > 0:
|
#if divClass.len > 0:
|
||||||
|
@ -149,6 +164,9 @@
|
||||||
<div class="status-el">
|
<div class="status-el">
|
||||||
<div class="status-body">
|
<div class="status-body">
|
||||||
${renderHeading(tweet)}
|
${renderHeading(tweet)}
|
||||||
|
#if tweet.reply.len > 0:
|
||||||
|
${renderReply(tweet)}
|
||||||
|
#end if
|
||||||
<div class="status-content-wrapper">
|
<div class="status-content-wrapper">
|
||||||
<div class="status-content media-body">${linkifyText(tweet.text)}</div>
|
<div class="status-content media-body">${linkifyText(tweet.text)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -164,6 +182,9 @@
|
||||||
${renderPoll(tweet.poll.get())}
|
${renderPoll(tweet.poll.get())}
|
||||||
#end if
|
#end if
|
||||||
${renderStats(tweet.stats)}
|
${renderStats(tweet.stats)}
|
||||||
|
#if tweet.hasThread and "timeline" in class:
|
||||||
|
${renderShowThread(tweet)}
|
||||||
|
#end if
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
#else:
|
#else:
|
||||||
|
|
Loading…
Reference in New Issue