Files
DiscordClone/convex/customEmojis.ts
Bryan1029384756 b7a4cf4ce8
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
2026-04-14 09:02:14 -05:00

156 lines
4.6 KiB
TypeScript

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getRolesForUser } from "./roles";
import { getPublicStorageUrl } from "./storageUrl";
export const list = query({
args: {},
returns: v.any(),
handler: async (ctx) => {
const emojis = await ctx.db.query("customEmojis").collect();
const results = await Promise.all(
emojis.map(async (emoji) => {
const src = await getPublicStorageUrl(ctx, emoji.storageId);
const user = await ctx.db.get(emoji.uploadedBy);
let avatarUrl: string | null = null;
if (user?.avatarStorageId) {
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
}
return {
_id: emoji._id,
name: emoji.name,
src,
createdAt: emoji.createdAt,
animated: emoji.animated ?? false,
uploadedById: emoji.uploadedBy,
uploadedByUsername: user?.username || "Unknown",
uploadedByDisplayName: user?.displayName || null,
uploadedByAvatarUrl: avatarUrl,
};
})
);
return results.filter((e) => e.src !== null);
},
});
export const upload = mutation({
args: {
userId: v.id("userProfiles"),
name: v.string(),
storageId: v.id("_storage"),
animated: v.optional(v.boolean()),
},
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 emojis");
}
// Validate name format
const name = args.name.trim();
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
throw new Error("Emoji name can only contain letters, numbers, and underscores");
}
if (name.length < 2 || name.length > 32) {
throw new Error("Emoji name must be between 2 and 32 characters");
}
// Check for duplicate name among custom emojis
const existing = await ctx.db
.query("customEmojis")
.withIndex("by_name", (q) => q.eq("name", name))
.first();
if (existing) {
throw new Error(`A custom emoji named "${name}" already exists`);
}
await ctx.db.insert("customEmojis", {
name,
storageId: args.storageId,
uploadedBy: args.userId,
animated: args.animated ?? false,
createdAt: Date.now(),
});
return null;
},
});
/**
* Rename a custom emoji in place. Enforces the same name validation
* as `upload` and rejects collisions with other existing emojis so
* two rows can't end up sharing a shortcode.
*/
export const rename = mutation({
args: {
userId: v.id("userProfiles"),
emojiId: v.id("customEmojis"),
name: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
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 emojis");
}
const emoji = await ctx.db.get(args.emojiId);
if (!emoji) throw new Error("Emoji not found");
const name = args.name.trim();
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
throw new Error("Emoji name can only contain letters, numbers, and underscores");
}
if (name.length < 2 || name.length > 32) {
throw new Error("Emoji name must be between 2 and 32 characters");
}
if (name !== emoji.name) {
const clash = await ctx.db
.query("customEmojis")
.withIndex("by_name", (q) => q.eq("name", name))
.first();
if (clash && clash._id !== args.emojiId) {
throw new Error(`A custom emoji named "${name}" already exists`);
}
}
await ctx.db.patch(args.emojiId, { name });
return null;
},
});
export const remove = mutation({
args: {
userId: v.id("userProfiles"),
emojiId: v.id("customEmojis"),
},
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 emojis");
}
const emoji = await ctx.db.get(args.emojiId);
if (!emoji) throw new Error("Emoji not found");
await ctx.storage.delete(emoji.storageId);
await ctx.db.delete(args.emojiId);
return null;
},
});