From 966b3d5d62f923cbb72ddc5e96f19cd905f5c751 Mon Sep 17 00:00:00 2001 From: Zed Date: Tue, 13 Aug 2019 19:44:29 +0200 Subject: [PATCH] Add client preferences --- public/style.css | 44 +++++++++++++++++- src/cache.nim | 8 ++-- src/config.nim | 2 +- src/nitter.nim | 78 +++++++++++++++++++++----------- src/prefs.nim | 95 +++++++++++++++++++++++++++++++++++++++ src/types.nim | 24 +++++----- src/views/general.nim | 10 ++--- src/views/preferences.nim | 69 ++++++++++++++++++++++++++++ src/views/profile.nim | 8 ++-- src/views/status.nim | 14 +++--- src/views/timeline.nim | 16 +++---- src/views/tweet.nim | 43 +++++++++++------- 12 files changed, 329 insertions(+), 82 deletions(-) create mode 100644 src/prefs.nim create mode 100644 src/views/preferences.nim diff --git a/public/style.css b/public/style.css index 85fa05e..9fa8570 100644 --- a/public/style.css +++ b/public/style.css @@ -277,7 +277,7 @@ nav { overflow: hidden; } -video { +video, .video-container img { height: 100%; width: 100%; } @@ -1073,3 +1073,45 @@ video { .poll-info { color: #868687; } + +.preferences-container { + max-width: 600px; + margin: 0 auto; + width: 100%; +} + +.preferences { + background-color: #222222; +} + +fieldset { + margin: .35em 0 .75em; + border: 0; +} + +legend { + width: 100%; + padding: .6em 0 .3em 0; + margin-bottom: .2em; + border: 0; + border-bottom: 1px solid #888888; + font-size: 16px; +} + +.pref-group { + margin: .2em; 0; +} + +.pref-submit:hover { + background-color: #a8a8a8; +} + +.pref-submit { + background-color: #e2e2e2; + color: #000; + border: none; + border-radius: 2px; + padding: 3px 6px; + margin-left: 6px; + margin-top: 4px; +} diff --git a/src/cache.nim b/src/cache.nim index f010973..ebba87a 100644 --- a/src/cache.nim +++ b/src/cache.nim @@ -1,7 +1,7 @@ import asyncdispatch, times import types, api -withDb: +withCustomDb("cache.db", "", "", ""): try: createTables() except DbError: @@ -13,7 +13,7 @@ proc isOutdated*(profile: Profile): bool = getTime() - profile.updated > profileCacheTime proc cache*(profile: var Profile) = - withDb: + withCustomDb("cache.db", "", "", ""): try: let p = Profile.getOne("lower(username) = ?", toLower(profile.username)) profile.id = p.id @@ -23,7 +23,7 @@ proc cache*(profile: var Profile) = profile.insert() proc hasCachedProfile*(username: string): Option[Profile] = - withDb: + withCustomDb("cache.db", "", "", ""): try: let p = Profile.getOne("lower(username) = ?", toLower(username)) doAssert not p.isOutdated @@ -32,7 +32,7 @@ proc hasCachedProfile*(username: string): Option[Profile] = result = none(Profile) proc getCachedProfile*(username, agent: string; force=false): Future[Profile] {.async.} = - withDb: + withCustomDb("cache.db", "", "", ""): try: result.getOne("lower(username) = ?", toLower(username)) doAssert not result.isOutdated diff --git a/src/config.nim b/src/config.nim index 34cd1f3..7c2bbdf 100644 --- a/src/config.nim +++ b/src/config.nim @@ -1,5 +1,5 @@ import parsecfg except Config -import os, net, types, strutils +import net, types, strutils proc get[T](config: parseCfg.Config; s, v: string; default: T): T = let val = config.getSectionValue(s, v) diff --git a/src/nitter.nim b/src/nitter.nim index a2dea9a..a406b49 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -3,13 +3,14 @@ from net import Port import jester, regex -import api, utils, types, cache, formatters, search, config, agents -import views/[general, profile, status] +import api, utils, types, cache, formatters, search, config, prefs, agents +import views/[general, profile, status, preferences] const configPath {.strdefine.} = "./nitter.conf" let cfg = getConfig(configPath) -proc showSingleTimeline(name, after, agent: string; query: Option[Query]): Future[string] {.async.} = +proc showSingleTimeline(name, after, agent: string; query: Option[Query]; + prefs: Prefs): Future[string] {.async.} = let railFut = getPhotoRail(name, agent) var timeline: Timeline @@ -34,33 +35,41 @@ proc showSingleTimeline(name, after, agent: string; query: Option[Query]): Futur if profile.username.len == 0: return "" - let profileHtml = renderProfile(profile, timeline, await railFut) - return renderMain(profileHtml, title=cfg.title, titleText=pageTitle(profile), desc=pageDesc(profile)) + let profileHtml = renderProfile(profile, timeline, await railFut, prefs) + return renderMain(profileHtml, prefs, title=cfg.title, titleText=pageTitle(profile), + desc=pageDesc(profile)) -proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query]): Future[string] {.async.} = +proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query]; + prefs: Prefs): Future[string] {.async.} = var q = query if q.isSome: get(q).fromUser = names else: q = some(Query(kind: multi, fromUser: names, excludes: @["replies"])) - var timeline = renderMulti(await getTimelineSearch(get(q), after, agent), names.join(",")) - return renderMain(timeline, title=cfg.title, titleText="Multi") + var timeline = renderMulti(await getTimelineSearch(get(q), after, agent), + names.join(","), prefs) -proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.async.} = + return renderMain(timeline, prefs, title=cfg.title, titleText="Multi") + +proc showTimeline(name, after: string; query: Option[Query]; + prefs: Prefs): Future[string] {.async.} = let agent = getAgent() let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0) if names.len == 1: - return await showSingleTimeline(names[0], after, agent, query) + return await showSingleTimeline(names[0], after, agent, query, prefs) else: - return await showMultiTimeline(names, after, agent, query) + return await showMultiTimeline(names, after, agent, query, prefs) template respTimeline(timeline: typed) = if timeline.len == 0: - resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title) + resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title, prefs) resp timeline +proc getCookiePrefs(request: Request): Prefs = + getPrefs(request.cookies.getOrDefault("preferences")) + setProfileCacheTime(cfg.profileCacheTime) settings: @@ -70,58 +79,76 @@ settings: routes: get "/": - resp renderMain(renderSearch(), title=cfg.title) + let prefs = getCookiePrefs(request) + resp renderMain(renderSearch(), prefs, title=cfg.title) post "/search": if @"query".len == 0: - resp Http404, showError("Please enter a username.", cfg.title) + resp Http404, showError("Please enter a username.", cfg.title, + getCookiePrefs(request)) redirect("/" & @"query") + post "/saveprefs": + var prefs = getCookiePrefs(request) + genUpdatePrefs() + setCookie("preferences", $prefs.id, daysForward(360)) + redirect("/settings") + + get "/settings": + let prefs = getCookiePrefs(request) + resp renderMain(renderPreferences(prefs), prefs, title=cfg.title, titleText="Preferences") + get "/@name/?": cond '.' notin @"name" - respTimeline(await showTimeline(@"name", @"after", none(Query))) + let prefs = getCookiePrefs(request) + respTimeline(await showTimeline(@"name", @"after", none(Query), prefs)) get "/@name/search": cond '.' notin @"name" + let prefs = getCookiePrefs(request) let query = initQuery(@"filter", @"include", @"not", @"sep", @"name") - respTimeline(await showTimeline(@"name", @"after", some(query))) + respTimeline(await showTimeline(@"name", @"after", some(query), prefs)) get "/@name/replies": cond '.' notin @"name" - respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")))) + let prefs = getCookiePrefs(request) + respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")), prefs)) get "/@name/media": cond '.' notin @"name" - respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")))) + let prefs = getCookiePrefs(request) + respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")), prefs)) get "/@name/status/@id": cond '.' notin @"name" + let prefs = getCookiePrefs(request) let conversation = await getTweet(@"name", @"id", getAgent()) if conversation == nil or conversation.tweet.id.len == 0: - resp Http404, showError("Tweet not found", cfg.title) + resp Http404, showError("Tweet not found", cfg.title, prefs) let title = pageTitle(conversation.tweet.profile) let desc = conversation.tweet.text - let html = renderConversation(conversation) + let html = renderConversation(conversation, prefs) if conversation.tweet.video.isSome(): let thumb = get(conversation.tweet.video).thumb let vidUrl = getVideoEmbed(conversation.tweet.id) - resp renderMain(html, title=cfg.title, titleText=title, desc=desc, + resp renderMain(html, prefs, title=cfg.title, titleText=title, desc=desc, images = @[thumb], `type`="video", video=vidUrl) elif conversation.tweet.gif.isSome(): let thumb = get(conversation.tweet.gif).thumb let vidUrl = getVideoEmbed(conversation.tweet.id) - resp renderMain(html, title=cfg.title, titleText=title, desc=desc, + resp renderMain(html, prefs, title=cfg.title, titleText=title, desc=desc, images = @[thumb], `type`="video", video=vidUrl) else: - resp renderMain(html, title=cfg.title, titleText=title, + resp renderMain(html, prefs, title=cfg.title, titleText=title, desc=desc, images=conversation.tweet.photos) get "/pic/@sig/@url": cond "http" in @"url" cond "twimg" in @"url" + let prefs = getCookiePrefs(request) let uri = parseUri(decodeUrl(@"url")) @@ -129,7 +156,7 @@ routes: filename = cfg.cacheDir / cleanFilename(path & uri.query) if getHmac($uri) != @"sig": - resp showError("Failed to verify signature", cfg.title) + resp showError("Failed to verify signature", cfg.title, prefs) if not existsDir(cfg.cacheDir): createDir(cfg.cacheDir) @@ -151,10 +178,11 @@ routes: get "/video/@sig/@url": cond "http" in @"url" cond "video.twimg" in @"url" + let prefs = getCookiePrefs(request) let url = decodeUrl(@"url") if getHmac(url) != @"sig": - resp showError("Failed to verify signature", cfg.title) + resp showError("Failed to verify signature", cfg.title, prefs) let client = newAsyncHttpClient() diff --git a/src/prefs.nim b/src/prefs.nim new file mode 100644 index 0000000..7e2f04f --- /dev/null +++ b/src/prefs.nim @@ -0,0 +1,95 @@ +import asyncdispatch, times, macros, tables +import types + +withCustomDb("prefs.db", "", "", ""): + try: + createTables() + except DbError: + discard + +type + PrefKind* = enum + checkbox, select, input + + Pref* = object + name*: string + label*: string + case kind*: PrefKind + of checkbox: + defaultState*: bool + of select: + defaultOption*: string + options*: seq[string] + of input: + defaultInput*: string + placeholder*: string + +const prefList*: Table[string, seq[Pref]] = { + "Media": @[ + Pref(kind: checkbox, name: "videoPlayback", + label: "Enable hls.js video playback (requires JavaScript)", + defaultState: false), + + Pref(kind: checkbox, name: "autoplayGifs", label: "Autoplay gifs", + defaultState: true), + ] +}.toTable + +iterator allPrefs(): Pref = + for k, v in prefList: + for pref in v: + yield pref + +macro genDefaultPrefs*(): untyped = + result = nnkObjConstr.newTree(ident("Prefs")) + + for pref in allPrefs(): + result.add nnkExprColonExpr.newTree( + ident(pref.name), + case pref.kind + of checkbox: newLit(pref.defaultState) + of select: newLit(pref.defaultOption) + of input: newLit(pref.defaultInput)) + +proc cache*(prefs: var Prefs) = + withCustomDb("prefs.db", "", "", ""): + try: + doAssert prefs.id != 0 + discard Prefs.getOne("id = ?", prefs.id) + prefs.update() + except AssertionError, KeyError: + prefs.insert() + +proc getPrefs*(id: string): Prefs = + if id.len == 0: return genDefaultPrefs() + + withCustomDb("prefs.db", "", "", ""): + try: + result.getOne("id = ?", id) + except KeyError: + result = genDefaultPrefs() + cache(result) + +macro genUpdatePrefs*(): untyped = + result = nnkStmtList.newTree() + + for pref in allPrefs(): + let ident = ident(pref.name) + let value = nnkPrefix.newTree(ident("@"), newLit(pref.name)) + + case pref.kind + of checkbox: + result.add quote do: + prefs.`ident` = `value` == "on" + of select: + let options = pref.options + let default = pref.defaultOption + result.add quote do: + if `value` in `options`: prefs.`ident` = `value` + else: prefs.`ident` = `default` + of input: + result.add quote do: + prefs.`ident` = `value` + + result.add quote do: + cache(prefs) diff --git a/src/types.nim b/src/types.nim index 40a795c..aa20b59 100644 --- a/src/types.nim +++ b/src/types.nim @@ -23,23 +23,17 @@ db("cache.db", "", "", ""): likes*: string media*: string verified* {. - dbType: "STRING", - parseIt: parseBool(it.s) - formatIt: $it - .}: bool + dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool protected* {. - dbType: "STRING", - parseIt: parseBool(it.s) - formatIt: $it - .}: bool + dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool joinDate* {. - dbType: "INTEGER", - parseIt: it.i.fromUnix(), + dbType: "INTEGER" + parseIt: it.i.fromUnix() formatIt: it.toUnix() .}: Time updated* {. - dbType: "INTEGER", - parseIt: it.i.fromUnix(), + dbType: "INTEGER" + parseIt: it.i.fromUnix() formatIt: getTime().toUnix() .}: Time @@ -61,6 +55,12 @@ db("cache.db", "", "", ""): formatIt: $it .}: bool + Prefs* = object + videoPlayback* {. + dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool + autoplayGifs* {. + dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool + type QueryKind* = enum replies, media, multi, custom = "search" diff --git a/src/views/general.nim b/src/views/general.nim index 305c9d9..d07037b 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -1,6 +1,6 @@ import karax/[karaxdsl, vdom] -import ../utils +import ../utils, ../types const doctype = "\n" @@ -14,9 +14,9 @@ proc renderNavbar*(title: string): VNode = tdiv(class="item right"): a(class="site-about", href="/about"): text "🛈" - a(class="site-settings", href="/settings"): text "⚙" + a(class="site-prefs", href="/settings"): text "⚙" -proc renderMain*(body: VNode; title="Nitter"; titleText=""; desc=""; +proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc=""; `type`="article"; video=""; images: seq[string] = @[]): string = let node = buildHtml(html(lang="en")): head: @@ -60,5 +60,5 @@ proc renderError*(error: string): VNode = tdiv(class="error-panel"): span: text error -proc showError*(error: string; title: string): string = - renderMain(renderError(error), title=title, titleText="Error") +proc showError*(error: string; title: string; prefs: Prefs): string = + renderMain(renderError(error), prefs, title=title, titleText="Error") diff --git a/src/views/preferences.nim b/src/views/preferences.nim new file mode 100644 index 0000000..d71916f --- /dev/null +++ b/src/views/preferences.nim @@ -0,0 +1,69 @@ +import tables, macros +import karax/[karaxdsl, vdom, vstyles] + +import ../types, ../prefs + +proc genCheckbox(pref: string; label: string; state: bool): VNode = + buildHtml(tdiv(class="pref-group")): + if state: + input(name=pref, `type`="checkbox", checked="") + else: + input(name=pref, `type`="checkbox") + label(`for`=pref): text label + +proc genSelect(pref: string; label: string; options: seq[string]; state: string): VNode = + buildHtml(tdiv(class="pref-group")): + select(name=pref): + for opt in options: + if opt == state: + option(value=opt, selected=""): text opt + else: + option(value=opt): text opt + label(`for`=pref): text label + +proc genInput(pref: string; label: string; placeholder, state: string): VNode = + buildHtml(tdiv(class="pref-group")): + input(name=pref, `type`="text", placeholder=placeholder, value=state) + label(`for`=pref): text label + +macro renderPrefs*(): untyped = + result = nnkCall.newTree( + ident("buildHtml"), ident("tdiv"), nnkStmtList.newTree()) + + for header, options in prefList: + result[2].add nnkCall.newTree( + ident("legend"), + nnkStmtList.newTree( + nnkCommand.newTree(ident("text"), newLit(header)))) + + for pref in options: + let name = newLit(pref.name) + let label = newLit(pref.label) + let field = ident(pref.name) + case pref.kind + of checkbox: + result[2].add nnkStmtList.newTree( + nnkCall.newTree( + ident("genCheckbox"), name, label, + nnkDotExpr.newTree(ident("prefs"), field))) + of select: + let options = newLit(pref.options) + result[2].add nnkStmtList.newTree( + nnkCall.newTree( + ident("genSelect"), name, label, options, + nnkDotExpr.newTree(ident("prefs"), field))) + of input: + let placeholder = newLit(pref.placeholder) + result[2].add nnkStmtList.newTree( + nnkCall.newTree( + ident("genInput"), name, label, placeholder, + nnkDotExpr.newTree(ident("prefs"), field))) + +proc renderPreferences*(prefs: Prefs): VNode = + buildHtml(tdiv(class="preferences-container")): + form(class="preferences", `method`="post", action="saveprefs"): + fieldset: + renderPrefs() + + button(`type`="submit", class="pref-submit"): + text "Save preferences" diff --git a/src/views/profile.nim b/src/views/profile.nim index 1f55f79..45f700a 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -68,7 +68,7 @@ proc renderBanner(profile: Profile): VNode = genImg(profile.banner) proc renderProfile*(profile: Profile; timeline: Timeline; - photoRail: seq[GalleryPhoto]): VNode = + photoRail: seq[GalleryPhoto]; prefs: Prefs): VNode = buildHtml(tdiv(class="profile-tabs")): tdiv(class="profile-banner"): renderBanner(profile) @@ -79,9 +79,9 @@ proc renderProfile*(profile: Profile; timeline: Timeline; renderPhotoRail(profile, photoRail) tdiv(class="timeline-tab"): - renderTimeline(timeline, profile.username, profile.protected) + renderTimeline(timeline, profile.username, profile.protected, prefs) -proc renderMulti*(timeline: Timeline; usernames: string): VNode = +proc renderMulti*(timeline: Timeline; usernames: string; prefs: Prefs): VNode = buildHtml(tdiv(class="multi-timeline")): tdiv(class="timeline-tab"): - renderTimeline(timeline, usernames, false, multi=true) + renderTimeline(timeline, usernames, false, prefs, multi=true) diff --git a/src/views/status.nim b/src/views/status.nim index 06ab7a2..8b0b838 100644 --- a/src/views/status.nim +++ b/src/views/status.nim @@ -4,11 +4,11 @@ import karax/[karaxdsl, vdom] import ../types import tweet, renderutils -proc renderReplyThread(thread: Thread): VNode = +proc renderReplyThread(thread: Thread; prefs: Prefs): VNode = buildHtml(tdiv(class="reply thread thread-line")): for i, tweet in thread.tweets: let last = (i == thread.tweets.high and thread.more == 0) - renderTweet(tweet, index=i, last=last) + renderTweet(tweet, prefs, index=i, last=last) if thread.more != 0: let num = if thread.more != -1: $thread.more & " " else: "" @@ -17,26 +17,26 @@ proc renderReplyThread(thread: Thread): VNode = a(class="more-replies-text", title="Not implemented yet"): text $num & "more " & reply -proc renderConversation*(conversation: Conversation): VNode = +proc renderConversation*(conversation: Conversation; prefs: Prefs): VNode = let hasAfter = conversation.after != nil buildHtml(tdiv(class="conversation", id="posts")): tdiv(class="main-thread"): if conversation.before != nil: tdiv(class="before-tweet thread-line"): for i, tweet in conversation.before.tweets: - renderTweet(tweet, index=i) + renderTweet(tweet, prefs, index=i) tdiv(class="main-tweet"): let afterClass = if hasAfter: "thread thread-line" else: "" - renderTweet(conversation.tweet, class=afterClass) + renderTweet(conversation.tweet, prefs, class=afterClass) if hasAfter: tdiv(class="after-tweet thread-line"): let total = conversation.after.tweets.high for i, tweet in conversation.after.tweets: - renderTweet(tweet, index=i, total=total) + renderTweet(tweet, prefs, index=i, total=total) if conversation.replies.len > 0: tdiv(class="replies"): for thread in conversation.replies: - renderReplyThread(thread) + renderReplyThread(thread, prefs) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 726ee63..f9d0744 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -54,28 +54,28 @@ proc renderProtected(username: string): VNode = h2: text "This account's tweets are protected." p: text &"Only confirmed followers have access to @{username}'s tweets." -proc renderThread(thread: seq[Tweet]): VNode = +proc renderThread(thread: seq[Tweet]; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline-tweet thread-line")): for i, threadTweet in thread.sortedByIt(it.time): - renderTweet(threadTweet, "thread", index=i, total=thread.high) + renderTweet(threadTweet, prefs, class="thread", index=i, total=thread.high) proc threadFilter(it: Tweet; tweetThread: string): bool = it.retweet.isNone and it.reply.len == 0 and it.threadId == tweetThread -proc renderTweets(timeline: Timeline): VNode = +proc renderTweets(timeline: Timeline; prefs: Prefs): VNode = buildHtml(tdiv(id="posts")): var threads: seq[string] for tweet in timeline.tweets: if tweet.threadId in threads: continue let thread = timeline.tweets.filterIt(threadFilter(it, tweet.threadId)) if thread.len < 2: - renderTweet(tweet, "timeline-tweet") + renderTweet(tweet, prefs, class="timeline-tweet") else: - renderThread(thread) + renderThread(thread, prefs) threads &= tweet.threadId -proc renderTimeline*(timeline: Timeline; username: string; - protected: bool; multi=false): VNode = +proc renderTimeline*(timeline: Timeline; username: string; protected: bool; + prefs: Prefs; multi=false): VNode = buildHtml(tdiv): if multi: tdiv(class="multi-header"): @@ -91,7 +91,7 @@ proc renderTimeline*(timeline: Timeline; username: string; elif timeline.tweets.len == 0: renderNoneFound() else: - renderTweets(timeline) + renderTweets(timeline, prefs) if timeline.hasMore or timeline.query.isSome: renderOlder(timeline, username) else: diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 0fd5d9d..d4ed0ea 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -44,26 +44,38 @@ proc renderAlbum(tweet: Tweet): VNode = target="_blank", style={display: flex}): genImg(photo) -proc renderVideo(video: Video): VNode = +proc renderVideo(video: Video; prefs: Prefs): VNode = buildHtml(tdiv(class="attachments")): tdiv(class="gallery-video"): tdiv(class="attachment video-container"): + let thumb = video.thumb.getSigUrl("pic") case video.playbackType of mp4: - video(poster=video.thumb.getSigUrl("pic"), controls=""): + video(poster=thumb, controls=""): source(src=video.url.getSigUrl("video"), `type`="video/mp4") of m3u8, vmap: - video(poster=video.thumb.getSigUrl("pic")) - tdiv(class="video-overlay"): - p: text "Video playback not supported" + if prefs.videoPlayback: + video(poster=thumb) + tdiv(class="video-overlay"): + p: text "Video playback not supported yet" + else: + img(src=thumb) + tdiv(class="video-overlay"): + p: text "Video playback disabled" -proc renderGif(gif: Gif): VNode = +proc renderGif(gif: Gif; prefs: Prefs): VNode = buildHtml(tdiv(class="attachments media-gif")): tdiv(class="gallery-gif", style=style(maxHeight, "unset")): tdiv(class="attachment"): - video(class="gif", poster=gif.thumb.getSigUrl("pic"), - autoplay="", muted="", loop=""): - source(src=gif.url.getSigUrl("video"), `type`="video/mp4") + let thumb = gif.thumb.getSigUrl("pic") + let url = gif.url.getSigUrl("video") + if prefs.autoplayGifs: + + video(class="gif", poster=thumb, autoplay="", muted="", loop=""): + source(src=url, `type`="video/mp4") + else: + video(class="gif", poster=thumb, controls="", muted="", loop=""): + source(src=url, `type`="video/mp4") proc renderPoll(poll: Poll): VNode = buildHtml(tdiv(class="poll")): @@ -86,7 +98,7 @@ proc renderCardImage(card: Card): VNode = tdiv(class="card-overlay-circle"): span(class="card-overlay-triangle") -proc renderCard(card: Card): VNode = +proc renderCard(card: Card; prefs: Prefs): VNode = const largeCards = {summaryLarge, liveEvent, promoWebsite, promoVideo} let large = if card.kind in largeCards: " large" else: "" @@ -95,7 +107,7 @@ proc renderCard(card: Card): VNode = if card.image.isSome: renderCardImage(card) elif card.video.isSome: - renderVideo(get(card.video)) + renderVideo(get(card.video), prefs) tdiv(class="card-content-container"): tdiv(class="card-content"): @@ -161,7 +173,8 @@ proc renderQuote(quote: Quote): VNode = a(href=getLink(quote)): text "Show this thread" -proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNode = +proc renderTweet*(tweet: Tweet; prefs: Prefs; class=""; + index=0; total=(-1); last=false): VNode = var divClass = class if index == total or last: divClass = "thread-last " & class @@ -187,13 +200,13 @@ proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNod renderQuote(tweet.quote.get()) if tweet.card.isSome: - renderCard(tweet.card.get()) + renderCard(tweet.card.get(), prefs) elif tweet.photos.len > 0: renderAlbum(tweet) elif tweet.video.isSome: - renderVideo(tweet.video.get()) + renderVideo(tweet.video.get(), prefs) elif tweet.gif.isSome: - renderGif(tweet.gif.get()) + renderGif(tweet.gif.get(), prefs) elif tweet.poll.isSome: renderPoll(tweet.poll.get())