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

103
convex/typing.ts Normal file
View File

@@ -0,0 +1,103 @@
import { query, mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
// Start typing indicator
export const startTyping = mutation({
args: {
channelId: v.id("channels"),
userId: v.id("userProfiles"),
username: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const expiresAt = Date.now() + 6000; // 6 second TTL
// Upsert: check if already exists
const existing = await ctx.db
.query("typingIndicators")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
const userTyping = existing.find((t) => t.userId === args.userId);
if (userTyping) {
await ctx.db.patch(userTyping._id, { expiresAt });
} else {
await ctx.db.insert("typingIndicators", {
channelId: args.channelId,
userId: args.userId,
username: args.username,
expiresAt,
});
}
// Schedule cleanup
await ctx.scheduler.runAfter(6000, internal.typing.cleanExpired, {});
return null;
},
});
// Stop typing indicator
export const stopTyping = mutation({
args: {
channelId: v.id("channels"),
userId: v.id("userProfiles"),
},
returns: v.null(),
handler: async (ctx, args) => {
const indicators = await ctx.db
.query("typingIndicators")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
const mine = indicators.find((t) => t.userId === args.userId);
if (mine) {
await ctx.db.delete(mine._id);
}
return null;
},
});
// Get typing users for a channel (reactive!)
export const getTyping = query({
args: { channelId: v.id("channels") },
returns: v.array(
v.object({
userId: v.id("userProfiles"),
username: v.string(),
})
),
handler: async (ctx, args) => {
const now = Date.now();
const indicators = await ctx.db
.query("typingIndicators")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
return indicators
.filter((t) => t.expiresAt > now)
.map((t) => ({
userId: t.userId,
username: t.username,
}));
},
});
// Internal: clean expired typing indicators
export const cleanExpired = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const now = Date.now();
const all = await ctx.db.query("typingIndicators").collect();
for (const t of all) {
if (t.expiresAt <= now) {
await ctx.db.delete(t._id);
}
}
return null;
},
});