diff --git a/Frontend/Electron/main.cjs b/Frontend/Electron/main.cjs index ca35819..054764f 100644 --- a/Frontend/Electron/main.cjs +++ b/Frontend/Electron/main.cjs @@ -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 diff --git a/Frontend/Electron/package.json b/Frontend/Electron/package.json index 4e71e79..696711b 100644 --- a/Frontend/Electron/package.json +++ b/Frontend/Electron/package.json @@ -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", diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index 07ba3bc..f941351 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -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); } diff --git a/Frontend/Electron/src/components/Sidebar.jsx b/Frontend/Electron/src/components/Sidebar.jsx index 28d69bd..964a545 100644 --- a/Frontend/Electron/src/components/Sidebar.jsx +++ b/Frontend/Electron/src/components/Sidebar.jsx @@ -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) { diff --git a/Frontend/Electron/src/contexts/VoiceContext.jsx b/Frontend/Electron/src/contexts/VoiceContext.jsx index 09c7baa..b0a3323 100644 --- a/Frontend/Electron/src/contexts/VoiceContext.jsx +++ b/Frontend/Electron/src/contexts/VoiceContext.jsx @@ -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, diff --git a/TODO.md b/TODO.md index 368708e..261a8e4 100644 --- a/TODO.md +++ b/TODO.md @@ -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. \ No newline at end of file + +- Add audio to screenshare + +- 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. \ No newline at end of file diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index ef4f35f..deed1f3 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -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; diff --git a/convex/auth.ts b/convex/auth.ts index 9228b2c..ff653b1 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -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 { 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, diff --git a/convex/dms.ts b/convex/dms.ts index b8a180b..5ec25fa 100644 --- a/convex/dms.ts +++ b/convex/dms.ts @@ -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 { diff --git a/convex/files.ts b/convex/files.ts index 54d8ed5..e1e031c 100644 --- a/convex/files.ts +++ b/convex/files.ts @@ -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); }, }); diff --git a/convex/members.ts b/convex/members.ts index 1cf6aed..dfc4578 100644 --- a/convex/members.ts +++ b/convex/members.ts @@ -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({ diff --git a/convex/messages.ts b/convex/messages.ts index ad6f9e5..3dbba3c 100644 --- a/convex/messages.ts +++ b/convex/messages.ts @@ -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); } } } diff --git a/convex/storageUrl.ts b/convex/storageUrl.ts new file mode 100644 index 0000000..2a84b08 --- /dev/null +++ b/convex/storageUrl.ts @@ -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 } }, + storageId: Id<"_storage"> +): Promise { + const url = await ctx.storage.getUrl(storageId); + if (!url) return null; + return rewriteToPublicUrl(url); +} diff --git a/convex/voiceState.ts b/convex/voiceState.ts index d0731e3..b4f4632 100644 --- a/convex/voiceState.ts +++ b/convex/voiceState.ts @@ -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({