feat: Add initial Electron frontend package.json with dependencies, scripts, and build configuration.

This commit is contained in:
Bryan1029384756
2026-02-12 02:38:06 -06:00
parent 1952a1fedf
commit e790db7029
14 changed files with 95 additions and 29 deletions

View File

@@ -94,7 +94,7 @@ function createWindow() {
const isDev = process.env.npm_lifecycle_event === 'electron:dev';
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.loadURL(process.env.VITE_DEV_URL || 'http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
// Production: Load the built file

View File

@@ -1,7 +1,7 @@
{
"name": "discord",
"private": true,
"version": "1.0.8",
"version": "1.0.9",
"description": "A Discord clone built with Convex, React, and Electron",
"author": "Moyettes",
"type": "module",

View File

@@ -28,6 +28,18 @@ import MessageItem, { getUserColor } from './MessageItem';
const metadataCache = new Map();
const attachmentCache = new Map();
const CONVEX_PUBLIC_URL = 'http://72.26.56.3:3210';
const rewriteStorageUrl = (url) => {
try {
const u = new URL(url);
const pub = new URL(CONVEX_PUBLIC_URL);
u.hostname = pub.hostname;
u.port = pub.port;
u.protocol = pub.protocol;
return u.toString();
} catch { return url; }
};
// Persistent global decryption cache (survives channel switches)
// Keyed by message _id, stores { content, isVerified, decryptedReply }
const messageDecryptionCache = new Map();
@@ -240,15 +252,16 @@ const LinkPreview = ({ url }) => {
};
const Attachment = ({ metadata, onLoad, onImageClick }) => {
const [url, setUrl] = useState(attachmentCache.get(metadata.url) || null);
const [loading, setLoading] = useState(!attachmentCache.has(metadata.url));
const fetchUrl = rewriteStorageUrl(metadata.url);
const [url, setUrl] = useState(attachmentCache.get(fetchUrl) || null);
const [loading, setLoading] = useState(!attachmentCache.has(fetchUrl));
const [error, setError] = useState(null);
const [showControls, setShowControls] = useState(false);
const videoRef = useRef(null);
useEffect(() => {
if (attachmentCache.has(metadata.url)) {
setUrl(attachmentCache.get(metadata.url));
if (attachmentCache.has(fetchUrl)) {
setUrl(attachmentCache.get(fetchUrl));
setLoading(false);
return;
}
@@ -256,7 +269,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => {
let isMounted = true;
const decryptFile = async () => {
try {
const res = await fetch(metadata.url);
const res = await fetch(fetchUrl);
const blob = await res.blob();
const arrayBuffer = await blob.arrayBuffer();
const hexInput = toHexString(new Uint8Array(arrayBuffer));
@@ -272,7 +285,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => {
const objectUrl = URL.createObjectURL(decryptedBlob);
if (isMounted) {
attachmentCache.set(metadata.url, objectUrl);
attachmentCache.set(fetchUrl, objectUrl);
setUrl(objectUrl);
setLoading(false);
}

View File

@@ -754,7 +754,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
keyVersion: 1
});
const link = `http://localhost:5173/#/register?code=${inviteCode}&key=${inviteSecret}`;
const baseUrl = import.meta.env.VITE_APP_URL || window.location.origin;
const link = `${baseUrl}/#/register?code=${inviteCode}&key=${inviteSecret}`;
navigator.clipboard.writeText(link);
alert(`Invite Link Copied to Clipboard!\n\n${link}`);
} catch (e) {

View File

@@ -79,19 +79,19 @@ export const VoiceProvider = ({ children }) => {
setToken(lkToken);
const newRoom = new Room({
adaptiveStream: false,
dynacast: false,
adaptiveStream: true,
dynacast: true,
autoSubscribe: true,
audioCaptureDefaults: {
autoGainControl: true,
echoCancellation: true,
noiseSuppression: true,
channelCount: 2,
noiseSuppression: false,
channelCount: 1,
sampleRate: 48000,
},
publishDefaults: {
audioPreset: { maxBitrate: 384_000 },
dtx: true,
audioPreset: { maxBitrate: 96_000 },
dtx: false,
red: true,
screenShareEncoding: {
maxBitrate: 10_000_000,

27
TODO.md
View File

@@ -11,12 +11,27 @@
- Owners should be able to delete anyones message in the server.
- When we collapse a category and lets say for example it has a text channel in that category and we have it selected we should still show that text channel but all the others are collapsed
- Next to the category name lets put the category_collapsed_icon.svg icon. This icon is facing down so we will show it normal when a category is not collapsed and rotate it -45 degrees when it is collapsed
For reactions that we didnt react to we have the background to var(--embed-background), lets make it
hsl(240 calc(1*4%) 60.784% /0.0784313725490196)
- When i click on my voice channel i dont join it anymore right away.
- When i click on my voice channel i dont join it anymore right away.
- Add audio to screenshare
<!-- - Figure out why audio is shit. -->
- Fix green status not updating correctly
- Move people between voice channels.
- Allow copy paste of images using CTRL + V in the message box to attach an iamge.
- When you collapse a category that has a voice channel lets still show the users in their.
- If you go afk for 5min switch to channel and to idle.
- Add server muting. Forcing user to mute.
- Allow users to mute other users for themself only.
- Independient voice volumes per user.
# Future
- Allow users to add custom join sounds.

View File

@@ -22,6 +22,7 @@ import type * as presence from "../presence.js";
import type * as reactions from "../reactions.js";
import type * as readState from "../readState.js";
import type * as roles from "../roles.js";
import type * as storageUrl from "../storageUrl.js";
import type * as typing from "../typing.js";
import type * as voice from "../voice.js";
import type * as voiceState from "../voiceState.js";
@@ -47,6 +48,7 @@ declare const fullApi: ApiFromModules<{
reactions: typeof reactions;
readState: typeof readState;
roles: typeof roles;
storageUrl: typeof storageUrl;
typing: typeof typing;
voice: typeof voice;
voiceState: typeof voiceState;

View File

@@ -1,5 +1,6 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getPublicStorageUrl } from "./storageUrl";
async function sha256Hex(input: string): Promise<string> {
const buffer = await crypto.subtle.digest(
@@ -210,7 +211,7 @@ export const getPublicKeys = query({
for (const u of users) {
let avatarUrl: string | null = null;
if (u.avatarStorageId) {
avatarUrl = await ctx.storage.getUrl(u.avatarStorageId);
avatarUrl = await getPublicStorageUrl(ctx, u.avatarStorageId);
}
results.push({
id: u._id,

View File

@@ -1,5 +1,6 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getPublicStorageUrl } from "./storageUrl";
export const openDM = mutation({
args: {
@@ -76,7 +77,7 @@ export const listDMs = query({
if (!otherUser) return null;
const avatarUrl = otherUser.avatarStorageId
? await ctx.storage.getUrl(otherUser.avatarStorageId)
? await getPublicStorageUrl(ctx, otherUser.avatarStorageId)
: null;
return {

View File

@@ -1,12 +1,14 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getPublicStorageUrl, rewriteToPublicUrl } from "./storageUrl";
// Generate upload URL for client-side uploads
export const generateUploadUrl = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
const url = await ctx.storage.generateUploadUrl();
return rewriteToPublicUrl(url);
},
});
@@ -15,6 +17,6 @@ export const getFileUrl = query({
args: { storageId: v.id("_storage") },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId);
return await getPublicStorageUrl(ctx, args.storageId);
},
});

View File

@@ -1,5 +1,6 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
import { getPublicStorageUrl } from "./storageUrl";
export const getChannelMembers = query({
args: {
@@ -44,7 +45,7 @@ export const getChannelMembers = query({
let avatarUrl: string | null = null;
if (user.avatarStorageId) {
avatarUrl = await ctx.storage.getUrl(user.avatarStorageId);
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
}
members.push({

View File

@@ -1,6 +1,7 @@
import { query, mutation } from "./_generated/server";
import { paginationOptsValidator } from "convex/server";
import { v } from "convex/values";
import { getPublicStorageUrl } from "./storageUrl";
export const list = query({
args: {
@@ -22,7 +23,7 @@ export const list = query({
let avatarUrl: string | null = null;
if (sender?.avatarStorageId) {
avatarUrl = await ctx.storage.getUrl(sender.avatarStorageId);
avatarUrl = await getPublicStorageUrl(ctx, sender.avatarStorageId);
}
const reactionDocs = await ctx.db
@@ -51,7 +52,7 @@ export const list = query({
replyToContent = repliedMsg.ciphertext;
replyToNonce = repliedMsg.nonce;
if (repliedSender?.avatarStorageId) {
replyToAvatarUrl = await ctx.storage.getUrl(repliedSender.avatarStorageId);
replyToAvatarUrl = await getPublicStorageUrl(ctx, repliedSender.avatarStorageId);
}
}
}

28
convex/storageUrl.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Id } from "./_generated/dataModel";
// Change this if your public IP changes
const PUBLIC_CONVEX_URL = "http://72.26.56.3:3210";
/** Rewrite any URL to use the public hostname/port/protocol */
export function rewriteToPublicUrl(url: string): string {
try {
const original = new URL(url);
const target = new URL(PUBLIC_CONVEX_URL);
original.hostname = target.hostname;
original.port = target.port;
original.protocol = target.protocol;
return original.toString();
} catch {
return url;
}
}
/** Get a storage file URL rewritten to the public address */
export async function getPublicStorageUrl(
ctx: { storage: { getUrl: (id: Id<"_storage">) => Promise<string | null> } },
storageId: Id<"_storage">
): Promise<string | null> {
const url = await ctx.storage.getUrl(storageId);
if (!url) return null;
return rewriteToPublicUrl(url);
}

View File

@@ -1,5 +1,6 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getPublicStorageUrl } from "./storageUrl";
async function removeUserVoiceStates(ctx: any, userId: any) {
const existing = await ctx.db
@@ -92,7 +93,7 @@ export const getAll = query({
const user = await ctx.db.get(s.userId);
let avatarUrl: string | null = null;
if (user?.avatarStorageId) {
avatarUrl = await ctx.storage.getUrl(user.avatarStorageId);
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
}
(grouped[s.channelId] ??= []).push({