feat: Implement dynamic node title metadata and enhance swarm user and post hydration.

This commit is contained in:
Christomatt
2026-01-26 17:09:21 +01:00
parent 3ba60cadf5
commit cf0dfa4b66
12 changed files with 380 additions and 105 deletions
+25 -9
View File
@@ -6,7 +6,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, posts, users, media, nodes } from '@/db';
import { eq, desc, and, isNull } from 'drizzle-orm';
import { eq, desc, and, isNull, lt } from 'drizzle-orm';
export interface SwarmPost {
id: string;
@@ -17,6 +17,7 @@ export interface SwarmPost {
displayName: string;
avatarUrl?: string;
isNsfw: boolean;
isBot?: boolean;
};
nodeDomain: string;
nodeIsNsfw: boolean;
@@ -43,18 +44,36 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50);
const cursor = searchParams.get('cursor');
if (!db) {
return NextResponse.json({ posts: [], nodeDomain: '', nodeIsNsfw: false });
}
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost';
// Get node NSFW status
const node = await db.query.nodes.findFirst({
where: eq(nodes.domain, nodeDomain),
});
const nodeIsNsfw = node?.isNsfw ?? false;
// Use query builder for better conditional logic
let whereCondition = and(
isNull(posts.replyToId), // Not a reply
eq(posts.isRemoved, false) // Not removed
);
if (cursor) {
// Find the cursor post or use timestamp directly if passed as ISO string
// Actually, for swarm, passing ISO timestamp is safer than ID because IDs are local UUIDs
// Let's assume cursor is an ISO date string for swarm timeline
const cursorDate = new Date(cursor);
if (!isNaN(cursorDate.getTime())) {
whereCondition = and(whereCondition, lt(posts.createdAt, cursorDate));
}
}
// Get recent public posts (not replies, local users only, not removed)
const recentPosts = await db
.select({
@@ -73,22 +92,18 @@ export async function GET(request: NextRequest) {
authorDisplayName: users.displayName,
authorAvatarUrl: users.avatarUrl,
authorIsNsfw: users.isNsfw,
authorIsBot: users.isBot,
authorNodeId: users.nodeId,
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(
and(
isNull(posts.replyToId), // Not a reply
eq(posts.isRemoved, false) // Not removed
)
)
.where(whereCondition)
.orderBy(desc(posts.createdAt))
.limit(limit);
// Fetch media for each post
const swarmPosts: SwarmPost[] = [];
for (const post of recentPosts) {
const postMedia = await db
.select({ url: media.url, mimeType: media.mimeType, altText: media.altText })
@@ -104,6 +119,7 @@ export async function GET(request: NextRequest) {
displayName: post.authorDisplayName || post.authorHandle,
avatarUrl: post.authorAvatarUrl || undefined,
isNsfw: post.authorIsNsfw,
isBot: post.authorIsBot,
},
nodeDomain,
nodeIsNsfw,
@@ -7,6 +7,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, follows, users, remoteFollowers } from '@/db';
import { eq } from 'drizzle-orm';
import { hydrateSwarmUsers } from '@/lib/swarm/user-hydration';
export interface SwarmFollowerUser {
handle: string;
@@ -88,8 +89,33 @@ export async function GET(request: NextRequest, context: RouteContext) {
// Merge all followers
const allFollowers = [...localFollowers, ...remoteFollowersList].slice(0, limit);
// Hydrate remote users (from 3rd party nodes)
// We need to map to the HydratedUser interface temporarily for the helper
const toHydrate = allFollowers.map(f => ({
id: f.handle,
handle: f.handle,
displayName: f.displayName,
avatarUrl: f.avatarUrl,
bio: f.bio,
isRemote: f.isRemote || false,
isBot: f.isBot,
nodeDomain: undefined,
}));
const hydrated = await hydrateSwarmUsers(toHydrate);
// Map back to SwarmFollowerUser
const finalFollowers: SwarmFollowerUser[] = hydrated.map(u => ({
handle: u.handle,
displayName: u.displayName || u.handle.split('@')[0], // Ensure non-null
avatarUrl: u.avatarUrl || undefined, // Map null to undefined
bio: u.bio || undefined,
isBot: u.isBot,
isRemote: u.isRemote,
}));
return NextResponse.json({
followers: allFollowers,
followers: finalFollowers,
nodeDomain,
timestamp: new Date().toISOString(),
});