Render tweet quotes

This commit is contained in:
Zed 2019-06-24 08:07:36 +02:00
parent 1213220ef0
commit af9a5d4872
6 changed files with 128 additions and 18 deletions

View file

@ -3,6 +3,7 @@ body {
color: #f8f8f2; color: #f8f8f2;
margin: 0; margin: 0;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
} }
#tweets { #tweets {
@ -575,6 +576,11 @@ nav {
font-weight: bold; font-weight: bold;
} }
video {
height: 100%;
width: 100%;
}
.video-overlay { .video-overlay {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -594,7 +600,67 @@ nav {
font-size: 20px; font-size: 20px;
} }
video { .quote {
margin-top: 10px;
border: solid 1px #404040;
border-radius: 10px;
padding: 6px;
background-color: #121212;
}
.quote:hover {
border-color: #808080;
}
.quote-container {
position: relative;
overflow: auto;
}
.quote-link {
height: 100%; height: 100%;
width: 100%; width: 100%;
left: 0;
top: 0;
position: absolute;
}
.quote-text {
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
}
.quote-media-container {
display: flex;
align-items: center;
overflow: hidden;
max-height: 102px;
width: 102px;
float: left;
margin-right: 7px;
border-radius: 7px;
}
.quote-media {
display: flex;
justify-content: center;
}
.quote-media img {
width: 100%;
height: 100%;
}
.quote-badge {
background: rgba(0,0,0,0.25);
border-radius: 4px;
bottom: 8px;
box-sizing: border-box;
color: #fffffff0;
left: 8px;
position: absolute;
z-index: 1;
line-height: 16px;
padding: 2px;
} }

View file

@ -21,7 +21,7 @@ proc toLink*(url, text: string; class="timeline-link"): string =
proc reUrlToLink*(m: RegexMatch; s: string): string = proc reUrlToLink*(m: RegexMatch; s: string): string =
let url = s[m.group(0)[0]] let url = s[m.group(0)[0]]
toLink(url, " " & shortLink(url)) toLink(url, shortLink(url))
proc reEmailToLink*(m: RegexMatch; s: string): string = proc reEmailToLink*(m: RegexMatch; s: string): string =
let url = s[m.group(0)[0]] let url = s[m.group(0)[0]]
@ -44,12 +44,13 @@ proc reUsernameToLink*(m: RegexMatch; s: string): string =
pretext & toLink("/" & username, "@" & username) pretext & toLink("/" & username, "@" & username)
proc linkifyText*(text: string): string = proc linkifyText*(text: string): string =
result = text.replace("\n", "<br>") result = text.strip()
result = result.replace("\n", "<br>")
result = result.replace(ellipsisRegex, "") result = result.replace(ellipsisRegex, "")
result = result.replace(usernameRegex, reUsernameToLink) result = result.replace(usernameRegex, reUsernameToLink)
result = result.replace(emailRegex, reEmailToLink) result = result.replace(emailRegex, reEmailToLink)
result = result.replace(urlRegex, reUrlToLink) result = result.replace(urlRegex, reUrlToLink)
result = result.replace(re"</a>\s+", "</a> ") result = result.replace(re"([A-z0-9])<a>", "$1 <a>")
result = result.replace(re"</a> ([.,\)])", "</a>$1") result = result.replace(re"</a> ([.,\)])", "</a>$1")
proc stripTwitterUrls*(text: string): string = proc stripTwitterUrls*(text: string): string =

View file

@ -1,7 +1,7 @@
import xmltree, sequtils, strtabs, strutils, strformat, json import xmltree, sequtils, strtabs, strutils, strformat, json
import nimquery import nimquery
import ./types, ./parserutils import ./types, ./parserutils, ./formatters
proc parsePopupProfile*(node: XmlNode): Profile = proc parsePopupProfile*(node: XmlNode): Profile =
let profile = node.querySelector(".profile-card") let profile = node.querySelector(".profile-card")
@ -16,6 +16,7 @@ proc parsePopupProfile*(node: XmlNode): Profile =
protected: isProtected(profile), protected: isProtected(profile),
banner: getBanner(profile) banner: getBanner(profile)
) )
result.getPopupStats(profile) result.getPopupStats(profile)
proc parseIntentProfile*(profile: XmlNode): Profile = proc parseIntentProfile*(profile: XmlNode): Profile =
@ -28,6 +29,7 @@ proc parseIntentProfile*(profile: XmlNode): Profile =
protected: not profile.querySelector("li.protected").isNil, protected: not profile.querySelector("li.protected").isNil,
banner: getBanner(profile) banner: getBanner(profile)
) )
result.getIntentStats(profile) result.getIntentStats(profile)
proc parseTweetProfile*(profile: XmlNode): Profile = proc parseTweetProfile*(profile: XmlNode): Profile =
@ -38,20 +40,21 @@ proc parseTweetProfile*(profile: XmlNode): Profile =
verified: isVerified(profile) verified: isVerified(profile)
) )
proc parseQuote*(tweet: XmlNode): Tweet = proc parseQuote*(quote: XmlNode): Quote =
let tweet = tweet.querySelector(".QuoteTweet-innerContainer") result = Quote(
result = Tweet( id: quote.getAttr("data-item-id"),
id: tweet.getAttr("data-item-id"), link: quote.getAttr("href"),
link: tweet.getAttr("href"), text: quote.selectText(".QuoteTweet-text").stripTwitterUrls()
text: tweet.selectText(".QuoteTweet-text")
) )
result.profile = Profile( result.profile = Profile(
fullname: tweet.getAttr("data-screen-name"), fullname: quote.selectText(".QuoteTweet-fullname"),
username: tweet.selectText(".QuteTweet-fullname"), username: quote.getAttr("data-screen-name"),
verified: isVerified(tweet) verified: isVerified(quote)
) )
result.getQuoteMedia(quote)
proc parseTweet*(tweet: XmlNode): Tweet = proc parseTweet*(tweet: XmlNode): Tweet =
result = Tweet( result = Tweet(
id: tweet.getAttr("data-item-id"), id: tweet.getAttr("data-item-id"),
@ -71,6 +74,10 @@ proc parseTweet*(tweet: XmlNode): Tweet =
result.retweetBy = some(by) result.retweetBy = some(by)
result.retweetId = some(tweet.getAttr("data-retweet-id")) result.retweetId = some(tweet.getAttr("data-retweet-id"))
let quote = tweet.querySelector(".QuoteTweet-innerContainer")
if not quote.isNil:
result.quote = some(parseQuote(quote))
proc parseTweets*(node: XmlNode): Tweets = proc parseTweets*(node: XmlNode): Tweets =
if node.isNil: return if node.isNil: return
node.querySelectorAll(".tweet").map(parseTweet) node.querySelectorAll(".tweet").map(parseTweet)

View file

@ -34,10 +34,10 @@ proc getUsername*(profile: XmlNode; selector: string): string =
proc getTweetText*(tweet: XmlNode): string = proc getTweetText*(tweet: XmlNode): string =
let selector = ".tweet-text > a.twitter-timeline-link.u-hidden" let selector = ".tweet-text > a.twitter-timeline-link.u-hidden"
let link = tweet.selectAttr(selector, "data-expanded-url") let link = tweet.selectAttr(selector, "data-expanded-url")
var text =tweet.selectText(".tweet-text") var text = tweet.selectText(".tweet-text")
if link.len > 0 and link in text: if link.len > 0 and link in text:
text = text.replace(link, " " & link) text = text.replace(link, "")
stripTwitterUrls(text) stripTwitterUrls(text)
@ -114,3 +114,12 @@ proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
tweet.gif = some(getGif(player.querySelector(".PlayableMedia-player"))) tweet.gif = some(getGif(player.querySelector(".PlayableMedia-player")))
elif "video" in player.getAttr("class"): elif "video" in player.getAttr("class"):
tweet.video = some(Video()) tweet.video = some(Video())
proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
let media = node.querySelector(".QuoteMedia")
if not media.isNil:
quote.thumb = some(media.selectAttr("img", "src"))
let badge = node.querySelector(".AdaptiveMedia-badgeText")
if not badge.isNil:
quote.badge = some(badge.innerText())

View file

@ -43,12 +43,13 @@ type
url*: string url*: string
thumb*: string thumb*: string
Quote* = ref object Quote* = object
id*: string id*: string
profile*: Profile profile*: Profile
link*: string link*: string
text*: string text*: string
video*: Option[Video] thumb*: Option[string]
badge*: Option[string]
Tweet* = ref object Tweet* = ref object
id*: string id*: string

View file

@ -29,6 +29,30 @@
</div> </div>
#end proc #end proc
# #
#proc renderQuote(quote: Quote): string =
#let hasMedia = quote.thumb.isSome()
<div class="quote">
<div class="quote-container" href="${quote.link}">
<a class="quote-link" href="${quote.link}"></a>
#if hasMedia:
<div class="quote-media-container">
<div class="quote-media">
<img src=${quote.thumb.get().getSigUrl("pic")}>
#if quote.badge.isSome:
<div class="quote-badge">${quote.badge.get()}</div>
#end if
</div>
</div>
#end if
<div class="profile-card-name">
${linkUser(quote.profile, "b", class="username", username=false)}
${linkUser(quote.profile, "span", class="account-name")}
</div>
<div class="quote-text">${linkifyText(xmltree.escape(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 display = if groups.len == 1 and groups[0].len == 1: "display: table-caption;" else: "" #let display = if groups.len == 1 and groups[0].len == 1: "display: table-caption;" else: ""
@ -105,6 +129,8 @@
${renderVideo(tweet.video.get())} ${renderVideo(tweet.video.get())}
#elif tweet.gif.isSome: #elif tweet.gif.isSome:
${renderGif(tweet.gif.get())} ${renderGif(tweet.gif.get())}
#elif tweet.quote.isSome:
${renderQuote(tweet.quote.get())}
#end if #end if
${renderStats(tweet)} ${renderStats(tweet)}
</div> </div>