feat: Initialize the Electron frontend with core UI components and integrate Convex backend services.
This commit is contained in:
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -15,6 +15,7 @@ import type * as dms from "../dms.js";
|
||||
import type * as files from "../files.js";
|
||||
import type * as gifs from "../gifs.js";
|
||||
import type * as invites from "../invites.js";
|
||||
import type * as members from "../members.js";
|
||||
import type * as messages from "../messages.js";
|
||||
import type * as reactions from "../reactions.js";
|
||||
import type * as roles from "../roles.js";
|
||||
@@ -36,6 +37,7 @@ declare const fullApi: ApiFromModules<{
|
||||
files: typeof files;
|
||||
gifs: typeof gifs;
|
||||
invites: typeof invites;
|
||||
members: typeof members;
|
||||
messages: typeof messages;
|
||||
reactions: typeof reactions;
|
||||
roles: typeof roles;
|
||||
|
||||
@@ -197,14 +197,62 @@ export const getPublicKeys = query({
|
||||
id: v.string(),
|
||||
username: v.string(),
|
||||
public_identity_key: v.string(),
|
||||
status: v.optional(v.string()),
|
||||
avatarUrl: v.optional(v.union(v.string(), v.null())),
|
||||
aboutMe: v.optional(v.string()),
|
||||
customStatus: v.optional(v.string()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const users = await ctx.db.query("userProfiles").collect();
|
||||
return users.map((u) => ({
|
||||
id: u._id,
|
||||
username: u.username,
|
||||
public_identity_key: u.publicIdentityKey,
|
||||
}));
|
||||
const results = [];
|
||||
for (const u of users) {
|
||||
let avatarUrl: string | null = null;
|
||||
if (u.avatarStorageId) {
|
||||
avatarUrl = await ctx.storage.getUrl(u.avatarStorageId);
|
||||
}
|
||||
results.push({
|
||||
id: u._id,
|
||||
username: u.username,
|
||||
public_identity_key: u.publicIdentityKey,
|
||||
status: u.status || "online",
|
||||
avatarUrl,
|
||||
aboutMe: u.aboutMe,
|
||||
customStatus: u.customStatus,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
},
|
||||
});
|
||||
|
||||
// Update user profile (aboutMe, avatar, customStatus)
|
||||
export const updateProfile = mutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
aboutMe: v.optional(v.string()),
|
||||
avatarStorageId: v.optional(v.id("_storage")),
|
||||
customStatus: v.optional(v.string()),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (args.aboutMe !== undefined) patch.aboutMe = args.aboutMe;
|
||||
if (args.avatarStorageId !== undefined) patch.avatarStorageId = args.avatarStorageId;
|
||||
if (args.customStatus !== undefined) patch.customStatus = args.customStatus;
|
||||
await ctx.db.patch(args.userId, patch);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Update user status
|
||||
export const updateStatus = mutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
status: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.userId, { status: args.status });
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,13 +31,16 @@ export const list = query({
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
type: v.string(),
|
||||
category: v.optional(v.string()),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const channels = await ctx.db.query("channels").collect();
|
||||
return channels
|
||||
.filter((c) => c.type !== "dm")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0) || a.name.localeCompare(b.name));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,6 +53,9 @@ export const get = query({
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
type: v.string(),
|
||||
category: v.optional(v.string()),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
@@ -63,6 +69,9 @@ export const create = mutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
type: v.optional(v.string()),
|
||||
category: v.optional(v.string()),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
},
|
||||
returns: v.object({ id: v.id("channels") }),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -82,12 +91,30 @@ export const create = mutation({
|
||||
const id = await ctx.db.insert("channels", {
|
||||
name: args.name,
|
||||
type: args.type || "text",
|
||||
category: args.category,
|
||||
topic: args.topic,
|
||||
position: args.position,
|
||||
});
|
||||
|
||||
return { id };
|
||||
},
|
||||
});
|
||||
|
||||
// Update channel topic
|
||||
export const updateTopic = mutation({
|
||||
args: {
|
||||
id: v.id("channels"),
|
||||
topic: v.string(),
|
||||
},
|
||||
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, { topic: args.topic });
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Rename channel
|
||||
export const rename = mutation({
|
||||
args: {
|
||||
@@ -99,6 +126,9 @@ export const rename = mutation({
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
type: v.string(),
|
||||
category: v.optional(v.string()),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
if (!args.name.trim()) {
|
||||
|
||||
@@ -49,6 +49,7 @@ export const listDMs = query({
|
||||
channel_name: v.string(),
|
||||
other_user_id: v.string(),
|
||||
other_username: v.string(),
|
||||
other_user_status: v.optional(v.string()),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -78,6 +79,7 @@ export const listDMs = query({
|
||||
channel_name: channel.name,
|
||||
other_user_id: otherUser._id as string,
|
||||
other_username: otherUser.username,
|
||||
other_user_status: otherUser.status || "online",
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
63
convex/members.ts
Normal file
63
convex/members.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const getChannelMembers = query({
|
||||
args: {
|
||||
channelId: v.id("channels"),
|
||||
},
|
||||
returns: v.any(),
|
||||
handler: async (ctx, args) => {
|
||||
const channelKeyDocs = await ctx.db
|
||||
.query("channelKeys")
|
||||
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
|
||||
.collect();
|
||||
|
||||
const seenUsers = new Set<string>();
|
||||
const members = [];
|
||||
|
||||
for (const doc of channelKeyDocs) {
|
||||
const odId = doc.userId.toString();
|
||||
if (seenUsers.has(odId)) continue;
|
||||
seenUsers.add(odId);
|
||||
|
||||
const user = await ctx.db.get(doc.userId);
|
||||
if (!user) continue;
|
||||
|
||||
const userRoleDocs = await ctx.db
|
||||
.query("userRoles")
|
||||
.withIndex("by_user", (q) => q.eq("userId", doc.userId))
|
||||
.collect();
|
||||
|
||||
const roles = [];
|
||||
for (const ur of userRoleDocs) {
|
||||
const role = await ctx.db.get(ur.roleId);
|
||||
if (role) {
|
||||
roles.push({
|
||||
id: role._id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
position: role.position,
|
||||
isHoist: role.isHoist,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
if (user.avatarStorageId) {
|
||||
avatarUrl = await ctx.storage.getUrl(user.avatarStorageId);
|
||||
}
|
||||
|
||||
members.push({
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
status: user.status || "online",
|
||||
roles: roles.sort((a, b) => b.position - a.position),
|
||||
avatarUrl,
|
||||
aboutMe: user.aboutMe,
|
||||
customStatus: user.customStatus,
|
||||
});
|
||||
}
|
||||
|
||||
return members;
|
||||
},
|
||||
});
|
||||
@@ -20,6 +20,11 @@ export const list = query({
|
||||
result.page.map(async (msg) => {
|
||||
const sender = await ctx.db.get(msg.senderId);
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
if (sender?.avatarStorageId) {
|
||||
avatarUrl = await ctx.storage.getUrl(sender.avatarStorageId);
|
||||
}
|
||||
|
||||
const reactionDocs = await ctx.db
|
||||
.query("messageReactions")
|
||||
.withIndex("by_message", (q) => q.eq("messageId", msg._id))
|
||||
@@ -34,6 +39,23 @@ export const list = query({
|
||||
}
|
||||
}
|
||||
|
||||
let replyToUsername: string | null = null;
|
||||
let replyToContent: string | null = null;
|
||||
let replyToNonce: string | null = null;
|
||||
let replyToAvatarUrl: string | null = null;
|
||||
if (msg.replyTo) {
|
||||
const repliedMsg = await ctx.db.get(msg.replyTo);
|
||||
if (repliedMsg) {
|
||||
const repliedSender = await ctx.db.get(repliedMsg.senderId);
|
||||
replyToUsername = repliedSender?.username || "Unknown";
|
||||
replyToContent = repliedMsg.ciphertext;
|
||||
replyToNonce = repliedMsg.nonce;
|
||||
if (repliedSender?.avatarStorageId) {
|
||||
replyToAvatarUrl = await ctx.storage.getUrl(repliedSender.avatarStorageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: msg._id,
|
||||
channel_id: msg.channelId,
|
||||
@@ -45,7 +67,15 @@ export const list = query({
|
||||
created_at: new Date(msg._creationTime).toISOString(),
|
||||
username: sender?.username || "Unknown",
|
||||
public_signing_key: sender?.publicSigningKey || "",
|
||||
avatarUrl,
|
||||
reactions: Object.keys(reactions).length > 0 ? reactions : null,
|
||||
replyToId: msg.replyTo || null,
|
||||
replyToUsername,
|
||||
replyToContent,
|
||||
replyToNonce,
|
||||
replyToAvatarUrl,
|
||||
editedAt: msg.editedAt || null,
|
||||
pinned: msg.pinned || false,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -62,6 +92,7 @@ export const send = mutation({
|
||||
nonce: v.string(),
|
||||
signature: v.string(),
|
||||
keyVersion: v.number(),
|
||||
replyTo: v.optional(v.id("messages")),
|
||||
},
|
||||
returns: v.object({ id: v.id("messages") }),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -72,11 +103,74 @@ export const send = mutation({
|
||||
nonce: args.nonce,
|
||||
signature: args.signature,
|
||||
keyVersion: args.keyVersion,
|
||||
replyTo: args.replyTo,
|
||||
});
|
||||
return { id };
|
||||
},
|
||||
});
|
||||
|
||||
export const edit = mutation({
|
||||
args: {
|
||||
id: v.id("messages"),
|
||||
ciphertext: v.string(),
|
||||
nonce: v.string(),
|
||||
signature: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.id, {
|
||||
ciphertext: args.ciphertext,
|
||||
nonce: args.nonce,
|
||||
signature: args.signature,
|
||||
editedAt: Date.now(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const pin = mutation({
|
||||
args: {
|
||||
id: v.id("messages"),
|
||||
pinned: v.boolean(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.id, { pinned: args.pinned });
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const listPinned = query({
|
||||
args: {
|
||||
channelId: v.id("channels"),
|
||||
},
|
||||
returns: v.any(),
|
||||
handler: async (ctx, args) => {
|
||||
const allMessages = await ctx.db
|
||||
.query("messages")
|
||||
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
|
||||
.collect();
|
||||
|
||||
const pinned = allMessages.filter((m) => m.pinned === true);
|
||||
|
||||
return Promise.all(
|
||||
pinned.map(async (msg) => {
|
||||
const sender = await ctx.db.get(msg.senderId);
|
||||
return {
|
||||
id: msg._id,
|
||||
ciphertext: msg.ciphertext,
|
||||
nonce: msg.nonce,
|
||||
signature: msg.signature,
|
||||
key_version: msg.keyVersion,
|
||||
created_at: new Date(msg._creationTime).toISOString(),
|
||||
username: sender?.username || "Unknown",
|
||||
public_signing_key: sender?.publicSigningKey || "",
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: { id: v.id("messages") },
|
||||
returns: v.null(),
|
||||
|
||||
@@ -11,11 +11,18 @@ export default defineSchema({
|
||||
publicSigningKey: v.string(),
|
||||
encryptedPrivateKeys: v.string(),
|
||||
isAdmin: v.boolean(),
|
||||
status: v.optional(v.string()),
|
||||
avatarStorageId: v.optional(v.id("_storage")),
|
||||
aboutMe: v.optional(v.string()),
|
||||
customStatus: v.optional(v.string()),
|
||||
}).index("by_username", ["username"]),
|
||||
|
||||
channels: defineTable({
|
||||
name: v.string(),
|
||||
type: v.string(), // 'text' | 'voice' | 'dm'
|
||||
category: v.optional(v.string()),
|
||||
topic: v.optional(v.string()),
|
||||
position: v.optional(v.number()),
|
||||
}).index("by_name", ["name"]),
|
||||
|
||||
messages: defineTable({
|
||||
@@ -25,6 +32,9 @@ export default defineSchema({
|
||||
nonce: v.string(),
|
||||
signature: v.string(),
|
||||
keyVersion: v.number(),
|
||||
replyTo: v.optional(v.id("messages")),
|
||||
editedAt: v.optional(v.number()),
|
||||
pinned: v.optional(v.boolean()),
|
||||
}).index("by_channel", ["channelId"]),
|
||||
|
||||
messageReactions: defineTable({
|
||||
|
||||
Reference in New Issue
Block a user