c3776238c7
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>
54 lines
2.3 KiB
TypeScript
54 lines
2.3 KiB
TypeScript
import {
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
index,
|
|
unique,
|
|
primaryKey,
|
|
} from "drizzle-orm/pg-core";
|
|
import { relations } from "drizzle-orm";
|
|
import { users } from "./users";
|
|
|
|
// 1:1 conversations only. userAId is always the lexicographically smaller
|
|
// of the two participant ids, so (userAId, userBId) uniquely identifies a
|
|
// pair regardless of who messaged first.
|
|
export const conversations = pgTable("conversations", {
|
|
id: text("id").primaryKey(),
|
|
userAId: text("user_a_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
userBId: text("user_b_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
lastMessageAt: timestamp("last_message_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
unique("conversations_pair_uniq").on(t.userAId, t.userBId),
|
|
index("conversations_last_message_idx").on(t.lastMessageAt),
|
|
]);
|
|
|
|
export const messages = pgTable("messages", {
|
|
id: text("id").primaryKey(),
|
|
conversationId: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
senderId: text("sender_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
content: text("content").notNull(),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("messages_conversation_idx").on(t.conversationId, t.createdAt),
|
|
]);
|
|
|
|
export const conversationReads = pgTable("conversation_reads", {
|
|
conversationId: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
lastReadAt: timestamp("last_read_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.conversationId, t.userId] }),
|
|
]);
|
|
|
|
export const conversationsRelations = relations(conversations, ({ one, many }) => ({
|
|
userA: one(users, { fields: [conversations.userAId], references: [users.id], relationName: "userA" }),
|
|
userB: one(users, { fields: [conversations.userBId], references: [users.id], relationName: "userB" }),
|
|
messages: many(messages),
|
|
}));
|
|
|
|
export const messagesRelations = relations(messages, ({ one }) => ({
|
|
conversation: one(conversations, { fields: [messages.conversationId], references: [conversations.id] }),
|
|
sender: one(users, { fields: [messages.senderId], references: [users.id] }),
|
|
}));
|