import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; import { GenericMutationCtx } from "convex/server"; import { DataModel, Id } from "./_generated/dataModel"; import { internal } from "./_generated/api"; type TableWithChannelIndex = | "channelKeys" | "dmParticipants" | "typingIndicators" | "voiceStates" | "channelReadState"; async function deleteByChannel( ctx: GenericMutationCtx, table: TableWithChannelIndex, channelId: Id<"channels"> ) { const docs = await (ctx.db.query(table) as any) .withIndex("by_channel", (q: any) => q.eq("channelId", channelId)) .collect(); for (const doc of docs) { await ctx.db.delete(doc._id); } } // List all non-DM channels export const list = query({ args: {}, returns: v.array( v.object({ _id: v.id("channels"), _creationTime: v.number(), name: v.string(), type: v.string(), categoryId: v.optional(v.id("categories")), topic: v.optional(v.string()), position: v.optional(v.number()), }) ), handler: async (ctx) => { const channels = await ctx.db.query("channels").collect(); return channels .filter((c) => c.type !== "dm") .sort((a, b) => (a.position ?? 0) - (b.position ?? 0) || a.name.localeCompare(b.name)); }, }); // Get single channel by ID export const get = query({ args: { id: v.id("channels") }, returns: v.union( v.object({ _id: v.id("channels"), _creationTime: v.number(), name: v.string(), type: v.string(), categoryId: v.optional(v.id("categories")), topic: v.optional(v.string()), position: v.optional(v.number()), }), v.null() ), handler: async (ctx, args) => { return await ctx.db.get(args.id); }, }); // Create new channel export const create = mutation({ args: { name: v.string(), type: v.optional(v.string()), categoryId: v.optional(v.id("categories")), topic: v.optional(v.string()), position: v.optional(v.number()), }, returns: v.object({ id: v.id("channels") }), handler: async (ctx, args) => { if (!args.name.trim()) { throw new Error("Channel name required"); } const existing = await ctx.db .query("channels") .withIndex("by_name", (q) => q.eq("name", args.name)) .unique(); if (existing) { throw new Error("Channel already exists"); } // Auto-calculate position if not provided let position = args.position; if (position === undefined) { const allChannels = await ctx.db.query("channels").collect(); const sameCategory = allChannels.filter( (c) => c.categoryId === args.categoryId && c.type !== "dm" ); const maxPos = sameCategory.reduce( (max, c) => Math.max(max, c.position ?? 0), -1000 ); position = maxPos + 1000; } const id = await ctx.db.insert("channels", { name: args.name, type: args.type || "text", categoryId: args.categoryId, topic: args.topic, position, }); return { id }; }, }); // Update channel topic export const updateTopic = mutation({ args: { id: v.id("channels"), topic: v.string(), }, returns: v.null(), handler: async (ctx, args) => { const channel = await ctx.db.get(args.id); if (!channel) throw new Error("Channel not found"); await ctx.db.patch(args.id, { topic: args.topic }); return null; }, }); // Rename channel export const rename = mutation({ args: { id: v.id("channels"), name: v.string(), }, returns: v.object({ _id: v.id("channels"), _creationTime: v.number(), name: v.string(), type: v.string(), categoryId: v.optional(v.id("categories")), topic: v.optional(v.string()), position: v.optional(v.number()), }), handler: async (ctx, args) => { if (!args.name.trim()) { throw new Error("Name required"); } const channel = await ctx.db.get(args.id); if (!channel) { throw new Error("Channel not found"); } await ctx.db.patch(args.id, { name: args.name }); return { ...channel, name: args.name }; }, }); // Move a channel to a different category with a new position export const moveChannel = mutation({ args: { id: v.id("channels"), categoryId: v.optional(v.id("categories")), position: v.number(), }, returns: v.null(), handler: async (ctx, args) => { const channel = await ctx.db.get(args.id); if (!channel) throw new Error("Channel not found"); await ctx.db.patch(args.id, { categoryId: args.categoryId, position: args.position, }); return null; }, }); // Batch reorder channels export const reorderChannels = mutation({ args: { updates: v.array( v.object({ id: v.id("channels"), categoryId: v.optional(v.id("categories")), position: v.number(), }) ), }, returns: v.null(), handler: async (ctx, args) => { for (const u of args.updates) { await ctx.db.patch(u.id, { categoryId: u.categoryId, position: u.position, }); } return null; }, }); // Delete channel + cascade messages and keys export const remove = mutation({ args: { id: v.id("channels") }, returns: v.object({ success: v.boolean() }), handler: async (ctx, args) => { const channel = await ctx.db.get(args.id); if (!channel) { throw new Error("Channel not found"); } // Delete reactions for all messages in this channel const messages = await ctx.db .query("messages") .withIndex("by_channel", (q) => q.eq("channelId", args.id)) .collect(); for (const msg of messages) { const reactions = await ctx.db .query("messageReactions") .withIndex("by_message", (q) => q.eq("messageId", msg._id)) .collect(); for (const r of reactions) { await ctx.db.delete(r._id); } await ctx.db.delete(msg._id); } await deleteByChannel(ctx, "channelKeys", args.id); await deleteByChannel(ctx, "dmParticipants", args.id); await deleteByChannel(ctx, "typingIndicators", args.id); await deleteByChannel(ctx, "voiceStates", args.id); await deleteByChannel(ctx, "channelReadState", args.id); // Clear AFK setting if this channel was the AFK channel await ctx.runMutation(internal.serverSettings.clearAfkChannel, { channelId: args.id }); await ctx.db.delete(args.id); return { success: true }; }, });