diff --git a/public/style.css b/public/style.css index 7351c20..8845422 100644 --- a/public/style.css +++ b/public/style.css @@ -140,7 +140,7 @@ a:hover { .replying-to { color: hsla(240,1%,73%,.9); - margin: 4px 0; + margin: -4px 0 4px 0; } .status-el .status-content { @@ -369,6 +369,20 @@ video { background-color: #282828; } +.multi-header { + background-color: #161616; + text-align: center; + padding: 10px; + display: block; + font-weight: bold; + margin-bottom: 5px; +} + +.multi-timeline { + max-width: 600px; + margin: 0 auto; +} + .profile-tabs { max-width: 900px; margin: 0 auto; @@ -387,6 +401,9 @@ video { margin: 0; text-align: left; vertical-align: top; +} + +.profile-tabs > .timeline-tab { width: 68% !important; } @@ -756,7 +773,7 @@ video { } .timeline-protected { - padding-left: 12px; + text-align: center; } .timeline-protected p { diff --git a/src/api.nim b/src/api.nim index 1b7c790..080259c 100644 --- a/src/api.nim +++ b/src/api.nim @@ -308,7 +308,7 @@ proc getTimeline*(username, after, agent: string): Future[Timeline] {.async.} = let json = await fetchJson(base / (timelineUrl % username) ? params, headers) result = await finishTimeline(json, none(Query), after, agent) -proc getTimelineSearch*(username, after, agent: string; query: Query): Future[Timeline] {.async.} = +proc getTimelineSearch*(query: Query; after, agent: string): Future[Timeline] {.async.} = let queryParam = genQueryParam(query) let queryEncoded = encodeUrl(queryParam, usePlus=false) diff --git a/src/nitter.nim b/src/nitter.nim index d3482fc..ba582be 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -9,18 +9,15 @@ import views/[general, profile, status] const configPath {.strdefine.} = "./nitter.conf" let cfg = getConfig(configPath) -proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.async.} = - let - agent = getAgent() - username = name.strip(chars={'/'}) - profileFut = getCachedProfile(username, agent) - railFut = getPhotoRail(username, agent) +proc showSingleTimeline(name, after, agent: string; query: Option[Query]): Future[string] {.async.} = + let profileFut = getCachedProfile(name, agent) + let railFut = getPhotoRail(name, agent) var timelineFut: Future[Timeline] if query.isNone: - timelineFut = getTimeline(username, after, agent) + timelineFut = getTimeline(name, after, agent) else: - timelineFut = getTimelineSearch(username, after, agent, get(query)) + timelineFut = getTimelineSearch(get(query), after, agent) let profile = await profileFut if profile.username.len == 0: @@ -29,6 +26,25 @@ proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.a let profileHtml = renderProfile(profile, await timelineFut, await railFut) return renderMain(profileHtml, title=cfg.title, titleText=pageTitle(profile)) +proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query]): Future[string] {.async.} = + var q = query + if q.isSome: + get(q).fromUser = names + else: + q = some(Query(kind: multi, fromUser: names, excludes: @["replies"])) + + var timeline = renderMulti(await getTimelineSearch(get(q), after, agent), names.join(",")) + return renderMain(timeline, title=cfg.title, titleText=names.join(" | ")) + +proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.async.} = + let agent = getAgent() + let names = name.strip(chars={'/'}).split(",") + + if names.len == 1: + return await showSingleTimeline(names[0], after, agent, query) + else: + return await showMultiTimeline(names, after, agent, query) + template respTimeline(timeline: typed) = if timeline.len == 0: resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title) diff --git a/src/search.nim b/src/search.nim index cc53eb2..bfc2a03 100644 --- a/src/search.nim +++ b/src/search.nim @@ -25,7 +25,7 @@ proc initQuery*(filters, includes, excludes, separator: string; name=""): Query filters: filters.split(",").filterIt(it in validFilters), includes: includes.split(",").filterIt(it in validFilters), excludes: excludes.split(",").filterIt(it in validFilters), - fromUser: name, + fromUser: @[name], sep: if sep in separators: sep else: "" ) @@ -33,7 +33,7 @@ proc getMediaQuery*(name: string): Query = Query( kind: media, filters: @["twimg", "native_video"], - fromUser: name, + fromUser: @[name], sep: "OR" ) @@ -41,15 +41,17 @@ proc getReplyQuery*(name: string): Query = Query( kind: replies, includes: @["nativeretweets"], - fromUser: name + fromUser: @[name] ) proc genQueryParam*(query: Query): string = var filters: seq[string] var param: string - if query.fromUser.len > 0: - param = &"from:{query.fromUser} " + for i, user in query.fromUser: + param &= &"from:{user} " + if i < query.fromUser.high: + param &= "OR " for f in query.filters: filters.add "filter:" & f diff --git a/src/types.nim b/src/types.nim index 23ba107..eef6d1e 100644 --- a/src/types.nim +++ b/src/types.nim @@ -32,14 +32,14 @@ db("cache.db", "", "", ""): type QueryKind* = enum - replies, media, custom = "search" + replies, media, multi, custom = "search" Query* = object kind*: QueryKind filters*: seq[string] includes*: seq[string] excludes*: seq[string] - fromUser*: string + fromUser*: seq[string] sep*: string VideoType* = enum diff --git a/src/views/general.nim b/src/views/general.nim index 8a156cb..622514e 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -26,7 +26,7 @@ proc renderSearch*(): VNode = buildHtml(tdiv(class="panel")): tdiv(class="search-panel"): form(`method`="post", action="search"): - input(`type`="text", name="query", placeholder="Enter username...") + input(`type`="text", name="query", placeholder="Enter usernames...") button(`type`="submit"): text "🔎" proc renderError*(error: string): VNode = diff --git a/src/views/profile.nim b/src/views/profile.nim index e515547..9ab9f08 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -64,4 +64,9 @@ proc renderProfile*(profile: Profile; timeline: Timeline; renderPhotoRail(profile.username, photoRail) tdiv(class="timeline-tab"): - renderTimeline(timeline, profile) + renderTimeline(timeline, profile.username, profile.protected) + +proc renderMulti*(timeline: Timeline; usernames: string): VNode = + buildHtml(tdiv(class="multi-timeline")): + tdiv(class="timeline-tab"): + renderTimeline(timeline, usernames, false, multi=true) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 17b3402..201d8fc 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -11,16 +11,16 @@ proc getQuery(timeline: Timeline): string = proc getTabClass(timeline: Timeline; tab: string): string = var classes = @["tab-item"] - if timeline.query.isNone: + if timeline.query.isNone or get(timeline.query).kind == multi: if tab == "posts": classes.add "active" - elif $timeline.query.get().kind == tab: + elif $get(timeline.query).kind == tab: classes.add "active" return classes.join(" ") -proc renderSearchTabs(timeline: Timeline; profile: Profile): VNode = - let link = "/" & profile.username +proc renderSearchTabs(timeline: Timeline; username: string): VNode = + let link = "/" & username buildHtml(ul(class="tab")): li(class=timeline.getTabClass("posts")): a(href=link): text "Tweets" @@ -29,14 +29,14 @@ proc renderSearchTabs(timeline: Timeline; profile: Profile): VNode = li(class=timeline.getTabClass("media")): a(href=(link & "/media")): text "Media" -proc renderNewer(timeline: Timeline; profile: Profile): VNode = +proc renderNewer(timeline: Timeline; username: string): VNode = buildHtml(tdiv(class="status-el show-more")): - a(href=("/" & profile.username & getQuery(timeline).strip(chars={'?'}))): + a(href=("/" & username & getQuery(timeline).strip(chars={'?'}))): text "Load newest tweets" -proc renderOlder(timeline: Timeline; profile: Profile): VNode = +proc renderOlder(timeline: Timeline; username: string): VNode = buildHtml(tdiv(class="show-more")): - a(href=(&"/{profile.username}{getQuery(timeline)}after={timeline.minId}")): + a(href=(&"/{username}{getQuery(timeline)}after={timeline.minId}")): text "Load older tweets" proc renderNoMore(): VNode = @@ -74,20 +74,25 @@ proc renderTweets(timeline: Timeline): VNode = renderThread(thread) threads &= tweet.threadId -proc renderTimeline*(timeline: Timeline; profile: Profile): VNode = +proc renderTimeline*(timeline: Timeline; username: string; + protected: bool; multi=false): VNode = buildHtml(tdiv): - renderSearchTabs(timeline, profile) + if multi: + tdiv(class="multi-header"): + text username.replace(",", " | ") - if not profile.protected and not timeline.beginning: - renderNewer(timeline, profile) + if not protected: + renderSearchTabs(timeline, username) + if not timeline.beginning: + renderNewer(timeline, username) - if profile.protected: - renderProtected(profile.username) + if protected: + renderProtected(username) elif timeline.tweets.len == 0: renderNoneFound() else: renderTweets(timeline) if timeline.hasMore or timeline.query.isSome: - renderOlder(timeline, profile) + renderOlder(timeline, username) else: renderNoMore()