Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a home timeline for followed accounts #363

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
9 changes: 4 additions & 5 deletions src/nitter.nim
Expand Up @@ -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"
Expand Down Expand Up @@ -58,9 +58,6 @@ settings:
bindAddr = cfg.address

routes:
get "/":
resp renderMain(renderSearch(), request, cfg, themePrefs())

get "/about":
resp renderMain(renderAbout(), request, cfg, themePrefs())

Expand Down Expand Up @@ -90,6 +87,8 @@ routes:
resp Http429, showError(
&"Instance has been rate limited.<br>Use {link} or try again later.", cfg)

extend home, ""
extend follow, ""
extend unsupported, ""
extend preferences, ""
extend resolver, ""
Expand Down
5 changes: 5 additions & 0 deletions src/prefs_impl.nim
Expand Up @@ -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"
Expand Down
42 changes: 42 additions & 0 deletions 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())
49 changes: 49 additions & 0 deletions 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)
11 changes: 9 additions & 2 deletions src/sass/profile/card.scss
Expand Up @@ -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 {
Expand All @@ -34,6 +37,10 @@
max-width: 100%;
}

.profile-card-follow-button {
float: none;
}

.profile-card-avatar {
display: inline-block;
position: relative;
Expand Down
32 changes: 32 additions & 0 deletions 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)
16 changes: 11 additions & 5 deletions src/views/profile.nim
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions src/views/renderutils.nim
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/views/timeline.nim
Expand Up @@ -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"):
Expand Down