Add client preferences

This commit is contained in:
Zed 2019-08-13 19:44:29 +02:00
parent c42b2893ff
commit 966b3d5d62
12 changed files with 329 additions and 82 deletions

View file

@ -277,7 +277,7 @@ nav {
overflow: hidden; overflow: hidden;
} }
video { video, .video-container img {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
@ -1073,3 +1073,45 @@ video {
.poll-info { .poll-info {
color: #868687; 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;
}

View file

@ -1,7 +1,7 @@
import asyncdispatch, times import asyncdispatch, times
import types, api import types, api
withDb: withCustomDb("cache.db", "", "", ""):
try: try:
createTables() createTables()
except DbError: except DbError:
@ -13,7 +13,7 @@ proc isOutdated*(profile: Profile): bool =
getTime() - profile.updated > profileCacheTime getTime() - profile.updated > profileCacheTime
proc cache*(profile: var Profile) = proc cache*(profile: var Profile) =
withDb: withCustomDb("cache.db", "", "", ""):
try: try:
let p = Profile.getOne("lower(username) = ?", toLower(profile.username)) let p = Profile.getOne("lower(username) = ?", toLower(profile.username))
profile.id = p.id profile.id = p.id
@ -23,7 +23,7 @@ proc cache*(profile: var Profile) =
profile.insert() profile.insert()
proc hasCachedProfile*(username: string): Option[Profile] = proc hasCachedProfile*(username: string): Option[Profile] =
withDb: withCustomDb("cache.db", "", "", ""):
try: try:
let p = Profile.getOne("lower(username) = ?", toLower(username)) let p = Profile.getOne("lower(username) = ?", toLower(username))
doAssert not p.isOutdated doAssert not p.isOutdated
@ -32,7 +32,7 @@ proc hasCachedProfile*(username: string): Option[Profile] =
result = none(Profile) result = none(Profile)
proc getCachedProfile*(username, agent: string; force=false): Future[Profile] {.async.} = proc getCachedProfile*(username, agent: string; force=false): Future[Profile] {.async.} =
withDb: withCustomDb("cache.db", "", "", ""):
try: try:
result.getOne("lower(username) = ?", toLower(username)) result.getOne("lower(username) = ?", toLower(username))
doAssert not result.isOutdated doAssert not result.isOutdated

View file

@ -1,5 +1,5 @@
import parsecfg except Config 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 = proc get[T](config: parseCfg.Config; s, v: string; default: T): T =
let val = config.getSectionValue(s, v) let val = config.getSectionValue(s, v)

View file

@ -3,13 +3,14 @@ from net import Port
import jester, regex import jester, regex
import api, utils, types, cache, formatters, search, config, agents import api, utils, types, cache, formatters, search, config, prefs, agents
import views/[general, profile, status] import views/[general, profile, status, preferences]
const configPath {.strdefine.} = "./nitter.conf" const configPath {.strdefine.} = "./nitter.conf"
let cfg = getConfig(configPath) 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) let railFut = getPhotoRail(name, agent)
var timeline: Timeline var timeline: Timeline
@ -34,33 +35,41 @@ proc showSingleTimeline(name, after, agent: string; query: Option[Query]): Futur
if profile.username.len == 0: if profile.username.len == 0:
return "" return ""
let profileHtml = renderProfile(profile, timeline, await railFut) let profileHtml = renderProfile(profile, timeline, await railFut, prefs)
return renderMain(profileHtml, title=cfg.title, titleText=pageTitle(profile), desc=pageDesc(profile)) 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 var q = query
if q.isSome: if q.isSome:
get(q).fromUser = names get(q).fromUser = names
else: else:
q = some(Query(kind: multi, fromUser: names, excludes: @["replies"])) q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
var timeline = renderMulti(await getTimelineSearch(get(q), after, agent), names.join(",")) var timeline = renderMulti(await getTimelineSearch(get(q), after, agent),
return renderMain(timeline, title=cfg.title, titleText="Multi") 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 agent = getAgent()
let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0) let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0)
if names.len == 1: if names.len == 1:
return await showSingleTimeline(names[0], after, agent, query) return await showSingleTimeline(names[0], after, agent, query, prefs)
else: else:
return await showMultiTimeline(names, after, agent, query) return await showMultiTimeline(names, after, agent, query, prefs)
template respTimeline(timeline: typed) = template respTimeline(timeline: typed) =
if timeline.len == 0: 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 resp timeline
proc getCookiePrefs(request: Request): Prefs =
getPrefs(request.cookies.getOrDefault("preferences"))
setProfileCacheTime(cfg.profileCacheTime) setProfileCacheTime(cfg.profileCacheTime)
settings: settings:
@ -70,58 +79,76 @@ settings:
routes: routes:
get "/": get "/":
resp renderMain(renderSearch(), title=cfg.title) let prefs = getCookiePrefs(request)
resp renderMain(renderSearch(), prefs, title=cfg.title)
post "/search": post "/search":
if @"query".len == 0: 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") 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/?": get "/@name/?":
cond '.' notin @"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": get "/@name/search":
cond '.' notin @"name" cond '.' notin @"name"
let prefs = getCookiePrefs(request)
let query = initQuery(@"filter", @"include", @"not", @"sep", @"name") 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": get "/@name/replies":
cond '.' notin @"name" 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": get "/@name/media":
cond '.' notin @"name" 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": get "/@name/status/@id":
cond '.' notin @"name" cond '.' notin @"name"
let prefs = getCookiePrefs(request)
let conversation = await getTweet(@"name", @"id", getAgent()) let conversation = await getTweet(@"name", @"id", getAgent())
if conversation == nil or conversation.tweet.id.len == 0: 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 title = pageTitle(conversation.tweet.profile)
let desc = conversation.tweet.text let desc = conversation.tweet.text
let html = renderConversation(conversation) let html = renderConversation(conversation, prefs)
if conversation.tweet.video.isSome(): if conversation.tweet.video.isSome():
let thumb = get(conversation.tweet.video).thumb let thumb = get(conversation.tweet.video).thumb
let vidUrl = getVideoEmbed(conversation.tweet.id) 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) images = @[thumb], `type`="video", video=vidUrl)
elif conversation.tweet.gif.isSome(): elif conversation.tweet.gif.isSome():
let thumb = get(conversation.tweet.gif).thumb let thumb = get(conversation.tweet.gif).thumb
let vidUrl = getVideoEmbed(conversation.tweet.id) 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) images = @[thumb], `type`="video", video=vidUrl)
else: else:
resp renderMain(html, title=cfg.title, titleText=title, resp renderMain(html, prefs, title=cfg.title, titleText=title,
desc=desc, images=conversation.tweet.photos) desc=desc, images=conversation.tweet.photos)
get "/pic/@sig/@url": get "/pic/@sig/@url":
cond "http" in @"url" cond "http" in @"url"
cond "twimg" in @"url" cond "twimg" in @"url"
let prefs = getCookiePrefs(request)
let let
uri = parseUri(decodeUrl(@"url")) uri = parseUri(decodeUrl(@"url"))
@ -129,7 +156,7 @@ routes:
filename = cfg.cacheDir / cleanFilename(path & uri.query) filename = cfg.cacheDir / cleanFilename(path & uri.query)
if getHmac($uri) != @"sig": 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): if not existsDir(cfg.cacheDir):
createDir(cfg.cacheDir) createDir(cfg.cacheDir)
@ -151,10 +178,11 @@ routes:
get "/video/@sig/@url": get "/video/@sig/@url":
cond "http" in @"url" cond "http" in @"url"
cond "video.twimg" in @"url" cond "video.twimg" in @"url"
let prefs = getCookiePrefs(request)
let url = decodeUrl(@"url") let url = decodeUrl(@"url")
if getHmac(url) != @"sig": if getHmac(url) != @"sig":
resp showError("Failed to verify signature", cfg.title) resp showError("Failed to verify signature", cfg.title, prefs)
let let
client = newAsyncHttpClient() client = newAsyncHttpClient()

