feat: Implement core Discord clone functionality including Convex backend, Electron frontend, and UI components for chat, voice, and settings.
This commit is contained in:
111
convex/messages.ts
Normal file
111
convex/messages.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user