feat: Implement initial Electron frontend with core UI, user and server settings, chat, and voice features, along with Convex backend schemas and functions.
Some checks failed
Build and Release / build-and-release (push) Has been cancelled
Some checks failed
Build and Release / build-and-release (push) Has been cancelled
This commit is contained in:
@@ -204,6 +204,7 @@ export const getPublicKeys = query({
|
||||
avatarUrl: v.optional(v.union(v.string(), v.null())),
|
||||
aboutMe: v.optional(v.string()),
|
||||
customStatus: v.optional(v.string()),
|
||||
joinSoundUrl: v.optional(v.union(v.string(), v.null())),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
@@ -214,6 +215,10 @@ export const getPublicKeys = query({
|
||||
if (u.avatarStorageId) {
|
||||
avatarUrl = await getPublicStorageUrl(ctx, u.avatarStorageId);
|
||||
}
|
||||
let joinSoundUrl: string | null = null;
|
||||
if (u.joinSoundStorageId) {
|
||||
joinSoundUrl = await getPublicStorageUrl(ctx, u.joinSoundStorageId);
|
||||
}
|
||||
results.push({
|
||||
id: u._id,
|
||||
username: u.username,
|
||||
@@ -223,6 +228,7 @@ export const getPublicKeys = query({
|
||||
avatarUrl,
|
||||
aboutMe: u.aboutMe,
|
||||
customStatus: u.customStatus,
|
||||
joinSoundUrl,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
@@ -237,6 +243,8 @@ export const updateProfile = mutation({
|
||||
aboutMe: v.optional(v.string()),
|
||||
avatarStorageId: v.optional(v.id("_storage")),
|
||||
customStatus: v.optional(v.string()),
|
||||
joinSoundStorageId: v.optional(v.id("_storage")),
|
||||
removeJoinSound: v.optional(v.boolean()),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -245,11 +253,24 @@ export const updateProfile = mutation({
|
||||
if (args.aboutMe !== undefined) patch.aboutMe = args.aboutMe;
|
||||
if (args.avatarStorageId !== undefined) patch.avatarStorageId = args.avatarStorageId;
|
||||
if (args.customStatus !== undefined) patch.customStatus = args.customStatus;
|
||||
if (args.joinSoundStorageId !== undefined) patch.joinSoundStorageId = args.joinSoundStorageId;
|
||||
if (args.removeJoinSound) patch.joinSoundStorageId = undefined;
|
||||
await ctx.db.patch(args.userId, patch);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Get the current user's join sound URL
|
||||
export const getMyJoinSoundUrl = query({
|
||||
args: { userId: v.id("userProfiles") },
|
||||
returns: v.union(v.string(), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await ctx.db.get(args.userId);
|
||||
if (!user?.joinSoundStorageId) return null;
|
||||
return await getPublicStorageUrl(ctx, user.joinSoundStorageId);
|
||||
},
|
||||
});
|
||||
|
||||
// Update user status
|
||||
export const updateStatus = mutation({
|
||||
args: {
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineSchema({
|
||||
avatarStorageId: v.optional(v.id("_storage")),
|
||||
aboutMe: v.optional(v.string()),
|
||||
customStatus: v.optional(v.string()),
|
||||
joinSoundStorageId: v.optional(v.id("_storage")),
|
||||
}).index("by_username", ["username"]),
|
||||
|
||||
categories: defineTable({
|
||||
@@ -126,7 +127,9 @@ export default defineSchema({
|
||||
.index("by_user_and_channel", ["userId", "channelId"]),
|
||||
|
||||
serverSettings: defineTable({
|
||||
serverName: v.optional(v.string()),
|
||||
afkChannelId: v.optional(v.id("channels")),
|
||||
afkTimeout: v.number(), // seconds (default 300 = 5 min)
|
||||
iconStorageId: v.optional(v.id("_storage")),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,13 @@ export const get = query({
|
||||
args: {},
|
||||
returns: v.any(),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query("serverSettings").first();
|
||||
const settings = await ctx.db.query("serverSettings").first();
|
||||
if (!settings) return null;
|
||||
let iconUrl = null;
|
||||
if (settings.iconStorageId) {
|
||||
iconUrl = await ctx.storage.getUrl(settings.iconStorageId);
|
||||
}
|
||||
return { ...settings, iconUrl };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,6 +62,74 @@ export const update = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const updateName = mutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
serverName: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Permission check
|
||||
const roles = await getRolesForUser(ctx, args.userId);
|
||||
const canManage = roles.some(
|
||||
(role) => (role.permissions as Record<string, boolean>)?.["manage_channels"]
|
||||
);
|
||||
if (!canManage) {
|
||||
throw new Error("You don't have permission to manage server settings");
|
||||
}
|
||||
|
||||
// Validate name
|
||||
const name = args.serverName.trim();
|
||||
if (name.length === 0 || name.length > 100) {
|
||||
throw new Error("Server name must be between 1 and 100 characters");
|
||||
}
|
||||
|
||||
const existing = await ctx.db.query("serverSettings").first();
|
||||
if (existing) {
|
||||
await ctx.db.patch(existing._id, { serverName: name });
|
||||
} else {
|
||||
await ctx.db.insert("serverSettings", {
|
||||
serverName: name,
|
||||
afkTimeout: 300,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const updateIcon = mutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
iconStorageId: v.optional(v.id("_storage")),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Permission check
|
||||
const roles = await getRolesForUser(ctx, args.userId);
|
||||
const canManage = roles.some(
|
||||
(role) => (role.permissions as Record<string, boolean>)?.["manage_channels"]
|
||||
);
|
||||
if (!canManage) {
|
||||
throw new Error("You don't have permission to manage server settings");
|
||||
}
|
||||
|
||||
const existing = await ctx.db.query("serverSettings").first();
|
||||
if (existing) {
|
||||
await ctx.db.patch(existing._id, {
|
||||
iconStorageId: args.iconStorageId,
|
||||
});
|
||||
} else {
|
||||
await ctx.db.insert("serverSettings", {
|
||||
iconStorageId: args.iconStorageId,
|
||||
afkTimeout: 300,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const clearAfkChannel = internalMutation({
|
||||
args: { channelId: v.id("channels") },
|
||||
returns: v.null(),
|
||||
|
||||
@@ -151,6 +151,7 @@ export const getAll = query({
|
||||
isScreenSharing: boolean;
|
||||
isServerMuted: boolean;
|
||||
avatarUrl: string | null;
|
||||
joinSoundUrl: string | null;
|
||||
watchingStream: string | null;
|
||||
}>> = {};
|
||||
|
||||
@@ -160,6 +161,10 @@ export const getAll = query({
|
||||
if (user?.avatarStorageId) {
|
||||
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
|
||||
}
|
||||
let joinSoundUrl: string | null = null;
|
||||
if (user?.joinSoundStorageId) {
|
||||
joinSoundUrl = await getPublicStorageUrl(ctx, user.joinSoundStorageId);
|
||||
}
|
||||
|
||||
(grouped[s.channelId] ??= []).push({
|
||||
userId: s.userId,
|
||||
@@ -169,6 +174,7 @@ export const getAll = query({
|
||||
isScreenSharing: s.isScreenSharing,
|
||||
isServerMuted: s.isServerMuted,
|
||||
avatarUrl,
|
||||
joinSoundUrl,
|
||||
watchingStream: s.watchingStream ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user