feat: implement initial Electron chat application with core UI components and Convex backend integration.
All checks were successful
Build and Release / build-and-release (push) Successful in 11m1s

This commit is contained in:
Bryan1029384756
2026-02-13 07:18:19 -06:00
parent 2201c56cb2
commit 56a9523e38
15 changed files with 436 additions and 178 deletions

View File

@@ -4,6 +4,68 @@ import { v } from "convex/values";
import { getPublicStorageUrl } from "./storageUrl";
import { getRolesForUser } from "./roles";
async function enrichMessage(ctx: any, msg: any, userId?: any) {
const sender = await ctx.db.get(msg.senderId);
let avatarUrl: string | null = null;
if (sender?.avatarStorageId) {
avatarUrl = await getPublicStorageUrl(ctx, sender.avatarStorageId);
}
const reactionDocs = await ctx.db
.query("messageReactions")
.withIndex("by_message", (q: any) => q.eq("messageId", msg._id))
.collect();
const reactions: Record<string, { count: number; me: boolean }> = {};
for (const r of reactionDocs) {
const entry = (reactions[r.emoji] ??= { count: 0, me: false });
entry.count++;
if (userId && r.userId === userId) {
entry.me = true;
}
}
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 getPublicStorageUrl(ctx, repliedSender.avatarStorageId);
}
}
}
return {
id: msg._id,
channel_id: msg.channelId,
sender_id: msg.senderId,
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 || "",
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,
};
}
export const list = query({
args: {
paginationOpts: paginationOptsValidator,
@@ -19,67 +81,7 @@ export const list = query({
.paginate(args.paginationOpts);
const enrichedPage = await Promise.all(
result.page.map(async (msg) => {
const sender = await ctx.db.get(msg.senderId);
let avatarUrl: string | null = null;
if (sender?.avatarStorageId) {
avatarUrl = await getPublicStorageUrl(ctx, sender.avatarStorageId);
}
const reactionDocs = await ctx.db
.query("messageReactions")
.withIndex("by_message", (q) => q.eq("messageId", msg._id))
.collect();
const reactions: Record<string, { count: number; me: boolean }> = {};
for (const r of reactionDocs) {
const entry = (reactions[r.emoji] ??= { count: 0, me: false });
entry.count++;
if (args.userId && r.userId === args.userId) {
entry.me = true;
}
}
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 getPublicStorageUrl(ctx, repliedSender.avatarStorageId);
}
}
}
return {
id: msg._id,
channel_id: msg.channelId,
sender_id: msg.senderId,
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 || "",
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,
};
})
result.page.map((msg) => enrichMessage(ctx, msg, args.userId))
);
return { ...result, page: enrichedPage };
@@ -145,30 +147,19 @@ export const pin = mutation({
export const listPinned = query({
args: {
channelId: v.id("channels"),
userId: v.optional(v.id("userProfiles")),
},
returns: v.any(),
handler: async (ctx, args) => {
const allMessages = await ctx.db
const pinned = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.withIndex("by_channel_pinned", (q) =>
q.eq("channelId", args.channelId).eq("pinned", true)
)
.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 || "",
};
})
pinned.map((msg) => enrichMessage(ctx, msg, args.userId))
);
},
});