コミットを比較

...

21 コミット

作成者 SHA1 メッセージ 日付
テクニカル諏訪子 9a1c51a131 マージ 2022-01-03 11:34:20 +09:00
Zed bc352cdb65 Simplify video rendering 2022-01-03 03:27:29 +01:00
Zed 47ed1a3ae8 Fix video placeholder thumbnail not showing 2022-01-03 02:55:25 +01:00
Zed bb981df657 Improve and simplify preferences page behavior 2022-01-03 02:40:28 +01:00
Zed ab51ff06c8
Merge pull request #499 from LainLayer/fix_snake_case
replaced newsletter_publication with newsletterPublication
2022-01-02 13:56:06 +01:00
Mitarashi b14fb0162f replaced newsletter_publication with newsletterPublication 2022-01-02 14:48:52 +02:00
Zed 47f47bb907 Remove .* from .dockerignore, fixes about page 2022-01-02 12:12:48 +01:00
Zed 74c4377198 More cleanup 2022-01-02 11:21:03 +01:00
Zed a9034928eb Fix video duration parser 2022-01-02 10:58:02 +01:00
Zed 9dd7419ecf Cleanup 2022-01-02 07:02:02 +01:00
Zed 9d117aa15b Link unixfox's ARM64 Docker image in readme 2021-12-31 20:34:42 +01:00
Zed 19a89b79f5 Remove RSS title truncation temporarily 2021-12-31 13:30:19 +01:00
Zed 1ce6ff2b2f Improve search and list error messages 2021-12-30 23:48:57 +01:00
Zed b8a3ffb0c4 Add description and verified to video cards 2021-12-30 23:24:53 +01:00
Zed aed31b2269 Add slug-based list RSS endpoint for compatibility 2021-12-30 20:55:10 +01:00
Zed 5501752fdb Merge branch 'rework-list' 2021-12-30 20:51:24 +01:00
Zed 47ec6ff3d2
Merge pull request #494 from jackyzy823/fix-datetime-fromflatty
fix datetime fromFlatty
2021-12-30 20:48:40 +01:00
jackyzy823 a25bd0855b fix datetime fromFlatty 2021-12-30 13:30:12 +01:00
jackyzy823 ef7ad67674 fix userid in list 2021-12-30 08:36:43 +01:00
jackyzy823 db090faf36 use separator in rss key for redis cache 2021-12-30 08:36:43 +01:00
jackyzy823 35bb5f9132 Rework list api 2021-12-30 08:36:43 +01:00
23個のファイルの変更149行の追加125行の削除

ファイルの表示

@ -1,4 +1,3 @@
.*
*.png
*.md
LICENSE

ファイルの表示

@ -107,6 +107,8 @@ performance reasons.
### Docker
#### NOTE: For ARM64/ARM support, please use [unixfox's image](https://quay.io/repository/unixfox/nitter?tab=tags), more info [here](https://github.com/zedeus/nitter/issues/399#issuecomment-997263495)
To run Nitter with Docker, you'll need to install and run Redis separately
before you can run the container. See below for how to also run Redis using
Docker.

ファイルの表示

@ -31,7 +31,7 @@ proc windows(): string =
trident = ["", "; Trident/5.0", "; Trident/6.0", "; Trident/7.0"]
"Windows " & sample(nt) & sample(enc) & sample(arch) & sample(trident)
let macs = toSeq(6..15).mapIt($it) & @["14_4", "10_1", "9_3"]
const macs = toSeq(6..15).mapIt($it) & @["14_4", "10_1", "9_3"]
proc mac(): string =
"Macintosh; Intel Mac OS X 10_" & sample(macs) & sample(enc)

ファイルの表示

@ -9,13 +9,13 @@ proc getGraphProfile*(username: string): Future[Profile] {.async.} =
js = await fetch(graphUser ? {"variables": $variables})
result = parseGraphProfile(js, username)
proc getGraphList*(name, list: string): Future[List] {.async.} =
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
let
variables = %*{"screenName": name, "listSlug": list, "withHighlightedLabel": false}
js = await fetch(graphList ? {"variables": $variables})
result = parseGraphList(js)
proc getGraphListById*(id: string): Future[List] {.async.} =
proc getGraphList*(id: string): Future[List] {.async.} =
let
variables = %*{"listId": id, "withHighlightedLabel": false}
js = await fetch(graphListId ? {"variables": $variables})

