Various fixes and improvments

This commit is contained in:
Christopher
2026-01-22 13:50:55 -08:00
parent fc86bc1901
commit c9951caa95
10 changed files with 947 additions and 558 deletions
+68 -1
View File
@@ -28,6 +28,19 @@ export async function POST(request: Request, context: RouteContext) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
// Check if already reposted by this user
const existingRepost = await db.query.posts.findFirst({
where: and(
eq(posts.userId, user.id),
eq(posts.repostOfId, postId),
eq(posts.isRemoved, false)
),
});
if (existingRepost) {
return NextResponse.json({ error: 'Already reposted' }, { status: 400 });
}
// Create repost
const [repost] = await db.insert(posts).values({
userId: user.id,
@@ -58,7 +71,7 @@ export async function POST(request: Request, context: RouteContext) {
// TODO: Federate the repost (Announce activity)
return NextResponse.json({ success: true, repost });
return NextResponse.json({ success: true, repost, reposted: true });
} catch (error) {
if (error instanceof Error && error.message === 'Authentication required') {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
@@ -66,3 +79,57 @@ export async function POST(request: Request, context: RouteContext) {
return NextResponse.json({ error: 'Failed to repost' }, { status: 500 });
}
}
// Unrepost a post
export async function DELETE(request: Request, context: RouteContext) {
try {
const user = await requireAuth();
const { id: postId } = await context.params;
if (user.isSuspended || user.isSilenced) {
return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
}
// Check if original post exists
const originalPost = await db.query.posts.findFirst({
where: eq(posts.id, postId),
});
// Find the repost by this user
const repost = await db.query.posts.findFirst({
where: and(
eq(posts.userId, user.id),
eq(posts.repostOfId, postId),
eq(posts.isRemoved, false)
),
});
if (!repost) {
return NextResponse.json({ error: 'Not reposted' }, { status: 400 });
}
// Mark repost as removed
await db.update(posts)
.set({ isRemoved: true })
.where(eq(posts.id, repost.id));
// Update original post's repost count
if (originalPost) {
await db.update(posts)
.set({ repostsCount: Math.max(0, originalPost.repostsCount - 1) })
.where(eq(posts.id, postId));
}
// Update user's post count
await db.update(users)
.set({ postsCount: Math.max(0, user.postsCount - 1) })
.where(eq(users.id, user.id));
return NextResponse.json({ success: true, reposted: false });
} catch (error) {
if (error instanceof Error && error.message === 'Authentication required') {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
}
return NextResponse.json({ error: 'Failed to unrepost' }, { status: 500 });
}
}
+96
View File
@@ -0,0 +1,96 @@
import { NextResponse } from 'next/server';
import { db, posts, users, media } from '@/db';
import { eq, desc, and } from 'drizzle-orm';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
// Fetch the main post
const post = await db.query.posts.findFirst({
where: eq(posts.id, id),
with: {
author: true,
media: true,
replyTo: {
with: { author: true },
},
},
});
if (!post) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
// Fetch replies to this post
const replies = await db.query.posts.findMany({
where: and(
eq(posts.replyToId, id),
eq(posts.isRemoved, false)
),
with: {
author: true,
media: true,
},
orderBy: [desc(posts.createdAt)],
});
let mainPost = post;
let replyPosts = replies;
try {
const { requireAuth } = await import('@/lib/auth');
const { likes } = await import('@/db');
const { inArray } = await import('drizzle-orm');
const viewer = await requireAuth();
const allPostIds = [post.id, ...replies.map(r => r.id)];
if (allPostIds.length > 0) {
const viewerLikes = await db.query.likes.findMany({
where: and(
eq(likes.userId, viewer.id),
inArray(likes.postId, allPostIds)
),
});
const likedPostIds = new Set(viewerLikes.map(l => l.postId));
const viewerReposts = await db.query.posts.findMany({
where: and(
eq(posts.userId, viewer.id),
inArray(posts.repostOfId, allPostIds)
),
});
const repostedPostIds = new Set(viewerReposts.map(r => r.repostOfId));
mainPost = {
...post,
isLiked: likedPostIds.has(post.id),
isReposted: repostedPostIds.has(post.id),
};
replyPosts = replies.map(r => ({
...r,
isLiked: likedPostIds.has(r.id),
isReposted: repostedPostIds.has(r.id),
}));
}
} catch {
// Not authenticated or other error, skip flags
}
return NextResponse.json({
post: mainPost,
replies: replyPosts,
});
} catch (error) {
console.error('Get post detail error:', error);
return NextResponse.json(
{ error: 'Failed to get post detail' },
{ status: 500 }
);
}
}
+58 -27
View File
@@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
import { db, posts, users, media, follows, mutes, blocks } from '@/db';
import { db, posts, users, media, follows, mutes, blocks, likes } from '@/db';
import { requireAuth } from '@/lib/auth';
import { eq, desc, and, inArray, isNull, notInArray, or } from 'drizzle-orm';
import type { SQL } from 'drizzle-orm';
@@ -128,13 +128,8 @@ export async function GET(request: Request) {
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50);
let feedPosts;
const moderatedUsers = await db.select({ id: users.id })
.from(users)
.where(or(eq(users.isSuspended, true), eq(users.isSilenced, true)));
const moderatedIds = moderatedUsers.map((item) => item.id);
const baseFilter = buildWhere(
eq(posts.isRemoved, false),
moderatedIds.length ? notInArray(posts.userId, moderatedIds) : undefined
eq(posts.isRemoved, false)
);
if (type === 'public') {
@@ -168,7 +163,9 @@ export async function GET(request: Request) {
} else if (type === 'curated') {
let viewer = null;
try {
viewer = await requireAuth();
const { getSession } = await import('@/lib/auth');
const session = await getSession();
viewer = session?.user || null;
} catch {
viewer = null;
}
@@ -229,7 +226,7 @@ export async function GET(request: Request) {
}
if (engagement >= 5) {
reasons.push(`Popular: ${post.likesCount} likes, ${post.repostsCount} reposts`);
} else if (engagement > 0) {
} else if (post.repliesCount > 0) {
reasons.push(`Active conversation: ${post.repliesCount} replies`);
}
if (ageHours <= 6) {
@@ -264,21 +261,7 @@ export async function GET(request: Request) {
})
.slice(0, limit);
return NextResponse.json({
posts: rankedPosts,
meta: {
algorithm: 'curated-v1',
windowHours: CURATION_WINDOW_HOURS,
seedLimit,
weights: {
engagement: 1.4,
recency: 1.1,
followBoost: 0.9,
selfBoost: 0.5,
},
},
nextCursor: rankedPosts.length === limit ? rankedPosts[rankedPosts.length - 1]?.id : null,
});
feedPosts = rankedPosts;
} else {
// Home timeline - need auth
try {
@@ -315,12 +298,60 @@ export async function GET(request: Request) {
}
}
// Populate isLiked and isReposted for authenticated users
try {
const { getSession } = await import('@/lib/auth');
const session = await getSession();
if (session?.user && feedPosts && feedPosts.length > 0) {
const viewer = session.user;
const postIds = feedPosts.map(p => p.id).filter(Boolean);
if (postIds.length > 0) {
const viewerLikes = await db.query.likes.findMany({
where: and(
eq(likes.userId, viewer.id),
inArray(likes.postId, postIds)
),
});
const likedPostIds = new Set(viewerLikes.map(l => l.postId));
const viewerReposts = await db.query.posts.findMany({
where: and(
eq(posts.userId, viewer.id),
inArray(posts.repostOfId, postIds)
),
});
const repostedPostIds = new Set(viewerReposts.map(r => r.repostOfId));
feedPosts = feedPosts.map(p => ({
...p,
isLiked: likedPostIds.has(p.id),
isReposted: repostedPostIds.has(p.id),
}));
}
}
} catch (error) {
console.error('Error populating interaction flags:', error);
}
return NextResponse.json({
posts: feedPosts,
nextCursor: feedPosts.length === limit ? feedPosts[feedPosts.length - 1]?.id : null,
posts: feedPosts || [],
meta: type === 'curated' ? {
algorithm: 'curated-v1',
windowHours: CURATION_WINDOW_HOURS,
seedLimit: Math.min(limit * CURATION_SEED_MULTIPLIER, CURATION_SEED_CAP),
weights: {
engagement: 1.4,
recency: 1.1,
followBoost: 0.9,
selfBoost: 0.5,
},
} : undefined,
nextCursor: (feedPosts?.length === limit) ? feedPosts[feedPosts.length - 1]?.id : null,
});
} catch (error) {
console.error('Get feed error:', error);
console.error('Get feed error details:', error);
return NextResponse.json(
{ error: 'Failed to get feed' },
{ status: 500 }