diff --git a/crates/adapters/postgres/migrations/016_engagement_indexes.sql b/crates/adapters/postgres/migrations/016_engagement_indexes.sql new file mode 100644 index 0000000..4168fea --- /dev/null +++ b/crates/adapters/postgres/migrations/016_engagement_indexes.sql @@ -0,0 +1,10 @@ +-- Indexes for feed engagement counts and sorting. +-- likes and boosts are joined/counted per thought on every feed query. +-- thoughts(in_reply_to_id) is scanned for reply_count. +CREATE INDEX IF NOT EXISTS idx_likes_thought_id ON likes(thought_id); +CREATE INDEX IF NOT EXISTS idx_boosts_thought_id ON boosts(thought_id); +CREATE INDEX IF NOT EXISTS idx_thoughts_in_reply_to_id ON thoughts(in_reply_to_id) WHERE in_reply_to_id IS NOT NULL; + +-- Viewer-context lookups: "did I like/boost this?" +CREATE INDEX IF NOT EXISTS idx_likes_user_thought ON likes(user_id, thought_id); +CREATE INDEX IF NOT EXISTS idx_boosts_user_thought ON boosts(user_id, thought_id); diff --git a/crates/adapters/postgres/src/feed/mod.rs b/crates/adapters/postgres/src/feed/mod.rs index da86ff8..8960f49 100644 --- a/crates/adapters/postgres/src/feed/mod.rs +++ b/crates/adapters/postgres/src/feed/mod.rs @@ -114,12 +114,19 @@ impl<'a> FeedSqlBuilder<'a> { } fn select(&self) -> String { - let viewer_checks = match self.viewer { - Some(uid) => format!( - "EXISTS(SELECT 1 FROM likes WHERE user_id='{uid}' AND thought_id=t.id) AS liked_by_viewer, - EXISTS(SELECT 1 FROM boosts WHERE user_id='{uid}' AND thought_id=t.id) AS boosted_by_viewer" + let (viewer_cols, viewer_joins) = match self.viewer { + Some(uid) => ( + "(lv.thought_id IS NOT NULL) AS liked_by_viewer, + (bv.thought_id IS NOT NULL) AS boosted_by_viewer".to_string(), + format!( + "LEFT JOIN (SELECT thought_id FROM likes WHERE user_id='{uid}') lv ON lv.thought_id = t.id + LEFT JOIN (SELECT thought_id FROM boosts WHERE user_id='{uid}') bv ON bv.thought_id = t.id" + ), + ), + None => ( + "false AS liked_by_viewer, false AS boosted_by_viewer".to_string(), + String::new(), ), - None => "false AS liked_by_viewer, false AS boosted_by_viewer".to_string(), }; format!( " @@ -143,13 +150,17 @@ impl<'a> FeedSqlBuilder<'a> { u.header_url, u.custom_css, u.local AS author_local, u.created_at AS author_created_at, u.updated_at AS author_updated_at, - (SELECT COUNT(*) FROM likes l WHERE l.thought_id=t.id) AS like_count, - (SELECT COUNT(*) FROM boosts b WHERE b.thought_id=t.id) AS boost_count, - (SELECT COUNT(*) FROM thoughts r WHERE r.in_reply_to_id=t.id) AS reply_count, - {viewer_checks} + COALESCE(l_agg.cnt, 0) AS like_count, + COALESCE(b_agg.cnt, 0) AS boost_count, + COALESCE(r_agg.cnt, 0) AS reply_count, + {viewer_cols} FROM thoughts t JOIN users u ON u.id=t.user_id - LEFT JOIN remote_actors ra ON u.ap_id = ra.url" + LEFT JOIN remote_actors ra ON u.ap_id = ra.url + LEFT JOIN (SELECT thought_id, COUNT(*) AS cnt FROM likes GROUP BY thought_id) l_agg ON l_agg.thought_id = t.id + LEFT JOIN (SELECT thought_id, COUNT(*) AS cnt FROM boosts GROUP BY thought_id) b_agg ON b_agg.thought_id = t.id + LEFT JOIN (SELECT in_reply_to_id, COUNT(*) AS cnt FROM thoughts WHERE in_reply_to_id IS NOT NULL GROUP BY in_reply_to_id) r_agg ON r_agg.in_reply_to_id = t.id + {viewer_joins}" ) } diff --git a/thoughts-frontend/components/footer.tsx b/thoughts-frontend/components/footer.tsx index 9259489..5ee1746 100644 --- a/thoughts-frontend/components/footer.tsx +++ b/thoughts-frontend/components/footer.tsx @@ -1,11 +1,5 @@ import Link from "next/link"; -import { - BookOpen, - Code2, - Fingerprint, - Globe, - Info, -} from "lucide-react"; +import { BookOpen, Code2, Globe, Info } from "lucide-react"; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? ""; @@ -15,30 +9,28 @@ const LINKS = [ label: "API Reference", icon: BookOpen, external: true, + title: undefined, }, { href: `${API_URL}/.well-known/nodeinfo`, label: "NodeInfo", icon: Info, external: true, - }, - { - href: `${API_URL}/.well-known/webfinger`, - label: "WebFinger", - icon: Fingerprint, - external: true, + title: undefined, }, { href: "/about/fediverse", label: "About the Fediverse", icon: Globe, external: false, + title: undefined, }, { href: "https://git.gabrielkaszewski.dev/GKaszewski/thoughts", label: "Source Code", icon: Code2, external: true, + title: undefined, }, ] as const; @@ -52,7 +44,7 @@ export function Footer() { />
- {LINKS.map(({ href, label, icon: Icon }, i) => ( + {LINKS.map(({ href, label, icon: Icon, title }, i) => ( {i > 0 && ( @@ -64,6 +56,7 @@ export function Footer() { {...(href.startsWith("http") || href.startsWith(API_URL) ? { target: "_blank", rel: "noopener noreferrer" } : {})} + title={title} className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary transition-colors duration-150 group" >