Skip to content

Commit 3a279e2

Browse files
committed
Implement Recommendations
1 parent ebffb6d commit 3a279e2

File tree

11 files changed

+119
-10
lines changed

11 files changed

+119
-10
lines changed

src/api.nim

+6
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
9191
if after.len > 0:
9292
result.replies = await getReplies(id, after)
9393

94+
proc getRecommendations*(id: string): Future[Recommendations] {.async.} =
95+
let
96+
ps = genParams({"user_id": id})
97+
url = recommendations ? ps
98+
result = parseRecommnedations(await fetch(url, oldApi=true))
99+
94100
proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} =
95101
let client = newAsyncHttpClient(maxRedirects=0)
96102
try:

src/consts.nim

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const
1111
userShow* = api / "1.1/users/show.json"
1212
photoRail* = api / "1.1/statuses/media_timeline.json"
1313
search* = api / "2/search/adaptive.json"
14+
recommendations* = api / "1.1/users/recommendations.json"
1415

1516
timelineApi = api / "2/timeline"
1617
tweet* = timelineApi / "conversation"

src/parser.nim

+4
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ proc parseTimeline*(js: JsonNode; after=""): Timeline =
434434
elif "cursor-bottom" in entry:
435435
result.bottom = e.getCursor
436436

437+
proc parseRecommnedations*(js: JsonNode): Recommendations =
438+
for u in js:
439+
result.add parseProfile(u{"user"})
440+
437441
proc parsePhotoRail*(js: JsonNode): PhotoRail =
438442
for tweet in js:
439443
let

src/redis_cache.nim

+12
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ proc cache*(data: List) {.async.} =
7272
proc cache*(data: PhotoRail; name: string) {.async.} =
7373
await setex("pr:" & name, baseCacheTime, compress(toFlatty(data)))
7474

75+
proc cache*(data: Recommendations; userId: string) {.async.} =
76+
await setex("rc:" & userId, listCacheTime, compress(toFlatty(data)))
77+
7578
proc cache*(data: Profile) {.async.} =
7679
if data.username.len == 0 or data.id.len == 0: return
7780
let name = toLower(data.username)
@@ -96,6 +99,15 @@ proc cacheRss*(query: string; rss: Rss) {.async.} =
9699
discard await r.expire(key, rssCacheTime)
97100
discard await r.flushPipeline()
98101

102+
proc getCachedRecommendations*(userId: string): Future[Recommendations] {.async.} =
103+
if userId.len == 0: return
104+
let recommendations = await get("rc:" & userId)
105+
if recommendations != redisNil:
106+
result = fromFlatty(uncompress(recommendations), Recommendations)
107+
else:
108+
result = await getRecommendations(userId)
109+
await cache(result, userId)
110+
99111
proc getProfileId*(username: string): Future[string] {.async.} =
100112
let name = toLower(username)
101113
pool.withAcquire(r):

src/routes/rss.nim

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
2020

2121
if names.len == 1:
2222
(profile, timeline) =
23-
await fetchSingleTimeline(after, query, skipRail=true)
23+
await fetchSingleTimeline(after, query, skipRail=true, skipRecommendations=true)
2424
else:
2525
var q = query
2626
q.fromUser = names

src/routes/timeline.nim

+13-6
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ proc getQuery*(request: Request; tab, name: string): Query =
1919
of "search": initQuery(params(request), name=name)
2020
else: Query(fromUser: @[name])
2121

22-
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
23-
Future[(Profile, Timeline, PhotoRail)] {.async.} =
22+
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false, skipRecommendations=false):
23+
Future[(Profile, Timeline, PhotoRail, Recommendations)] {.async.} =
2424
let name = query.fromUser[0]
2525

2626
var
@@ -52,6 +52,13 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
5252
else:
5353
rail = getCachedPhotoRail(name)
5454

55+
var recommendations: Future[Recommendations]
56+
if skipRecommendations:
57+
recommendations = newFuture[Recommendations]()
58+
recommendations.complete(@[])
59+
else:
60+
recommendations = getCachedRecommendations(profileId)
61+
5562
var timeline =
5663
case query.kind
5764
of posts: await getTimeline(profileId, after)
@@ -76,7 +83,7 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
7683
if fetched and not found:
7784
await cache(profile)
7885

79-
return (profile, timeline, await rail)
86+
return (profile, timeline, await rail, await recommendations)
8087

8188
proc get*(req: Request; key: string): string =
8289
params(req).getOrDefault(key)
@@ -89,12 +96,12 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
8996
html = renderTweetSearch(timeline, prefs, getPath())
9097
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
9198

92-
var (p, t, r) = await fetchSingleTimeline(after, query)
99+
var (p, t, r, rc) = await fetchSingleTimeline(after, query)
93100

94101
if p.suspended: return showError(getSuspended(p.username), cfg)
95102
if p.id.len == 0: return
96103

97-
let pHtml = renderProfile(p, t, r, prefs, getPath())
104+
let pHtml = renderProfile(p, t, r, rc, prefs, getPath())
98105
result = renderMain(pHtml, request, cfg, prefs, pageTitle(p), pageDesc(p),
99106
rss=rss, images = @[p.getUserpic("_400x400")],
100107
banner=p.banner)
@@ -127,7 +134,7 @@ proc createTimelineRouter*(cfg: Config) =
127134
timeline.beginning = true
128135
resp $renderTweetSearch(timeline, prefs, getPath())
129136
else:
130-
var (_, timeline, _) = await fetchSingleTimeline(after, query, skipRail=true)
137+
var (_, timeline, _, _) = await fetchSingleTimeline(after, query, skipRail=true, skipRecommendations=true)
131138
if timeline.content.len == 0: resp Http404
132139
timeline.beginning = true
133140
resp $renderTimelineTweets(timeline, prefs, getPath())

