perf(frontend): eliminate author profile waterfall — use thought.author directly
This commit is contained in:
@@ -5,9 +5,7 @@ import {
|
||||
getFriends,
|
||||
getMe,
|
||||
getTopFriends,
|
||||
getUserProfile,
|
||||
Me,
|
||||
User,
|
||||
} from "@/lib/api";
|
||||
import { PostThoughtForm } from "@/components/post-thought-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -62,17 +60,6 @@ async function FeedPage({
|
||||
const { items: allThoughts, totalPages } = feedData!;
|
||||
const thoughtThreads = buildThoughtThreads(allThoughts);
|
||||
|
||||
const authors = [...new Set(allThoughts.map((t) => t.author.username))];
|
||||
const userProfiles = await Promise.all(
|
||||
authors.map((username) => getUserProfile(username, token).catch(() => null))
|
||||
);
|
||||
|
||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
|
||||
userProfiles
|
||||
.filter((u): u is User => !!u)
|
||||
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
|
||||
);
|
||||
|
||||
const friends = (await getFriends(token)).users.map((user) => user.username);
|
||||
const topFriendsData = me
|
||||
? await getTopFriends(me.username, token).catch(() => ({ topFriends: [] }))
|
||||
@@ -111,7 +98,6 @@ async function FeedPage({
|
||||
<ThoughtThread
|
||||
key={thought.id}
|
||||
thought={thought}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={me}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { getMe, search, lookupRemoteActor, User } from "@/lib/api";
|
||||
import { getMe, search, lookupRemoteActor } from "@/lib/api";
|
||||
|
||||
export async function generateMetadata({
|
||||
searchParams,
|
||||
@@ -51,13 +51,6 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
token ? getMe(token).catch(() => null) : null,
|
||||
]);
|
||||
|
||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
||||
if (results) {
|
||||
results.users.forEach((user: User) => {
|
||||
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
|
||||
<header className="my-6">
|
||||
@@ -91,7 +84,6 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
<TabsContent value="thoughts">
|
||||
<ThoughtList
|
||||
thoughts={results.thoughts}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={me}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// app/tags/[tagName]/page.tsx
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { getThoughtsByTag, getUserProfile, getMe, Me, User } from "@/lib/api";
|
||||
import { getThoughtsByTag, getMe, Me } from "@/lib/api";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -49,16 +49,6 @@ export default async function TagPage({ params }: TagPageProps) {
|
||||
const thoughtThreads = buildThoughtThreads(allThoughts);
|
||||
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
|
||||
|
||||
const authors = [...new Set(allThoughts.map((t) => t.author.username))];
|
||||
const userProfiles = await Promise.all(
|
||||
authors.map((username) => getUserProfile(username, token).catch(() => null))
|
||||
);
|
||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
|
||||
userProfiles
|
||||
.filter((u): u is User => !!u)
|
||||
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
|
||||
<header className="my-6">
|
||||
@@ -72,7 +62,6 @@ export default async function TagPage({ params }: TagPageProps) {
|
||||
<ThoughtThread
|
||||
key={thought.id}
|
||||
thought={thought}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={me}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -3,10 +3,8 @@ import { cookies } from "next/headers";
|
||||
import {
|
||||
getThoughtById,
|
||||
getThoughtThread,
|
||||
getUserProfile,
|
||||
getMe,
|
||||
Me,
|
||||
User,
|
||||
ThoughtThread as ThoughtThreadType,
|
||||
} from "@/lib/api";
|
||||
import { ThoughtThread } from "@/components/thought-thread";
|
||||
@@ -52,14 +50,6 @@ export async function generateMetadata({
|
||||
};
|
||||
}
|
||||
|
||||
function collectAuthors(thread: ThoughtThreadType): string[] {
|
||||
const authors = new Set<string>([thread.author.username]);
|
||||
for (const reply of thread.replies) {
|
||||
collectAuthors(reply).forEach((author) => authors.add(author));
|
||||
}
|
||||
return Array.from(authors);
|
||||
}
|
||||
|
||||
export default async function ThoughtPage({ params }: ThoughtPageProps) {
|
||||
const { thoughtId } = await params;
|
||||
const token = (await cookies()).get("auth_token")?.value ?? null;
|
||||
@@ -76,20 +66,6 @@ export default async function ThoughtPage({ params }: ThoughtPageProps) {
|
||||
const thread = threadResult.value;
|
||||
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
|
||||
|
||||
// Fetch details for all authors in the thread efficiently
|
||||
const authorUsernames = collectAuthors(thread);
|
||||
const userProfiles = await Promise.all(
|
||||
authorUsernames.map((username) =>
|
||||
getUserProfile(username, token).catch(() => null)
|
||||
)
|
||||
);
|
||||
|
||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
|
||||
userProfiles
|
||||
.filter((u): u is User => !!u)
|
||||
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
|
||||
<header className="my-6">
|
||||
@@ -98,7 +74,6 @@ export default async function ThoughtPage({ params }: ThoughtPageProps) {
|
||||
<main>
|
||||
<ThoughtThread
|
||||
thought={thread}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={me}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -126,9 +126,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||
const fediverseHandle =
|
||||
user.local && apiDomain ? `@${user.username}@${apiDomain}` : null;
|
||||
|
||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
||||
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
|
||||
|
||||
// Show who the profile owner follows (uses the already-fetched followingResult).
|
||||
const friends =
|
||||
followingResult.status === "fulfilled"
|
||||
@@ -277,7 +274,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||
<ThoughtThread
|
||||
key={thought.id}
|
||||
thought={thought}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={me}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -91,11 +91,6 @@ export function RemoteUserProfile({
|
||||
|
||||
const isOwnProfile = me?.username === actor.handle;
|
||||
|
||||
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
|
||||
initialPosts.forEach((t) => {
|
||||
authorDetails.set(t.author.username, { avatarUrl: actor.avatarUrl });
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -220,7 +215,6 @@ export function RemoteUserProfile({
|
||||
{initialPosts.length > 0 ? (
|
||||
<ThoughtList
|
||||
thoughts={initialPosts}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={me}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import { deleteThought, Me, Thought } from "@/lib/api";
|
||||
import { Me, Thought } from "@/lib/api";
|
||||
import { deleteThought } from "@/app/actions/thoughts";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -42,25 +42,19 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
interface ThoughtCardProps {
|
||||
thought: Thought;
|
||||
author: {
|
||||
username: string;
|
||||
displayName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
};
|
||||
currentUser: Me | null;
|
||||
isReply?: boolean;
|
||||
}
|
||||
|
||||
export function ThoughtCard({
|
||||
thought,
|
||||
author,
|
||||
currentUser,
|
||||
isReply = false,
|
||||
}: ThoughtCardProps) {
|
||||
const { author } = thought;
|
||||
const [isAlertOpen, setIsAlertOpen] = useState(false);
|
||||
const [isReplyOpen, setIsReplyOpen] = useState(false);
|
||||
const { token } = useAuth();
|
||||
const router = useRouter();
|
||||
const timeAgo = formatDistanceToNow(new Date(thought.createdAt), {
|
||||
addSuffix: true,
|
||||
});
|
||||
@@ -68,11 +62,9 @@ export function ThoughtCard({
|
||||
const isAuthor = currentUser?.username === thought.author.username;
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
await deleteThought(thought.id, token);
|
||||
await deleteThought(thought.id);
|
||||
toast.success("Thought deleted successfully.");
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete thought:", error);
|
||||
toast.error("Failed to delete thought.");
|
||||
|
||||
@@ -4,15 +4,10 @@ import { Card, CardContent } from "./ui/card";
|
||||
|
||||
interface ThoughtListProps {
|
||||
thoughts: Thought[];
|
||||
authorDetails: Map<string, { avatarUrl?: string | null }>;
|
||||
currentUser: Me | null;
|
||||
}
|
||||
|
||||
export function ThoughtList({
|
||||
thoughts,
|
||||
authorDetails,
|
||||
currentUser,
|
||||
}: ThoughtListProps) {
|
||||
export function ThoughtList({ thoughts, currentUser }: ThoughtListProps) {
|
||||
if (thoughts.length === 0) {
|
||||
return (
|
||||
<p className="text-center text-muted-foreground pt-8">
|
||||
@@ -25,21 +20,13 @@ export function ThoughtList({
|
||||
<Card>
|
||||
<CardContent className="divide-y p-0">
|
||||
<div className="space-y-6 p-4">
|
||||
{thoughts.map((thought) => {
|
||||
const author = {
|
||||
username: thought.author.username,
|
||||
displayName: thought.author.displayName,
|
||||
...authorDetails.get(thought.author.username),
|
||||
};
|
||||
return (
|
||||
{thoughts.map((thought) => (
|
||||
<ThoughtCard
|
||||
key={thought.id}
|
||||
thought={thought}
|
||||
author={author}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3,29 +3,19 @@ import { ThoughtCard } from "./thought-card";
|
||||
|
||||
interface ThoughtThreadProps {
|
||||
thought: ThoughtThreadType;
|
||||
authorDetails: Map<string, { avatarUrl?: string | null }>;
|
||||
currentUser: Me | null;
|
||||
isReply?: boolean;
|
||||
}
|
||||
|
||||
export function ThoughtThread({
|
||||
thought,
|
||||
authorDetails,
|
||||
currentUser,
|
||||
isReply = false,
|
||||
}: ThoughtThreadProps) {
|
||||
const author = {
|
||||
username: thought.author.username,
|
||||
displayName: thought.author.displayName,
|
||||
avatarUrl: thought.author.avatarUrl, // API-provided avatar (from DB COALESCE)
|
||||
...authorDetails.get(thought.author.username), // override for local users
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={`thought-thread-${thought.id}`} className="flex flex-col gap-0">
|
||||
<ThoughtCard
|
||||
thought={thought}
|
||||
author={author}
|
||||
currentUser={currentUser}
|
||||
isReply={isReply}
|
||||
/>
|
||||
@@ -39,7 +29,6 @@ export function ThoughtThread({
|
||||
<ThoughtThread
|
||||
key={reply.id}
|
||||
thought={reply}
|
||||
authorDetails={authorDetails}
|
||||
currentUser={currentUser}
|
||||
isReply={true}
|
||||
/>
|
||||
|
||||
8567
thoughts-frontend/package-lock.json
generated
Normal file
8567
thoughts-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user