nandi/gleanpublic⑂ Fork 0
⑂ 109590a
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.

Add person dismissal feature and UI and refactor endpoints urlUnverified

Julien Robert committed 2026-04-28T09:47:43+02:00 Browse files
109590a parent: 16ed9f6
modified internal/cluster/dismiss.go +9 -0
@@ -29,6 +29,15 @@ func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason
2929 return err
3030 }
3131
32+func (e *Engine) DismissPerson(ctx context.Context, userDID, targetDID, reason string) error {
33+ _, err := e.db.ExecContext(ctx, `
34+ INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
35+ VALUES (?, 'person', ?, ?)
36+ ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
37+ `, userDID, targetDID, reason)
38+ return err
39+}
40+
3241 func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error {
3342 tx, err := e.db.BeginTx(ctx, nil)
3443 if err != nil {
@@ -29,6 +29,15 @@ func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason
29 return err29 return err
30 }30 }
31 31
32+func (e *Engine) DismissPerson(ctx context.Context, userDID, targetDID, reason string) error {
33+ _, err := e.db.ExecContext(ctx, `
34+ INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
35+ VALUES (?, 'person', ?, ?)
36+ ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
37+ `, userDID, targetDID, reason)
38+ return err
39+}
40+
32 func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error {41 func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error {
33 tx, err := e.db.BeginTx(ctx, nil)42 tx, err := e.db.BeginTx(ctx, nil)
34 if err != nil {43 if err != nil {
modified internal/cluster/scoring.go +2 -1
@@ -469,9 +469,10 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD
469469 JOIN main.users u ON u.did = sim.peer_did
470470 LEFT JOIN main.follows f ON f.user_did = ? AND f.target_did = u.did
471471 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)
472+ AND NOT EXISTS (SELECT 1 FROM main.dismissed_recommendations d WHERE d.user_did = ? AND d.target_type = 'person' AND d.target_id = u.did)
472473 ORDER BY sim.jaccard DESC
473474 LIMIT ?
474- `, userDID, userDID, userDID, limit)
475+ `, userDID, userDID, userDID, userDID, limit)
475476 if err != nil {
476477 return nil, err
477478 }
@@ -469,9 +469,10 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD
469 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.did470 LEFT JOIN main.follows f ON f.user_did = ? AND f.target_did = u.did
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)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)
472+ AND NOT EXISTS (SELECT 1 FROM main.dismissed_recommendations d WHERE d.user_did = ? AND d.target_type = 'person' AND d.target_id = u.did)
472 ORDER BY sim.jaccard DESC473 ORDER BY sim.jaccard DESC
473 LIMIT ?474 LIMIT ?
474- `, userDID, userDID, userDID, limit)475+ `, userDID, userDID, userDID, userDID, limit)
475 if err != nil {476 if err != nil {
476 return nil, err477 return nil, err
477 }478 }
modified internal/db/db.go +2 -2
@@ -209,7 +209,7 @@ var usersSchema = []string{
209209
210210 `CREATE TABLE IF NOT EXISTS dismissed_recommendations (
211211 user_did TEXT NOT NULL,
212- target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')),
212+ target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')),
213213 target_id TEXT NOT NULL,
214214 reason TEXT,
215215 dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -218,7 +218,7 @@ var usersSchema = []string{
218218
219219 `CREATE TABLE IF NOT EXISTS recommendation_impressions (
220220 user_did TEXT NOT NULL,
221- target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')),
221+ target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')),
222222 target_id TEXT NOT NULL,
223223 first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
224224 last_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -209,7 +209,7 @@ var usersSchema = []string{
209 209
210 `CREATE TABLE IF NOT EXISTS dismissed_recommendations (210 `CREATE TABLE IF NOT EXISTS dismissed_recommendations (
211 user_did TEXT NOT NULL,211 user_did TEXT NOT NULL,
212- target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')),212+ target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')),
213 target_id TEXT NOT NULL,213 target_id TEXT NOT NULL,
214 reason TEXT,214 reason TEXT,
215 dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,215 dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -218,7 +218,7 @@ var usersSchema = []string{
218 218
219 `CREATE TABLE IF NOT EXISTS recommendation_impressions (219 `CREATE TABLE IF NOT EXISTS recommendation_impressions (
220 user_did TEXT NOT NULL,220 user_did TEXT NOT NULL,
221- target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')),221+ target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article', 'person')),
222 target_id TEXT NOT NULL,222 target_id TEXT NOT NULL,
223 first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,223 first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
224 last_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,224 last_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified internal/server/dashboard_handler.go +0 -22
@@ -104,28 +104,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
104104 })
105105 }
106106
107-func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {
108- user := currentUser(r)
109- articleURL := r.FormValue("article_url")
110- if articleURL == "" {
111- http.Error(w, "article_url required", http.StatusBadRequest)
112- return
113- }
114-
115- reason := r.FormValue("reason")
116- if reason == "" {
117- reason = "not_interested"
118- }
119-
120- if err := s.engine.DismissArticle(r.Context(), user.DID, articleURL, reason); err != nil {
121- s.logger.Error("failed to dismiss article recommendation", "error", err)
122- http.Error(w, err.Error(), http.StatusInternalServerError)
123- return
124- }
125-
126- w.WriteHeader(http.StatusOK)
127-}
128-
129107 func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) {
130108 g, gCtx := errgroup.WithContext(ctx)
131109 g.SetLimit(5)
@@ -104,28 +104,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
104 })104 })
105 }105 }
106 106
107-func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {
108- user := currentUser(r)
109- articleURL := r.FormValue("article_url")
110- if articleURL == "" {
111- http.Error(w, "article_url required", http.StatusBadRequest)
112- return
113- }
114-
115- reason := r.FormValue("reason")
116- if reason == "" {
117- reason = "not_interested"
118- }
119-
120- if err := s.engine.DismissArticle(r.Context(), user.DID, articleURL, reason); err != nil {
121- s.logger.Error("failed to dismiss article recommendation", "error", err)
122- http.Error(w, err.Error(), http.StatusInternalServerError)
123- return
124- }
125-
126- w.WriteHeader(http.StatusOK)
127-}
128-
129 func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) {107 func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) {
130 g, gCtx := errgroup.WithContext(ctx)108 g, gCtx := errgroup.WithContext(ctx)
131 g.SetLimit(5)109 g.SetLimit(5)
modified internal/server/feeds_handler.go +0 -22
@@ -444,28 +444,6 @@ func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
444444 })
445445 }
446446
447-func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
448- user := currentUser(r)
449- feedURL := r.FormValue("feed_url")
450- if feedURL == "" {
451- http.Error(w, "feed_url required", http.StatusBadRequest)
452- return
453- }
454-
455- reason := r.FormValue("reason")
456- if reason == "" {
457- reason = "not_interested"
458- }
459-
460- if err := s.engine.DismissFeed(r.Context(), user.DID, feedURL, reason); err != nil {
461- s.logger.Error("failed to dismiss feed recommendation", "error", err)
462- http.Error(w, err.Error(), http.StatusInternalServerError)
463- return
464- }
465-
466- w.WriteHeader(http.StatusOK)
467-}
468-
469447 func (s *Server) discoverFeed(ctx context.Context, feedURL string) (*feed.ParseResult, string, error) {
470448 discovered, err := feed.Discover(ctx, feedURL)
471449 if err != nil || len(discovered.FeedURLs) == 0 {
@@ -444,28 +444,6 @@ func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
444 })444 })
445 }445 }
446 446
447-func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
448- user := currentUser(r)
449- feedURL := r.FormValue("feed_url")
450- if feedURL == "" {
451- http.Error(w, "feed_url required", http.StatusBadRequest)
452- return
453- }
454-
455- reason := r.FormValue("reason")
456- if reason == "" {
457- reason = "not_interested"
458- }
459-
460- if err := s.engine.DismissFeed(r.Context(), user.DID, feedURL, reason); err != nil {
461- s.logger.Error("failed to dismiss feed recommendation", "error", err)
462- http.Error(w, err.Error(), http.StatusInternalServerError)
463- return
464- }
465-
466- w.WriteHeader(http.StatusOK)
467-}
468-
469 func (s *Server) discoverFeed(ctx context.Context, feedURL string) (*feed.ParseResult, string, error) {447 func (s *Server) discoverFeed(ctx context.Context, feedURL string) (*feed.ParseResult, string, error) {
470 discovered, err := feed.Discover(ctx, feedURL)448 discovered, err := feed.Discover(ctx, feedURL)
471 if err != nil || len(discovered.FeedURLs) == 0 {449 if err != nil || len(discovered.FeedURLs) == 0 {
added internal/server/recs_handler.go +71 -0
new file mode 100644
@@ -0,0 +1,71 @@
1+package server
2+
3+import (
4+ "net/http"
5+)
6+
7+func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
8+ user := currentUser(r)
9+ feedURL := r.FormValue("feed_url")
10+ if feedURL == "" {
11+ http.Error(w, "feed_url required", http.StatusBadRequest)
12+ return
13+ }
14+
15+ reason := r.FormValue("reason")
16+ if reason == "" {
17+ reason = "not_interested"
18+ }
19+
20+ if err := s.engine.DismissFeed(r.Context(), user.DID, feedURL, reason); err != nil {
21+ s.logger.Error("failed to dismiss feed recommendation", "error", err)
22+ http.Error(w, err.Error(), http.StatusInternalServerError)
23+ return
24+ }
25+
26+ w.WriteHeader(http.StatusOK)
27+}
28+
29+func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {
30+ user := currentUser(r)
31+ articleURL := r.FormValue("article_url")
32+ if articleURL == "" {
33+ http.Error(w, "article_url required", http.StatusBadRequest)
34+ return
35+ }
36+
37+ reason := r.FormValue("reason")
38+ if reason == "" {
39+ reason = "not_interested"
40+ }
41+
42+ if err := s.engine.DismissArticle(r.Context(), user.DID, articleURL, reason); err != nil {
43+ s.logger.Error("failed to dismiss article recommendation", "error", err)
44+ http.Error(w, err.Error(), http.StatusInternalServerError)
45+ return
46+ }
47+
48+ w.WriteHeader(http.StatusOK)
49+}
50+
51+func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *http.Request) {
52+ user := currentUser(r)
53+ targetDID := r.FormValue("target_did")
54+ if targetDID == "" {
55+ http.Error(w, "target_did required", http.StatusBadRequest)
56+ return
57+ }
58+
59+ reason := r.FormValue("reason")
60+ if reason == "" {
61+ reason = "not_interested"
62+ }
63+
64+ if err := s.engine.DismissPerson(r.Context(), user.DID, targetDID, reason); err != nil {
65+ s.logger.Error("failed to dismiss person recommendation", "error", err)
66+ http.Error(w, err.Error(), http.StatusInternalServerError)
67+ return
68+ }
69+
70+ w.WriteHeader(http.StatusOK)
71+}
new file mode 100644
@@ -0,0 +1,71 @@
1+package server
2+
3+import (
4+ "net/http"
5+)
6+
7+func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
8+ user := currentUser(r)
9+ feedURL := r.FormValue("feed_url")
10+ if feedURL == "" {
11+ http.Error(w, "feed_url required", http.StatusBadRequest)
12+ return
13+ }
14+
15+ reason := r.FormValue("reason")
16+ if reason == "" {
17+ reason = "not_interested"
18+ }
19+
20+ if err := s.engine.DismissFeed(r.Context(), user.DID, feedURL, reason); err != nil {
21+ s.logger.Error("failed to dismiss feed recommendation", "error", err)
22+ http.Error(w, err.Error(), http.StatusInternalServerError)
23+ return
24+ }
25+
26+ w.WriteHeader(http.StatusOK)
27+}
28+
29+func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {
30+ user := currentUser(r)
31+ articleURL := r.FormValue("article_url")
32+ if articleURL == "" {
33+ http.Error(w, "article_url required", http.StatusBadRequest)
34+ return
35+ }
36+
37+ reason := r.FormValue("reason")
38+ if reason == "" {
39+ reason = "not_interested"
40+ }
41+
42+ if err := s.engine.DismissArticle(r.Context(), user.DID, articleURL, reason); err != nil {
43+ s.logger.Error("failed to dismiss article recommendation", "error", err)
44+ http.Error(w, err.Error(), http.StatusInternalServerError)
45+ return
46+ }
47+
48+ w.WriteHeader(http.StatusOK)
49+}
50+
51+func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *http.Request) {
52+ user := currentUser(r)
53+ targetDID := r.FormValue("target_did")
54+ if targetDID == "" {
55+ http.Error(w, "target_did required", http.StatusBadRequest)
56+ return
57+ }
58+
59+ reason := r.FormValue("reason")
60+ if reason == "" {
61+ reason = "not_interested"
62+ }
63+
64+ if err := s.engine.DismissPerson(r.Context(), user.DID, targetDID, reason); err != nil {
65+ s.logger.Error("failed to dismiss person recommendation", "error", err)
66+ http.Error(w, err.Error(), http.StatusInternalServerError)
67+ return
68+ }
69+
70+ w.WriteHeader(http.StatusOK)
71+}
modified internal/server/server.go +7 -2
@@ -166,7 +166,6 @@ func (s *Server) setupRoutes() {
166166 r.Post("/retry", s.handleRetryFeed)
167167 r.Get("/list", s.handleFeedList)
168168 r.Post("/clear", s.handleClearAllSubscriptions)
169- r.Post("/dismiss", s.handleDismissFeedRecommendation)
170169 })
171170
172171 s.router.Route("/articles", func(r chi.Router) {
@@ -179,7 +178,6 @@ func (s *Server) setupRoutes() {
179178 r.Post("/{id}/like", s.handleLikeArticle)
180179 r.Post("/{id}/fetch-content", s.handleFetchContent)
181180 r.Post("/mark-all-read", s.handleMarkAllRead)
182- r.Post("/dismiss", s.handleDismissArticleRecommendation)
183181 })
184182
185183 s.router.Route("/trending", func(r chi.Router) {
@@ -198,6 +196,13 @@ func (s *Server) setupRoutes() {
198196 r.Post("/{id}/delete", s.handleDeleteAnnotation)
199197 })
200198
199+ s.router.Route("/recs", func(r chi.Router) {
200+ r.Use(s.requireAuth)
201+ r.Post("/dismiss-feed", s.handleDismissFeedRecommendation)
202+ r.Post("/dismiss-article", s.handleDismissArticleRecommendation)
203+ r.Post("/dismiss-person", s.handleDismissPersonRecommendation)
204+ })
205+
201206 s.router.Get("/auth/login", s.handleAuthLogin)
202207 s.router.Post("/auth/start", s.handleAuthStart)
203208 s.router.Get("/auth/callback", s.handleAuthCallback)
@@ -166,7 +166,6 @@ func (s *Server) setupRoutes() {
166 r.Post("/retry", s.handleRetryFeed)166 r.Post("/retry", s.handleRetryFeed)
167 r.Get("/list", s.handleFeedList)167 r.Get("/list", s.handleFeedList)
168 r.Post("/clear", s.handleClearAllSubscriptions)168 r.Post("/clear", s.handleClearAllSubscriptions)
169- r.Post("/dismiss", s.handleDismissFeedRecommendation)
170 })169 })
171 170
172 s.router.Route("/articles", func(r chi.Router) {171 s.router.Route("/articles", func(r chi.Router) {
@@ -179,7 +178,6 @@ func (s *Server) setupRoutes() {
179 r.Post("/{id}/like", s.handleLikeArticle)178 r.Post("/{id}/like", s.handleLikeArticle)
180 r.Post("/{id}/fetch-content", s.handleFetchContent)179 r.Post("/{id}/fetch-content", s.handleFetchContent)
181 r.Post("/mark-all-read", s.handleMarkAllRead)180 r.Post("/mark-all-read", s.handleMarkAllRead)
182- r.Post("/dismiss", s.handleDismissArticleRecommendation)
183 })181 })
184 182
185 s.router.Route("/trending", func(r chi.Router) {183 s.router.Route("/trending", func(r chi.Router) {
@@ -198,6 +196,13 @@ func (s *Server) setupRoutes() {
198 r.Post("/{id}/delete", s.handleDeleteAnnotation)196 r.Post("/{id}/delete", s.handleDeleteAnnotation)
199 })197 })
200 198
199+ s.router.Route("/recs", func(r chi.Router) {
200+ r.Use(s.requireAuth)
201+ r.Post("/dismiss-feed", s.handleDismissFeedRecommendation)
202+ r.Post("/dismiss-article", s.handleDismissArticleRecommendation)
203+ r.Post("/dismiss-person", s.handleDismissPersonRecommendation)
204+ })
205+
201 s.router.Get("/auth/login", s.handleAuthLogin)206 s.router.Get("/auth/login", s.handleAuthLogin)
202 s.router.Post("/auth/start", s.handleAuthStart)207 s.router.Post("/auth/start", s.handleAuthStart)
203 s.router.Get("/auth/callback", s.handleAuthCallback)208 s.router.Get("/auth/callback", s.handleAuthCallback)
modified internal/tmpl/partials/profile-card.html +13 -8
@@ -1,15 +1,20 @@
11 {{define "profile-card.html"}}
2-<a href="/profile/{{.DID}}" class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
2+<div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition relative group">
33 {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}}
44 <div class="min-w-0 flex-1">
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>
5+ <a href="/profile/{{.DID}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a>
6+ {{if .IsFollowed}}
7+ <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 ml-1.5">Following</span>
8+ {{end}}
119 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}
1210 </div>
1311 <span class="text-xs text-spot-secondary shrink-0">{{.CommonFeeds}} shared</span>
14-</a>
12+ {{if not .IsFollowed}}
13+ <button hx-post="/recs/dismiss-person" hx-target="closest .group" hx-swap="outerHTML swap:0.3s"
14+ hx-vals='{"target_did": "{{.DID}}"}'
15+ class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition text-spot-muted hover:text-spot-red p-1 rounded-md hover:bg-spot-red/10" title="Hide">
16+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
17+ </button>
18+ {{end}}
19+</div>
1520 {{end}}
@@ -1,15 +1,20 @@
1 {{define "profile-card.html"}}1 {{define "profile-card.html"}}
2-<a href="/profile/{{.DID}}" class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">2+<div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition relative group">
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- <div class="flex items-center gap-1.5">5+ <a href="/profile/{{.DID}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a>
6- <span class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</span>6+ {{if .IsFollowed}}
7- {{if .IsFollowed}}7+ <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 ml-1.5">Following</span>
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>8+ {{end}}
9- {{end}}
10- </div>
11 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}9 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}
12 </div>10 </div>
13 <span class="text-xs text-spot-secondary shrink-0">{{.CommonFeeds}} shared</span>11 <span class="text-xs text-spot-secondary shrink-0">{{.CommonFeeds}} shared</span>
14-</a>12+ {{if not .IsFollowed}}
13+ <button hx-post="/recs/dismiss-person" hx-target="closest .group" hx-swap="outerHTML swap:0.3s"
14+ hx-vals='{"target_did": "{{.DID}}"}'
15+ class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition text-spot-muted hover:text-spot-red p-1 rounded-md hover:bg-spot-red/10" title="Hide">
16+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
17+ </button>
18+ {{end}}
19+</div>
15 {{end}}20 {{end}}
modified internal/tmpl/partials/recommendation-article-card.html +1 -1
@@ -15,7 +15,7 @@
1515 {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}}
1616 </div>
1717 <div class="shrink-0 pt-1">
18- <button hx-post="/articles/dismiss" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}"
18+ <button hx-post="/recs/dismiss-article" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}"
1919 class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1">
2020 <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}">
2121 <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
@@ -15,7 +15,7 @@
15 {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}}15 {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}}
16 </div>16 </div>
17 <div class="shrink-0 pt-1">17 <div class="shrink-0 pt-1">
18- <button hx-post="/articles/dismiss" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}"18+ <button hx-post="/recs/dismiss-article" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}"
19 class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1">19 class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1">
20 <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}">20 <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}">
21 <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>21 <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
modified internal/tmpl/partials/recommendation-feed-card.html +1 -1
@@ -10,7 +10,7 @@
1010 </a>
1111 <div class="flex items-center gap-2 shrink-0">
1212 <span class="text-xs text-spot-secondary">{{.subscriber_count}} subs</span>
13- <form hx-post="/feeds/dismiss" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline">
13+ <form hx-post="/recs/dismiss-feed" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline">
1414 {{csrfInput .CSRFToken}}
1515 <input type="hidden" name="feed_url" value="{{.feed_url}}">
1616 <button type="submit" title="Not interested" class="text-spot-muted hover:text-spot-red transition -m-1 p-1 rounded-full hover:bg-spot-hover-50 flex items-center">
@@ -10,7 +10,7 @@
10 </a>10 </a>
11 <div class="flex items-center gap-2 shrink-0">11 <div class="flex items-center gap-2 shrink-0">
12 <span class="text-xs text-spot-secondary">{{.subscriber_count}} subs</span>12 <span class="text-xs text-spot-secondary">{{.subscriber_count}} subs</span>
13- <form hx-post="/feeds/dismiss" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline">13+ <form hx-post="/recs/dismiss-feed" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline">
14 {{csrfInput .CSRFToken}}14 {{csrfInput .CSRFToken}}
15 <input type="hidden" name="feed_url" value="{{.feed_url}}">15 <input type="hidden" name="feed_url" value="{{.feed_url}}">
16 <button type="submit" title="Not interested" class="text-spot-muted hover:text-spot-red transition -m-1 p-1 rounded-full hover:bg-spot-hover-50 flex items-center">16 <button type="submit" title="Not interested" class="text-spot-muted hover:text-spot-red transition -m-1 p-1 rounded-full hover:bg-spot-hover-50 flex items-center">