feat: Implement core Discord clone functionality including Convex backend services for authentication, channels, messages, roles, and voice state, alongside new Electron frontend components for chat, voice, server settings, and user interface.
All checks were successful
Build and Release / build-and-release (push) Successful in 14m19s
All checks were successful
Build and Release / build-and-release (push) Successful in 14m19s
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { getPublicStorageUrl } from "./storageUrl";
|
||||
import { getRolesForUser } from "./roles";
|
||||
|
||||
async function removeUserVoiceStates(ctx: any, userId: any) {
|
||||
const existing = await ctx.db
|
||||
@@ -31,6 +32,7 @@ export const join = mutation({
|
||||
isMuted: args.isMuted,
|
||||
isDeafened: args.isDeafened,
|
||||
isScreenSharing: false,
|
||||
isServerMuted: false,
|
||||
});
|
||||
|
||||
return null;
|
||||
@@ -74,6 +76,35 @@ export const updateState = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const serverMute = mutation({
|
||||
args: {
|
||||
actorUserId: v.id("userProfiles"),
|
||||
targetUserId: v.id("userProfiles"),
|
||||
isServerMuted: v.boolean(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const roles = await getRolesForUser(ctx, args.actorUserId);
|
||||
const canMute = roles.some(
|
||||
(role) => (role.permissions as Record<string, boolean>)?.["mute_members"]
|
||||
);
|
||||
if (!canMute) {
|
||||
throw new Error("You don't have permission to server mute members");
|
||||
}
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("voiceStates")
|
||||
.withIndex("by_user", (q: any) => q.eq("userId", args.targetUserId))
|
||||
.first();
|
||||
|
||||
if (!existing) throw new Error("Target user is not in a voice channel");
|
||||
|
||||
await ctx.db.patch(existing._id, { isServerMuted: args.isServerMuted });
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const getAll = query({
|
||||
args: {},
|
||||
returns: v.any(),
|
||||
@@ -86,6 +117,7 @@ export const getAll = query({
|
||||
isMuted: boolean;
|
||||
isDeafened: boolean;
|
||||
isScreenSharing: boolean;
|
||||
isServerMuted: boolean;
|
||||
avatarUrl: string | null;
|
||||
}>> = {};
|
||||
|
||||
@@ -102,6 +134,7 @@ export const getAll = query({
|
||||
isMuted: s.isMuted,
|
||||
isDeafened: s.isDeafened,
|
||||
isScreenSharing: s.isScreenSharing,
|
||||
isServerMuted: s.isServerMuted,
|
||||
avatarUrl,
|
||||
});
|
||||
}
|
||||
@@ -109,3 +142,90 @@ export const getAll = query({
|
||||
return grouped;
|
||||
},
|
||||
});
|
||||
|
||||
export const afkMove = mutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
afkChannelId: v.id("channels"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Validate afkChannelId matches server settings
|
||||
const settings = await ctx.db.query("serverSettings").first();
|
||||
if (!settings || settings.afkChannelId !== args.afkChannelId) {
|
||||
throw new Error("Invalid AFK channel");
|
||||
}
|
||||
|
||||
// Get current voice state
|
||||
const currentState = await ctx.db
|
||||
.query("voiceStates")
|
||||
.withIndex("by_user", (q: any) => q.eq("userId", args.userId))
|
||||
.first();
|
||||
|
||||
// No-op if not in voice or already in AFK channel
|
||||
if (!currentState || currentState.channelId === args.afkChannelId) return null;
|
||||
|
||||
// Move to AFK channel: delete old state, insert new one muted
|
||||
await ctx.db.delete(currentState._id);
|
||||
await ctx.db.insert("voiceStates", {
|
||||
channelId: args.afkChannelId,
|
||||
userId: args.userId,
|
||||
username: currentState.username,
|
||||
isMuted: true,
|
||||
isDeafened: currentState.isDeafened,
|
||||
isScreenSharing: false,
|
||||
isServerMuted: currentState.isServerMuted,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const moveUser = mutation({
|
||||
args: {
|
||||
actorUserId: v.id("userProfiles"),
|
||||
targetUserId: v.id("userProfiles"),
|
||||
targetChannelId: v.id("channels"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Check actor has move_members permission
|
||||
const roles = await getRolesForUser(ctx, args.actorUserId);
|
||||
const canMove = roles.some(
|
||||
(role) => (role.permissions as Record<string, boolean>)?.["move_members"]
|
||||
);
|
||||
if (!canMove) {
|
||||
throw new Error("You don't have permission to move members");
|
||||
}
|
||||
|
||||
// Validate target channel exists and is voice
|
||||
const targetChannel = await ctx.db.get(args.targetChannelId);
|
||||
if (!targetChannel) throw new Error("Target channel not found");
|
||||
if (targetChannel.type !== "voice") throw new Error("Target channel is not a voice channel");
|
||||
|
||||
// Get target user's current voice state
|
||||
const currentState = await ctx.db
|
||||
.query("voiceStates")
|
||||
.withIndex("by_user", (q: any) => q.eq("userId", args.targetUserId))
|
||||
.first();
|
||||
|
||||
if (!currentState) throw new Error("Target user is not in a voice channel");
|
||||
|
||||
// No-op if already in the target channel
|
||||
if (currentState.channelId === args.targetChannelId) return null;
|
||||
|
||||
// Delete old voice state and insert new one preserving mute/deaf/screenshare
|
||||
await ctx.db.delete(currentState._id);
|
||||
await ctx.db.insert("voiceStates", {
|
||||
channelId: args.targetChannelId,
|
||||
userId: args.targetUserId,
|
||||
username: currentState.username,
|
||||
isMuted: currentState.isMuted,
|
||||
isDeafened: currentState.isDeafened,
|
||||
isScreenSharing: currentState.isScreenSharing,
|
||||
isServerMuted: currentState.isServerMuted,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user