95
src/prefs.nim Normal file
View file

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

View file

@ -23,23 +23,17 @@ db("cache.db", "", "", ""):
likes*: string likes*: string
media*: string media*: string
verified* {. verified* {.
dbType: "STRING", dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool
parseIt: parseBool(it.s)
formatIt: $it
.}: bool
protected* {. protected* {.
dbType: "STRING", dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool
parseIt: parseBool(it.s)
formatIt: $it
.}: bool
joinDate* {. joinDate* {.
dbType: "INTEGER", dbType: "INTEGER"
parseIt: it.i.fromUnix(), parseIt: it.i.fromUnix()
formatIt: it.toUnix() formatIt: it.toUnix()
.}: Time .}: Time
updated* {. updated* {.
dbType: "INTEGER", dbType: "INTEGER"
parseIt: it.i.fromUnix(), parseIt: it.i.fromUnix()
formatIt: getTime().toUnix() formatIt: getTime().toUnix()
.}: Time .}: Time
@ -61,6 +55,12 @@ db("cache.db", "", "", ""):
formatIt: $it formatIt: $it
.}: bool .}: bool
Prefs* = object
videoPlayback* {.
dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool
autoplayGifs* {.
dbType: "STRING", parseIt: parseBool(it.s), formatIt: $it.}: bool
type type
QueryKind* = enum QueryKind* = enum
replies, media, multi, custom = "search" replies, media, multi, custom = "search"

View file

@ -1,6 +1,6 @@
import karax/[karaxdsl, vdom] import karax/[karaxdsl, vdom]
import ../utils import ../utils, ../types
const doctype = "<!DOCTYPE html>\n" const doctype = "<!DOCTYPE html>\n"
@ -14,9 +14,9 @@ proc renderNavbar*(title: string): VNode =
tdiv(class="item right"): tdiv(class="item right"):
a(class="site-about", href="/about"): text "🛈" 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 = `type`="article"; video=""; images: seq[string] = @[]): string =
let node = buildHtml(html(lang="en")): let node = buildHtml(html(lang="en")):
head: head:
@ -60,5 +60,5 @@ proc renderError*(error: string): VNode =
tdiv(class="error-panel"): tdiv(class="error-panel"):
span: text error span: text error
proc showError*(error: string; title: string): string = proc showError*(error: string; title: string; prefs: Prefs): string =
renderMain(renderError(error), title=title, titleText="Error") renderMain(renderError(error), prefs, title=title, titleText="Error")

