diff --git a/src/api.nim b/src/api.nim index 07465da09..577d553c5 100644 --- a/src/api.nim +++ b/src/api.nim @@ -39,6 +39,12 @@ proc getProfile*(username: string): Future[Profile] {.async.} = url = userShow ? ps result = parseUserShow(await fetch(url, oldApi=true), username) +proc getRecommendations*(id: string) : Future[Recommendations] {.async.} = + let + ps = genParams({"user_id": id }) # , "limit": "100" 1) limit here or 2) fetch all and limit in display? + url = recommendations ? ps + result = parseRecommnedations(await fetch(url, oldApi = true)) + proc getTimeline*(id: string; after=""; replies=false): Future[Timeline] {.async.} = let ps = genParams({"userId": id, "include_tweet_replies": $replies}, after) diff --git a/src/consts.nim b/src/consts.nim index 3676588d1..bba07e3fe 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -10,6 +10,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" diff --git a/src/parser.nim b/src/parser.nim index fffb3c9b9..6fd689877 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -37,6 +37,10 @@ proc parseUserShow*(js: JsonNode; username: string): Profile = result = parseProfile(js) +proc parseRecommnedations*(js: JsonNode) : Recommendations = + for u in js: + result.add parseProfile(u{"user"}) + proc parseGraphProfile*(js: JsonNode; username: string): Profile = if js.isNull: return with error, js{"errors"}: diff --git a/src/redis_cache.nim b/src/redis_cache.nim index 1cb3bed9e..384786e09 100644 --- a/src/redis_cache.nim +++ b/src/redis_cache.nim @@ -62,6 +62,9 @@ proc cache*(data: List) {.async.} = proc cache*(data: PhotoRail; name: string) {.async.} = await setex("pr:" & name, baseCacheTime, compress(freeze(data))) +proc cache*(data: Recommendations; user_id: string) {.async.} = + await setex("rc:" & user_id , listCacheTime , compress(freeze(data))) + proc cache*(data: Profile) {.async.} = if data.username.len == 0 or data.id.len == 0: return let name = toLower(data.username) @@ -109,6 +112,15 @@ proc getCachedPhotoRail*(name: string): Future[PhotoRail] {.async.} = result = await getPhotoRail(name) await cache(result, name) +proc getCachedRecommendations*(user_id: string ): Future[Recommendations] {.async.} = + if user_id.len == 0: return + let recommendations = await get("rc:" & user_id ) + if recommendations != redisNil: + uncompress(recommendations).thaw(result) + else: + result = await getRecommendations(user_id) + await cache(result,user_id) + proc getCachedList*(username=""; name=""; id=""): Future[List] {.async.} = let list = if id.len > 0: redisNil else: await get(toLower("l:" & username & '/' & name)) diff --git a/src/routes/rss.nim b/src/routes/rss.nim index 69264a699..7cc18125a 100644 --- a/src/routes/rss.nim +++ b/src/routes/rss.nim @@ -19,7 +19,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 diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index 8d75520d9..627f1ed82 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -18,8 +18,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 @@ -51,6 +51,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) @@ -75,7 +82,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 get*(req: Request; key: string): string = params(req).getOrDefault(key) @@ -88,12 +95,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) @@ -126,7 +133,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()) diff --git a/src/sass/profile/_base.scss b/src/sass/profile/_base.scss index 23ac4f2ce..b1f0c0508 100644 --- a/src/sass/profile/_base.scss +++ b/src/sass/profile/_base.scss @@ -3,6 +3,7 @@ @import 'card'; @import 'photo-rail'; +@import 'recommendations'; .profile-tabs { @include panel(auto, 900px); diff --git a/src/sass/profile/recommendations.scss b/src/sass/profile/recommendations.scss new file mode 100644 index 000000000..e21781bf9 --- /dev/null +++ b/src/sass/profile/recommendations.scss @@ -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; + } +} diff --git a/src/types.nim b/src/types.nim index d405e2694..5c16f42e9 100644 --- a/src/types.nim +++ b/src/types.nim @@ -97,6 +97,8 @@ type PhotoRail* = seq[GalleryPhoto] + Recommendations* = seq[Profile] + Poll* = object options*: seq[string] values*: seq[int] diff --git a/src/views/profile.nim b/src/views/profile.nim index 08404f12e..c4caed56c 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -1,7 +1,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 = @@ -80,6 +80,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: @@ -95,7 +109,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: @@ -107,6 +121,10 @@ 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) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 76e47c158..ffae3d450 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -56,7 +56,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"):