src/sass/profile/_base.scss

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
@import 'card';
55
@import 'photo-rail';
6+
@import 'recommendations';
67

78
.profile-tabs {
89
@include panel(auto, 900px);

src/sass/profile/recommendations.scss

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@import '_variables';
2+
3+
.recommendations {
4+
&-card {
5+
float: left;
6+
background: var(--bg_panel);
7+
border-radius: 0 0 4px 4px;
8+
width: 100%;
9+
margin: 5px 0;
10+
}
11+
12+
&-header {
13+
padding: 5px 12px 0;
14+
}
15+
16+
&-header-mobile {
17+
display: none;
18+
box-sizing: border-box;
19+
padding: 5px 12px 0;
20+
width: 100%;
21+
float: unset;
22+
color: var(--accent);
23+
justify-content: space-between;
24+
}
25+
}
26+
27+
@include create-toggle(recommendations-list, 640px);
28+
#recommendations-list-toggle:checked ~ .recommendations-list {
29+
padding-bottom: 12px;
30+
}
31+
32+
@media(max-width: 600px) {
33+
.recommendations-header {
34+
display: none;
35+
}
36+
37+
.recommendations-header-mobile {
38+
display: flex;
39+
}
40+
41+
.recommendations-list {
42+
max-height: 0;
43+
padding-bottom: 0;
44+
overflow: scroll;
45+
transition: max-height 0.4s;
46+
}
47+
}
48+
49+
@media(max-width: 600px) {
50+
#recommendations-list-toggle:checked ~ .recommendations-list {
51+
max-height: 160px;
52+
}
53+
}
54+
55+
@media(max-width: 450px) {
56+
#recommendations-list-toggle:checked ~ .recommendations-list {
57+
max-height: 160px;
58+
}
59+
}

src/types.nim

+2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ type
9999

100100
PhotoRail* = seq[GalleryPhoto]
101101

102+
Recommendations* = seq[Profile]
103+
102104
Poll* = object
103105
options*: seq[string]
104106
values*: seq[int]

src/views/profile.nim

+19-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import strutils, strformat
33
import karax/[karaxdsl, vdom, vstyles]
44

5-
import renderutils, search
5+
import renderutils, search, timeline
66
import ".."/[types, utils, formatters]
77

88
proc renderStat(num, class: string; text=""): VNode =
@@ -81,6 +81,20 @@ proc renderPhotoRail(profile: Profile; photoRail: PhotoRail): VNode =
8181
style={backgroundColor: col}):
8282
genImg(photo.url & (if "format" in photo.url: "" else: ":thumb"))
8383

84+
proc renderRecommendations(recommendations: Recommendations; prefs: Prefs): VNode =
85+
buildHtml(tdiv(class="recommendations-card")):
86+
tdiv(class="recommendations-header"):
87+
span: text "You might like"
88+
89+
input(id="recommendations-list-toggle", `type`="checkbox")
90+
label(`for`="recommendations-list-toggle", class="recommendations-header-mobile"):
91+
span: text "You might like"
92+
icon "down"
93+
94+
tdiv(class="recommendations-list"):
95+
for i, recommendation in recommendations:
96+
renderUser(recommendation, prefs)
97+
8498
proc renderBanner(profile: Profile): VNode =
8599
buildHtml():
86100
if "#" in profile.banner:
@@ -96,7 +110,7 @@ proc renderProtected(username: string): VNode =
96110
p: text &"Only confirmed followers have access to @{username}'s tweets."
97111

98112
proc renderProfile*(profile: Profile; timeline: var Timeline;
99-
photoRail: PhotoRail; prefs: Prefs; path: string): VNode =
113+
photoRail: PhotoRail; recommendations: Recommendations; prefs: Prefs; path: string): VNode =
100114
timeline.query.fromUser = @[profile.username]
101115
buildHtml(tdiv(class="profile-tabs")):
102116
if not prefs.hideBanner:
@@ -108,6 +122,9 @@ proc renderProfile*(profile: Profile; timeline: var Timeline;
108122
renderProfileCard(profile, prefs)
109123
if photoRail.len > 0:
110124
renderPhotoRail(profile, photoRail)
125+
if recommendations.len > 0:
126+
renderRecommendations(recommendations, prefs)
127+
111128

112129
if profile.protected:
113130
renderProtected(profile.username)

src/views/timeline.nim

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ proc threadFilter(tweets: openArray[Tweet]; threads: openArray[int64]; it: Tweet
5757
elif t.replyId == result[0].id:
5858
result.add t
5959

60-
proc renderUser(user: Profile; prefs: Prefs): VNode =
60+
proc renderUser*(user: Profile; prefs: Prefs): VNode =
6161
buildHtml(tdiv(class="timeline-item")):
6262
a(class="tweet-link", href=("/" & user.username))
6363
tdiv(class="tweet-body profile-result"):

0 commit comments

Comments
 (0)