Jelajahi Sumber

Support "Replying to" and "Show thread"

Zed 7 tahun lalu
induk
melakukan
a67d27e0c4
5 mengubah file dengan 83 tambahan dan 43 penghapusan
  1. 5 0
      public/style.css
  2. 6 2
      src/parser.nim
  3. 10 4
      src/parserutils.nim
  4. 7 3
      src/types.nim
  5. 55 34
      src/views/tweet.nimf

+ 5 - 0
public/style.css

@@ -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;

+ 6 - 2
src/parser.nim

@@ -41,8 +41,10 @@ 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"),
-    text: getQuoteText(quote)
+    id:    quote.attr("data-item-id"),
+    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
   )
   )

+ 10 - 4
src/parserutils.nim

@@ -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

+ 7 - 3
src/types.nim

@@ -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]
-    badge*: Option[string]
+    thumb*: 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]

+ 55 - 34
src/views/tweet.nimf

@@ -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>
@@ -161,9 +179,12 @@
     #elif tweet.quote.isSome:
     #elif tweet.quote.isSome:
     ${renderQuote(tweet.quote.get())}
     ${renderQuote(tweet.quote.get())}
     #elif tweet.poll.isSome:
     #elif tweet.poll.isSome:
-     ${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: