feat: Add initial Electron frontend package.json with dependencies, scripts, and build configuration.
This commit is contained in:
@@ -94,7 +94,7 @@ function createWindow() {
|
|||||||
const isDev = process.env.npm_lifecycle_event === 'electron:dev';
|
const isDev = process.env.npm_lifecycle_event === 'electron:dev';
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
mainWindow.loadURL(process.env.VITE_DEV_URL || 'http://localhost:5173');
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
// Production: Load the built file
|
// Production: Load the built file
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "discord",
|
"name": "discord",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"description": "A Discord clone built with Convex, React, and Electron",
|
"description": "A Discord clone built with Convex, React, and Electron",
|
||||||
"author": "Moyettes",
|
"author": "Moyettes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ import MessageItem, { getUserColor } from './MessageItem';
|
|||||||
const metadataCache = new Map();
|
const metadataCache = new Map();
|
||||||
const attachmentCache = 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)
|
// Persistent global decryption cache (survives channel switches)
|
||||||
// Keyed by message _id, stores { content, isVerified, decryptedReply }
|
// Keyed by message _id, stores { content, isVerified, decryptedReply }
|
||||||
const messageDecryptionCache = new Map();
|
const messageDecryptionCache = new Map();
|
||||||
@@ -240,15 +252,16 @@ const LinkPreview = ({ url }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Attachment = ({ metadata, onLoad, onImageClick }) => {
|
const Attachment = ({ metadata, onLoad, onImageClick }) => {
|
||||||
const [url, setUrl] = useState(attachmentCache.get(metadata.url) || null);
|
const fetchUrl = rewriteStorageUrl(metadata.url);
|
||||||
const [loading, setLoading] = useState(!attachmentCache.has(metadata.url));
|
const [url, setUrl] = useState(attachmentCache.get(fetchUrl) || null);
|
||||||
|
const [loading, setLoading] = useState(!attachmentCache.has(fetchUrl));
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [showControls, setShowControls] = useState(false);
|
const [showControls, setShowControls] = useState(false);
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (attachmentCache.has(metadata.url)) {
|
if (attachmentCache.has(fetchUrl)) {
|
||||||
setUrl(attachmentCache.get(metadata.url));
|
setUrl(attachmentCache.get(fetchUrl));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -256,7 +269,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => {
|
|||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
const decryptFile = async () => {
|
const decryptFile = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(metadata.url);
|
const res = await fetch(fetchUrl);
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const arrayBuffer = await blob.arrayBuffer();
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
const hexInput = toHexString(new Uint8Array(arrayBuffer));
|
const hexInput = toHexString(new Uint8Array(arrayBuffer));
|
||||||
@@ -272,7 +285,7 @@ const Attachment = ({ metadata, onLoad, onImageClick }) => {
|
|||||||
const objectUrl = URL.createObjectURL(decryptedBlob);
|
const objectUrl = URL.createObjectURL(decryptedBlob);
|
||||||
|
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
attachmentCache.set(metadata.url, objectUrl);
|
attachmentCache.set(fetchUrl, objectUrl);
|
||||||
setUrl(objectUrl);
|
setUrl(objectUrl);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -754,7 +754,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
|
|||||||
keyVersion: 1
|
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);
|
navigator.clipboard.writeText(link);
|
||||||
alert(`Invite Link Copied to Clipboard!\n\n${link}`);
|
alert(`Invite Link Copied to Clipboard!\n\n${link}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -79,19 +79,19 @@ export const VoiceProvider = ({ children }) => {
|
|||||||
setToken(lkToken);
|
setToken(lkToken);
|
||||||
|
|
||||||
const newRoom = new Room({
|
const newRoom = new Room({
|
||||||
adaptiveStream: false,
|
adaptiveStream: true,
|
||||||
dynacast: false,
|
dynacast: true,
|
||||||
autoSubscribe: true,
|
autoSubscribe: true,
|
||||||
audioCaptureDefaults: {
|
audioCaptureDefaults: {
|
||||||
autoGainControl: true,
|
autoGainControl: true,
|
||||||
echoCancellation: true,
|
echoCancellation: true,
|
||||||
noiseSuppression: true,
|
noiseSuppression: false,
|
||||||
channelCount: 2,
|
channelCount: 1,
|
||||||
sampleRate: 48000,
|
sampleRate: 48000,
|
||||||
},
|
},
|
||||||
publishDefaults: {
|
publishDefaults: {
|
||||||
audioPreset: { maxBitrate: 384_000 },
|
audioPreset: { maxBitrate: 96_000 },
|
||||||
dtx: true,
|
dtx: false,
|
||||||
red: true,
|
red: true,
|
||||||
screenShareEncoding: {
|
screenShareEncoding: {
|
||||||
maxBitrate: 10_000_000,
|
maxBitrate: 10_000_000,
|
||||||
|
|||||||
29
TODO.md
29
TODO.md
@@ -11,12 +11,27 @@
|
|||||||
|
|
||||||
- Owners should be able to delete anyones message in the server.
|
- 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.
|
||||||
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -22,6 +22,7 @@ import type * as presence from "../presence.js";
|
|||||||
import type * as reactions from "../reactions.js";
|
import type * as reactions from "../reactions.js";
|
||||||
import type * as readState from "../readState.js";
|
import type * as readState from "../readState.js";
|
||||||
import type * as roles from "../roles.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 typing from "../typing.js";
|
||||||
import type * as voice from "../voice.js";
|
import type * as voice from "../voice.js";
|
||||||
import type * as voiceState from "../voiceState.js";
|
import type * as voiceState from "../voiceState.js";
|
||||||
@@ -47,6 +48,7 @@ declare const fullApi: ApiFromModules<{
|
|||||||
reactions: typeof reactions;
|
reactions: typeof reactions;
|
||||||
readState: typeof readState;
|
readState: typeof readState;
|
||||||
roles: typeof roles;
|
roles: typeof roles;
|
||||||
|
storageUrl: typeof storageUrl;
|
||||||
typing: typeof typing;
|
typing: typeof typing;
|
||||||
voice: typeof voice;
|
voice: typeof voice;
|
||||||
voiceState: typeof voiceState;
|
voiceState: typeof voiceState;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { query, mutation } from "./_generated/server";
|
import { query, mutation } from "./_generated/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { getPublicStorageUrl } from "./storageUrl";
|
||||||
|
|
||||||
async function sha256Hex(input: string): Promise<string> {
|
async function sha256Hex(input: string): Promise<string> {
|
||||||
const buffer = await crypto.subtle.digest(
|
const buffer = await crypto.subtle.digest(
|
||||||
@@ -210,7 +211,7 @@ export const getPublicKeys = query({
|
|||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
let avatarUrl: string | null = null;
|
let avatarUrl: string | null = null;
|
||||||
if (u.avatarStorageId) {
|
if (u.avatarStorageId) {
|
||||||
avatarUrl = await ctx.storage.getUrl(u.avatarStorageId);
|
avatarUrl = await getPublicStorageUrl(ctx, u.avatarStorageId);
|
||||||
}
|
}
|
||||||
results.push({
|
results.push({
|
||||||
id: u._id,
|
id: u._id,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { query, mutation } from "./_generated/server";
|
import { query, mutation } from "./_generated/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { getPublicStorageUrl } from "./storageUrl";
|
||||||
|
|
||||||
export const openDM = mutation({
|
export const openDM = mutation({
|
||||||
args: {
|
args: {
|
||||||
@@ -76,7 +77,7 @@ export const listDMs = query({
|
|||||||
if (!otherUser) return null;
|
if (!otherUser) return null;
|
||||||
|
|
||||||
const avatarUrl = otherUser.avatarStorageId
|
const avatarUrl = otherUser.avatarStorageId
|
||||||
? await ctx.storage.getUrl(otherUser.avatarStorageId)
|
? await getPublicStorageUrl(ctx, otherUser.avatarStorageId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { getPublicStorageUrl, rewriteToPublicUrl } from "./storageUrl";
|
||||||
|
|
||||||
// Generate upload URL for client-side uploads
|
// Generate upload URL for client-side uploads
|
||||||
export const generateUploadUrl = mutation({
|
export const generateUploadUrl = mutation({
|
||||||
args: {},
|
args: {},
|
||||||
returns: v.string(),
|
returns: v.string(),
|
||||||
handler: async (ctx) => {
|
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") },
|
args: { storageId: v.id("_storage") },
|
||||||
returns: v.union(v.string(), v.null()),
|
returns: v.union(v.string(), v.null()),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
return await ctx.storage.getUrl(args.storageId);
|
return await getPublicStorageUrl(ctx, args.storageId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { query } from "./_generated/server";
|
import { query } from "./_generated/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { getPublicStorageUrl } from "./storageUrl";
|
||||||
|
|
||||||
export const getChannelMembers = query({
|
export const getChannelMembers = query({
|
||||||
args: {
|
args: {
|
||||||
@@ -44,7 +45,7 @@ export const getChannelMembers = query({
|
|||||||
|
|
||||||
let avatarUrl: string | null = null;
|
let avatarUrl: string | null = null;
|
||||||
if (user.avatarStorageId) {
|
if (user.avatarStorageId) {
|
||||||
avatarUrl = await ctx.storage.getUrl(user.avatarStorageId);
|
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
members.push({
|
members.push({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { query, mutation } from "./_generated/server";
|
import { query, mutation } from "./_generated/server";
|
||||||
import { paginationOptsValidator } from "convex/server";
|
import { paginationOptsValidator } from "convex/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { getPublicStorageUrl } from "./storageUrl";
|
||||||
|
|
||||||
export const list = query({
|
export const list = query({
|
||||||
args: {
|
args: {
|
||||||
@@ -22,7 +23,7 @@ export const list = query({
|
|||||||
|
|
||||||
let avatarUrl: string | null = null;
|
let avatarUrl: string | null = null;
|
||||||
if (sender?.avatarStorageId) {
|
if (sender?.avatarStorageId) {
|
||||||
avatarUrl = await ctx.storage.getUrl(sender.avatarStorageId);
|
avatarUrl = await getPublicStorageUrl(ctx, sender.avatarStorageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reactionDocs = await ctx.db
|
const reactionDocs = await ctx.db
|
||||||
@@ -51,7 +52,7 @@ export const list = query({
|
|||||||
replyToContent = repliedMsg.ciphertext;
|
replyToContent = repliedMsg.ciphertext;
|
||||||
replyToNonce = repliedMsg.nonce;
|
replyToNonce = repliedMsg.nonce;
|
||||||
if (repliedSender?.avatarStorageId) {
|
if (repliedSender?.avatarStorageId) {
|
||||||
replyToAvatarUrl = await ctx.storage.getUrl(repliedSender.avatarStorageId);
|
replyToAvatarUrl = await getPublicStorageUrl(ctx, repliedSender.avatarStorageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
convex/storageUrl.ts
Normal file
28
convex/storageUrl.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { query, mutation } from "./_generated/server";
|
import { query, mutation } from "./_generated/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { getPublicStorageUrl } from "./storageUrl";
|
||||||
|
|
||||||
async function removeUserVoiceStates(ctx: any, userId: any) {
|
async function removeUserVoiceStates(ctx: any, userId: any) {
|
||||||
const existing = await ctx.db
|
const existing = await ctx.db
|
||||||
@@ -92,7 +93,7 @@ export const getAll = query({
|
|||||||
const user = await ctx.db.get(s.userId);
|
const user = await ctx.db.get(s.userId);
|
||||||
let avatarUrl: string | null = null;
|
let avatarUrl: string | null = null;
|
||||||
if (user?.avatarStorageId) {
|
if (user?.avatarStorageId) {
|
||||||
avatarUrl = await ctx.storage.getUrl(user.avatarStorageId);
|
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
(grouped[s.channelId] ??= []).push({
|
(grouped[s.channelId] ??= []).push({
|
||||||
|
|||||||
Reference in New Issue
Block a user