nandi/gleanpublic⑂ Fork 0
⑂ 16ed9f6
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Separate followed users from people recommendationsUnverified

Julien Robert committed 2026-04-27T23:05:08+02:00 Browse files
16ed9f6 parent: 381f2e4
modified docs/specs.md +1 -1
@@ -267,7 +267,7 @@ Input:
267267
268268 Output:
269269 feeds: [{ feedUrl, title, siteUrl, description, subscriberCount, score }]
270- people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }]
270+ people: [{ did, handle, displayName, avatar, jaccard, commonFeeds, isFollowed }]
271271 ```
272272
273273 ### 3.9 AppView Jetstream Consumption
@@ -267,7 +267,7 @@ Input:
267 267
268 Output:268 Output:
269 feeds: [{ feedUrl, title, siteUrl, description, subscriberCount, score }]269 feeds: [{ feedUrl, title, siteUrl, description, subscriberCount, score }]
270- people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }]270+ people: [{ did, handle, displayName, avatar, jaccard, commonFeeds, isFollowed }]
271 ```271 ```
272 272
273 ### 3.9 AppView Jetstream Consumption273 ### 3.9 AppView Jetstream Consumption
modified internal/cluster/jaccard_test.go +10 -0
@@ -486,6 +486,7 @@ func TestOnDemandPeopleRecommendations(t *testing.T) {
486486 ctx := context.Background()
487487 dbs := setupClusterTestDB(t)
488488 seedClusterData(t, ctx, dbs)
489+ seedFollowData(t, ctx, dbs)
489490
490491 engine := newTestEngine(dbs)
491492 assert.NilError(t, engine.ComputeUserSimilarity(ctx))
@@ -493,6 +494,15 @@ func TestOnDemandPeopleRecommendations(t *testing.T) {
493494 recs, err := engine.GetPeopleRecommendations(ctx, "did:test:carol", 10)
494495 assert.NilError(t, err)
495496 assert.Assert(t, len(recs) > 0, "carol should get people recommendations")
497+
498+ for _, r := range recs {
499+ if r.DID == "did:test:dave" {
500+ assert.Assert(t, r.IsFollowed, "carol follows dave, should be marked as followed")
501+ }
502+ if r.DID == "did:test:alice" || r.DID == "did:test:bob" {
503+ assert.Assert(t, !r.IsFollowed, "carol does not follow %s, should not be marked as followed", r.DID)
504+ }
505+ }
496506 }
497507
498508 func TestDismissArticle(t *testing.T) {
@@ -486,6 +486,7 @@ func TestOnDemandPeopleRecommendations(t *testing.T) {
486 ctx := context.Background()486 ctx := context.Background()
487 dbs := setupClusterTestDB(t)487 dbs := setupClusterTestDB(t)
488 seedClusterData(t, ctx, dbs)488 seedClusterData(t, ctx, dbs)
489+ seedFollowData(t, ctx, dbs)
489 490
490 engine := newTestEngine(dbs)491 engine := newTestEngine(dbs)
491 assert.NilError(t, engine.ComputeUserSimilarity(ctx))492 assert.NilError(t, engine.ComputeUserSimilarity(ctx))
@@ -493,6 +494,15 @@ func TestOnDemandPeopleRecommendations(t *testing.T) {
493 recs, err := engine.GetPeopleRecommendations(ctx, "did:test:carol", 10)494 recs, err := engine.GetPeopleRecommendations(ctx, "did:test:carol", 10)
494 assert.NilError(t, err)495 assert.NilError(t, err)
495 assert.Assert(t, len(recs) > 0, "carol should get people recommendations")496 assert.Assert(t, len(recs) > 0, "carol should get people recommendations")
497+
498+ for _, r := range recs {
499+ if r.DID == "did:test:dave" {
500+ assert.Assert(t, r.IsFollowed, "carol follows dave, should be marked as followed")
501+ }
502+ if r.DID == "did:test:alice" || r.DID == "did:test:bob" {
503+ assert.Assert(t, !r.IsFollowed, "carol does not follow %s, should not be marked as followed", r.DID)
504+ }
505+ }
496 }506 }
497 507
498 func TestDismissArticle(t *testing.T) {508 func TestDismissArticle(t *testing.T) {
modified internal/cluster/scoring.go +9 -3
@@ -25,6 +25,7 @@ type PersonRecommendation struct {
2525 CommonFeeds int
2626 CommonLikes int
2727 CommonTags int
28+ IsFollowed bool
2829 }
2930
3031 type ArticleRecommendation struct {
@@ -458,17 +459,19 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
458459 func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {
459460 rows, err := e.db.QueryContext(ctx, `
460461 SELECT u.did,
461- sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0)
462+ sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0),
463+ CASE WHEN f.target_did IS NOT NULL THEN 1 ELSE 0 END
462464 FROM (
463465 SELECT user_b AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_a = ?
464466 UNION ALL
465467 SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ?
466468 ) sim
467469 JOIN main.users u ON u.did = sim.peer_did
470+ LEFT JOIN main.follows f ON f.user_did = ? AND f.target_did = u.did
468471 WHERE EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0)
469472 ORDER BY sim.jaccard DESC
470473 LIMIT ?
471- `, userDID, userDID, limit)
474+ `, userDID, userDID, userDID, limit)
472475 if err != nil {
473476 return nil, err
474477 }
@@ -477,10 +480,13 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD
477480 var results []*PersonRecommendation
478481 for rows.Next() {
479482 rec := &PersonRecommendation{}
483+ var isFollowed int
480484 if err := rows.Scan(&rec.DID,
481- &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags); err != nil {
485+ &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags,
486+ &isFollowed); err != nil {
482487 return nil, err
483488 }
489+ rec.IsFollowed = isFollowed == 1
484490 results = append(results, rec)
485491 }
486492 return results, rows.Err()
@@ -25,6 +25,7 @@ type PersonRecommendation struct {
25 CommonFeeds int25 CommonFeeds int
26 CommonLikes int26 CommonLikes int
27 CommonTags int27 CommonTags int
28+ IsFollowed bool
28 }29 }
29 30
30 type ArticleRecommendation struct {31 type ArticleRecommendation struct {
@@ -458,17 +459,19 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
458 func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {459 func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {
459 rows, err := e.db.QueryContext(ctx, `460 rows, err := e.db.QueryContext(ctx, `
460 SELECT u.did,461 SELECT u.did,
461- sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0)462+ sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0),
463+ CASE WHEN f.target_did IS NOT NULL THEN 1 ELSE 0 END
462 FROM (464 FROM (
463 SELECT user_b AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_a = ?465 SELECT user_b AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_a = ?
464 UNION ALL466 UNION ALL
465 SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ?467 SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ?
466 ) sim468 ) sim
467 JOIN main.users u ON u.did = sim.peer_did469 JOIN main.users u ON u.did = sim.peer_did
470+ LEFT JOIN main.follows f ON f.user_did = ? AND f.target_did = u.did
468 WHERE EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0)471 WHERE EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0)
469 ORDER BY sim.jaccard DESC472 ORDER BY sim.jaccard DESC
470 LIMIT ?473 LIMIT ?
471- `, userDID, userDID, limit)474+ `, userDID, userDID, userDID, limit)
472 if err != nil {475 if err != nil {
473 return nil, err476 return nil, err
474 }477 }
@@ -477,10 +480,13 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD
477 var results []*PersonRecommendation480 var results []*PersonRecommendation
478 for rows.Next() {481 for rows.Next() {
479 rec := &PersonRecommendation{}482 rec := &PersonRecommendation{}
483+ var isFollowed int
480 if err := rows.Scan(&rec.DID,484 if err := rows.Scan(&rec.DID,
481- &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags); err != nil {485+ &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags,
486+ &isFollowed); err != nil {
482 return nil, err487 return nil, err
483 }488 }
489+ rec.IsFollowed = isFollowed == 1
484 results = append(results, rec)490 results = append(results, rec)
485 }491 }
486 return results, rows.Err()492 return results, rows.Err()
modified internal/server/dashboard_handler.go +11 -1
@@ -77,6 +77,15 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
7777 s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)
7878 }
7979
80+ var followedPeople, discoverPeople []*cluster.PersonRecommendation
81+ for _, p := range peopleRecs {
82+ if p.IsFollowed {
83+ followedPeople = append(followedPeople, p)
84+ } else {
85+ discoverPeople = append(discoverPeople, p)
86+ }
87+ }
88+
8089 s.render(w, r, "dashboard.html", map[string]any{
8190 "User": user,
8291 "UnreadCount": unreadCount,
@@ -84,7 +93,8 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
8493 "Articles": articles,
8594 "ArticleRecommendations": articleRecs,
8695 "FeedRecommendations": feedRecs,
87- "PeopleRecommendations": peopleRecs,
96+ "FollowedPeople": followedPeople,
97+ "DiscoverPeople": discoverPeople,
8898 "PersonalTrending": personalTrending,
8999 "GlobalTrending": globalTrending,
90100 "Page": page,
@@ -77,6 +77,15 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
77 s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)77 s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)
78 }78 }
79 79
80+ var followedPeople, discoverPeople []*cluster.PersonRecommendation
81+ for _, p := range peopleRecs {
82+ if p.IsFollowed {
83+ followedPeople = append(followedPeople, p)
84+ } else {
85+ discoverPeople = append(discoverPeople, p)
86+ }
87+ }
88+
80 s.render(w, r, "dashboard.html", map[string]any{89 s.render(w, r, "dashboard.html", map[string]any{
81 "User": user,90 "User": user,
82 "UnreadCount": unreadCount,91 "UnreadCount": unreadCount,
@@ -84,7 +93,8 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
84 "Articles": articles,93 "Articles": articles,
85 "ArticleRecommendations": articleRecs,94 "ArticleRecommendations": articleRecs,
86 "FeedRecommendations": feedRecs,95 "FeedRecommendations": feedRecs,
87- "PeopleRecommendations": peopleRecs,96+ "FollowedPeople": followedPeople,
97+ "DiscoverPeople": discoverPeople,
88 "PersonalTrending": personalTrending,98 "PersonalTrending": personalTrending,
89 "GlobalTrending": globalTrending,99 "GlobalTrending": globalTrending,
90 "Page": page,100 "Page": page,
modified internal/server/feeds_handler.go +11 -1
@@ -68,6 +68,15 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
6868 s.logger.Warn("failed to get categories", "error", err, "did", user.DID)
6969 }
7070
71+ var followedPeople, discoverPeople []*cluster.PersonRecommendation
72+ for _, p := range peopleRecs {
73+ if p.IsFollowed {
74+ followedPeople = append(followedPeople, p)
75+ } else {
76+ discoverPeople = append(discoverPeople, p)
77+ }
78+ }
79+
7180 s.render(w, r, "feeds.html", map[string]any{
7281 "User": user,
7382 "Subscriptions": subs,
@@ -75,7 +84,8 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
7584 "Categories": categories,
7685 "Category": category,
7786 "FeedRecommendations": feedRecs,
78- "PeopleRecommendations": peopleRecs,
87+ "FollowedPeople": followedPeople,
88+ "DiscoverPeople": discoverPeople,
7989 "DeadFeeds": deadFeeds,
8090 "Page": page,
8191 "BaseURL": "/feeds",
@@ -68,6 +68,15 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
68 s.logger.Warn("failed to get categories", "error", err, "did", user.DID)68 s.logger.Warn("failed to get categories", "error", err, "did", user.DID)
69 }69 }
70 70
71+ var followedPeople, discoverPeople []*cluster.PersonRecommendation
72+ for _, p := range peopleRecs {
73+ if p.IsFollowed {
74+ followedPeople = append(followedPeople, p)
75+ } else {
76+ discoverPeople = append(discoverPeople, p)
77+ }
78+ }
79+
71 s.render(w, r, "feeds.html", map[string]any{80 s.render(w, r, "feeds.html", map[string]any{
72 "User": user,81 "User": user,
73 "Subscriptions": subs,82 "Subscriptions": subs,
@@ -75,7 +84,8 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
75 "Categories": categories,84 "Categories": categories,
76 "Category": category,85 "Category": category,
77 "FeedRecommendations": feedRecs,86 "FeedRecommendations": feedRecs,
78- "PeopleRecommendations": peopleRecs,87+ "FollowedPeople": followedPeople,
88+ "DiscoverPeople": discoverPeople,
79 "DeadFeeds": deadFeeds,89 "DeadFeeds": deadFeeds,
80 "Page": page,90 "Page": page,
81 "BaseURL": "/feeds",91 "BaseURL": "/feeds",
modified internal/tmpl/dashboard.html +35 -9
@@ -94,14 +94,24 @@
9494 </div>
9595 {{end}}
9696
97- {{if .PeopleRecommendations}}
97+ {{if or .FollowedPeople .DiscoverPeople}}
9898 <div>
99- <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>
99+ {{if .FollowedPeople}}
100+ <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2>
100101 <div class="space-y-3">
101- {{range .PeopleRecommendations}}
102+ {{range .FollowedPeople}}
102103 {{template "profile-card.html" .}}
103104 {{end}}
104105 </div>
106+ {{end}}
107+ {{if .DiscoverPeople}}
108+ <h2 class="text-lg font-semibold text-spot-text mb-4 {{if .FollowedPeople}}mt-6{{end}}">Discover new readers</h2>
109+ <div class="space-y-3">
110+ {{range .DiscoverPeople}}
111+ {{template "profile-card.html" .}}
112+ {{end}}
113+ </div>
114+ {{end}}
105115 </div>
106116 {{end}}
107117
@@ -138,7 +148,7 @@
138148 </div>
139149 {{end}}
140150
141-{{if or .PersonalTrending .PeopleRecommendations .FeedRecommendations}}
151+{{if or .PersonalTrending (or .FollowedPeople .DiscoverPeople) .FeedRecommendations}}
142152 <div class="space-y-8">
143153 {{if .PersonalTrending}}
144154 <div>
@@ -154,12 +164,28 @@
154164 </div>
155165 {{end}}
156166
157- {{if .PeopleRecommendations}}
167+ {{if or .FollowedPeople .DiscoverPeople}}
158168 <div>
159- <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>
160- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
161- {{range .PeopleRecommendations}}
162- {{template "profile-card.html" .}}
169+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
170+ {{if .FollowedPeople}}
171+ <div>
172+ <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2>
173+ <div class="space-y-3">
174+ {{range .FollowedPeople}}
175+ {{template "profile-card.html" .}}
176+ {{end}}
177+ </div>
178+ </div>
179+ {{end}}
180+ {{if .DiscoverPeople}}
181+ <div>
182+ <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2>
183+ <div class="space-y-3">
184+ {{range .DiscoverPeople}}
185+ {{template "profile-card.html" .}}
186+ {{end}}
187+ </div>
188+ </div>
163189 {{end}}
164190 </div>
165191 </div>
@@ -94,14 +94,24 @@
94 </div>94 </div>
95 {{end}}95 {{end}}
96 96
97- {{if .PeopleRecommendations}}97+ {{if or .FollowedPeople .DiscoverPeople}}
98 <div>98 <div>
99- <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>99+ {{if .FollowedPeople}}
100+ <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2>
100 <div class="space-y-3">101 <div class="space-y-3">
101- {{range .PeopleRecommendations}}102+ {{range .FollowedPeople}}
102 {{template "profile-card.html" .}}103 {{template "profile-card.html" .}}
103 {{end}}104 {{end}}
104 </div>105 </div>
106+ {{end}}
107+ {{if .DiscoverPeople}}
108+ <h2 class="text-lg font-semibold text-spot-text mb-4 {{if .FollowedPeople}}mt-6{{end}}">Discover new readers</h2>
109+ <div class="space-y-3">
110+ {{range .DiscoverPeople}}
111+ {{template "profile-card.html" .}}
112+ {{end}}
113+ </div>
114+ {{end}}
105 </div>115 </div>
106 {{end}}116 {{end}}
107 117
@@ -138,7 +148,7 @@
138 </div>148 </div>
139 {{end}}149 {{end}}
140 150
141-{{if or .PersonalTrending .PeopleRecommendations .FeedRecommendations}}151+{{if or .PersonalTrending (or .FollowedPeople .DiscoverPeople) .FeedRecommendations}}
142 <div class="space-y-8">152 <div class="space-y-8">
143 {{if .PersonalTrending}}153 {{if .PersonalTrending}}
144 <div>154 <div>
@@ -154,12 +164,28 @@
154 </div>164 </div>
155 {{end}}165 {{end}}
156 166
157- {{if .PeopleRecommendations}}167+ {{if or .FollowedPeople .DiscoverPeople}}
158 <div>168 <div>
159- <h2 class="text-lg font-semibold text-spot-text mb-4">Similar readers</h2>169+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
160- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">170+ {{if .FollowedPeople}}
161- {{range .PeopleRecommendations}}171+ <div>
162- {{template "profile-card.html" .}}172+ <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2>
173+ <div class="space-y-3">
174+ {{range .FollowedPeople}}
175+ {{template "profile-card.html" .}}
176+ {{end}}
177+ </div>
178+ </div>
179+ {{end}}
180+ {{if .DiscoverPeople}}
181+ <div>
182+ <h2 class="text-lg font-semibold text-spot-text mb-4">Discover new readers</h2>
183+ <div class="space-y-3">
184+ {{range .DiscoverPeople}}
185+ {{template "profile-card.html" .}}
186+ {{end}}
187+ </div>
188+ </div>
163 {{end}}189 {{end}}
164 </div>190 </div>
165 </div>191 </div>
modified internal/tmpl/feeds.html +13 -3
@@ -86,14 +86,24 @@ function gleanRefreshPoll(){setTimeout(function(){htmx.ajax('GET','/feeds/list',
8686 </div>
8787 </div>
8888
89- {{if .PeopleRecommendations}}
89+ {{if or .FollowedPeople .DiscoverPeople}}
9090 <div>
91- <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-4">Similar readers</h2>
91+ {{if .FollowedPeople}}
92+ <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2>
9293 <div class="space-y-3">
93- {{range .PeopleRecommendations}}
94+ {{range .FollowedPeople}}
9495 {{template "profile-card.html" .}}
9596 {{end}}
9697 </div>
98+ {{end}}
99+ {{if .DiscoverPeople}}
100+ <h2 class="text-lg font-semibold text-spot-text mb-4 {{if .FollowedPeople}}mt-6{{end}}">Discover new readers</h2>
101+ <div class="space-y-3">
102+ {{range .DiscoverPeople}}
103+ {{template "profile-card.html" .}}
104+ {{end}}
105+ </div>
106+ {{end}}
97107 </div>
98108 {{end}}
99109
@@ -86,14 +86,24 @@ function gleanRefreshPoll(){setTimeout(function(){htmx.ajax('GET','/feeds/list',
86 </div>86 </div>
87 </div>87 </div>
88 88
89- {{if .PeopleRecommendations}}89+ {{if or .FollowedPeople .DiscoverPeople}}
90 <div>90 <div>
91- <h2 class="text-sm font-bold text-spot-text uppercase tracking-wide mb-4">Similar readers</h2>91+ {{if .FollowedPeople}}
92+ <h2 class="text-lg font-semibold text-spot-text mb-4">Your network</h2>
92 <div class="space-y-3">93 <div class="space-y-3">
93- {{range .PeopleRecommendations}}94+ {{range .FollowedPeople}}
94 {{template "profile-card.html" .}}95 {{template "profile-card.html" .}}
95 {{end}}96 {{end}}
96 </div>97 </div>
98+ {{end}}
99+ {{if .DiscoverPeople}}
100+ <h2 class="text-lg font-semibold text-spot-text mb-4 {{if .FollowedPeople}}mt-6{{end}}">Discover new readers</h2>
101+ <div class="space-y-3">
102+ {{range .DiscoverPeople}}
103+ {{template "profile-card.html" .}}
104+ {{end}}
105+ </div>
106+ {{end}}
97 </div>107 </div>
98 {{end}}108 {{end}}
99 109
modified internal/tmpl/partials/profile-card.html +9 -4
@@ -1,10 +1,15 @@
11 {{define "profile-card.html"}}
2-<div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
2+<a href="/profile/{{.DID}}" class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
33 {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}}
44 <div class="min-w-0 flex-1">
5- <a href="/profile/{{.DID}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a>
5+ <div class="flex items-center gap-1.5">
6+ <span class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</span>
7+ {{if .IsFollowed}}
8+ <span class="inline-flex items-center gap-0.5 text-[10px] font-medium text-spot-green bg-spot-green/10 px-1.5 py-0.5 rounded-full">Following</span>
9+ {{end}}
10+ </div>
611 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}
712 </div>
8- <span class="text-xs text-spot-secondary">{{.CommonFeeds}} shared</span>
9-</div>
13+ <span class="text-xs text-spot-secondary shrink-0">{{.CommonFeeds}} shared</span>
14+</a>
1015 {{end}}
@@ -1,10 +1,15 @@
1 {{define "profile-card.html"}}1 {{define "profile-card.html"}}
2-<div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">2+<a href="/profile/{{.DID}}" class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
3 {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}}3 {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}}
4 <div class="min-w-0 flex-1">4 <div class="min-w-0 flex-1">
5- <a href="/profile/{{.DID}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a>5+ <div class="flex items-center gap-1.5">
6+ <span class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</span>
7+ {{if .IsFollowed}}
8+ <span class="inline-flex items-center gap-0.5 text-[10px] font-medium text-spot-green bg-spot-green/10 px-1.5 py-0.5 rounded-full">Following</span>
9+ {{end}}
10+ </div>
6 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}11 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}
7 </div>12 </div>
8- <span class="text-xs text-spot-secondary">{{.CommonFeeds}} shared</span>13+ <span class="text-xs text-spot-secondary shrink-0">{{.CommonFeeds}} shared</span>
9-</div>14+</a>
10 {{end}}15 {{end}}
modified readme.md +1 -1
@@ -22,7 +22,7 @@ Glean looks at what you and other users subscribe to, read, and like to suggest
2222
2323 **Feed suggestions** come from readers who share your subscriptions. If a lot of people who follow the same blogs as you also follow a blog you haven't seen, that blog shows up as a recommendation. The system also considers which articles you've liked, whether you follow the person on Bluesky, and how popular the feed is overall.
2424
25-**People suggestions** are readers whose subscriptions overlap with yours. The more feeds you share, the higher they rank. You also see whether you have any Bluesky follows in common.
25+**People suggestions** are split into two groups: "Your network" shows people you already follow on Bluesky who share your reading habits, and "Discover new readers" surfaces readers you don't follow but who have overlapping subscriptions and likes.
2626
2727 **Dismissals** keep things tidy. If you dismiss a recommendation, it won't come back. If a suggestion sits ignored for more than 5 days, it's automatically removed so newer recommendations can take its place.
2828
@@ -22,7 +22,7 @@ Glean looks at what you and other users subscribe to, read, and like to suggest
22 22
23 **Feed suggestions** come from readers who share your subscriptions. If a lot of people who follow the same blogs as you also follow a blog you haven't seen, that blog shows up as a recommendation. The system also considers which articles you've liked, whether you follow the person on Bluesky, and how popular the feed is overall.23 **Feed suggestions** come from readers who share your subscriptions. If a lot of people who follow the same blogs as you also follow a blog you haven't seen, that blog shows up as a recommendation. The system also considers which articles you've liked, whether you follow the person on Bluesky, and how popular the feed is overall.
24 24
25-**People suggestions** are readers whose subscriptions overlap with yours. The more feeds you share, the higher they rank. You also see whether you have any Bluesky follows in common.25+**People suggestions** are split into two groups: "Your network" shows people you already follow on Bluesky who share your reading habits, and "Discover new readers" surfaces readers you don't follow but who have overlapping subscriptions and likes.
26 26
27 **Dismissals** keep things tidy. If you dismiss a recommendation, it won't come back. If a suggestion sits ignored for more than 5 days, it's automatically removed so newer recommendations can take its place.27 **Dismissals** keep things tidy. If you dismiss a recommendation, it won't come back. If a suggestion sits ignored for more than 5 days, it's automatically removed so newer recommendations can take its place.
28 28