feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1

Merged
GKaszewski merged 334 commits from v2 into master 2026-05-16 09:42:43 +00:00
4 changed files with 59 additions and 26 deletions
Showing only changes of commit 072d06cb46 - Show all commits

View File

@@ -0,0 +1,35 @@
import { notFound } from "next/navigation";
import { cookies } from "next/headers";
import { getMe, lookupRemoteActor, getRemoteActorPosts, Me } from "@/lib/api";
import { RemoteUserProfile } from "@/components/remote-user-profile";
interface RemoteActorPageProps {
searchParams: Promise<{ handle?: string }>;
}
export default async function RemoteActorPage({
searchParams,
}: RemoteActorPageProps) {
const { handle } = await searchParams;
if (!handle) notFound();
const token = (await cookies()).get("auth_token")?.value ?? null;
const [actorResult, postsResult, meResult] = await Promise.allSettled([
lookupRemoteActor(handle, token),
getRemoteActorPosts(handle, 1, token),
token ? getMe(token) : Promise.resolve(null),
]);
if (actorResult.status === "rejected") {
notFound();
}
const actor = actorResult.value;
const posts =
postsResult.status === "fulfilled" ? postsResult.value.items : [];
const me =
meResult.status === "fulfilled" ? (meResult.value as Me | null) : null;
return <RemoteUserProfile actor={actor} initialPosts={posts} me={me} />;
}

View File

@@ -5,11 +5,8 @@ import {
getTopFriends,
getUserProfile,
getUserThoughts,
lookupRemoteActor,
getRemoteActorPosts,
Me,
} from "@/lib/api";
import { RemoteUserProfile } from "@/components/remote-user-profile";
import { UserAvatar } from "@/components/user-avatar";
import { Calendar, Settings } from "lucide-react";
import { Card } from "@/components/ui/card";
@@ -30,28 +27,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
const { username } = await params;
const token = (await cookies()).get("auth_token")?.value ?? null;
const HANDLE_RE = /^@[\w.-]+@[\w.-]+\.\w+$/;
if (HANDLE_RE.test(username)) {
const [actorResult, postsResult, meResult] = await Promise.allSettled([
lookupRemoteActor(username, token),
getRemoteActorPosts(username, 1, token),
token ? getMe(token) : Promise.resolve(null),
]);
if (actorResult.status === "rejected") {
notFound();
}
const actor = actorResult.value as Awaited<ReturnType<typeof lookupRemoteActor>>;
const posts =
postsResult.status === "fulfilled" ? postsResult.value.items : [];
const me =
meResult.status === "fulfilled" ? (meResult.value as Me | null) : null;
return <RemoteUserProfile actor={actor} initialPosts={posts} me={me} />;
}
const userProfilePromise = getUserProfile(username, token);
const thoughtsPromise = getUserThoughts(username, token);
const mePromise = token ? getMe(token) : Promise.resolve(null);

View File

@@ -38,7 +38,7 @@ export function RemoteUserCard({ actor }: RemoteUserCardProps) {
return (
<div className="flex items-center justify-between p-4 border rounded-lg">
<Link
href={`/users/${encodeURIComponent("@" + actor.handle)}`}
href={`/users/@${actor.handle}`}
className="flex items-center gap-3 hover:opacity-80"
>
<UserAvatar src={actor.avatarUrl} alt={actor.displayName ?? actor.handle} />

View File

@@ -0,0 +1,23 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const parts = request.nextUrl.pathname.split("/");
// /users/@user@instance or /users/%40user%40instance
if (parts.length === 3 && parts[1] === "users") {
const decoded = decodeURIComponent(parts[2]);
if (decoded.startsWith("@") && decoded.indexOf("@", 1) !== -1) {
const url = request.nextUrl.clone();
url.pathname = "/remote-actor";
url.searchParams.set("handle", decoded);
return NextResponse.rewrite(url);
}
}
return NextResponse.next();
}
export const config = {
matcher: "/users/:path*",
};