feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s

- 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.
This commit is contained in:
Bryan1029384756
2026-04-14 09:02:14 -05:00
parent 9ef839938e
commit b7a4cf4ce8
376 changed files with 52619 additions and 167641 deletions

View File

@@ -1,6 +1,78 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
/**
* Rotate the symmetric key for a DM channel. Inserts a brand-new
* versioned row for each participant — existing rows are left alone
* so previously-encrypted messages remain decryptable.
*
* The caller proves they're a DM participant by passing their own
* userId; the server cross-checks against `dmParticipants` for the
* channel. Every recipient userId in `entries` must also be a
* participant — no leaking keys to random users.
*
* The new rows are tagged with `maxExistingVersion + 1`.
*/
export const rotateDMKey = mutation({
args: {
channelId: v.id("channels"),
initiatorUserId: v.id("userProfiles"),
entries: v.array(
v.object({
userId: v.id("userProfiles"),
encryptedKeyBundle: v.string(),
}),
),
},
returns: v.object({ keyVersion: v.number() }),
handler: async (ctx, args) => {
const channel = await ctx.db.get(args.channelId);
if (!channel) throw new Error("Channel not found");
if (channel.type !== "dm") {
throw new Error("rotateDMKey is only supported for DM channels");
}
// Verify every (initiator + entries) userId is in dmParticipants.
const participants = await ctx.db
.query("dmParticipants")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
const participantSet = new Set(participants.map((p) => p.userId as string));
if (!participantSet.has(args.initiatorUserId as unknown as string)) {
throw new Error("Not a participant in this DM");
}
for (const entry of args.entries) {
if (!participantSet.has(entry.userId as unknown as string)) {
throw new Error("Target userId is not a participant in this DM");
}
}
// Find the current max keyVersion for this channel. New rows go
// one above that. If no rows exist yet, start at 2 so legacy
// messages tagged version 1 still hit their original key.
const existing = await ctx.db
.query("channelKeys")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
const maxVersion = existing.reduce(
(m, k) => (k.keyVersion > m ? k.keyVersion : m),
0,
);
const newVersion = maxVersion + 1;
for (const entry of args.entries) {
await ctx.db.insert("channelKeys", {
channelId: args.channelId,
userId: entry.userId,
encryptedKeyBundle: entry.encryptedKeyBundle,
keyVersion: newVersion,
});
}
return { keyVersion: newVersion };
},
});
// Batch upsert encrypted key bundles
export const uploadKeys = mutation({
args: {