ファイルの表示

@ -5,7 +5,7 @@ import types, tokens, consts, parserutils, http_pool
const rl = "x-rate-limit-"
var pool {.threadvar.}: HttpPool
var pool: HttpPool
proc genParams*(pars: openarray[(string, string)] = @[]; cursor="";
count="20"; ext=true): seq[(string, string)] =

ファイルの表示

@ -5,8 +5,8 @@ type
HttpPool* = ref object
conns*: seq[AsyncHttpClient]
var maxConns {.threadvar.}: int
var proxy {.threadvar.}: Proxy
var maxConns: int
var proxy: Proxy
proc setMaxHttpConns*(n: int) =
maxConns = n

ファイルの表示

@ -73,9 +73,9 @@ proc parseGraphList*(js: JsonNode): List =
result = List(
id: list{"id_str"}.getStr,
name: list{"name"}.getStr.replace(' ', '-'),
name: list{"name"}.getStr,
username: list{"user", "legacy", "screen_name"}.getStr,
userId: list{"user", "legacy", "id_str"}.getStr,
userId: list{"user", "rest_id"}.getStr,
description: list{"description"}.getStr,
members: list{"member_count"}.getInt,
banner: list{"custom_banner_media", "media_info", "url"}.getImageStr
@ -128,13 +128,16 @@ proc parseVideo(js: JsonNode): Video =
views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr,
available: js{"ext_media_availability", "status"}.getStr == "available",
title: js{"ext_alt_text"}.getStr,
durationMs: js{"duration_millis"}.getInt
durationMs: js{"video_info", "duration_millis"}.getInt
# playbackType: mp4
)
with title, js{"additional_media_info", "title"}:
result.title = title.getStr
with description, js{"additional_media_info", "description"}:
result.description = description.getStr
for v in js{"video_info", "variants"}:
result.variants.add VideoVariant(
videoType: parseEnum[VideoType](v{"content_type"}.getStr("summary")),

ファイルの表示

@ -118,7 +118,7 @@ proc getBanner*(js: JsonNode): string =
if color.len > 0:
return '#' & color
# use primary color from profile picture color histrogram
# use primary color from profile picture color histogram
with p, js{"profile_image_extensions", "mediaColor", "r", "ok", "palette"}:
if p.len > 0:
let pal = p[0]{"rgb"}

ファイルの表示

@ -6,7 +6,7 @@ from parsecfg import nil
export genUpdatePrefs, genResetPrefs
var defaultPrefs* {.threadvar.}: Prefs
var defaultPrefs*: Prefs
proc updateDefaultPrefs*(cfg: parsecfg.Config) =
genDefaultPrefs()

ファイルの表示

@ -4,11 +4,12 @@ import redis, redpool, flatty, supersnappy
import types, api
const redisNil = "\0\0"
const
redisNil = "\0\0"
baseCacheTime = 60 * 60
var
pool {.threadvar.}: RedisPool
baseCacheTime = 60 * 60
pool: RedisPool
rssCacheTime: int
listCacheTime*: int
@ -17,7 +18,9 @@ proc toFlatty*(s: var string, x: DateTime) =
s.toFlatty(x.toTime().toUnix())
proc fromFlatty*(s: string, i: var int, x: var DateTime) =
x = fromUnix(s.fromFlatty(int64)).utc()
var unix: int64
s.fromFlatty(i, unix)
x = fromUnix(unix).utc()
proc setCacheTimes*(cfg: Config) =
rssCacheTime = cfg.rssCacheTime * 60
@ -56,7 +59,7 @@ proc initRedisPool*(cfg: Config) {.async.} =
template pidKey(name: string): string = "pid:" & $(hash(name) div 1_000_000)
template profileKey(name: string): string = "p:" & name
template listKey(l: List): string = toLower("l:" & l.username & '/' & l.name)
template listKey(l: List): string = "l:" & l.id
proc get(query: string): Future[string] {.async.} =
pool.withAcquire(r):
@ -129,17 +132,17 @@ proc getCachedPhotoRail*(name: string): Future[PhotoRail] {.async.} =
result = await getPhotoRail(name)
await cache(result, name)
proc getCachedList*(username=""; name=""; id=""): Future[List] {.async.} =
let list = if id.len > 0: redisNil
else: await get(toLower("l:" & username & '/' & name))
proc getCachedList*(username=""; slug=""; id=""): Future[List] {.async.} =
let list = if id.len == 0: redisNil
else: await get("l:" & id)
if list != redisNil:
result = fromFlatty(uncompress(list), List)
else:
if id.len > 0:
result = await getGraphListById(id)
result = await getGraphList(id)
else:
result = await getGraphList(username, name)
result = await getGraphListBySlug(username, slug)
await cache(result)
proc getCachedRss*(key: string): Future[Rss] {.async.} =

ファイルの表示

@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only
import strutils
import strutils, uri
import jester
@ -8,41 +8,44 @@ import ".."/[types, redis_cache, api]
import ../views/[general, timeline, list]
export getListTimeline, getGraphList
template respList*(list, timeline, vnode: typed) =
if list.id.len == 0:
resp Http404, showError("List \"" & @"list" & "\" not found", cfg)
template respList*(list, timeline, title, vnode: typed) =
if list.id.len == 0 or list.name.len == 0:
resp Http404, showError("List " & @"id" & " not found", cfg)
let
html = renderList(vnode, timeline.query, list)
rss = "/$1/lists/$2/rss" % [@"name", @"list"]
rss = "/i/lists/$1/rss" % [@"id"]
resp renderMain(html, request, cfg, prefs, rss=rss, banner=list.banner)
resp renderMain(html, request, cfg, prefs, titleText=title, rss=rss, banner=list.banner)
proc createListRouter*(cfg: Config) =
router list:
get "/@name/lists/@list/?":
get "/@name/lists/@slug/?":
cond '.' notin @"name"
cond @"name" != "i"
cond @"slug" != "memberships"
let
prefs = cookiePrefs()
list = await getCachedList(@"name", @"list")
timeline = await getListTimeline(list.id, getCursor())
vnode = renderTimelineTweets(timeline, prefs, request.path)
respList(list, timeline, vnode)
get "/@name/lists/@list/members":
cond '.' notin @"name"
cond @"name" != "i"
let
prefs = cookiePrefs()
list = await getCachedList(@"name", @"list")
members = await getListMembers(list, getCursor())
respList(list, members, renderTimelineUsers(members, prefs, request.path))
slug = decodeUrl(@"slug")
list = await getCachedList(@"name", slug)
if list.id.len == 0:
resp Http404, showError("List \"" & @"slug" & "\" not found", cfg)
redirect("/i/lists/" & list.id)
get "/i/lists/@id/?":
cond '.' notin @"id"
let list = await getCachedList(id=(@"id"))
if list.id.len == 0:
resp Http404
await cache(list)
redirect("/" & list.username & "/lists/" & list.name)
let
prefs = cookiePrefs()
list = await getCachedList(id=(@"id"))
title = "@" & list.username & "/" & list.name
timeline = await getListTimeline(list.id, getCursor())
vnode = renderTimelineTweets(timeline, prefs, request.path)
respList(list, timeline, title, vnode)
get "/i/lists/@id/members":
cond '.' notin @"id"
let
prefs = cookiePrefs()
list = await getCachedList(id=(@"id"))
title = "@" & list.username & "/" & list.name
members = await getListMembers(list, getCursor())
respList(list, members, title, renderTimelineUsers(members, prefs, request.path))

ファイルの表示

@ -32,7 +32,7 @@ proc createPrefRouter*(cfg: Config) =
post "/resetprefs":
genResetPrefs()
redirect($(parseUri("/settings") ? filterParams(request.params)))
redirect("/settings?referer=" & encodeUrl(refPath()))
post "/enablehls":
savePref("hlsPlayback", "on", request)

ファイルの表示

@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch, strutils, tables, times, hashes, supersnappy
import asyncdispatch, strutils, tables, times, hashes, uri
import jester
import jester, supersnappy
import router_utils, timeline
import ../query
@ -36,12 +36,18 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
return Rss(feed: profile.username, cursor: "suspended")
if profile.fullname.len > 0:
let rss = compress renderTimelineRss(timeline, profile, cfg, multi=(names.len > 1))
let rss = compress renderTimelineRss(timeline, profile, cfg,
multi=(names.len > 1))
return Rss(feed: rss, cursor: timeline.bottom)
template respRss*(rss) =
template respRss*(rss, page) =
if rss.cursor.len == 0:
resp Http404, showError("User \"" & @"name" & "\" not found", cfg)
let info = case page
of "User": " \"$1\" " % @"name"
of "List": " $1 " % @"id"
else: " "
resp Http404, showError(page & info & "not found", cfg)
elif rss.cursor.len == 9 and rss.cursor == "suspended":
resp Http404, showError(getSuspended(rss.feed), cfg)
@ -62,11 +68,11 @@ proc createRssRouter*(cfg: Config) =
let
cursor = getCursor()
key = $hash(genQueryUrl(query)) & cursor
key = "search:" & $hash(genQueryUrl(query)) & ":" & cursor
var rss = await getCachedRss(key)
if rss.cursor.len > 0:
respRss(rss)
respRss(rss, "Search")
let tweets = await getSearch[Tweet](query, cursor)
rss.cursor = tweets.bottom
@ -74,7 +80,7 @@ proc createRssRouter*(cfg: Config) =
genQueryUrl(query), cfg)
await cacheRss(key, rss)
respRss(rss)
respRss(rss, "Search")
get "/@name/rss":
cond cfg.enableRss
@ -82,16 +88,16 @@ proc createRssRouter*(cfg: Config) =
let
cursor = getCursor()
name = @"name"
key = name & cursor
key = "twitter:" & name & ":" & cursor
var rss = await getCachedRss(key)
if rss.cursor.len > 0:
respRss(rss)
respRss(rss, "User")
rss = await timelineRss(request, cfg, Query(fromUser: @[name]))
await cacheRss(key, rss)
respRss(rss)
respRss(rss, "User")
get "/@name/@tab/rss":
cond cfg.enableRss
@ -105,36 +111,54 @@ proc createRssRouter*(cfg: Config) =
of "search": initQuery(params(request), name=name)
else: Query(fromUser: @[name])
var key = @"name" & "/" & @"tab"
var key = @"tab" & ":" & @"name" & ":"
if @"tab" == "search":
key &= $hash(genQueryUrl(query))
key &= $hash(genQueryUrl(query)) & ":"
key &= getCursor()
var rss = await getCachedRss(key)
if rss.cursor.len > 0:
respRss(rss)
respRss(rss, "User")
rss = await timelineRss(request, cfg, query)
await cacheRss(key, rss)
respRss(rss)
respRss(rss, "User")
get "/@name/lists/@list/rss":
get "/@name/lists/@slug/rss":
cond cfg.enableRss
cond @"name" != "i"
let
slug = decodeUrl(@"slug")
list = await getCachedList(@"name", slug)
cursor = getCursor()
if list.id.len == 0:
resp Http404, showError("List \"" & @"slug" & "\" not found", cfg)
let url = "/i/lists/" & list.id & "/rss"
if cursor.len > 0:
redirect(url & "?cursor=" & encodeUrl(cursor, false))
else:
redirect(url)
get "/i/lists/@id/rss":
cond cfg.enableRss
cond '.' notin @"name"
let
cursor = getCursor()
key = @"name" & "/" & @"list" & cursor
key =
if cursor.len == 0: "lists:" & @"id"
else: "lists:" & @"id" & ":" & cursor
var rss = await getCachedRss(key)
if rss.cursor.len > 0:
respRss(rss)
respRss(rss, "List")
let
list = await getCachedList(@"name", @"list")
list = await getCachedList(id=(@"id"))
timeline = await getListTimeline(list.id, cursor)
rss.cursor = timeline.bottom
rss.feed = compress renderListRss(timeline.content, list, cfg)
await cacheRss(key, rss)
respRss(rss)
respRss(rss, "List")