69
src/views/preferences.nim Normal file
View file

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

View file

@ -68,7 +68,7 @@ proc renderBanner(profile: Profile): VNode =
genImg(profile.banner) genImg(profile.banner)
proc renderProfile*(profile: Profile; timeline: Timeline; proc renderProfile*(profile: Profile; timeline: Timeline;
photoRail: seq[GalleryPhoto]): VNode = photoRail: seq[GalleryPhoto]; prefs: Prefs): VNode =
buildHtml(tdiv(class="profile-tabs")): buildHtml(tdiv(class="profile-tabs")):
tdiv(class="profile-banner"): tdiv(class="profile-banner"):
renderBanner(profile) renderBanner(profile)
@ -79,9 +79,9 @@ proc renderProfile*(profile: Profile; timeline: Timeline;
renderPhotoRail(profile, photoRail) renderPhotoRail(profile, photoRail)
tdiv(class="timeline-tab"): 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")): buildHtml(tdiv(class="multi-timeline")):
tdiv(class="timeline-tab"): tdiv(class="timeline-tab"):
renderTimeline(timeline, usernames, false, multi=true) renderTimeline(timeline, usernames, false, prefs, multi=true)

View file

@ -4,11 +4,11 @@ import karax/[karaxdsl, vdom]
import ../types import ../types
import tweet, renderutils import tweet, renderutils
proc renderReplyThread(thread: Thread): VNode = proc renderReplyThread(thread: Thread; prefs: Prefs): VNode =
buildHtml(tdiv(class="reply thread thread-line")): buildHtml(tdiv(class="reply thread thread-line")):
for i, tweet in thread.tweets: for i, tweet in thread.tweets:
let last = (i == thread.tweets.high and thread.more == 0) 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: if thread.more != 0:
let num = if thread.more != -1: $thread.more & " " else: "" 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"): a(class="more-replies-text", title="Not implemented yet"):
text $num & "more " & reply text $num & "more " & reply
proc renderConversation*(conversation: Conversation): VNode = proc renderConversation*(conversation: Conversation; prefs: Prefs): VNode =
let hasAfter = conversation.after != nil let hasAfter = conversation.after != nil
buildHtml(tdiv(class="conversation", id="posts")): buildHtml(tdiv(class="conversation", id="posts")):
tdiv(class="main-thread"): tdiv(class="main-thread"):
if conversation.before != nil: if conversation.before != nil:
tdiv(class="before-tweet thread-line"): tdiv(class="before-tweet thread-line"):
for i, tweet in conversation.before.tweets: for i, tweet in conversation.before.tweets:
renderTweet(tweet, index=i) renderTweet(tweet, prefs, index=i)
tdiv(class="main-tweet"): tdiv(class="main-tweet"):
let afterClass = if hasAfter: "thread thread-line" else: "" let afterClass = if hasAfter: "thread thread-line" else: ""
renderTweet(conversation.tweet, class=afterClass) renderTweet(conversation.tweet, prefs, class=afterClass)
if hasAfter: if hasAfter:
tdiv(class="after-tweet thread-line"): tdiv(class="after-tweet thread-line"):
let total = conversation.after.tweets.high let total = conversation.after.tweets.high
for i, tweet in conversation.after.tweets: 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: if conversation.replies.len > 0:
tdiv(class="replies"): tdiv(class="replies"):
for thread in conversation.replies: for thread in conversation.replies:
renderReplyThread(thread) renderReplyThread(thread, prefs)

View file

@ -54,28 +54,28 @@ proc renderProtected(username: string): VNode =
h2: text "This account's tweets are protected." h2: text "This account's tweets are protected."
p: text &"Only confirmed followers have access to @{username}'s tweets." 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")): buildHtml(tdiv(class="timeline-tweet thread-line")):
for i, threadTweet in thread.sortedByIt(it.time): 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 = proc threadFilter(it: Tweet; tweetThread: string): bool =
it.retweet.isNone and it.reply.len == 0 and it.threadId == tweetThread 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")): buildHtml(tdiv(id="posts")):
var threads: seq[string] var threads: seq[string]
for tweet in timeline.tweets: for tweet in timeline.tweets:
if tweet.threadId in threads: continue if tweet.threadId in threads: continue
let thread = timeline.tweets.filterIt(threadFilter(it, tweet.threadId)) let thread = timeline.tweets.filterIt(threadFilter(it, tweet.threadId))
if thread.len < 2: if thread.len < 2:
renderTweet(tweet, "timeline-tweet") renderTweet(tweet, prefs, class="timeline-tweet")
else: else:
renderThread(thread) renderThread(thread, prefs)
threads &= tweet.threadId threads &= tweet.threadId
proc renderTimeline*(timeline: Timeline; username: string; proc renderTimeline*(timeline: Timeline; username: string; protected: bool;
protected: bool; multi=false): VNode = prefs: Prefs; multi=false): VNode =
buildHtml(tdiv): buildHtml(tdiv):
if multi: if multi:
tdiv(class="multi-header"): tdiv(class="multi-header"):
@ -91,7 +91,7 @@ proc renderTimeline*(timeline: Timeline; username: string;
elif timeline.tweets.len == 0: elif timeline.tweets.len == 0:
renderNoneFound() renderNoneFound()
else: else:
renderTweets(timeline) renderTweets(timeline, prefs)
if timeline.hasMore or timeline.query.isSome: if timeline.hasMore or timeline.query.isSome:
renderOlder(timeline, username) renderOlder(timeline, username)
else: else:

View file

@ -44,26 +44,38 @@ proc renderAlbum(tweet: Tweet): VNode =
target="_blank", style={display: flex}): target="_blank", style={display: flex}):
genImg(photo) genImg(photo)
proc renderVideo(video: Video): VNode = proc renderVideo(video: Video; prefs: Prefs): VNode =
buildHtml(tdiv(class="attachments")): buildHtml(tdiv(class="attachments")):
tdiv(class="gallery-video"): tdiv(class="gallery-video"):
tdiv(class="attachment video-container"): tdiv(class="attachment video-container"):
let thumb = video.thumb.getSigUrl("pic")
case video.playbackType case video.playbackType
of mp4: of mp4:
video(poster=video.thumb.getSigUrl("pic"), controls=""): video(poster=thumb, controls=""):
source(src=video.url.getSigUrl("video"), `type`="video/mp4") source(src=video.url.getSigUrl("video"), `type`="video/mp4")
of m3u8, vmap: of m3u8, vmap:
video(poster=video.thumb.getSigUrl("pic")) if prefs.videoPlayback:
tdiv(class="video-overlay"): video(poster=thumb)
p: text "Video playback not supported" 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")): buildHtml(tdiv(class="attachments media-gif")):
tdiv(class="gallery-gif", style=style(maxHeight, "unset")): tdiv(class="gallery-gif", style=style(maxHeight, "unset")):
tdiv(class="attachment"): tdiv(class="attachment"):
video(class="gif", poster=gif.thumb.getSigUrl("pic"), let thumb = gif.thumb.getSigUrl("pic")
autoplay="", muted="", loop=""): let url = gif.url.getSigUrl("video")
source(src=gif.url.getSigUrl("video"), `type`="video/mp4") 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 = proc renderPoll(poll: Poll): VNode =
buildHtml(tdiv(class="poll")): buildHtml(tdiv(class="poll")):
@ -86,7 +98,7 @@ proc renderCardImage(card: Card): VNode =
tdiv(class="card-overlay-circle"): tdiv(class="card-overlay-circle"):
span(class="card-overlay-triangle") span(class="card-overlay-triangle")
proc renderCard(card: Card): VNode = proc renderCard(card: Card; prefs: Prefs): VNode =
const largeCards = {summaryLarge, liveEvent, promoWebsite, promoVideo} const largeCards = {summaryLarge, liveEvent, promoWebsite, promoVideo}
let large = if card.kind in largeCards: " large" else: "" let large = if card.kind in largeCards: " large" else: ""
@ -95,7 +107,7 @@ proc renderCard(card: Card): VNode =
if card.image.isSome: if card.image.isSome:
renderCardImage(card) renderCardImage(card)
elif card.video.isSome: elif card.video.isSome:
renderVideo(get(card.video)) renderVideo(get(card.video), prefs)
tdiv(class="card-content-container"): tdiv(class="card-content-container"):
tdiv(class="card-content"): tdiv(class="card-content"):
@ -161,7 +173,8 @@ proc renderQuote(quote: Quote): VNode =
a(href=getLink(quote)): a(href=getLink(quote)):
text "Show this thread" 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 var divClass = class
if index == total or last: if index == total or last:
divClass = "thread-last " & class divClass = "thread-last " & class
@ -187,13 +200,13 @@ proc renderTweet*(tweet: Tweet; class=""; index=0; total=(-1); last=false): VNod
renderQuote(tweet.quote.get()) renderQuote(tweet.quote.get())
if tweet.card.isSome: if tweet.card.isSome:
renderCard(tweet.card.get()) renderCard(tweet.card.get(), prefs)
elif tweet.photos.len > 0: elif tweet.photos.len > 0:
renderAlbum(tweet) renderAlbum(tweet)
elif tweet.video.isSome: elif tweet.video.isSome:
renderVideo(tweet.video.get()) renderVideo(tweet.video.get(), prefs)
elif tweet.gif.isSome: elif tweet.gif.isSome:
renderGif(tweet.gif.get()) renderGif(tweet.gif.get(), prefs)
elif tweet.poll.isSome: elif tweet.poll.isSome:
renderPoll(tweet.poll.get()) renderPoll(tweet.poll.get())