Files
DiscordClone/convex/channels.ts

246 lines
6.4 KiB
TypeScript

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<DataModel>,
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 };
},
});