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

111
convex/messages.ts Normal file
View File

@@ -0,0 +1,111 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
// List recent messages for a channel with reactions + username
export const list = query({
args: {
channelId: v.id("channels"),
userId: v.optional(v.id("userProfiles")),
},
returns: v.array(v.any()),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(50);
// Reverse to get chronological order
const chronological = messages.reverse();
// Enrich with username, signing key, and reactions
const enriched = await Promise.all(
chronological.map(async (msg) => {
// Get sender info
const sender = await ctx.db.get(msg.senderId);
// Get reactions for this message
const reactionDocs = await ctx.db
.query("messageReactions")
.withIndex("by_message", (q) => q.eq("messageId", msg._id))
.collect();
// Aggregate reactions
const reactions: Record<
string,
{ count: number; me: boolean }
> = {};
for (const r of reactionDocs) {
if (!reactions[r.emoji]) {
reactions[r.emoji] = { count: 0, me: false };
}
reactions[r.emoji].count++;
if (args.userId && r.userId === args.userId) {
reactions[r.emoji].me = true;
}
}
return {
id: msg._id,
channel_id: msg.channelId,
sender_id: msg.senderId,
ciphertext: msg.ciphertext,
nonce: msg.nonce,
signature: msg.signature,
key_version: msg.keyVersion,
created_at: new Date(msg._creationTime).toISOString(),
username: sender?.username || "Unknown",
public_signing_key: sender?.publicSigningKey || "",
reactions:
Object.keys(reactions).length > 0 ? reactions : null,
};
})
);
return enriched;
},
});
// Send encrypted message
export const send = mutation({
args: {
channelId: v.id("channels"),
senderId: v.id("userProfiles"),
ciphertext: v.string(),
nonce: v.string(),
signature: v.string(),
keyVersion: v.number(),
},
returns: v.object({ id: v.id("messages") }),
handler: async (ctx, args) => {
const id = await ctx.db.insert("messages", {
channelId: args.channelId,
senderId: args.senderId,
ciphertext: args.ciphertext,
nonce: args.nonce,
signature: args.signature,
keyVersion: args.keyVersion,
});
return { id };
},
});
// Delete a message
export const remove = mutation({
args: { id: v.id("messages") },
returns: v.null(),
handler: async (ctx, args) => {
// Delete reactions first
const reactions = await ctx.db
.query("messageReactions")
.withIndex("by_message", (q) => q.eq("messageId", args.id))
.collect();
for (const r of reactions) {
await ctx.db.delete(r._id);
}
await ctx.db.delete(args.id);
return null;
},
});