mirror of
https://github.com/zedeus/nitter.git
synced 2025-05-02 21:34:43 +00:00
Implement Recommendations
This commit is contained in:
parent
9d117aa15b
commit
d95311bf8f
11 changed files with 119 additions and 10 deletions
|
@ -98,6 +98,12 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
|
|||
if after.len > 0:
|
||||
result.replies = await getReplies(id, after)
|
||||
|
||||
proc getRecommendations*(id: string): Future[Recommendations] {.async.} =
|
||||
let
|
||||
ps = genParams({"user_id": id})
|
||||
url = recommendations ? ps
|
||||
result = parseRecommnedations(await fetch(url, oldApi=true))
|
||||
|
||||
proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} =
|
||||
let client = newAsyncHttpClient(maxRedirects=0)
|
||||
try:
|
||||
|
|
|
@ -11,6 +11,7 @@ const
|
|||
userShow* = api / "1.1/users/show.json"
|
||||
photoRail* = api / "1.1/statuses/media_timeline.json"
|
||||
search* = api / "2/search/adaptive.json"
|
||||
recommendations* = api / "1.1/users/recommendations.json"
|
||||
|
||||
timelineApi = api / "2/timeline"
|
||||
tweet* = timelineApi / "conversation"
|
||||
|
|
|
@ -449,6 +449,10 @@ proc parseTimeline*(js: JsonNode; after=""): Timeline =
|
|||
elif "cursor-bottom" in entry:
|
||||
result.bottom = e.getCursor
|
||||
|
||||
proc parseRecommnedations*(js: JsonNode): Recommendations =
|
||||
for u in js:
|
||||
result.add parseProfile(u{"user"})
|
||||
|
||||
proc parsePhotoRail*(js: JsonNode): PhotoRail =
|
||||
for tweet in js:
|
||||
let
|
||||
|
|
|
@ -74,6 +74,9 @@ proc cache*(data: List) {.async.} =
|
|||
proc cache*(data: PhotoRail; name: string) {.async.} =
|
||||
await setex("pr:" & name, baseCacheTime, compress(toFlatty(data)))
|
||||
|
||||
proc cache*(data: Recommendations; userId: string) {.async.} =
|
||||
await setex("rc:" & userId, listCacheTime, compress(toFlatty(data)))
|
||||
|
||||
proc cache*(data: Profile) {.async.} =
|
||||
if data.username.len == 0 or data.id.len == 0: return
|
||||
let name = toLower(data.username)
|
||||
|
@ -99,6 +102,15 @@ proc cacheRss*(query: string; rss: Rss) {.async.} =
|
|||
discard await r.expire(key, rssCacheTime)
|
||||
discard await r.flushPipeline()
|
||||
|
||||
proc getCachedRecommendations*(userId: string): Future[Recommendations] {.async.} =
|
||||
if userId.len == 0: return
|
||||
let recommendations = await get("rc:" & userId)
|
||||
if recommendations != redisNil:
|
||||
result = fromFlatty(uncompress(recommendations), Recommendations)
|
||||
else:
|
||||
result = await getRecommendations(userId)
|
||||
await cache(result, userId)
|
||||
|
||||
proc getProfileId*(username: string): Future[string] {.async.} =
|
||||
let name = toLower(username)
|
||||
pool.withAcquire(r):
|
||||
|
|
|
@ -20,7 +20,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
|
|||
|
||||
if names.len == 1:
|
||||
(profile, timeline) =
|
||||
await fetchSingleTimeline(after, query, skipRail=true)
|
||||
await fetchSingleTimeline(after, query, skipRail=true, skipRecommendations=true)
|
||||
else:
|
||||
var q = query
|
||||
q.fromUser = names
|
||||
|
|
|
@ -19,8 +19,8 @@ proc getQuery*(request: Request; tab, name: string): Query =
|
|||
of "search": initQuery(params(request), name=name)
|
||||
else: Query(fromUser: @[name])
|
||||
|
||||
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
|
||||
Future[(Profile, Timeline, PhotoRail)] {.async.} =
|
||||
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false, skipRecommendations=false):
|
||||
Future[(Profile, Timeline, PhotoRail, Recommendations)] {.async.} =
|
||||
let name = query.fromUser[0]
|
||||
|
||||
var
|
||||
|
@ -52,6 +52,13 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
|
|||
else:
|
||||
rail = getCachedPhotoRail(name)
|
||||
|
||||
var recommendations: Future[Recommendations]
|
||||
if skipRecommendations:
|
||||
recommendations = newFuture[Recommendations]()
|
||||
recommendations.complete(@[])
|
||||
else:
|
||||
recommendations = getCachedRecommendations(profileId)
|
||||
|
||||
var timeline =
|
||||
case query.kind
|
||||
of posts: await getTimeline(profileId, after)
|
||||
|
@ -76,7 +83,7 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
|
|||
if fetched and not found:
|
||||
await cache(profile)
|
||||
|
||||
return (profile, timeline, await rail)
|
||||
return (profile, timeline, await rail, await recommendations)
|
||||
|
||||
proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
||||
rss, after: string): Future[string] {.async.} =
|
||||
|
@ -86,12 +93,12 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
|||
html = renderTweetSearch(timeline, prefs, getPath())
|
||||
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
|
||||
|
||||
var (p, t, r) = await fetchSingleTimeline(after, query)
|
||||
var (p, t, r, rc) = await fetchSingleTimeline(after, query)
|
||||
|
||||
if p.suspended: return showError(getSuspended(p.username), cfg)
|
||||
if p.id.len == 0: return
|
||||
|
||||
let pHtml = renderProfile(p, t, r, prefs, getPath())
|
||||
let pHtml = renderProfile(p, t, r, rc, prefs, getPath())
|
||||
result = renderMain(pHtml, request, cfg, prefs, pageTitle(p), pageDesc(p),
|
||||
rss=rss, images = @[p.getUserpic("_400x400")],
|
||||
banner=p.banner)
|
||||
|
@ -139,7 +146,7 @@ proc createTimelineRouter*(cfg: Config) =
|
|||
timeline.beginning = true
|
||||
resp $renderTweetSearch(timeline, prefs, getPath())
|
||||
else:
|
||||
var (_, timeline, _) = await fetchSingleTimeline(after, query, skipRail=true)
|
||||
var (_, timeline, _, _) = await fetchSingleTimeline(after, query, skipRail=true, skipRecommendations=true)
|
||||
if timeline.content.len == 0: resp Http404
|
||||
timeline.beginning = true
|
||||
resp $renderTimelineTweets(timeline, prefs, getPath())
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
@import 'card';
|
||||
@import 'photo-rail';
|
||||
@import 'recommendations';
|
||||
|
||||
.profile-tabs {
|
||||
@include panel(auto, 900px);
|
||||
|
|
59
src/sass/profile/recommendations.scss
Normal file
59
src/sass/profile/recommendations.scss
Normal file
|
@ -0,0 +1,59 @@
|
|||
@import '_variables';
|
||||
|
||||
.recommendations {
|
||||
&-card {
|
||||
float: left;
|
||||
background: var(--bg_panel);
|
||||
border-radius: 0 0 4px 4px;
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
&-header {
|
||||
padding: 5px 12px 0;
|
||||
}
|
||||
|
||||
&-header-mobile {
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
padding: 5px 12px 0;
|
||||
width: 100%;
|
||||
float: unset;
|
||||
color: var(--accent);
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@include create-toggle(recommendations-list, 640px);
|
||||
#recommendations-list-toggle:checked ~ .recommendations-list {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
@media(max-width: 600px) {
|
||||
.recommendations-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.recommendations-header-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.recommendations-list {
|
||||
max-height: 0;
|
||||
padding-bottom: 0;
|
||||
overflow: scroll;
|
||||
transition: max-height 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 600px) {
|
||||
#recommendations-list-toggle:checked ~ .recommendations-list {
|
||||
max-height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 450px) {
|
||||
#recommendations-list-toggle:checked ~ .recommendations-list {
|
||||
max-height: 160px;
|
||||
}
|
||||
}
|
|
@ -99,6 +99,8 @@ type
|
|||
|
||||
PhotoRail* = seq[GalleryPhoto]
|
||||
|
||||
Recommendations* = seq[Profile]
|
||||
|
||||
Poll* = object
|
||||
options*: seq[string]
|
||||
values*: seq[int]
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import strutils, strformat
|
||||
import karax/[karaxdsl, vdom, vstyles]
|
||||
|
||||
import renderutils, search
|
||||
import renderutils, search, timeline
|
||||
import ".."/[types, utils, formatters]
|
||||
|
||||
proc renderStat(num, class: string; text=""): VNode =
|
||||
|
@ -81,6 +81,20 @@ proc renderPhotoRail(profile: Profile; photoRail: PhotoRail): VNode =
|
|||
style={backgroundColor: col}):
|
||||
genImg(photo.url & (if "format" in photo.url: "" else: ":thumb"))
|
||||
|
||||
proc renderRecommendations(recommendations: Recommendations; prefs: Prefs): VNode =
|
||||
buildHtml(tdiv(class="recommendations-card")):
|
||||
tdiv(class="recommendations-header"):
|
||||
span: text "You might like"
|
||||
|
||||
input(id="recommendations-list-toggle", `type`="checkbox")
|
||||
label(`for`="recommendations-list-toggle", class="recommendations-header-mobile"):
|
||||
span: text "You might like"
|
||||
icon "down"
|
||||
|
||||
tdiv(class="recommendations-list"):
|
||||
for i, recommendation in recommendations:
|
||||
renderUser(recommendation, prefs)
|
||||
|
||||
proc renderBanner(profile: Profile): VNode =
|
||||
buildHtml():
|
||||
if "#" in profile.banner:
|
||||
|
@ -96,7 +110,7 @@ proc renderProtected(username: string): VNode =
|
|||
p: text &"Only confirmed followers have access to @{username}'s tweets."
|
||||
|
||||
proc renderProfile*(profile: Profile; timeline: var Timeline;
|
||||
photoRail: PhotoRail; prefs: Prefs; path: string): VNode =
|
||||
photoRail: PhotoRail; recommendations: Recommendations; prefs: Prefs; path: string): VNode =
|
||||
timeline.query.fromUser = @[profile.username]
|
||||
buildHtml(tdiv(class="profile-tabs")):
|
||||
if not prefs.hideBanner:
|
||||
|
@ -108,6 +122,9 @@ proc renderProfile*(profile: Profile; timeline: var Timeline;
|
|||
renderProfileCard(profile, prefs)
|
||||
if photoRail.len > 0:
|
||||
renderPhotoRail(profile, photoRail)
|
||||
if recommendations.len > 0:
|
||||
renderRecommendations(recommendations, prefs)
|
||||
|
||||
|
||||
if profile.protected:
|
||||
renderProtected(profile.username)
|
||||
|
|
|
@ -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: Profile; prefs: Prefs): VNode =
|
||||
proc renderUser*(user: Profile; prefs: Prefs): VNode =
|
||||
buildHtml(tdiv(class="timeline-item")):
|
||||
a(class="tweet-link", href=("/" & user.username))
|
||||
tdiv(class="tweet-body profile-result"):
|
||||
|
|
Loading…
Reference in a new issue