feat: Implement core Discord clone functionality including Convex backend, Electron frontend, and UI components for chat, voice, and settings.

This commit is contained in:
Bryan1029384756
2026-02-10 04:41:10 -06:00
parent 516cfdbbd8
commit 47f173c79b
63 changed files with 4467 additions and 5292 deletions

166
convex/channels.ts Normal file
View File

@@ -0,0 +1,166 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
// 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(),
})
),
handler: async (ctx) => {
const channels = await ctx.db.query("channels").collect();
return channels
.filter((c) => c.type !== "dm")
.sort((a, b) => 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(),
}),
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()),
},
returns: v.object({ id: v.id("channels") }),
handler: async (ctx, args) => {
if (!args.name.trim()) {
throw new Error("Channel name required");
}
// Check for duplicate name
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");
}
const id = await ctx.db.insert("channels", {
name: args.name,
type: args.type || "text",
});
return { id };
},
});
// 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(),
}),
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 };
},
});
// 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 messages
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.id))
.collect();
for (const msg of messages) {
// Delete reactions for this message
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);
}
// Delete channel keys
const keys = await ctx.db
.query("channelKeys")
.withIndex("by_channel", (q) => q.eq("channelId", args.id))
.collect();
for (const key of keys) {
await ctx.db.delete(key._id);
}
// Delete DM participants
const dmParts = await ctx.db
.query("dmParticipants")
.withIndex("by_channel", (q) => q.eq("channelId", args.id))
.collect();
for (const dp of dmParts) {
await ctx.db.delete(dp._id);
}
// Delete typing indicators
const typing = await ctx.db
.query("typingIndicators")
.withIndex("by_channel", (q) => q.eq("channelId", args.id))
.collect();
for (const t of typing) {
await ctx.db.delete(t._id);
}
// Delete voice states
const voiceStates = await ctx.db
.query("voiceStates")
.withIndex("by_channel", (q) => q.eq("channelId", args.id))
.collect();
for (const vs of voiceStates) {
await ctx.db.delete(vs._id);
}
// Delete channel itself
await ctx.db.delete(args.id);
return { success: true };
},
});