feat: Implement core chat page with channel navigation, direct messages, and voice chat integration.
All checks were successful
Build and Release / build-and-release (push) Successful in 9m12s
All checks were successful
Build and Release / build-and-release (push) Successful in 9m12s
This commit is contained in:
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as categories from "../categories.js";
|
||||
import type * as channelKeys from "../channelKeys.js";
|
||||
import type * as channels from "../channels.js";
|
||||
import type * as dms from "../dms.js";
|
||||
@@ -33,6 +34,7 @@ import type {
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auth: typeof auth;
|
||||
categories: typeof categories;
|
||||
channelKeys: typeof channelKeys;
|
||||
channels: typeof channels;
|
||||
dms: typeof dms;
|
||||
|
||||
99
convex/categories.ts
Normal file
99
convex/categories.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
// List all categories ordered by position
|
||||
export const list = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("categories"),
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
position: v.number(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db
|
||||
.query("categories")
|
||||
.withIndex("by_position")
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
// Create a new category
|
||||
export const create = mutation({
|
||||
args: { name: v.string() },
|
||||
returns: v.object({ id: v.id("categories") }),
|
||||
handler: async (ctx, args) => {
|
||||
const name = args.name.trim();
|
||||
if (!name) throw new Error("Category name required");
|
||||
|
||||
// Auto-assign position at end
|
||||
const all = await ctx.db.query("categories").collect();
|
||||
const maxPos = all.reduce((max, c) => Math.max(max, c.position), -1000);
|
||||
|
||||
const id = await ctx.db.insert("categories", {
|
||||
name,
|
||||
position: maxPos + 1000,
|
||||
});
|
||||
return { id };
|
||||
},
|
||||
});
|
||||
|
||||
// Rename a category
|
||||
export const rename = mutation({
|
||||
args: {
|
||||
id: v.id("categories"),
|
||||
name: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const name = args.name.trim();
|
||||
if (!name) throw new Error("Category name required");
|
||||
const cat = await ctx.db.get(args.id);
|
||||
if (!cat) throw new Error("Category not found");
|
||||
await ctx.db.patch(args.id, { name });
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Delete a category (moves its channels to uncategorized)
|
||||
export const remove = mutation({
|
||||
args: { id: v.id("categories") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const cat = await ctx.db.get(args.id);
|
||||
if (!cat) throw new Error("Category not found");
|
||||
|
||||
// Move channels to uncategorized
|
||||
const channels = await ctx.db
|
||||
.query("channels")
|
||||
.withIndex("by_category", (q) => q.eq("categoryId", args.id))
|
||||
.collect();
|
||||
for (const ch of channels) {
|
||||
await ctx.db.patch(ch._id, { categoryId: undefined });
|
||||
}
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Batch reorder categories
|
||||
export const reorder = mutation({
|
||||
args: {
|
||||
updates: v.array(
|
||||
v.object({
|
||||
id: v.id("categories"),
|
||||
position: v.number(),
|
||||
})
|
||||
),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
for (const u of args.updates) {
|
||||
await ctx.db.patch(u.id, { position: u.position });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -32,7 +32,7 @@ export const list = query({
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
type: v.string(),
|
||||
category: v.optional(v.string()),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
})
|
||||
@@ -54,7 +54,7 @@ export const get = query({
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
type: v.string(),
|
||||
category: v.optional(v.string()),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
}),
|
||||
@@ -70,7 +70,7 @@ export const create = mutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
type: v.optional(v.string()),
|
||||
category: v.optional(v.string()),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
},
|
||||
@@ -89,12 +89,26 @@ export const create = mutation({
|
||||
throw new Error("Channel already exists");
|
||||
}
|
||||
|
||||
// Auto-calculate position if not provided
|
||||
let position = args.position;
|
||||
if (position === undefined) {
|
||||
const allChannels = await ctx.db.query("channels").collect();
|
||||
const sameCategory = allChannels.filter(
|
||||
(c) => c.categoryId === args.categoryId && c.type !== "dm"
|
||||
);
|
||||
const maxPos = sameCategory.reduce(
|
||||
(max, c) => Math.max(max, c.position ?? 0),
|
||||
-1000
|
||||
);
|
||||
position = maxPos + 1000;
|
||||
}
|
||||
|
||||
const id = await ctx.db.insert("channels", {
|
||||
name: args.name,
|
||||
type: args.type || "text",
|
||||
category: args.category,
|
||||
categoryId: args.categoryId,
|
||||
topic: args.topic,
|
||||
position: args.position,
|
||||
position,
|
||||
});
|
||||
|
||||
return { id };
|
||||
@@ -127,7 +141,7 @@ export const rename = mutation({
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
type: v.string(),
|
||||
category: v.optional(v.string()),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
}),
|
||||
@@ -146,6 +160,48 @@ export const rename = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
// Move a channel to a different category with a new position
|
||||
export const moveChannel = mutation({
|
||||
args: {
|
||||
id: v.id("channels"),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
position: v.number(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const channel = await ctx.db.get(args.id);
|
||||
if (!channel) throw new Error("Channel not found");
|
||||
await ctx.db.patch(args.id, {
|
||||
categoryId: args.categoryId,
|
||||
position: args.position,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Batch reorder channels
|
||||
export const reorderChannels = mutation({
|
||||
args: {
|
||||
updates: v.array(
|
||||
v.object({
|
||||
id: v.id("channels"),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
position: v.number(),
|
||||
})
|
||||
),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
for (const u of args.updates) {
|
||||
await ctx.db.patch(u.id, {
|
||||
categoryId: u.categoryId,
|
||||
position: u.position,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Delete channel + cascade messages and keys
|
||||
export const remove = mutation({
|
||||
args: { id: v.id("channels") },
|
||||
|
||||
@@ -18,13 +18,19 @@ export default defineSchema({
|
||||
customStatus: v.optional(v.string()),
|
||||
}).index("by_username", ["username"]),
|
||||
|
||||
categories: defineTable({
|
||||
name: v.string(),
|
||||
position: v.number(),
|
||||
}).index("by_position", ["position"]),
|
||||
|
||||
channels: defineTable({
|
||||
name: v.string(),
|
||||
type: v.string(), // 'text' | 'voice' | 'dm'
|
||||
category: v.optional(v.string()),
|
||||
categoryId: v.optional(v.id("categories")),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
}).index("by_name", ["name"]),
|
||||
}).index("by_name", ["name"])
|
||||
.index("by_category", ["categoryId"]),
|
||||
|
||||
messages: defineTable({
|
||||
channelId: v.id("channels"),
|
||||
|
||||
Reference in New Issue
Block a user