diff --git a/public/css/fontello.css b/public/css/fontello.css index d022bb5..9ba8e6f 100644 --- a/public/css/fontello.css +++ b/public/css/fontello.css @@ -1,53 +1,121 @@ @font-face { - font-family: 'fontello'; - src: url('/fonts/fontello.eot?21002321'); - src: url('/fonts/fontello.eot?21002321#iefix') format('embedded-opentype'), - url('/fonts/fontello.woff2?21002321') format('woff2'), - url('/fonts/fontello.woff?21002321') format('woff'), - url('/fonts/fontello.ttf?21002321') format('truetype'), - url('/fonts/fontello.svg?21002321#fontello') format('svg'); + font-family: "fontello"; + src: url("/fonts/fontello.eot?76162212"); + src: + url("/fonts/fontello.eot?76162212#iefix") format("embedded-opentype"), + url("/fonts/fontello.woff2?76162212") format("woff2"), + url("/fonts/fontello.woff?76162212") format("woff"), + url("/fonts/fontello.ttf?76162212") format("truetype"), + url("/fonts/fontello.svg?76162212#fontello") format("svg"); font-weight: normal; font-style: normal; } - - [class^="icon-"]:before, [class*=" icon-"]:before { +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?76162212#fontello') format('svg'); + } +} +*/ +[class^="icon-"]:before, +[class*=" icon-"]:before { font-family: "fontello"; font-style: normal; font-weight: normal; speak: never; - + display: inline-block; text-decoration: inherit; width: 1em; + margin-right: 0.2em; text-align: center; + /* opacity: .8; */ /* For safety - reset parent styles, that can break glyph codes*/ font-variant: normal; text-transform: none; - + /* fix buttons height, for twitter bootstrap */ line-height: 1em; - + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: 0.2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + /* Font smoothing. That was taken from TWBS */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } - -.icon-heart:before { content: '\2665'; } /* '♥' */ -.icon-quote:before { content: '\275e'; } /* '❞' */ -.icon-comment:before { content: '\e802'; } /* '' */ -.icon-ok:before { content: '\e803'; } /* '' */ -.icon-play:before { content: '\e804'; } /* '' */ -.icon-link:before { content: '\e805'; } /* '' */ -.icon-calendar:before { content: '\e806'; } /* '' */ -.icon-location:before { content: '\e807'; } /* '' */ -.icon-picture:before { content: '\e809'; } /* '' */ -.icon-lock:before { content: '\e80a'; } /* '' */ -.icon-down:before { content: '\e80b'; } /* '' */ -.icon-retweet:before { content: '\e80d'; } /* '' */ -.icon-search:before { content: '\e80e'; } /* '' */ -.icon-pin:before { content: '\e80f'; } /* '' */ -.icon-cog:before { content: '\e812'; } /* '' */ -.icon-rss-feed:before { content: '\e813'; } /* '' */ -.icon-info:before { content: '\f128'; } /* '' */ -.icon-bird:before { content: '\f309'; } /* '' */ + +.icon-heart:before { + content: "\2665"; +} /* '♥' */ +.icon-quote:before { + content: "\275e"; +} /* '❞' */ +.icon-ok:before { + content: "\e800"; +} /* '' */ +.icon-play:before { + content: "\e801"; +} /* '' */ +.icon-comment:before { + content: "\e802"; +} /* '' */ +.icon-link:before { + content: "\e803"; +} /* '' */ +.icon-calendar:before { + content: "\e804"; +} /* '' */ +.icon-picture:before { + content: "\e805"; +} /* '' */ +.icon-lock:before { + content: "\e806"; +} /* '' */ +.icon-down:before { + content: "\e807"; +} /* '' */ +.icon-retweet:before { + content: "\e808"; +} /* '' */ +.icon-search:before { + content: "\e809"; +} /* '' */ +.icon-pin:before { + content: "\e80a"; +} /* '' */ +.icon-cog:before { + content: "\e80b"; +} /* '' */ +.icon-info:before { + content: "\e80c"; +} /* '' */ +.icon-bookmark:before { + content: "\e80d"; +} /* '' */ +.icon-eye:before { + content: "\e80e"; +} /* '' */ +.icon-pcf:before { + content: "\e83a"; +} /* '' */ +.icon-location:before { + content: "\f031"; +} /* '' */ +.icon-bird:before { + content: "\f099"; +} /* '' */ +.icon-rss-feed:before { + content: "\f09e"; +} /* '' */ diff --git a/public/fonts/fontello.eot b/public/fonts/fontello.eot index aaddd6b..acc7a20 100644 Binary files a/public/fonts/fontello.eot and b/public/fonts/fontello.eot differ diff --git a/public/fonts/fontello.svg b/public/fonts/fontello.svg index 1f30ccc..53871b9 100644 --- a/public/fonts/fontello.svg +++ b/public/fonts/fontello.svg @@ -1,46 +1,52 @@ -Copyright (C) 2020 by original authors @ fontello.com +Copyright (C) 2025 by original authors @ fontello.com - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - \ No newline at end of file + diff --git a/public/fonts/fontello.ttf b/public/fonts/fontello.ttf index 29f1ec6..dcc5682 100644 Binary files a/public/fonts/fontello.ttf and b/public/fonts/fontello.ttf differ diff --git a/public/fonts/fontello.woff b/public/fonts/fontello.woff index 8428cf8..8ec4ef8 100644 Binary files a/public/fonts/fontello.woff and b/public/fonts/fontello.woff differ diff --git a/public/fonts/fontello.woff2 b/public/fonts/fontello.woff2 index 551f49d..be42172 100644 Binary files a/public/fonts/fontello.woff2 and b/public/fonts/fontello.woff2 differ diff --git a/src/consts.nim b/src/consts.nim index 4d5c0b6..dc0cbe0 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -17,8 +17,8 @@ const graphql = api / "graphql" graphUser* = graphql / "32pL5BWe9WKeSK1MoPvFQQ/UserByScreenName" graphUserById* = graphql / "5vdJ5sWkbSRDiiNZvwc2Yg/UserByRestId" - graphUserTweets* = graphql / "3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2" - graphUserTweetsAndReplies* = graphql / "8IS8MaO-2EN6GZZZb8jF0g/UserWithProfileTweetsAndRepliesQueryV2" + graphUserTweets* = graphql / "M3Hpkrb8pjWkEuGdLeXMOA/UserTweets" + graphUserTweetsAndReplies* = graphql / "pz0IHaV_t7T4HJavqqqcIA/UserTweetsAndReplies" graphUserMedia* = graphql / "dexO_2tohK86JDudXXG3Yw/UserMedia" graphTweet* = graphql / "y90SwUGBZ3yz0yNUmCHgTw/TweetDetail" graphTweetResult* = graphql / "sITyJdhRPpvpEjg4waUmTA/TweetResultByIdQuery" @@ -95,7 +95,7 @@ const "unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false, "verified_phone_label_enabled": false, "vibe_api_enabled": false, - "view_counts_everywhere_api_enabled": false, + "view_counts_everywhere_api_enabled": true, "premium_content_api_read_enabled": false, "responsive_web_grok_analyze_button_fetch_trends_enabled": false, "responsive_web_grok_analysis_button_from_backend": false, @@ -108,7 +108,8 @@ const "responsive_web_twitter_article_notes_tab_enabled": false, "subscriptions_feature_can_gift_premium": false, "hidden_profile_subscriptions_enabled": true, - "subscriptions_verification_info_is_identity_verified_enabled": true + "subscriptions_verification_info_is_identity_verified_enabled": true, + "responsive_web_grok_show_grok_translated_post": false }""".replace(" ", "").replace("\n", "") tweetVariables* = """{ @@ -131,9 +132,13 @@ const }""".replace(" ", "").replace("\n", "") userTweetsVariables* = """{ - "rest_id": "$1", + "userId": "$1", $2 - "count": 20 + "count": 20, + "includePromotedContent": false, + "withCommunity": true, + "withQuickPromoteEligibilityTweetFields": false, + "withVoice": true }""".replace(" ", "").replace("\n", "") listTweetsVariables* = """{ diff --git a/src/formatters.nim b/src/formatters.nim index 8e5fa47..c7c0d7e 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -7,6 +7,7 @@ const cards = "cards.twitter.com/cards" tco = "https://t.co" twitter = parseUri("https://twitter.com") + sameProto = "//" let twRegex = re"(?<=(? 0 and ("reddit.com" in result or "redd.it" in result): result = result.replace(rdShortRegex, prefs.replaceReddit & "/comments/") diff --git a/src/nitter.nim b/src/nitter.nim index 1f4c7d9..a813490 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -14,7 +14,7 @@ import routes/[ activityspoof] const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" -const issuesUrl = "https://github.com/zedeus/nitter/issues" +const issuesUrl = "https://gitlab.com/Cynosphere/nitter/issues" #let accountsPath = getEnv("NITTER_ACCOUNTS_FILE", "./guest_accounts.json") @@ -80,7 +80,7 @@ routes: error InternalError: echo error.exc.name, ": ", error.exc.msg - const link = a("open a GitHub issue", href = issuesUrl) + const link = a("open an issue", href = issuesUrl) resp Http500, showError( &"An error occurred, please {link} with the URL you tried to visit.", cfg) diff --git a/src/parser.nim b/src/parser.nim index 1759e8c..c691792 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -39,14 +39,28 @@ proc parseGraphUser*(js: JsonNode): User = result = parseUser(user{"legacy"}, user{"rest_id"}.getStr) let label = user{"affiliates_highlighted_label", "label"} - if not label.isNull and label{"userLabelType"}.getStr == "AutomatedLabel": - result.bot = true - let entities = label{"longDescription", "entities"} - if not entities.isNull: - for ent in entities: - if ent{"ref", "type"}.getStr != "TimelineRichTextMention": continue - result.botOwner = ent{"ref", "screen_name"}.getStr - break + if not label.isNull: + let labelType = label{"userLabelType"}.getStr + if labelType == "AutomatedLabel": + result.bot = true + let entities = label{"longDescription", "entities"} + if not entities.isNull: + for ent in entities: + if ent{"ref", "type"}.getStr != "TimelineRichTextMention": continue + result.botOwner = ent{"ref", "screen_name"}.getStr + break + # TODO: are there other types than the two + # TODO: find profile with "userLabelDisplayType" not equal to "Badge" + elif labelType == "BusinessLabel": + result.badge = Badge( + name: label{"description"}.getStr, + icon: label{"badge", "url"}.getStr, + url: label{"url", "url"}.getStr + ) + + let pcf = user{"parody_commentary_fan_label"}.getStr + if pcf.len > 0 and pcf != "None": + result.pcf = pcf if result.verifiedType == VerifiedType.none and user{"is_blue_verified"}.getBool(false): result.verifiedType = blue @@ -230,7 +244,8 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet = replies: js{"reply_count"}.getInt, retweets: js{"retweet_count"}.getInt, likes: js{"favorite_count"}.getInt, - quotes: js{"quote_count"}.getInt + quotes: js{"quote_count"}.getInt, + bookmarks: js{"bookmark_count"}.getInt ) ) @@ -491,6 +506,11 @@ proc parseGraphTweet(js: JsonNode; isLegacy=false): Tweet = note.expandBirdwatchEntities(communityNote{"subtitle"}) result.birdwatch = some(note) + if not js{"views", "count"}.isNull: + result.stats.views = parseInt(js{"views", "count"}.getStr) + else: + result.stats.views = -1 + proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = for t in js{"content", "items"}: let entryId = t{"entryId"}.getStr @@ -562,28 +582,12 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile = let instructions = if root == "list": ? js{"data", "list", "timeline_response", "timeline", "instructions"} - elif root == "user": ? js{"data", "user_result", "result", "timeline_response", "timeline", "instructions"} else: ? js{"data", "user", "result", "timeline", "timeline", "instructions"} if instructions.len == 0: return for i in instructions: - if i{"__typename"}.getStr == "TimelineAddEntries": - for e in i{"entries"}: - let entryId = e{"entryId"}.getStr - if entryId.startsWith("tweet"): - with tweetResult, e{"content", "content", "tweetResult", "result"}: - let tweet = parseGraphTweet(tweetResult, false) - if not tweet.available: - tweet.id = entryId.getId() - result.tweets.content.add tweet - elif "-conversation-" in entryId or entryId.startsWith("homeConversation"): - let (thread, self) = parseGraphThread(e) - result.tweets.content.add thread.content - elif entryId.startsWith("cursor-bottom"): - result.tweets.bottom = e{"content", "value"}.getStr - # TODO cleanup if i{"type"}.getStr == "TimelineAddEntries": for e in i{"entries"}: let entryId = e{"entryId"}.getStr @@ -598,8 +602,8 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile = result.tweets.content.add thread.content elif entryId.startsWith("cursor-bottom"): result.tweets.bottom = e{"content", "value"}.getStr - if after.len == 0 and i{"__typename"}.getStr == "TimelinePinEntry": - with tweetResult, i{"entry", "content", "content", "tweetResult", "result"}: + if after.len == 0 and i{"type"}.getStr == "TimelinePinEntry": + with tweetResult, i{"entry", "content", "itemContent", "tweet_results", "result"}: let tweet = parseGraphTweet(tweetResult, false) tweet.pinned = true if not tweet.available and tweet.tombstone.len == 0: diff --git a/src/sass/general.scss b/src/sass/general.scss index 9feb3d3..debf89f 100644 --- a/src/sass/general.scss +++ b/src/sass/general.scss @@ -1,39 +1,49 @@ -@import '_variables'; -@import '_mixins'; +@import "_variables"; +@import "_mixins"; .panel-container { - margin: auto; - font-size: 130%; + margin: auto; + font-size: 130%; } .error-panel { - @include center-panel(var(--error_red)); - text-align: center; + @include center-panel(var(--error_red)); + text-align: center; } .search-bar > form { - @include center-panel(var(--darkest_grey)); + @include center-panel(var(--darkest_grey)); - button { - background: var(--bg_elements); - color: var(--fg_color); - border: 0; - border-radius: 3px; - cursor: pointer; - font-weight: bold; - width: 30px; - height: 30px; - } + button { + background: var(--bg_elements); + color: var(--fg_color); + border: 0; + border-radius: 3px; + cursor: pointer; + font-weight: bold; + width: 30px; + height: 30px; + } - input { - font-size: 16px; - width: 100%; - background: var(--bg_elements); - color: var(--fg_color); - border: 0; - border-radius: 4px; - padding: 4px; - margin-right: 8px; - height: unset; - } + input { + font-size: 16px; + width: 100%; + background: var(--bg_elements); + color: var(--fg_color); + border: 0; + border-radius: 4px; + padding: 4px; + margin-right: 8px; + height: unset; + } +} + +.brand-badge { + margin-left: 4px; +} +.brand-badge-image { + width: 16px; + height: 16px; + border: 1px solid var(--accent_border); + margin-bottom: -4px; } diff --git a/src/sass/profile/card.scss b/src/sass/profile/card.scss index f3e25f5..781ddae 100644 --- a/src/sass/profile/card.scss +++ b/src/sass/profile/card.scss @@ -83,7 +83,8 @@ .profile-joindate, .profile-location, .profile-website, - .profile-automated { + .profile-automated, + .profile-pcf { color: var(--fg_faded); margin: 1px 0; width: 100%; diff --git a/src/sass/tweet/_base.scss b/src/sass/tweet/_base.scss index 513d4f4..e5e8667 100644 --- a/src/sass/tweet/_base.scss +++ b/src/sass/tweet/_base.scss @@ -47,6 +47,11 @@ display: flex; justify-content: space-between; } +.tweet-label-row { + padding: 0; + display: flex; + gap: 0.4em; +} .fullname-and-username { display: flex; @@ -69,10 +74,10 @@ word-wrap: normal; } -.user-automated { +.user-automated, +.user-pcf { @include ellipsis; min-width: 1px; - margin-left: 0.4em; color: var(--fg_faded); } diff --git a/src/types.nim b/src/types.nim index 496e300..b3c69e2 100644 --- a/src/types.nim +++ b/src/types.nim @@ -80,6 +80,11 @@ type business = "Business" government = "Government" + Badge* = object + name*: string + icon*: string + url*: string + User* = object id*: string username*: string @@ -101,6 +106,8 @@ type joinDate*: DateTime bot*: bool botOwner*: string + pcf*: string + badge*: Badge VideoType* = enum m3u8 = "application/x-mpegURL" @@ -203,6 +210,8 @@ type retweets*: int likes*: int quotes*: int + bookmarks*: int + views*: int BirdwatchNote* = ref object id*: string diff --git a/src/views/general.nim b/src/views/general.nim index 6e96a0c..82197da 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -55,7 +55,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; buildHtml(head): link(rel="stylesheet", type="text/css", href="/css/style.css?v=20") - link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2") + link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=3") link(rel="stylesheet", href="/css/baguetteBox.min.css") script(src="/js/baguetteBox.min.js", `async`="") script(src="/js/zoom.js") @@ -183,7 +183,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; # this is last so images are also preloaded # if this is done earlier, Chrome only preloads one image for some reason link(rel="preload", type="font/woff2", `as`="font", - href="/fonts/fontello.woff2?21002321", crossorigin="anonymous") + href="/fonts/fontello.woff2?76162212", crossorigin="anonymous") proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs; titleText=""; desc=""; ogTitle=""; rss=""; video=""; diff --git a/src/views/profile.nim b/src/views/profile.nim index 62b3c3b..625294c 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -26,8 +26,8 @@ proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = tdiv(class="profile-card-tabs-name-and-follow"): tdiv(): - linkUser(user, class="profile-card-fullname") - linkUser(user, class="profile-card-username") + linkUser(user, class="profile-card-fullname", prefs) + linkUser(user, class="profile-card-username", prefs) let following = isFollowing(user.username, prefs.following) if not following: buttonReferer "/follow/" & user.username, "Follow", path, "profile-card-follow-button" @@ -44,6 +44,11 @@ proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = else: icon "cog", "Automated" + if user.pcf.len > 0: + tdiv(class="profile-pcf"): + span: + icon "pcf", &"{user.pcf} account" + if user.bio.len > 0: tdiv(class="profile-bio"): p(dir="auto"): diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index 310d283..87ecf84 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only import strutils, strformat import karax/[karaxdsl, vdom, vstyles] -import ".."/[types, utils] +import ".."/[types, utils, formatters] const smallWebp* = "?name=small&format=webp" @@ -30,7 +30,7 @@ template verifiedIcon*(user: User): untyped {.dirty.} = else: text "" -proc linkUser*(user: User, class=""): VNode = +proc linkUser*(user: User, class="", prefs: Prefs): VNode = let isName = "username" notin class href = "/" & user.username @@ -44,6 +44,10 @@ proc linkUser*(user: User, class=""): VNode = if user.protected: text " " icon "lock", title="Protected account" + if user.badge.name.len > 0: + span(class="brand-badge"): + a(href=replaceUrls(user.badge.url, prefs), title=user.badge.name): + img(class="brand-badge-image", src=user.badge.icon, alt=user.badge.name) proc linkText*(text: string; class=""): VNode = let url = if "http" notin text: https & text else: text diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 34a6e4e..a941965 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -74,8 +74,8 @@ proc renderUser*(user: User; prefs: Prefs): VNode = tdiv(class="tweet-name-row"): tdiv(class="fullname-and-username"): - linkUser(user, class="fullname") - linkUser(user, class="username") + linkUser(user, class="fullname", prefs) + linkUser(user, class="username", prefs) tdiv(class="tweet-content media-body", dir="auto"): verbatim replaceUrls(user.bio, prefs) diff --git a/src/views/tweet.nim b/src/views/tweet.nim index f990cb4..1ad0866 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -15,32 +15,40 @@ proc renderMiniAvatar(user: User; prefs: Prefs): VNode = img(class=(prefs.getAvatarClass & " mini"), src=url) proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VNode = + let user = tweet.user + buildHtml(tdiv): if pinned: tdiv(class="pinned"): - span: icon "pin", "Pinned Tweet" + span: icon "pin", "Pinned" elif retweet.len > 0: tdiv(class="retweet-header"): span: icon "retweet", retweet & " retweeted" tdiv(class="tweet-header"): - a(class="tweet-avatar", href=("/" & tweet.user.username)): + a(class="tweet-avatar", href=("/" & user.username)): var size = "_bigger" - if not prefs.autoplayGifs and tweet.user.userPic.endsWith("gif"): + if not prefs.autoplayGifs and user.userPic.endsWith("gif"): size = "_400x400" - genImg(tweet.user.getUserPic(size), class=prefs.getAvatarClass) + genImg(user.getUserPic(size), class=prefs.getAvatarClass) tdiv(class="tweet-name-row"): tdiv(class="fullname-and-username"): - linkUser(tweet.user, class="fullname") - linkUser(tweet.user, class="username") - if tweet.user.bot: - tdiv(class="user-automated"): icon "cog", "Automated" + linkUser(user, class="fullname", prefs) + linkUser(user, class="username", prefs) span(class="tweet-date"): a(href=getLink(tweet), title=tweet.getTime): text tweet.getShortTime + if user.pcf.len > 0 or user.bot: + tdiv(class="tweet-label-row"): + if user.bot: + tdiv(class="user-automated"): icon "cog", "Automated" + + if user.pcf.len > 0: + tdiv(class="user-pcf"): icon "pcf", &"{user.pcf} account" + proc renderAlbum(tweet: Tweet): VNode = let groups = if tweet.photos.len < 3: @[tweet.photos] @@ -185,7 +193,7 @@ func formatStat(stat: int): string = if stat > 0: insertSep($stat, ',') else: "" -proc renderStats(stats: TweetStats; views: string; tweet: Tweet): VNode = +proc renderStats(stats: TweetStats; tweet: Tweet): VNode = buildHtml(tdiv(class="tweet-stats")): a(href=getLink(tweet)): span(class="tweet-stat"): icon "comment", formatStat(stats.replies) @@ -193,11 +201,10 @@ proc renderStats(stats: TweetStats; views: string; tweet: Tweet): VNode = span(class="tweet-stat"): icon "retweet", formatStat(stats.retweets) a(href="/search?q=quoted_tweet_id:" & $tweet.id): span(class="tweet-stat"): icon "quote", formatStat(stats.quotes) - a(): - span(class="tweet-stat"): icon "heart", formatStat(stats.likes) - a(href=getLink(tweet)): - if views.len > 0: - span(class="tweet-stat"): icon "play", insertSep(views, ',') + a(): span(class="tweet-stat"): icon "heart", formatStat(stats.likes) + a(): span(class="tweet-stat"): icon "bookmark", formatStat(stats.bookmarks) + if stats.views > -1: + a(): span(class="tweet-stat"): icon "eye", formatStat(stats.views) proc renderReply(tweet: Tweet): VNode = buildHtml(tdiv(class="replying-to")): @@ -248,8 +255,8 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = tdiv(class="tweet-name-row"): tdiv(class="fullname-and-username"): renderMiniAvatar(quote.user, prefs) - linkUser(quote.user, class="fullname") - linkUser(quote.user, class="username") + linkUser(quote.user, class="fullname", prefs) + linkUser(quote.user, class="username", prefs) span(class="tweet-date"): a(href=getLink(quote), title=quote.getTime): @@ -322,7 +329,6 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; a(class="tweet-link", href=getLink(tweet)) tdiv(class="tweet-body"): - var views = "" renderHeader(tweet, retweet, pinned, prefs) if not afterTweet and index == 0 and tweet.reply.len > 0 and @@ -347,10 +353,8 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; if tweet.video.isSome: renderVideo(tweet.video.get(), prefs, path) - views = tweet.video.get().views elif tweet.gif.isSome: renderGif(tweet.gif.get(), prefs) - views = "GIF" if tweet.poll.isSome: renderPoll(tweet.poll.get()) @@ -368,7 +372,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; renderMediaTags(tweet.mediaTags) if not prefs.hideTweetStats: - renderStats(tweet.stats, views, tweet) + renderStats(tweet.stats, tweet) if showThread: a(class="show-thread", href=("/i/status/" & $tweet.threadId)):