diff --git a/src/nitter.nim b/src/nitter.nim index d743599..2258829 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -9,8 +9,8 @@ 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, - unsupported, embed, resolver, router_utils] + home, preferences, timeline, status, media, search, rss, list, debug, + unsupported, embed, resolver, router_utils, follow] const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" const issuesUrl = "https://github.com/zedeus/nitter/issues" @@ -58,9 +58,6 @@ settings: bindAddr = cfg.address routes: - get "/": - resp renderMain(renderSearch(), request, cfg, themePrefs()) - get "/about": resp renderMain(renderAbout(), request, cfg, themePrefs()) @@ -89,6 +86,8 @@ routes: resp Http429, showError( &"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/prefs_impl.nim b/src/prefs_impl.nim index 0223c82..3a63354 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" diff --git a/src/routes/follow.nim b/src/routes/follow.nim new file mode 100644 index 0000000..a0d90ba --- /dev/null +++ b/src/routes/follow.nim @@ -0,0 +1,42 @@ +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 + elif toAdd in updated: + return following + 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/@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/@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/routes/home.nim b/src/routes/home.nim new file mode 100644 index 0000000..712c739 --- /dev/null +++ b/src/routes/home.nim @@ -0,0 +1,49 @@ +import jester +import asyncdispatch, strutils, options, router_utils, timeline +import ".."/[prefs, types, utils, redis_cache] +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) + +proc createHomeRouter*(cfg: Config) = + router home: + get "/": + let + prefs = cookiePrefs() + after = getCursor() + names = getNames(prefs.following) + + var query = request.getQuery("", prefs.following) + query.fromUser = names + + if @"scroll".len > 0: + var timeline = await getSearch[Tweet](query, after) + if timeline.content.len == 0: resp Http404 + timeline.beginning = true + resp $renderHome(timeline, prefs, getPath()) + + 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[User] + query = request.getQuery("", prefs.following) + query.fromUser = names + query.kind = userList + + for name in names: + let prof = await getCachedUser(name) + profs &= @[prof] + + resp renderMain(renderFollowing(query, profs, prefs), request, cfg, prefs) diff --git a/src/sass/profile/card.scss b/src/sass/profile/card.scss index 85878e4..3b9a66a 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/home.nim b/src/views/home.nim new file mode 100644 index 0000000..53abd21 --- /dev/null +++ b/src/views/home.nim @@ -0,0 +1,32 @@ +import karax/[karaxdsl, vdom] +import search, timeline, renderutils +import ../types + +proc renderFollowingUsers*(results: seq[User]; 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: + 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[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 73ce59d..f7decaa 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/" & user.username, "Follow", path, "profile-card-follow-button" + else: + buttonReferer "/unfollow/" & user.username, "Unfollow", path, "profile-card-follow-button" tdiv(class="profile-card-extra"): if user.bio.len > 0: @@ -109,7 +115,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 bab01cd..5bf6604 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 diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 54cad7a..f2e8da5 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"):