tweet.nim 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import strutils, sequtils
  2. import karax/[karaxdsl, vdom, vstyles]
  3. import renderutils
  4. import ".."/[types, utils, formatters]
  5. proc renderHeader(tweet: Tweet): VNode =
  6. buildHtml(tdiv):
  7. if tweet.retweet.isSome:
  8. tdiv(class="retweet"):
  9. span: icon "retweet", get(tweet.retweet).by & " retweeted"
  10. if tweet.pinned:
  11. tdiv(class="pinned"):
  12. span: icon "pin", "Pinned Tweet"
  13. tdiv(class="tweet-header"):
  14. a(class="tweet-avatar", href=("/" & tweet.profile.username)):
  15. genImg(tweet.profile.getUserpic("_bigger"), class="avatar")
  16. tdiv(class="tweet-name-row"):
  17. tdiv(class="fullname-and-username"):
  18. linkUser(tweet.profile, class="fullname")
  19. linkUser(tweet.profile, class="username")
  20. span(class="tweet-date"):
  21. a(href=getLink(tweet), title=tweet.getTime()):
  22. text tweet.shortTime
  23. proc renderAlbum(tweet: Tweet): VNode =
  24. let
  25. groups = if tweet.photos.len < 3: @[tweet.photos]
  26. else: tweet.photos.distribute(2)
  27. class = if groups.len == 1 and groups[0].len == 1: "single-image"
  28. else: ""
  29. buildHtml(tdiv(class=("attachments " & class))):
  30. for i, photos in groups:
  31. let margin = if i > 0: ".25em" else: ""
  32. let flex = if photos.len > 1 or groups.len > 1: "flex" else: "block"
  33. tdiv(class="gallery-row", style={marginTop: margin}):
  34. for photo in photos:
  35. tdiv(class="attachment image"):
  36. a(href=getSigUrl(photo & "?name=orig", "pic"), class="still-image",
  37. target="_blank", style={display: flex}):
  38. genImg(photo)
  39. proc isPlaybackEnabled(prefs: Prefs; video: Video): bool =
  40. case video.playbackType
  41. of mp4: prefs.mp4Playback
  42. of m3u8, vmap: prefs.hlsPlayback
  43. proc renderVideoDisabled(video: Video; path: string): VNode =
  44. buildHtml(tdiv):
  45. img(src=video.thumb.getSigUrl("pic"))
  46. tdiv(class="video-overlay"):
  47. case video.playbackType
  48. of mp4:
  49. p: text "mp4 playback disabled in preferences"
  50. of m3u8, vmap:
  51. buttonReferer "/enablehls", "Enable hls playback", path
  52. proc renderVideoUnavailable(video: Video): VNode =
  53. buildHtml(tdiv):
  54. img(src=video.thumb.getSigUrl("pic"))
  55. tdiv(class="video-overlay"):
  56. case video.reason
  57. of "dmcaed":
  58. p: text "This media has been disabled in response to a report by the copyright owner"
  59. else:
  60. p: text "This media is unavailable"
  61. proc renderVideo(video: Video; prefs: Prefs; path: string): VNode =
  62. buildHtml(tdiv(class="attachments")):
  63. tdiv(class="gallery-video"):
  64. tdiv(class="attachment video-container"):
  65. let thumb = video.thumb.getSigUrl("pic")
  66. if not video.available:
  67. renderVideoUnavailable(video)
  68. elif not prefs.isPlaybackEnabled(video):
  69. renderVideoDisabled(video, path)
  70. else:
  71. let source = video.url.getSigUrl("video")
  72. case video.playbackType
  73. of mp4:
  74. if prefs.muteVideos:
  75. video(poster=thumb, controls="", muted=""):
  76. source(src=source, `type`="video/mp4")
  77. else:
  78. video(poster=thumb, controls=""):
  79. source(src=source, `type`="video/mp4")
  80. of m3u8, vmap:
  81. video(poster=thumb, data-url=source, data-autoload="false")
  82. verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
  83. verbatim "<div class=\"overlay-circle\">"
  84. verbatim "<span class=\"overlay-triangle\"</span></div></div>"
  85. proc renderGif(gif: Gif; prefs: Prefs): VNode =
  86. buildHtml(tdiv(class="attachments media-gif")):
  87. tdiv(class="gallery-gif", style=style(maxHeight, "unset")):
  88. tdiv(class="attachment"):
  89. let thumb = gif.thumb.getSigUrl("pic")
  90. let url = gif.url.getSigUrl("video")
  91. if prefs.autoplayGifs:
  92. video(class="gif", poster=thumb, autoplay="", muted="", loop=""):
  93. source(src=url, `type`="video/mp4")
  94. else:
  95. video(class="gif", poster=thumb, controls="", muted="", loop=""):
  96. source(src=url, `type`="video/mp4")
  97. proc renderPoll(poll: Poll): VNode =
  98. buildHtml(tdiv(class="poll")):
  99. for i in 0 ..< poll.options.len:
  100. let leader = if poll.leader == i: " leader" else: ""
  101. let perc = $poll.values[i] & "%"
  102. tdiv(class=("poll-meter" & leader)):
  103. span(class="poll-choice-bar", style=style(width, perc))
  104. span(class="poll-choice-value"): text perc
  105. span(class="poll-choice-option"): text poll.options[i]
  106. span(class="poll-info"):
  107. text $poll.votes & " votes • " & poll.status
  108. proc renderCardImage(card: Card): VNode =
  109. buildHtml(tdiv(class="card-image-container")):
  110. tdiv(class="card-image"):
  111. img(src=getSigUrl(get(card.image), "pic"))
  112. if card.kind == player:
  113. tdiv(class="card-overlay"):
  114. tdiv(class="overlay-circle"):
  115. span(class="overlay-triangle")
  116. proc renderCardContent(card: Card): VNode =
  117. buildHtml(tdiv(class="card-content")):
  118. h2(class="card-title"): text card.title
  119. p(class="card-description"): text card.text
  120. span(class="card-destination"): text card.dest
  121. proc renderCard(card: Card; prefs: Prefs; path: string): VNode =
  122. const largeCards = {summaryLarge, liveEvent, promoWebsite, promoVideo}
  123. let large = if card.kind in largeCards: " large" else: ""
  124. let url = replaceUrl(card.url, prefs)
  125. buildHtml(tdiv(class=("card" & large))):
  126. if card.video.isSome:
  127. tdiv(class="card-container"):
  128. renderVideo(get(card.video), prefs, path)
  129. a(class="card-content-container", href=url):
  130. renderCardContent(card)
  131. else:
  132. a(class="card-container", href=url):
  133. if card.image.isSome:
  134. renderCardImage(card)
  135. tdiv(class="card-content-container"):
  136. renderCardContent(card)
  137. proc renderStats(stats: TweetStats; views: string): VNode =
  138. buildHtml(tdiv(class="tweet-stats")):
  139. span(class="tweet-stat"): icon "comment", $stats.replies
  140. span(class="tweet-stat"): icon "retweet", $stats.retweets
  141. span(class="tweet-stat"): icon "thumbs-up", $stats.likes
  142. if views.len > 0:
  143. span(class="tweet-stat"): icon "play", views
  144. proc renderReply(tweet: Tweet): VNode =
  145. buildHtml(tdiv(class="replying-to")):
  146. text "Replying to "
  147. for i, u in tweet.reply:
  148. if i > 0: text " "
  149. a(href=("/" & u)): text "@" & u
  150. proc renderReply(quote: Quote): VNode =
  151. buildHtml(tdiv(class="replying-to")):
  152. text "Replying to "
  153. for i, u in quote.reply:
  154. if i > 0: text " "
  155. a(href=("/" & u)): text "@" & u
  156. proc renderQuoteMedia(quote: Quote): VNode =
  157. buildHtml(tdiv(class="quote-media-container")):
  158. if quote.thumb.len > 0:
  159. tdiv(class="quote-media"):
  160. genImg(quote.thumb)
  161. if quote.badge.len > 0:
  162. tdiv(class="quote-badge"):
  163. tdiv(class="quote-badge-text"): text quote.badge
  164. elif quote.sensitive:
  165. tdiv(class="quote-sensitive"):
  166. icon "attention", class="quote-sensitive-icon"
  167. proc renderQuote(quote: Quote; prefs: Prefs): VNode =
  168. if not quote.available:
  169. return buildHtml(tdiv(class="quote unavailable")):
  170. tdiv(class="unavailable-quote"):
  171. if quote.tombstone.len > 0:
  172. text quote.tombstone
  173. else:
  174. text "This tweet is unavailable"
  175. buildHtml(tdiv(class="quote")):
  176. a(class="quote-link", href=getLink(quote))
  177. if quote.thumb.len > 0 or quote.sensitive:
  178. renderQuoteMedia(quote)
  179. tdiv(class="fullname-and-username"):
  180. linkUser(quote.profile, class="fullname")
  181. linkUser(quote.profile, class="username")
  182. if quote.reply.len > 0:
  183. renderReply(quote)
  184. tdiv(class="quote-text"):
  185. verbatim linkifyText(quote.text, prefs)
  186. if quote.hasThread:
  187. a(class="show-thread", href=getLink(quote)):
  188. text "Show this thread"
  189. proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class="";
  190. index=0; total=(-1); last=false): VNode =
  191. var divClass = class
  192. if index == total or last:
  193. divClass = "thread-last " & class
  194. if not tweet.available:
  195. return buildHtml(tdiv(class=divClass)):
  196. tdiv(class="status-el unavailable"):
  197. tdiv(class="unavailable-box"):
  198. if tweet.tombstone.len > 0:
  199. text tweet.tombstone
  200. else:
  201. text "This tweet is unavailable"
  202. buildHtml(tdiv(class=divClass)):
  203. tdiv(class="status-el"):
  204. tdiv(class="status-body"):
  205. var views = ""
  206. renderHeader(tweet)
  207. if index == 0 and tweet.reply.len > 0:
  208. renderReply(tweet)
  209. tdiv(class="status-content media-body"):
  210. verbatim linkifyText(tweet.text, prefs)
  211. if tweet.quote.isSome:
  212. renderQuote(tweet.quote.get(), prefs)
  213. if tweet.card.isSome:
  214. renderCard(tweet.card.get(), prefs, path)
  215. elif tweet.photos.len > 0:
  216. renderAlbum(tweet)
  217. elif tweet.video.isSome:
  218. renderVideo(tweet.video.get(), prefs, path)
  219. views = tweet.video.get().views
  220. elif tweet.gif.isSome:
  221. renderGif(tweet.gif.get(), prefs)
  222. elif tweet.poll.isSome:
  223. renderPoll(tweet.poll.get())
  224. if not prefs.hideTweetStats:
  225. renderStats(tweet.stats, views)
  226. if tweet.hasThread and "timeline" in class:
  227. a(class="show-thread", href=getLink(tweet)):
  228. text "Show this thread"