From b79ef438095af6c7b21a6e68fbb109e179091983 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Wed, 10 Mar 2021 12:53:28 -0800 Subject: [PATCH 01/11] Add a Timeline section to the preferences page The timeline section currently has one preference, following, which is a comma-separated field of people to display on the home timeline. --- src/prefs_impl.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim index 0223c82c1..3a633548b 100644 --- a/src/prefs_impl.nim +++ b/src/prefs_impl.nim @@ -50,6 +50,11 @@ macro genPrefs*(prefDsl: untyped) = const `name`*: PrefList = toOrderedTable(`table`) genPrefs: + Timeline: + following(input, ""): + "A comma-separated list of users to follow." + placeholder: "one,two,three" + Display: theme(select, "Nitter"): "Theme" From 5ce94e37ddf749049d0a7bbbc0b5920ed4603aaa Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Wed, 10 Mar 2021 20:39:33 -0800 Subject: [PATCH 02/11] Add base home route --- src/nitter.nim | 6 ++---- src/routes/home.nim | 11 +++++++++++ src/views/home.nim | 7 +++++++ 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/routes/home.nim create mode 100644 src/views/home.nim diff --git a/src/nitter.nim b/src/nitter.nim index 9f8fcb7b8..c79ff2970 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -9,7 +9,7 @@ import jester import types, config, prefs, formatters, redis_cache, http_pool, tokens import views/[general, about] import routes/[ - preferences, timeline, status, media, search, rss, list, debug, + home, preferences, timeline, status, media, search, rss, list, debug, unsupported, embed, resolver, router_utils] const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" @@ -58,9 +58,6 @@ settings: bindAddr = cfg.address routes: - get "/": - resp renderMain(renderSearch(), request, cfg, themePrefs()) - get "/about": resp renderMain(renderAbout(), request, cfg, themePrefs()) @@ -90,6 +87,7 @@ routes: resp Http429, showError( &"Instance has been rate limited.
Use {link} or try again later.", cfg) + extend home, "" extend unsupported, "" extend preferences, "" extend resolver, "" diff --git a/src/routes/home.nim b/src/routes/home.nim new file mode 100644 index 000000000..a1e4e18e2 --- /dev/null +++ b/src/routes/home.nim @@ -0,0 +1,11 @@ +import jester +import asyncdispatch, strutils, options, router_utils +import ".."/[prefs, types, utils] +import ../views/[general, home] + +export home + +proc createHomeRouter*(cfg: Config) = + router home: + get "/": + resp renderMain(renderHome(), request, cfg, themePrefs()) diff --git a/src/views/home.nim b/src/views/home.nim new file mode 100644 index 000000000..2d140ac7d --- /dev/null +++ b/src/views/home.nim @@ -0,0 +1,7 @@ +import karax/[karaxdsl, vdom] +import ../types + +proc renderHome*(): VNode = + buildHtml(tdiv(class="overlay-panel")): + h2: text "Timeline" + p: text "Coming soon!" From 3ad92e13ebafc5ad06f9c048d6feba1216af5ce3 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Sat, 13 Mar 2021 12:52:27 -0800 Subject: [PATCH 03/11] Add initial implementation of home timeline Currently just a copy of timeline.nim that takes names from the following cookie rather than the url. --- src/routes/home.nim | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/routes/home.nim b/src/routes/home.nim index a1e4e18e2..421c2c83d 100644 --- a/src/routes/home.nim +++ b/src/routes/home.nim @@ -1,11 +1,40 @@ import jester -import asyncdispatch, strutils, options, router_utils +import asyncdispatch, strutils, options, router_utils, timeline import ".."/[prefs, types, utils] -import ../views/[general, home] +import ../views/[general, home, search] export home proc createHomeRouter*(cfg: Config) = router home: get "/": - resp renderMain(renderHome(), request, cfg, themePrefs()) + let + prefs = cookiePrefs() + after = getCursor() + names = getNames(prefs.following) + + var query = request.getQuery("", prefs.following) + if names.len != 1: + query.fromUser = names + + if @"scroll".len > 0: + if query.fromUser.len != 1: + var timeline = await getSearch[Tweet](query, after) + if timeline.content.len == 0: resp Http404 + timeline.beginning = true + resp $renderTweetSearch(timeline, prefs, getPath()) + else: + var (_, timeline, _) = await fetchSingleTimeline(after, query, skipRail=true) + if timeline.content.len == 0: resp Http404 + timeline.beginning = true + resp $renderTimelineTweets(timeline, prefs, getPath()) + + var rss = "/$1/$2/rss" % [@"name", @"tab"] + if @"tab".len == 0: + rss = "/$1/rss" % @"name" + elif @"tab" == "search": + rss &= "?" & genQueryUrl(query) + + if names.len == 0: + resp renderMain(renderSearch(), request, cfg, themePrefs()) + respTimeline(await showTimeline(request, query, cfg, prefs, rss, after)) From 3e2f08f202566d874dbbf9c5ea8f838a2b8d9b01 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Sat, 13 Mar 2021 23:19:58 -0800 Subject: [PATCH 04/11] Add functional timeline to home page --- src/routes/home.nim | 32 +++++++++++++------------------- src/views/home.nim | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/routes/home.nim b/src/routes/home.nim index 421c2c83d..7720c2459 100644 --- a/src/routes/home.nim +++ b/src/routes/home.nim @@ -5,6 +5,13 @@ import ../views/[general, home, search] export home +proc showHome*(request: Request; query: Query; cfg: Config; prefs: Prefs; + after: string): Future[string] {.async.} = + let + timeline = await getSearch[Tweet](query, after) + html = renderHome(timeline, prefs, getPath()) + return renderMain(html, request, cfg, prefs, "Multi") + proc createHomeRouter*(cfg: Config) = router home: get "/": @@ -14,27 +21,14 @@ proc createHomeRouter*(cfg: Config) = names = getNames(prefs.following) var query = request.getQuery("", prefs.following) - if names.len != 1: - query.fromUser = names + query.fromUser = names if @"scroll".len > 0: - if query.fromUser.len != 1: - var timeline = await getSearch[Tweet](query, after) - if timeline.content.len == 0: resp Http404 - timeline.beginning = true - resp $renderTweetSearch(timeline, prefs, getPath()) - else: - var (_, timeline, _) = await fetchSingleTimeline(after, query, skipRail=true) - if timeline.content.len == 0: resp Http404 - timeline.beginning = true - resp $renderTimelineTweets(timeline, prefs, getPath()) + var timeline = await getSearch[Tweet](query, after) + if timeline.content.len == 0: resp Http404 + timeline.beginning = true + resp $renderHome(timeline, prefs, getPath()) - var rss = "/$1/$2/rss" % [@"name", @"tab"] - if @"tab".len == 0: - rss = "/$1/rss" % @"name" - elif @"tab" == "search": - rss &= "?" & genQueryUrl(query) - if names.len == 0: resp renderMain(renderSearch(), request, cfg, themePrefs()) - respTimeline(await showTimeline(request, query, cfg, prefs, rss, after)) + resp (await showHome(request, query, cfg, prefs, after)) diff --git a/src/views/home.nim b/src/views/home.nim index 2d140ac7d..0ef738c85 100644 --- a/src/views/home.nim +++ b/src/views/home.nim @@ -1,7 +1,15 @@ -import karax/[karaxdsl, vdom] +import karax/[karaxdsl, vdom], strutils +import search, timeline import ../types -proc renderHome*(): VNode = - buildHtml(tdiv(class="overlay-panel")): - h2: text "Timeline" - p: text "Coming soon!" +proc renderHome*(results: Result[Tweet]; prefs: Prefs; path: string): VNode = + let query = results.query + buildHtml(tdiv(class="timeline-container")): + if query.fromUser.len > 0: + renderProfileTabs(query, query.fromUser.join(",")) + + if query.fromUser.len == 0 or query.kind == tweets: + tdiv(class="timeline-header"): + renderSearchPanel(query) + + renderTimelineTweets(results, prefs, path) From d2756e60d3e0b006e4e7b7752bcfa7a24926b1da Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Sun, 14 Mar 2021 19:49:30 -0700 Subject: [PATCH 05/11] Remove Multi from homepage title with timeline --- src/routes/home.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/home.nim b/src/routes/home.nim index 7720c2459..d3ae48775 100644 --- a/src/routes/home.nim +++ b/src/routes/home.nim @@ -10,7 +10,7 @@ proc showHome*(request: Request; query: Query; cfg: Config; prefs: Prefs; let timeline = await getSearch[Tweet](query, after) html = renderHome(timeline, prefs, getPath()) - return renderMain(html, request, cfg, prefs, "Multi") + return renderMain(html, request, cfg, prefs) proc createHomeRouter*(cfg: Config) = router home: From fef03ac70f5b4ec3e366a1488a1c51bf58035502 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Tue, 16 Mar 2021 19:16:42 -0700 Subject: [PATCH 06/11] Change layout of home timeline to match lists --- src/routes/home.nim | 17 ++++++++++++++++- src/views/home.nim | 21 +++++++++++++++++++-- src/views/timeline.nim | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/routes/home.nim b/src/routes/home.nim index d3ae48775..d2d6c3298 100644 --- a/src/routes/home.nim +++ b/src/routes/home.nim @@ -1,6 +1,6 @@ import jester import asyncdispatch, strutils, options, router_utils, timeline -import ".."/[prefs, types, utils] +import ".."/[prefs, types, utils, redis_cache] import ../views/[general, home, search] export home @@ -32,3 +32,18 @@ proc createHomeRouter*(cfg: Config) = if names.len == 0: resp renderMain(renderSearch(), request, cfg, themePrefs()) resp (await showHome(request, query, cfg, prefs, after)) + get "/following": + let + prefs = cookiePrefs() + names = getNames(prefs.following) + var + profs: seq[Profile] + query = request.getQuery("", prefs.following) + query.fromUser = names + query.kind = userList + + for name in names: + let prof = await getCachedProfile(name) + profs &= @[prof] + + resp renderMain(renderFollowing(query, profs, prefs), request, cfg, prefs) diff --git a/src/views/home.nim b/src/views/home.nim index 0ef738c85..be1322857 100644 --- a/src/views/home.nim +++ b/src/views/home.nim @@ -1,15 +1,32 @@ import karax/[karaxdsl, vdom], strutils -import search, timeline +import search, timeline, renderutils import ../types +proc renderFollowingUsers*(results: seq[Profile]; prefs: Prefs): VNode = + buildHtml(tdiv(class="timeline")): + for user in results: + renderUser(user, prefs) + +proc renderHomeTabs*(query: Query): VNode = + buildHtml(ul(class="tab")): + li(class=query.getTabClass(posts)): + a(href="/"): text "Tweets" + li(class=query.getTabClass(userList)): + a(href=("/following")): text "Following" + proc renderHome*(results: Result[Tweet]; prefs: Prefs; path: string): VNode = let query = results.query buildHtml(tdiv(class="timeline-container")): if query.fromUser.len > 0: - renderProfileTabs(query, query.fromUser.join(",")) + renderHomeTabs(query) if query.fromUser.len == 0 or query.kind == tweets: tdiv(class="timeline-header"): renderSearchPanel(query) renderTimelineTweets(results, prefs, path) + +proc renderFollowing*(query: Query; following: seq[Profile]; prefs: Prefs): VNode = + buildHtml(tdiv(class="timeline-container")): + renderHomeTabs(query) + renderFollowingUsers(following, prefs) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 54cad7ab3..f2e8da57a 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -57,7 +57,7 @@ proc threadFilter(tweets: openArray[Tweet]; threads: openArray[int64]; it: Tweet elif t.replyId == result[0].id: result.add t -proc renderUser(user: User; prefs: Prefs): VNode = +proc renderUser*(user: User; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline-item")): a(class="tweet-link", href=("/" & user.username)) tdiv(class="tweet-body profile-result"): From cfcc0da37a1e5fa4a682a622363bb81a9e79a346 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Tue, 16 Mar 2021 23:32:55 -0700 Subject: [PATCH 07/11] Remove unused import in src/views/home.nim --- src/views/home.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/home.nim b/src/views/home.nim index be1322857..83b49030f 100644 --- a/src/views/home.nim +++ b/src/views/home.nim @@ -1,4 +1,4 @@ -import karax/[karaxdsl, vdom], strutils +import karax/[karaxdsl, vdom] import search, timeline, renderutils import ../types From bc64bb67a0bb9a6bd47ce9bbb2b33f27feef50a1 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Wed, 17 Mar 2021 19:17:49 -0700 Subject: [PATCH 08/11] Add non-functional follow/unfollow buttons The buttons currently send a post request to two new routes, /follow and /unfollow, which immediately redirect back to the referrer. They do not modify the list of followers, but do accurately change based on whether or not the viewed profile is currently in the list of followers. --- src/nitter.nim | 3 ++- src/routes/follow.nim | 12 ++++++++++++ src/sass/profile/card.scss | 11 +++++++++-- src/views/profile.nim | 16 +++++++++++----- src/views/renderutils.nim | 4 ++++ 5 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 src/routes/follow.nim diff --git a/src/nitter.nim b/src/nitter.nim index c79ff2970..96802c477 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -10,7 +10,7 @@ import types, config, prefs, formatters, redis_cache, http_pool, tokens import views/[general, about] import routes/[ home, preferences, timeline, status, media, search, rss, list, debug, - unsupported, embed, resolver, router_utils] + unsupported, embed, resolver, router_utils, follow] const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" const issuesUrl = "https://github.com/zedeus/nitter/issues" @@ -88,6 +88,7 @@ routes: &"Instance has been rate limited.
Use {link} or try again later.", cfg) extend home, "" + extend follow, "" extend unsupported, "" extend preferences, "" extend resolver, "" diff --git a/src/routes/follow.nim b/src/routes/follow.nim new file mode 100644 index 000000000..b35dd4134 --- /dev/null +++ b/src/routes/follow.nim @@ -0,0 +1,12 @@ +import jester +import asyncdispatch, strutils, options, router_utils +import ".."/[prefs, types, utils, redis_cache] + +export follow + +proc createFollowRouter*(cfg: Config) = + router follow: + post "/follow": + redirect(refPath()) + post "/unfollow": + redirect(refPath()) diff --git a/src/sass/profile/card.scss b/src/sass/profile/card.scss index cc68d7d30..fb1b61ee8 100644 --- a/src/sass/profile/card.scss +++ b/src/sass/profile/card.scss @@ -13,9 +13,12 @@ width: 100%; } -.profile-card-tabs-name { +.profile-card-tabs-name-and-follow { @include breakable; - max-width: 100%; + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: space-between; } .profile-card-username { @@ -34,6 +37,10 @@ max-width: 100%; } +.profile-card-follow-button { + float: none; +} + .profile-card-avatar { display: inline-block; position: relative; diff --git a/src/views/profile.nim b/src/views/profile.nim index 9eda46da1..4552978d2 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -12,7 +12,7 @@ proc renderStat(num: int; class: string; text=""): VNode = span(class="profile-stat-num"): text insertSep($num, ',') -proc renderUserCard*(user: User; prefs: Prefs): VNode = +proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="profile-card")): tdiv(class="profile-card-info"): let @@ -24,9 +24,15 @@ proc renderUserCard*(user: User; prefs: Prefs): VNode = a(class="profile-card-avatar", href=url, target="_blank"): genImg(user.getUserPic(size)) - tdiv(class="profile-card-tabs-name"): - linkUser(user, class="profile-card-fullname") - linkUser(user, class="profile-card-username") + tdiv(class="profile-card-tabs-name-and-follow"): + tdiv(): + linkUser(user, class="profile-card-fullname") + linkUser(user, class="profile-card-username") + let following = isFollowing(user.username, prefs.following) + if not following: + buttonReferer "/follow", "Follow", path, "profile-card-follow-button" + else: + buttonReferer "/unfollow", "Unfollow", path, "profile-card-follow-button" tdiv(class="profile-card-extra"): if user.bio.len > 0: @@ -106,7 +112,7 @@ proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode = let sticky = if prefs.stickyProfile: " sticky" else: "" tdiv(class=(&"profile-tab{sticky}")): - renderUserCard(profile.user, prefs) + renderUserCard(profile.user, prefs, path) if profile.photoRail.len > 0: renderPhotoRail(profile) diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index 3e0cd19d5..8a80e701b 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -94,3 +94,7 @@ proc getAvatarClass*(prefs: Prefs): string = "avatar" else: "avatar round" + +proc isFollowing*(name, following: string): bool = + let following = following.split(",") + return name in following From 76e05a1bfb1375850d413e4bdedae6926707d73c Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Fri, 19 Mar 2021 16:58:38 -0700 Subject: [PATCH 09/11] Add functionality to un/follow buttons The router_utils savePref function didn't work in my testing, so I am setting the cookie directly through Jester's setCookie procedure. --- src/routes/follow.nim | 38 +++++++++++++++++++++++++++++++++----- src/views/profile.nim | 4 ++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/routes/follow.nim b/src/routes/follow.nim index b35dd4134..208e23067 100644 --- a/src/routes/follow.nim +++ b/src/routes/follow.nim @@ -1,12 +1,40 @@ -import jester -import asyncdispatch, strutils, options, router_utils -import ".."/[prefs, types, utils, redis_cache] +import jester, asyncdispatch, strutils, sequtils +import router_utils +import ../types export follow +proc addUserToFollowing*(following, toAdd: string): string = + var updated = following.split(",") + if updated == @[""]: + return toAdd + else: + updated = concat(updated, @[toAdd]) + result = updated.join(",") + +proc removeUserFromFollowing*(following, remove: string): string = + var updated = following.split(",") + if updated == @[""]: + return "" + else: + updated = filter(updated, proc(x: string): bool = x != remove) + result = updated.join(",") + proc createFollowRouter*(cfg: Config) = router follow: - post "/follow": + post "/follow/@name": + let + following = cookiePrefs().following + toAdd = @"name" + updated = addUserToFollowing(following, toAdd) + setCookie("following", updated, daysForward(360), + httpOnly=true, secure=cfg.useHttps, path="/") redirect(refPath()) - post "/unfollow": + post "/unfollow/@name": + let + following = cookiePrefs().following + remove = @"name" + updated = removeUserFromFollowing(following, remove) + setCookie("following", updated, daysForward(360), + httpOnly=true, secure=cfg.useHttps, path="/") redirect(refPath()) diff --git a/src/views/profile.nim b/src/views/profile.nim index 4552978d2..57ee85b15 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -30,9 +30,9 @@ proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = linkUser(user, class="profile-card-username") let following = isFollowing(user.username, prefs.following) if not following: - buttonReferer "/follow", "Follow", path, "profile-card-follow-button" + buttonReferer "/follow/" & profile.username, "Follow", path, "profile-card-follow-button" else: - buttonReferer "/unfollow", "Unfollow", path, "profile-card-follow-button" + buttonReferer "/unfollow/" & profile.username, "Unfollow", path, "profile-card-follow-button" tdiv(class="profile-card-extra"): if user.bio.len > 0: From 69d4aaa8037264c6e18e040b1c4de8a9685105b2 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Fri, 19 Mar 2021 17:30:36 -0700 Subject: [PATCH 10/11] Add a check to prevent following someone twice --- src/routes/follow.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/follow.nim b/src/routes/follow.nim index 208e23067..a0d90bacc 100644 --- a/src/routes/follow.nim +++ b/src/routes/follow.nim @@ -8,6 +8,8 @@ proc addUserToFollowing*(following, toAdd: string): string = var updated = following.split(",") if updated == @[""]: return toAdd + elif toAdd in updated: + return following else: updated = concat(updated, @[toAdd]) result = updated.join(",") From 3056d7c2db15754c6538687d03c7ddf2ee5be7a9 Mon Sep 17 00:00:00 2001 From: FIGBERT Date: Mon, 9 May 2022 10:46:42 -0700 Subject: [PATCH 11/11] Use the User type instead of Profile --- src/routes/home.nim | 4 ++-- src/views/home.nim | 4 ++-- src/views/profile.nim | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/routes/home.nim b/src/routes/home.nim index d2d6c3298..712c73978 100644 --- a/src/routes/home.nim +++ b/src/routes/home.nim @@ -37,13 +37,13 @@ proc createHomeRouter*(cfg: Config) = prefs = cookiePrefs() names = getNames(prefs.following) var - profs: seq[Profile] + profs: seq[User] query = request.getQuery("", prefs.following) query.fromUser = names query.kind = userList for name in names: - let prof = await getCachedProfile(name) + let prof = await getCachedUser(name) profs &= @[prof] resp renderMain(renderFollowing(query, profs, prefs), request, cfg, prefs) diff --git a/src/views/home.nim b/src/views/home.nim index 83b49030f..53abd213b 100644 --- a/src/views/home.nim +++ b/src/views/home.nim @@ -2,7 +2,7 @@ import karax/[karaxdsl, vdom] import search, timeline, renderutils import ../types -proc renderFollowingUsers*(results: seq[Profile]; prefs: Prefs): VNode = +proc renderFollowingUsers*(results: seq[User]; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline")): for user in results: renderUser(user, prefs) @@ -26,7 +26,7 @@ proc renderHome*(results: Result[Tweet]; prefs: Prefs; path: string): VNode = renderTimelineTweets(results, prefs, path) -proc renderFollowing*(query: Query; following: seq[Profile]; prefs: Prefs): VNode = +proc renderFollowing*(query: Query; following: seq[User]; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline-container")): renderHomeTabs(query) renderFollowingUsers(following, prefs) diff --git a/src/views/profile.nim b/src/views/profile.nim index 57ee85b15..05caa3780 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -30,9 +30,9 @@ proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = linkUser(user, class="profile-card-username") let following = isFollowing(user.username, prefs.following) if not following: - buttonReferer "/follow/" & profile.username, "Follow", path, "profile-card-follow-button" + buttonReferer "/follow/" & user.username, "Follow", path, "profile-card-follow-button" else: - buttonReferer "/unfollow/" & profile.username, "Unfollow", path, "profile-card-follow-button" + buttonReferer "/unfollow/" & user.username, "Unfollow", path, "profile-card-follow-button" tdiv(class="profile-card-extra"): if user.bio.len > 0: