feat: direct messages
1:1 conversations (userAId < userBId dedup pair), messages, per-participant read tracking (conversation_reads). Block relationship is enforced on every send. UI: /messages list + /messages/[id] thread (5s poll), MessageButton on profiles, unread-badged nav icon. This completes the social-feature backlog: notifications, rate limiting, blocking, reporting, search/discovery, mentions, DMs, plus fixes for the recipe-visibility 404, follow race, and 2-level comment thread cap found during the earlier audit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { db, conversations, eq, and } from "@epicure/db";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
/** Normalizes a pair so (userAId, userBId) is order-independent. */
|
||||
function orderPair(idA: string, idB: string): [string, string] {
|
||||
return idA < idB ? [idA, idB] : [idB, idA];
|
||||
}
|
||||
|
||||
export async function findOrCreateConversation(userId1: string, userId2: string) {
|
||||
const [userAId, userBId] = orderPair(userId1, userId2);
|
||||
|
||||
const existing = await db.query.conversations.findFirst({
|
||||
where: and(eq(conversations.userAId, userAId), eq(conversations.userBId, userBId)),
|
||||
});
|
||||
if (existing) return existing;
|
||||
|
||||
const [created] = await db
|
||||
.insert(conversations)
|
||||
.values({ id: randomUUID(), userAId, userBId })
|
||||
.onConflictDoNothing()
|
||||
.returning();
|
||||
|
||||
if (created) return created;
|
||||
|
||||
// Lost a race with a concurrent insert — fetch what the winner created.
|
||||
const row = await db.query.conversations.findFirst({
|
||||
where: and(eq(conversations.userAId, userAId), eq(conversations.userBId, userBId)),
|
||||
});
|
||||
if (!row) throw new Error("Failed to create conversation");
|
||||
return row;
|
||||
}
|
||||
|
||||
export function isParticipant(
|
||||
conversation: { userAId: string; userBId: string },
|
||||
userId: string
|
||||
): boolean {
|
||||
return conversation.userAId === userId || conversation.userBId === userId;
|
||||
}
|
||||
|
||||
export function otherParticipantId(
|
||||
conversation: { userAId: string; userBId: string },
|
||||
userId: string
|
||||
): string {
|
||||
return conversation.userAId === userId ? conversation.userBId : conversation.userAId;
|
||||
}
|
||||
Reference in New Issue
Block a user