tweet.nim 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import strutils, sequtils, strformat, options
  2. import karax/[karaxdsl, vdom, vstyles]
  3. import renderutils
  4. import ".."/[types, utils, formatters]
  5. proc getSmallPic(url: string): string =
  6. result = url
  7. if "?" notin url:
  8. result &= ":small"
  9. result = getPicUrl(result)
  10. proc renderMiniAvatar(profile: Profile): VNode =
  11. let url = getPicUrl(profile.getUserpic("_mini"))
  12. buildHtml():
  13. img(class="avatar mini", src=url)
  14. proc renderHeader(tweet: Tweet; retweet: string; prefs: Prefs): VNode =
  15. buildHtml(tdiv):
  16. if retweet.len > 0:
  17. tdiv(class="retweet-header"):
  18. span: icon "retweet", retweet & " retweeted"
  19. if tweet.pinned:
  20. tdiv(class="pinned"):
  21. span: icon "pin", "Pinned Tweet"
  22. tdiv(class="tweet-header"):
  23. a(class="tweet-avatar", href=("/" & tweet.profile.username)):
  24. var size = "_bigger"
  25. if not prefs.autoplayGifs and tweet.profile.userpic.endsWith("gif"):
  26. size = "_400x400"
  27. genImg(tweet.profile.getUserpic(size), class="avatar")
  28. tdiv(class="tweet-name-row"):
  29. tdiv(class="fullname-and-username"):
  30. linkUser(tweet.profile, class="fullname")
  31. linkUser(tweet.profile, class="username")
  32. span(class="tweet-date"):
  33. a(href=getLink(tweet), title=tweet.getTime):
  34. text tweet.getShortTime
  35. proc renderAlbum(tweet: Tweet): VNode =
  36. let
  37. groups = if tweet.photos.len < 3: @[tweet.photos]
  38. else: tweet.photos.distribute(2)
  39. buildHtml(tdiv(class="attachments")):
  40. for i, photos in groups:
  41. let margin = if i > 0: ".25em" else: ""
  42. tdiv(class="gallery-row", style={marginTop: margin}):
  43. for photo in photos:
  44. tdiv(class="attachment image"):
  45. let
  46. named = "name=" in photo
  47. orig = if named: photo else: photo & "?name=orig"
  48. small = if named: photo else: photo & "?name=small"
  49. a(href=getPicUrl(orig), class="still-image", target="_blank"):
  50. genImg(small)
  51. proc isPlaybackEnabled(prefs: Prefs; video: Video): bool =
  52. case video.playbackType
  53. of mp4: prefs.mp4Playback
  54. of m3u8, vmap: prefs.hlsPlayback
  55. proc renderVideoDisabled(video: Video; path: string): VNode =
  56. buildHtml(tdiv):
  57. img(src=getSmallPic(video.thumb))
  58. tdiv(class="video-overlay"):
  59. case video.playbackType
  60. of mp4:
  61. p: text "mp4 playback disabled in preferences"
  62. of m3u8, vmap:
  63. buttonReferer "/enablehls", "Enable hls playback", path
  64. proc renderVideoUnavailable(video: Video): VNode =
  65. buildHtml(tdiv):
  66. img(src=getSmallPic(video.thumb))
  67. tdiv(class="video-overlay"):
  68. case video.reason
  69. of "dmcaed":
  70. p: text "This media has been disabled in response to a report by the copyright owner"
  71. else:
  72. p: text "This media is unavailable"
  73. proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
  74. let container =
  75. if video.description.len > 0 or video.title.len > 0: " card-container"
  76. else: ""
  77. buildHtml(tdiv(class="attachments card")):
  78. tdiv(class="gallery-video" & container):
  79. tdiv(class="attachment video-container"):
  80. let thumb = getSmallPic(video.thumb)
  81. if not video.available:
  82. renderVideoUnavailable(video)
  83. elif not prefs.isPlaybackEnabled(video):
  84. renderVideoDisabled(video, path)
  85. else:
  86. let vid = video.variants.filterIt(it.videoType == video.playbackType)
  87. let source = getVidUrl(vid[0].url)
  88. case video.playbackType
  89. of mp4:
  90. if prefs.muteVideos:
  91. video(poster=thumb, controls="", muted=""):
  92. source(src=source, `type`="video/mp4")
  93. else:
  94. video(poster=thumb, controls=""):
  95. source(src=source, `type`="video/mp4")
  96. of m3u8, vmap:
  97. video(poster=thumb, data-url=source, data-autoload="false")
  98. verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
  99. tdiv(class="overlay-circle"): span(class="overlay-triangle")
  100. verbatim "</div>"
  101. if container.len > 0:
  102. tdiv(class="card-content"):
  103. h2(class="card-title"): text video.title
  104. if video.description.len > 0:
  105. p(class="card-description"): text video.description
  106. proc renderGif(gif: Gif; prefs: Prefs): VNode =
  107. buildHtml(tdiv(class="attachments media-gif")):
  108. tdiv(class="gallery-gif", style={maxHeight: "unset"}):
  109. tdiv(class="attachment"):
  110. let thumb = getSmallPic(gif.thumb)
  111. let url = getPicUrl(gif.url)
  112. if prefs.autoplayGifs:
  113. video(class="gif", poster=thumb, controls="", autoplay="", muted="", loop=""):
  114. source(src=url, `type`="video/mp4")
  115. else:
  116. video(class="gif", poster=thumb, controls="", muted="", loop=""):
  117. source(src=url, `type`="video/mp4")
  118. proc renderPoll(poll: Poll): VNode =
  119. buildHtml(tdiv(class="poll")):
  120. for i in 0 ..< poll.options.len:
  121. let
  122. leader = if poll.leader == i: " leader" else: ""
  123. val = poll.values[i]
  124. perc = if val > 0: val / poll.votes * 100 else: 0
  125. percStr = (&"{perc:>3.0f}").strip(chars={'.'}) & '%'
  126. tdiv(class=("poll-meter" & leader)):
  127. span(class="poll-choice-bar", style={width: percStr})
  128. span(class="poll-choice-value"): text percStr
  129. span(class="poll-choice-option"): text poll.options[i]
  130. span(class="poll-info"):
  131. text insertSep($poll.votes, ',') & " votes • " & poll.status
  132. proc renderCardImage(card: Card): VNode =
  133. buildHtml(tdiv(class="card-image-container")):
  134. tdiv(class="card-image"):
  135. img(src=getPicUrl(card.image), alt="")
  136. if card.kind == player:
  137. tdiv(class="card-overlay"):
  138. tdiv(class="overlay-circle"):
  139. span(class="overlay-triangle")
  140. proc renderCardContent(card: Card): VNode =
  141. buildHtml(tdiv(class="card-content")):
  142. h2(class="card-title"): text card.title
  143. if card.text.len > 0:
  144. p(class="card-description"): text card.text
  145. if card.dest.len > 0:
  146. span(class="card-destination"): text card.dest
  147. proc renderCard(card: Card; prefs: Prefs; path: string): VNode =
  148. const smallCards = {app, player, summary, storeLink}
  149. let large = if card.kind notin smallCards: " large" else: ""
  150. let url = replaceUrl(card.url, prefs)
  151. buildHtml(tdiv(class=("card" & large))):
  152. if card.video.isSome:
  153. tdiv(class="card-container"):
  154. renderVideo(get(card.video), prefs, path)
  155. a(class="card-content-container", href=url):
  156. renderCardContent(card)
  157. else:
  158. a(class="card-container", href=url):
  159. if card.image.len > 0:
  160. renderCardImage(card)
  161. tdiv(class="card-content-container"):
  162. renderCardContent(card)
  163. proc renderStats(stats: TweetStats; views: string): VNode =
  164. buildHtml(tdiv(class="tweet-stats")):
  165. span(class="tweet-stat"): icon "comment", insertSep($stats.replies, ',')
  166. span(class="tweet-stat"): icon "retweet", insertSep($stats.retweets, ',')
  167. span(class="tweet-stat"): icon "heart", insertSep($stats.likes, ',')
  168. span(class="tweet-stat"): icon "quote", insertSep($stats.quotes, ',')
  169. if views.len > 0:
  170. span(class="tweet-stat"): icon "play", insertSep(views, ',')
  171. proc renderReply(tweet: Tweet): VNode =
  172. buildHtml(tdiv(class="replying-to")):
  173. text "Replying to "
  174. for i, u in tweet.reply:
  175. if i > 0: text " "
  176. a(href=("/" & u)): text "@" & u
  177. proc renderAttribution(profile: Profile): VNode =
  178. buildHtml(a(class="attribution", href=("/" & profile.username))):
  179. renderMiniAvatar(profile)
  180. strong: text profile.fullname
  181. proc renderMediaTags(tags: seq[Profile]): VNode =
  182. buildHtml(tdiv(class="media-tag-block")):
  183. icon "user"
  184. for i, p in tags:
  185. a(class="media-tag", href=("/" & p.username), title=p.username):
  186. text p.fullname
  187. if i < tags.high:
  188. text ", "
  189. proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
  190. buildHtml(tdiv(class="quote-media-container")):
  191. if quote.photos.len > 0:
  192. renderAlbum(quote)
  193. elif quote.video.isSome:
  194. renderVideo(quote.video.get(), prefs, path)
  195. elif quote.gif.isSome:
  196. renderGif(quote.gif.get(), prefs)
  197. proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
  198. if not quote.available:
  199. return buildHtml(tdiv(class="quote unavailable")):
  200. tdiv(class="unavailable-quote"):
  201. if quote.tombstone.len > 0:
  202. text quote.tombstone
  203. else:
  204. text "This tweet is unavailable"
  205. buildHtml(tdiv(class="quote quote-big")):
  206. a(class="quote-link", href=getLink(quote))
  207. tdiv(class="tweet-name-row"):
  208. tdiv(class="fullname-and-username"):
  209. renderMiniAvatar(quote.profile)
  210. linkUser(quote.profile, class="fullname")
  211. linkUser(quote.profile, class="username")
  212. span(class="tweet-date"):
  213. a(href=getLink(quote), title=quote.getTime):
  214. text quote.getShortTime
  215. if quote.reply.len > 0:
  216. renderReply(quote)
  217. if quote.text.len > 0:
  218. tdiv(class="quote-text", dir="auto"):
  219. verbatim replaceUrl(quote.text, prefs)
  220. if quote.hasThread:
  221. a(class="show-thread", href=getLink(quote)):
  222. text "Show this thread"
  223. if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
  224. renderQuoteMedia(quote, prefs, path)
  225. proc renderLocation*(tweet: Tweet): string =
  226. let (place, url) = tweet.getLocation()
  227. if place.len == 0: return
  228. let node = buildHtml(span(class="tweet-geo")):
  229. text " – at "
  230. if url.len > 1:
  231. a(href=url): text place
  232. else:
  233. text place
  234. return $node
  235. proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
  236. last=false; showThread=false; mainTweet=false; afterTweet=false): VNode =
  237. var divClass = class
  238. if index == -1 or last:
  239. divClass = "thread-last " & class
  240. if not tweet.available:
  241. return buildHtml(tdiv(class=divClass & "unavailable timeline-item")):
  242. tdiv(class="unavailable-box"):
  243. if tweet.tombstone.len > 0:
  244. text tweet.tombstone
  245. else:
  246. text "This tweet is unavailable"
  247. let fullTweet = tweet
  248. var retweet: string
  249. var tweet = fullTweet
  250. if tweet.retweet.isSome:
  251. tweet = tweet.retweet.get
  252. retweet = fullTweet.profile.fullname
  253. buildHtml(tdiv(class=("timeline-item " & divClass))):
  254. if not mainTweet:
  255. a(class="tweet-link", href=getLink(tweet))
  256. tdiv(class="tweet-body"):
  257. var views = ""
  258. renderHeader(tweet, retweet, prefs)
  259. if not afterTweet and index == 0 and tweet.reply.len > 0 and
  260. (tweet.reply.len > 1 or tweet.reply[0] != tweet.profile.username):
  261. renderReply(tweet)
  262. var tweetClass = "tweet-content media-body"
  263. if prefs.bidiSupport:
  264. tweetClass &= " tweet-bidi"
  265. tdiv(class=tweetClass, dir="auto"):
  266. verbatim replaceUrl(tweet.text, prefs) & renderLocation(tweet)
  267. if tweet.attribution.isSome:
  268. renderAttribution(tweet.attribution.get())
  269. if tweet.card.isSome:
  270. renderCard(tweet.card.get(), prefs, path)
  271. if tweet.photos.len > 0:
  272. renderAlbum(tweet)
  273. elif tweet.video.isSome:
  274. renderVideo(tweet.video.get(), prefs, path)
  275. views = tweet.video.get().views
  276. elif tweet.gif.isSome:
  277. renderGif(tweet.gif.get(), prefs)
  278. views = "GIF"
  279. if tweet.poll.isSome:
  280. renderPoll(tweet.poll.get())
  281. if tweet.quote.isSome:
  282. renderQuote(tweet.quote.get(), prefs, path)
  283. if mainTweet:
  284. p(class="tweet-published"): text getTweetTime(tweet)
  285. if tweet.mediaTags.len > 0:
  286. renderMediaTags(tweet.mediaTags)
  287. if not prefs.hideTweetStats:
  288. renderStats(tweet.stats, views)
  289. if showThread:
  290. a(class="show-thread", href=("/i/status/" & $tweet.threadId)):
  291. text "Show this thread"