diff --git a/.gitignore b/.gitignore index dbd2f6b..09bdaa4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ nitter.conf guest_accounts.json* sessions.json* dump.rdb +*.bak +/tools/*.json* diff --git a/src/apiutils.nim b/src/apiutils.nim index ddb5027..42664cf 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -19,6 +19,7 @@ proc setDisableTid*(disable: bool) = disableTid = disable proc setApiProxy*(url: string) = + apiProxy = "" if url.len > 0: apiProxy = url.strip(chars={'/'}) & "/" if "http" notin apiProxy: diff --git a/src/parser.nim b/src/parser.nim index da0ffb4..a566fb1 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -140,27 +140,37 @@ proc parseVideo(js: JsonNode): Video = result.variants = parseVideoVariants(js{"video_info", "variants"}) +proc addMedia(media: var MediaEntities; photo: Photo) = + media.add Media(kind: photoMedia, photo: photo) + +proc addMedia(media: var MediaEntities; video: Video) = + media.add Media(kind: videoMedia, video: video) + +proc addMedia(media: var MediaEntities; gif: Gif) = + media.add Media(kind: gifMedia, gif: gif) + proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) = with jsMedia, js{"extended_entities", "media"}: for m in jsMedia: case m.getTypeName: of "photo": - result.photos.add Photo( + result.media.addMedia(Photo( url: m{"media_url_https"}.getImageStr, altText: m{"ext_alt_text"}.getStr - ) + )) of "video": - result.video = some(parseVideo(m)) + result.media.addMedia(parseVideo(m)) with user, m{"additional_media_info", "source_user"}: if user{"id"}.getInt > 0: result.attribution = some(parseUser(user)) else: result.attribution = some(parseGraphUser(user)) of "animated_gif": - result.gif = some Gif( + result.media.addMedia(Gif( url: m{"video_info", "variants"}[0]{"url"}.getImageStr, - thumb: m{"media_url_https"}.getImageStr - ) + thumb: m{"media_url_https"}.getImageStr, + altText: m{"ext_alt_text"}.getStr + )) else: discard with url, m{"url"}: @@ -170,29 +180,35 @@ proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) = proc parseMediaEntities(js: JsonNode; result: var Tweet) = with mediaEntities, js{"media_entities"}: + var parsedMedia: MediaEntities for mediaEntity in mediaEntities: with mediaInfo, mediaEntity{"media_results", "result", "media_info"}: case mediaInfo.getTypeName of "ApiImage": - result.photos.add Photo( + parsedMedia.addMedia(Photo( url: mediaInfo{"original_img_url"}.getImageStr, altText: mediaInfo{"alt_text"}.getStr - ) + )) of "ApiVideo": let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"} - result.video = some Video( + parsedMedia.addMedia(Video( available: status.getStr == "Available", thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr, + title: mediaInfo{"alt_text"}.getStr, durationMs: mediaInfo{"duration_millis"}.getInt, variants: parseVideoVariants(mediaInfo{"variants"}) - ) + )) of "ApiGif": - result.gif = some Gif( + parsedMedia.addMedia(Gif( url: mediaInfo{"variants"}[0]{"url"}.getImageStr, - thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr - ) + thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr, + altText: mediaInfo{"alt_text"}.getStr + )) else: discard + if mediaEntities.len > 0 and parsedMedia.len == mediaEntities.len: + result.media = parsedMedia + # Remove media URLs from text with mediaList, js{"legacy", "entities", "media"}: for url in mediaList: @@ -348,13 +364,13 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull(); let name = jsCard{"name"}.getStr if "poll" in name: if "image" in name: - result.photos.add Photo( + result.media.addMedia(Photo( url: jsCard{"binding_values", "image_large"}.getImageVal - ) + )) result.poll = some parsePoll(jsCard) elif name == "amplify": - result.video = some parsePromoVideo(jsCard{"binding_values"}) + result.media.addMedia(parsePromoVideo(jsCard{"binding_values"})) else: result.card = some parseCard(jsCard, js{"entities", "urls"}) diff --git a/src/parserutils.nim b/src/parserutils.nim index 98dd2ee..ea37468 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -352,9 +352,7 @@ proc expandBirdwatchEntities*(text: string; entities: JsonNode): string = proc extractGalleryPhoto*(t: Tweet): GalleryPhoto = let url = - if t.photos.len > 0: t.photos[0].url - elif t.video.isSome: get(t.video).thumb - elif t.gif.isSome: get(t.gif).thumb + if t.media.len > 0: t.media[0].getThumb elif t.card.isSome: get(t.card).image else: "" diff --git a/src/routes/embed.nim b/src/routes/embed.nim index 0527d3d..bdca60f 100644 --- a/src/routes/embed.nim +++ b/src/routes/embed.nim @@ -11,7 +11,7 @@ proc createEmbedRouter*(cfg: Config) = router embed: get "/i/videos/tweet/@id": let tweet = await getGraphTweetResult(@"id") - if tweet == nil or tweet.video.isNone: + if tweet == nil or not tweet.hasVideos: resp Http404 resp renderVideoEmbed(tweet, cfg, request) diff --git a/src/routes/status.nim b/src/routes/status.nim index ce093cb..f7fb1bb 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -44,15 +44,19 @@ proc createStatusRouter*(cfg: Config) = desc = conv.tweet.text var - images = conv.tweet.photos.mapIt(it.url) + images = conv.tweet.getPhotos.mapIt(it.url) video = "" - if conv.tweet.video.isSome(): - images = @[get(conv.tweet.video).thumb] + let + firstMediaKind = if conv.tweet.media.len > 0: conv.tweet.media[0].kind + else: photoMedia + + if firstMediaKind == videoMedia: + images = @[conv.tweet.media[0].getThumb] video = getVideoEmbed(cfg, conv.tweet.id) - elif conv.tweet.gif.isSome(): - images = @[get(conv.tweet.gif).thumb] - video = getPicUrl(get(conv.tweet.gif).url) + elif firstMediaKind == gifMedia: + images = @[conv.tweet.media[0].getThumb] + video = getPicUrl(conv.tweet.media[0].gif.url) elif conv.tweet.card.isSome(): let card = conv.tweet.card.get() if card.image.len > 0: diff --git a/src/sass/tweet/embed.scss b/src/sass/tweet/embed.scss index fbdbd41..bee23d3 100644 --- a/src/sass/tweet/embed.scss +++ b/src/sass/tweet/embed.scss @@ -11,7 +11,7 @@ left: 0%; } - .video-container { + .gallery-video > .attachment { max-height: unset; } } diff --git a/src/sass/tweet/media.scss b/src/sass/tweet/media.scss index 040edda..d7b443e 100644 --- a/src/sass/tweet/media.scss +++ b/src/sass/tweet/media.scss @@ -10,9 +10,47 @@ max-width: 533px; pointer-events: all; - .still-image { - width: 100%; - align-self: center; + &.mixed-row { + .attachment { + min-width: 0; + min-height: 0; + flex: 1 1 0; + max-height: 379.5px; + display: flex; + align-items: center; + justify-content: center; + background-color: #101010; + } + + .still-image, + .still-image img, + .attachment > video, + .attachment > img { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + } + + .still-image { + display: flex; + align-self: stretch; + } + + .still-image img { + flex-basis: auto; + flex-grow: 0; + object-fit: cover; + } + + .attachment > video, + .attachment > img { + object-fit: cover; + } + + .attachment > video { + object-fit: contain; + } } } @@ -28,10 +66,6 @@ background-color: var(--bg_color); align-items: center; pointer-events: all; - - .image-attachment { - width: 100%; - } } .attachment { @@ -49,7 +83,14 @@ } } -.gallery-gif video { +.media-gif { + display: table; + background-color: unset; + width: unset; + max-height: unset; +} + +.media-gif video { max-height: 530px; background-color: #101010; } @@ -96,22 +137,6 @@ transition-property: max-height; } -.image { - display: flex; -} - -// .single-image { -// display: inline-block; -// width: 100%; -// max-height: 600px; - -// .attachments { -// width: unset; -// max-height: unset; -// display: inherit; -// } -// } - .overlay-circle { border-radius: 50%; background-color: var(--dark_grey); @@ -133,12 +158,6 @@ margin-left: 14px; } -.media-gif { - display: table; - background-color: unset; - width: unset; -} - .media-body { flex: 1; padding: 0; diff --git a/src/sass/tweet/quote.scss b/src/sass/tweet/quote.scss index 2f3122a..d6a75a9 100644 --- a/src/sass/tweet/quote.scss +++ b/src/sass/tweet/quote.scss @@ -95,7 +95,7 @@ justify-content: center; } - .gallery-gif .attachment { + .media-gif > .attachment { display: flex; justify-content: center; background-color: var(--bg_color); @@ -108,8 +108,9 @@ } } - .gallery-video, - .gallery-gif { + .gallery-row .attachment, + .gallery-row .attachment > video, + .gallery-row .attachment > img { max-height: 300px; } diff --git a/src/sass/tweet/video.scss b/src/sass/tweet/video.scss index ba77b14..c20d348 100644 --- a/src/sass/tweet/video.scss +++ b/src/sass/tweet/video.scss @@ -9,22 +9,22 @@ video { .gallery-video { display: flex; overflow: hidden; -} + + &.card-container { + flex-direction: column; + width: 100%; + } -.gallery-video.card-container { - flex-direction: column; - width: 100%; -} + > .attachment { + min-height: 80px; + min-width: 200px; + max-height: 530px; + margin: 0; -.video-container { - min-height: 80px; - min-width: 200px; - max-height: 530px; - margin: 0; - - img { - max-height: 100%; - max-width: 100%; + img { + max-height: 100%; + max-width: 100%; + } } } diff --git a/src/types.nim b/src/types.nim index 56bc2b2..3f655b6 100644 --- a/src/types.nim +++ b/src/types.nim @@ -136,11 +136,28 @@ type Gif* = object url*: string thumb*: string + altText*: string Photo* = object url*: string altText*: string + MediaKind* = enum + photoMedia + videoMedia + gifMedia + + Media* = object + case kind*: MediaKind + of photoMedia: + photo*: Photo + of videoMedia: + video*: Video + of gifMedia: + gif*: Gif + + MediaEntities* = seq[Media] + GalleryPhoto* = object url*: string tweetId*: string @@ -219,9 +236,7 @@ type quote*: Option[Tweet] card*: Option[Card] poll*: Option[Poll] - gif*: Option[Gif] - video*: Option[Video] - photos*: seq[Photo] + media*: MediaEntities history*: seq[int64] note*: string @@ -310,3 +325,24 @@ proc contains*(thread: Chain; tweet: Tweet): bool = proc add*(timeline: var seq[Tweets]; tweet: Tweet) = timeline.add @[tweet] + +proc getPhotos*(tweet: Tweet): seq[Photo] = + tweet.media.filterIt(it.kind == photoMedia).mapIt(it.photo) + +proc getVideos*(tweet: Tweet): seq[Video] = + tweet.media.filterIt(it.kind == videoMedia).mapIt(it.video) + +proc hasPhotos*(tweet: Tweet): bool = + tweet.media.anyIt(it.kind == photoMedia) + +proc hasVideos*(tweet: Tweet): bool = + tweet.media.anyIt(it.kind == videoMedia) + +proc hasGifs*(tweet: Tweet): bool = + tweet.media.anyIt(it.kind == gifMedia) + +proc getThumb*(media: Media): string = + case media.kind + of photoMedia: media.photo.url + of videoMedia: media.video.thumb + of gifMedia: media.gif.thumb diff --git a/src/views/embed.nim b/src/views/embed.nim index ba49f45..62cc76f 100644 --- a/src/views/embed.nim +++ b/src/views/embed.nim @@ -9,14 +9,17 @@ import general, tweet const doctype = "\n" proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string = - let thumb = get(tweet.video).thumb - let vidUrl = getVideoEmbed(cfg, tweet.id) - let prefs = Prefs(hlsPlayback: true, mp4Playback: true) + let + video = tweet.getVideos()[0] + thumb = video.thumb + vidUrl = getVideoEmbed(cfg, tweet.id) + prefs = Prefs(hlsPlayback: true, mp4Playback: true) + let node = buildHtml(html(lang="en")): renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb])) body: tdiv(class="embed-video"): - renderVideo(get(tweet.video), prefs, "") + renderVideo(video, prefs, "") result = doctype & $node diff --git a/src/views/general.nim b/src/views/general.nim index ac087c1..9671c73 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; let opensearchUrl = getUrlPrefix(cfg) & "/opensearch" buildHtml(head): - link(rel="stylesheet", type="text/css", href="/css/style.css?v=28") + link(rel="stylesheet", type="text/css", href="/css/style.css?v=29") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4") if theme.len > 0: diff --git a/src/views/rss.nimf b/src/views/rss.nimf index 2c71591..4c1a86f 100644 --- a/src/views/rss.nimf +++ b/src/views/rss.nimf @@ -1,29 +1,38 @@ #? stdtmpl(subsChar = '$', metaChar = '#') ## SPDX-License-Identifier: AGPL-3.0-only -#import strutils, xmltree, strformat, options, unicode +#import strutils, sequtils, xmltree, strformat, options, unicode #import ../types, ../utils, ../formatters, ../prefs ## Snowflake ID cutoff for RSS GUID format transition ## Corresponds to approximately December 14, 2025 UTC #const guidCutoff = 2000000000000000000'i64 # #proc getTitle(tweet: Tweet; retweet: string): string = -#if tweet.pinned: result = "Pinned: " -#elif retweet.len > 0: result = &"RT by @{retweet}: " -#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: " +#var prefix = "" +#if tweet.pinned: prefix = "Pinned: " +#elif retweet.len > 0: prefix = &"RT by @{retweet}: " +#elif tweet.reply.len > 0: prefix = &"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 -#result &= xmltree.escape(text) -#if result.len > 0: return +#text = xmltree.escape(text) +#if text.len > 0: +# result = prefix & text +# return #end if -#if tweet.photos.len > 0: -# result &= "Image" -#elif tweet.video.isSome: -# result &= "Video" -#elif tweet.gif.isSome: -# result &= "Gif" +#if tweet.media.len > 0: +# result = prefix +# let firstKind = tweet.media[0].kind +# if tweet.media.anyIt(it.kind != firstKind): +# result &= "Media" +# else: +# case firstKind +# of photoMedia: result &= "Image" +# of videoMedia: result &= "Video" +# of gifMedia: result &= "Gif" +# end case +# end if #end if #end proc # @@ -31,6 +40,26 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)} #end proc # +#proc renderRssMedia(media: Media; tweet: Tweet; urlPrefix: string): string = +#case media.kind +#of photoMedia: +# let photo = media.photo + +#of videoMedia: +# let video = media.video + +
Video
+ +
+#of gifMedia: +# let gif = media.gif +# let thumb = &"{urlPrefix}{getPicUrl(gif.thumb)}" +# let url = &"{urlPrefix}{getPicUrl(gif.url)}" + +#end case +#end proc +# #proc getTweetsWithPinned(profile: Profile): seq[Tweets] = #result = profile.tweets.content #if profile.pinned.isSome and result.len > 0: @@ -54,20 +83,10 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)} #let urlPrefix = getUrlPrefix(cfg) #let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix)

${text.replace("\n", "
\n")}

-#if tweet.photos.len > 0: -# for photo in tweet.photos: - +#if tweet.media.len > 0: +# for media in tweet.media: +${renderRssMedia(media, tweet, urlPrefix)} # end for -#elif tweet.video.isSome: - -
Video
- -
-#elif tweet.gif.isSome: -# let thumb = &"{urlPrefix}{getPicUrl(get(tweet.gif).thumb)}" -# let url = &"{urlPrefix}{getPicUrl(get(tweet.gif).url)}" - #elif tweet.card.isSome: # let card = tweet.card.get() # if card.image.len > 0: diff --git a/src/views/tweet.nim b/src/views/tweet.nim index b0578c1..b1f805c 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -38,24 +38,19 @@ proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VN a(href=getLink(tweet), title=tweet.getTime): text tweet.getShortTime -proc renderAlbum(tweet: Tweet): VNode = - let - groups = if tweet.photos.len < 3: @[tweet.photos] - else: tweet.photos.distribute(2) +proc renderAltText(altText: string): VNode = + buildHtml(p(class="alt-text")): + text "ALT " & altText - buildHtml(tdiv(class="attachments")): - for i, photos in groups: - let margin = if i > 0: ".25em" else: "" - tdiv(class="gallery-row", style={marginTop: margin}): - for photo in photos: - tdiv(class="attachment image"): - let - named = "name=" in photo.url - small = if named: photo.url else: photo.url & smallWebp - a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"): - genImg(small, alt=photo.altText) - if photo.altText.len > 0: - p(class="alt-text"): text "ALT " & photo.altText +proc renderPhotoAttachment(photo: Photo): VNode = + buildHtml(tdiv(class="attachment")): + let + named = "name=" in photo.url + small = if named: photo.url else: photo.url & smallWebp + a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"): + genImg(small, alt=photo.altText) + if photo.altText.len > 0: + renderAltText(photo.altText) proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool = case playbackType @@ -65,7 +60,7 @@ proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool = proc hasMp4Url(video: Video): bool = video.variants.anyIt(it.contentType == mp4) -proc renderVideoDisabled(playbackType: VideoType; path: string): VNode = +proc renderVideoDisabled(playbackType: VideoType; path=""): VNode = buildHtml(tdiv(class="video-overlay")): case playbackType of mp4: @@ -81,52 +76,97 @@ proc renderVideoUnavailable(video: Video): VNode = else: p: text "This media is unavailable" -proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode = +proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""): VNode = let - container = if video.description.len == 0 and video.title.len == 0: "" - else: " card-container" - playbackType = if not prefs.proxyVideos and video.hasMp4Url: mp4 - else: video.playbackType + playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4 + else: videoData.playbackType + thumb = getSmallPic(videoData.thumb) + + buildHtml(tdiv(class="attachment")): + if not videoData.available: + img(src=thumb, loading="lazy") + renderVideoUnavailable(videoData) + elif not prefs.isPlaybackEnabled(playbackType): + img(src=thumb, loading="lazy") + renderVideoDisabled(playbackType, path) + else: + let + vars = videoData.variants.filterIt(it.contentType == playbackType) + vidUrl = vars.sortedByIt(it.resolution)[^1].url + source = if prefs.proxyVideos: getVidUrl(vidUrl) + else: vidUrl + case playbackType + of mp4: + video(poster=thumb, controls="", muted=prefs.muteVideos): + source(src=source, `type`="video/mp4") + of m3u8, vmap: + video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos) + verbatim "
" + tdiv(class="overlay-circle"): span(class="overlay-triangle") + tdiv(class="overlay-duration"): text getDuration(videoData) + verbatim "
" + +proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode = + let hasCardContent = video.description.len > 0 or video.title.len > 0 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, loading="lazy") - renderVideoUnavailable(video) - elif not prefs.isPlaybackEnabled(playbackType): - img(src=thumb, loading="lazy") - renderVideoDisabled(playbackType, path) - else: - let - vars = video.variants.filterIt(it.contentType == playbackType) - vidUrl = vars.sortedByIt(it.resolution)[^1].url - source = if prefs.proxyVideos: getVidUrl(vidUrl) - else: vidUrl - case playbackType - of mp4: - video(poster=thumb, controls="", muted=prefs.muteVideos): - source(src=source, `type`="video/mp4") - of m3u8, vmap: - video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos) - verbatim "
" - tdiv(class="overlay-circle"): span(class="overlay-triangle") - tdiv(class="overlay-duration"): text getDuration(video) - verbatim "
" - if container.len > 0: + tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))): + renderVideoAttachment(video, prefs, path) + if hasCardContent: tdiv(class="card-content"): h2(class="card-title"): text video.title if video.description.len > 0: p(class="card-description"): text video.description +proc renderGifAttachment(gif: Gif; prefs: Prefs): VNode = + let thumb = getSmallPic(gif.thumb) + + buildHtml(tdiv(class="attachment")): + if not prefs.mp4Playback: + img(src=thumb, loading="lazy") + renderVideoDisabled(mp4) + elif prefs.autoplayGifs: + video(class="gif", poster=thumb, autoplay="", muted="", loop=""): + source(src=getPicUrl(gif.url), `type`="video/mp4") + else: + video(class="gif", poster=thumb, controls="", muted="", loop=""): + source(src=getPicUrl(gif.url), `type`="video/mp4") + if gif.altText.len > 0: + renderAltText(gif.altText) + proc renderGif(gif: Gif; prefs: Prefs): VNode = buildHtml(tdiv(class="attachments media-gif")): - tdiv(class="gallery-gif", style={maxHeight: "unset"}): - tdiv(class="attachment"): - video(class="gif", poster=getSmallPic(gif.thumb), autoplay=prefs.autoplayGifs, - controls="", muted="", loop=""): - source(src=getPicUrl(gif.url), `type`="video/mp4") + renderGifAttachment(gif, prefs) + +proc renderMedia(media: seq[Media]; prefs: Prefs; path: string): VNode = + if media.len == 0: + return nil + + if media.len == 1: + let item = media[0] + if item.kind == videoMedia: + return renderVideo(item.video, prefs, path) + if item.kind == gifMedia: + return renderGif(item.gif, prefs) + + let + groups = if media.len < 3: @[media] + else: media.distribute(2) + + buildHtml(tdiv(class="attachments")): + for i, mediaGroup in groups: + let margin = if i > 0: ".25em" else: "" + let rowClass = "gallery-row" & + (if mediaGroup.allIt(it.kind == photoMedia): "" else: " mixed-row") + tdiv(class=rowClass, style={marginTop: margin}): + for mediaItem in mediaGroup: + case mediaItem.kind + of photoMedia: + renderPhotoAttachment(mediaItem.photo) + of videoMedia: + renderVideoAttachment(mediaItem.video, prefs, path) + of gifMedia: + renderGifAttachment(mediaItem.gif, prefs) proc renderPoll(poll: Poll): VNode = buildHtml(tdiv(class="poll")): @@ -219,12 +259,7 @@ proc renderLatestPost(username: string; id: int64): VNode = proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="quote-media-container")): - if quote.photos.len > 0: - renderAlbum(quote) - elif quote.video.isSome: - renderVideo(quote.video.get(), prefs, path) - elif quote.gif.isSome: - renderGif(quote.gif.get(), prefs) + renderMedia(quote.media, prefs, path) proc renderCommunityNote(note: string; prefs: Prefs): VNode = buildHtml(tdiv(class="community-note")): @@ -266,7 +301,7 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = tdiv(class="quote-text", dir="auto"): verbatim replaceUrls(quote.text, prefs) - if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome: + if quote.media.len > 0: renderQuoteMedia(quote, prefs, path) if quote.note.len > 0 and not prefs.hideCommunityNotes: @@ -344,12 +379,8 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; if tweet.card.isSome and tweet.card.get().kind != hidden: renderCard(tweet.card.get(), prefs, path) - if tweet.photos.len > 0: - renderAlbum(tweet) - elif tweet.video.isSome: - renderVideo(tweet.video.get(), prefs, path) - elif tweet.gif.isSome: - renderGif(tweet.gif.get(), prefs) + if tweet.media.len > 0: + renderMedia(tweet.media, prefs, path) if tweet.poll.isSome: renderPoll(tweet.poll.get())