ファイルの表示

@ -37,7 +37,7 @@
@include ellipsis;
white-space: unset;
font-weight: bold;
font-size: 1.15em;
font-size: 1.1em;
}
.card-description {

ファイルの表示

@ -2,8 +2,8 @@
@import '_mixins';
video {
height: 100%;
width: 100%;
max-height: 100%;
max-width: 100%;
}
.gallery-video {
@ -18,10 +18,13 @@ video {
.video-container {
max-height: 530px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
img {
height: 100%;
width: 100%;
max-height: 100%;
max-width: 100%;
}
}

ファイルの表示

@ -11,8 +11,8 @@ const
failDelay = initDuration(minutes=30)
var
clientPool {.threadvar.}: HttpPool
tokenPool {.threadvar.}: seq[Token]
clientPool: HttpPool
tokenPool: seq[Token]
lastFailed: Time
proc getPoolInfo*: string =

ファイルの表示

@ -128,7 +128,7 @@ type
videoDirectMessage = "video_direct_message"
imageDirectMessage = "image_direct_message"
audiospace = "audiospace"
newsletter_publication = "newsletter_publication"
newsletterPublication = "newsletter_publication"
unknown
Card* = object

ファイルの表示

@ -1,16 +1,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
import strutils, strformat, sequtils, uri, tables, base64
import strutils, strformat, uri, tables, base64
import nimcrypto, regex
var
hmacKey {.threadvar.}: string
hmacKey: string
base64Media = false
const
https* = "https://"
twimg* = "pbs.twimg.com/"
badJpgExts = @["1500x500", "jpgn", "jpg:", "jpg_", "_jpg"]
badPngExts = @["pngn", "png:", "png_", "_png"]
nitterParams = ["name", "tab", "id", "list", "referer", "scroll"]
twitterDomains = @[
"twitter.com",
"pic.twitter.com",
@ -43,18 +42,9 @@ proc getPicUrl*(link: string): string =
else:
&"/pic/{encodeUrl(link)}"
proc cleanFilename*(filename: string): string =
const reg = re"[^A-Za-z0-9._-]"
result = filename.replace(reg, "_")
if badJpgExts.anyIt(it in result):
result &= ".jpg"
elif badPngExts.anyIt(it in result):
result &= ".png"
proc filterParams*(params: Table): seq[(string, string)] =
const filter = ["name", "tab", "id", "list", "referer", "scroll"]
for p in params.pairs():
if p[1].len > 0 and p[0] notin filter:
if p[1].len > 0 and p[0] notin nitterParams:
result.add p
proc isTwitterUrl*(uri: Uri): bool =

ファイルの表示

@ -12,8 +12,10 @@ const
lp = readFile("public/lp.svg")
proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
var path = $(parseUri(req.path) ? filterParams(req.params))
if "/status" in path: path.add "#m"
var path = req.params.getOrDefault("referer")
if path.len == 0:
path = $(parseUri(req.path) ? filterParams(req.params))
if "/status/" in path: path.add "#m"
buildHtml(nav):
tdiv(class="inner-nav"):
@ -29,7 +31,7 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
icon "bird", title="Open in Twitter", href=canonical
a(href="https://liberapay.com/zedeus"): verbatim lp
icon "info", title="About", href="/about"
iconReferer "cog", "/settings", path, title="Preferences"
icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path))
proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
images: seq[string] = @[]; banner=""; ogTitle=""; theme="";
@ -43,7 +45,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=6")
link(rel="stylesheet", type="text/css", href="/css/style.css?v=7")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")
if theme.len > 0:
@ -86,7 +88,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; titleText=""; desc=""; video="";
if banner.len > 0:
let bannerUrl = getPicUrl(banner)
link(rel="preload", type="image/png", href=getPicUrl(banner), `as`="image")
link(rel="preload", type="image/png", href=bannerUrl, `as`="image")
for url in images:
let suffix = if "400x400" in url: "" else: "?name=small"

ファイルの表示

@ -25,5 +25,5 @@ proc renderList*(body: VNode; query: Query; list: List): VNode =
tdiv(class="timeline-description"):
text list.description
renderListTabs(query, &"/{list.username}/lists/{list.name}")
renderListTabs(query, &"/i/lists/{list.id}")
body

ファイルの表示

@ -42,12 +42,6 @@ proc hiddenField*(name, value: string): VNode =
proc refererField*(path: string): VNode =
hiddenField("referer", path)
proc iconReferer*(icon, action, path: string, title=""): VNode =
buildHtml(form(`method`="get", action=action, class="icon-button")):
refererField path
button(`type`="submit"):
icon icon, title=title
proc buttonReferer*(action, text, path: string; class=""; `method`="post"): VNode =
buildHtml(form(`method`=`method`, action=action, class=class)):
refererField path

ファイルの表示

@ -9,9 +9,9 @@
#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: "
#end if
#var text = stripHtml(tweet.text)
#if unicode.runeLen(text) > 32:
# text = unicode.runeSubStr(text, 0, 32) & "..."
#end if
##if unicode.runeLen(text) > 32:
## text = unicode.runeSubStr(text, 0, 32) & "..."
##end if
#result &= xmltree.escape(text)
#if result.len > 0: return
#end if
@ -111,7 +111,7 @@ ${renderRssTweets(timeline.content, cfg)}
#
#proc renderListRss*(tweets: seq[Tweet]; list: List; cfg: Config): string =
#let prefs = Prefs(replaceTwitter: cfg.hostname, replaceYouTube: cfg.replaceYouTube, replaceOdysee: cfg.replaceOdysee)
#let link = &"{getUrlPrefix(cfg)}/{list.username}/lists/{list.name}"
#let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}"
#result = ""
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">

ファイルの表示

@ -7,7 +7,7 @@ import ".."/[types, utils, formatters]
proc getSmallPic(url: string): string =
result = url
if "?" notin url:
if "?" notin url and not url.endsWith("placeholder.png"):
result &= ":small"
result = getPicUrl(result)
@ -66,36 +66,35 @@ proc isPlaybackEnabled(prefs: Prefs; video: Video): bool =
of m3u8, vmap: prefs.hlsPlayback
proc renderVideoDisabled(video: Video; path: string): VNode =
buildHtml(tdiv):
img(src=getSmallPic(video.thumb))
tdiv(class="video-overlay"):
case video.playbackType
of mp4:
p: text "mp4 playback disabled in preferences"
of m3u8, vmap:
buttonReferer "/enablehls", "Enable hls playback", path
buildHtml(tdiv(class="video-overlay")):
case video.playbackType
of mp4:
p: text "mp4 playback disabled in preferences"
of m3u8, vmap:
buttonReferer "/enablehls", "Enable hls playback", path
proc renderVideoUnavailable(video: Video): VNode =
buildHtml(tdiv):
img(src=getSmallPic(video.thumb))
tdiv(class="video-overlay"):
case video.reason
of "dmcaed":
p: text "This media has been disabled in response to a report by the copyright owner"
else:
p: text "This media is unavailable"
buildHtml(tdiv(class="video-overlay")):
case video.reason
of "dmcaed":
p: text "This media has been disabled in response to a report by the copyright owner"
else:
p: text "This media is unavailable"
proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
let container =
if video.description.len > 0 or video.title.len > 0: " card-container"
else: ""
buildHtml(tdiv(class="attachments card")):
tdiv(class="gallery-video" & container):
tdiv(class="attachment video-container"):
let thumb = getSmallPic(video.thumb)
if not video.available:
img(src=thumb)
renderVideoUnavailable(video)
elif not prefs.isPlaybackEnabled(video):
img(src=thumb)
renderVideoDisabled(video, path)
else:
let vid = video.variants.filterIt(it.videoType == video.playbackType)
@ -202,6 +201,8 @@ proc renderAttribution(profile: Profile): VNode =
buildHtml(a(class="attribution", href=("/" & profile.username))):
renderMiniAvatar(profile)
strong: text profile.fullname
if profile.verified:
icon "ok", class="verified-icon", title="Verified account"
proc renderMediaTags(tags: seq[Profile]): VNode =
buildHtml(tdiv(class="media-tag-block")):