Add person dismissal feature and UI and refactor endpoints urlUnverified
109590a parent: 16ed9f6 modified
internal/cluster/dismiss.go +9 -0 | @@ -29,6 +29,15 @@ func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason | ||
| 29 | 29 | 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 | 41 | func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error { |
| 33 | 42 | tx, err := e.db.BeginTx(ctx, nil) |
| 34 | 43 | if err != nil { |
| @@ -29,6 +29,15 @@ func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason | |||
| 29 | return err | 29 | 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 | ||
| 469 | 469 | JOIN main.users u ON u.did = sim.peer_did |
| 470 | 470 | LEFT JOIN main.follows f ON f.user_did = ? AND f.target_did = u.did |
| 471 | 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 | 473 | ORDER BY sim.jaccard DESC |
| 473 | 474 | LIMIT ? |
| 474 | - `, userDID, userDID, userDID, limit) | |
| 475 | + `, userDID, userDID, userDID, userDID, limit) | |
| 475 | 476 | if err != nil { |
| 476 | 477 | return nil, err |
| 477 | 478 | } |
| @@ -469,9 +469,10 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD | |||
| 469 | JOIN main.users u ON u.did = sim.peer_did | 469 | 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 | 470 | 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 DESC | 473 | 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, err | 477 | return nil, err |
| 477 | } | 478 | } |
modified
internal/db/db.go +2 -2 | @@ -209,7 +209,7 @@ var usersSchema = []string{ | ||
| 209 | 209 | |
| 210 | 210 | `CREATE TABLE IF NOT EXISTS dismissed_recommendations ( |
| 211 | 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 | 213 | target_id TEXT NOT NULL, |
| 214 | 214 | reason TEXT, |
| 215 | 215 | dismissed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| @@ -218,7 +218,7 @@ var usersSchema = []string{ | ||
| 218 | 218 | |
| 219 | 219 | `CREATE TABLE IF NOT EXISTS recommendation_impressions ( |
| 220 | 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 | 222 | target_id TEXT NOT NULL, |
| 223 | 223 | first_shown_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 224 | 224 | 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) { | ||
| 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 | 107 | func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) { |
| 130 | 108 | g, gCtx := errgroup.WithContext(ctx) |
| 131 | 109 | 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) { | ||
| 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 | 447 | func (s *Server) discoverFeed(ctx context.Context, feedURL string) (*feed.ParseResult, string, error) { |
| 470 | 448 | discovered, err := feed.Discover(ctx, feedURL) |
| 471 | 449 | 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() { | ||
| 166 | 166 | r.Post("/retry", s.handleRetryFeed) |
| 167 | 167 | r.Get("/list", s.handleFeedList) |
| 168 | 168 | r.Post("/clear", s.handleClearAllSubscriptions) |
| 169 | - r.Post("/dismiss", s.handleDismissFeedRecommendation) | |
| 170 | 169 | }) |
| 171 | 170 | |
| 172 | 171 | s.router.Route("/articles", func(r chi.Router) { |
| @@ -179,7 +178,6 @@ func (s *Server) setupRoutes() { | ||
| 179 | 178 | r.Post("/{id}/like", s.handleLikeArticle) |
| 180 | 179 | r.Post("/{id}/fetch-content", s.handleFetchContent) |
| 181 | 180 | r.Post("/mark-all-read", s.handleMarkAllRead) |
| 182 | - r.Post("/dismiss", s.handleDismissArticleRecommendation) | |
| 183 | 181 | }) |
| 184 | 182 | |
| 185 | 183 | s.router.Route("/trending", func(r chi.Router) { |
| @@ -198,6 +196,13 @@ func (s *Server) setupRoutes() { | ||
| 198 | 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 | 206 | s.router.Get("/auth/login", s.handleAuthLogin) |
| 202 | 207 | s.router.Post("/auth/start", s.handleAuthStart) |
| 203 | 208 | 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 @@ | ||
| 1 | 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 | 3 | {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}} |
| 4 | 4 | <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}} | |
| 11 | 9 | {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}} |
| 12 | 10 | </div> |
| 13 | 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 | 20 | {{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 @@ | ||
| 15 | 15 | {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}} |
| 16 | 16 | </div> |
| 17 | 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 | 19 | class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1"> |
| 20 | 20 | <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}"> |
| 21 | 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> |
| @@ -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 @@ | ||
| 10 | 10 | </a> |
| 11 | 11 | <div class="flex items-center gap-2 shrink-0"> |
| 12 | 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 | 14 | {{csrfInput .CSRFToken}} |
| 15 | 15 | <input type="hidden" name="feed_url" value="{{.feed_url}}"> |
| 16 | 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"> |
| @@ -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"> |