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,32 @@
|
||||
CREATE TABLE "conversation_reads" (
|
||||
"conversation_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"last_read_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "conversation_reads_conversation_id_user_id_pk" PRIMARY KEY("conversation_id","user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "conversations" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_a_id" text NOT NULL,
|
||||
"user_b_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"last_message_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "conversations_pair_uniq" UNIQUE("user_a_id","user_b_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "messages" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"conversation_id" text NOT NULL,
|
||||
"sender_id" text NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "conversation_reads" ADD CONSTRAINT "conversation_reads_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "conversation_reads" ADD CONSTRAINT "conversation_reads_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "conversations" ADD CONSTRAINT "conversations_user_a_id_users_id_fk" FOREIGN KEY ("user_a_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "conversations" ADD CONSTRAINT "conversations_user_b_id_users_id_fk" FOREIGN KEY ("user_b_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "messages" ADD CONSTRAINT "messages_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "messages" ADD CONSTRAINT "messages_sender_id_users_id_fk" FOREIGN KEY ("sender_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "conversations_last_message_idx" ON "conversations" USING btree ("last_message_at");--> statement-breakpoint
|
||||
CREATE INDEX "messages_conversation_idx" ON "messages" USING btree ("conversation_id","created_at");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -141,6 +141,13 @@
|
||||
"when": 1783108786252,
|
||||
"tag": "0019_clumsy_leader",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "7",
|
||||
"when": 1783109825851,
|
||||
"tag": "0020_acoustic_exiles",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from "./social";
|
||||
export * from "./meal-planning";
|
||||
export * from "./tiers";
|
||||
export * from "./webhooks";
|
||||
export * from "./messaging";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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] }),
|
||||
}));
|
||||
Reference in New Issue
Block a user