From b26a1d0b4bd73ff3b5faf761daf2af165a099436 Mon Sep 17 00:00:00 2001
From: bryan <23323626+Bryan1029384756@users.noreply.github.com>
Date: Tue, 30 Dec 2025 15:09:17 -0600
Subject: [PATCH] feat: Add initial Electron app structure with real-time chat,
user authentication, and encrypted messaging.
---
Backend/routes/auth.js | 2 +-
Frontend/Electron/main.cjs | 162 +++++++++
Frontend/Electron/main.js | 101 ------
Frontend/Electron/package.json | 4 +-
Frontend/Electron/preload.cjs | 14 +-
Frontend/Electron/src/components/ChatArea.jsx | 267 +++++++++++----
Frontend/Electron/src/index.css | 320 ++++++++++++++++--
Frontend/Electron/src/pages/Login.jsx | 11 +-
Frontend/Electron/src/pages/Register.jsx | 2 +-
9 files changed, 682 insertions(+), 201 deletions(-)
create mode 100644 Frontend/Electron/main.cjs
delete mode 100644 Frontend/Electron/main.js
diff --git a/Backend/routes/auth.js b/Backend/routes/auth.js
index 1fbd536..e0aaaca 100644
--- a/Backend/routes/auth.js
+++ b/Backend/routes/auth.js
@@ -50,7 +50,7 @@ router.post('/login/verify', async (req, res) => {
try {
const result = await db.query(
- 'SELECT hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1',
+ 'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1',
[username]
);
diff --git a/Frontend/Electron/main.cjs b/Frontend/Electron/main.cjs
new file mode 100644
index 0000000..3fdfa25
--- /dev/null
+++ b/Frontend/Electron/main.cjs
@@ -0,0 +1,162 @@
+const { app, BrowserWindow, ipcMain, shell } = require('electron');
+const path = require('path');
+const https = require('https');
+const http = require('http');
+
+function createWindow() {
+ const win = new BrowserWindow({
+ width: 1200,
+ height: 800,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, 'preload.cjs'),
+ sandbox: false
+ }
+ });
+
+ const startUrl = process.env.ELECTRON_START_URL || `file://${path.join(__dirname, '../dist/index.html')}`;
+
+ if (process.env.npm_lifecycle_event === 'electron:dev') {
+ win.loadURL('http://localhost:5173');
+ win.webContents.openDevTools();
+ } else {
+ win.loadURL(startUrl);
+ }
+}
+
+app.whenReady().then(() => {
+ createWindow();
+
+ // Helper to fetch metadata (Zero-Knowledge: Client fetches previews)
+ ipcMain.handle('fetch-metadata', async (event, url) => {
+ return new Promise((resolve) => {
+ const client = url.startsWith('https') ? https : http;
+ const req = client.get(url, (res) => {
+ let data = '';
+ res.on('data', (chunk) => data += chunk);
+ res.on('end', () => {
+ // Simple Regex Parser for OG Tags to avoid dependencies
+ const getMeta = (prop) => {
+ const regex = new RegExp(` {
+ const regex = /
(.*?)<\/title>/i;
+ const match = data.match(regex);
+ return match ? match[1] : null;
+ };
+
+ const metadata = {
+ title: getMeta('title') || getTitle(),
+ description: getMeta('description'),
+ image: getMeta('image'),
+ siteName: getMeta('site_name'),
+ themeColor: getMeta('theme-color')
+ };
+ resolve(metadata);
+ });
+ });
+ req.on('error', (err) => {
+ console.error('Metadata fetch error:', err);
+ resolve(null);
+ });
+ });
+ });
+
+ ipcMain.handle('open-external', async (event, url) => {
+ await shell.openExternal(url);
+ });
+
+ // Crypto Handlers
+ const crypto = require('crypto');
+
+ ipcMain.handle('generate-keys', async () => {
+ const generateRSA = () => new Promise((resolve, reject) => {
+ crypto.generateKeyPair('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
+ }, (err, pub, priv) => { /* args are (err, publicKey, privateKey) */
+ if (err) reject(err); else resolve({ pub, priv });
+ });
+ });
+
+ const generateEd = () => new Promise((resolve, reject) => {
+ crypto.generateKeyPair('ed25519', {
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
+ }, (err, pub, priv) => {
+ if (err) reject(err); else resolve({ pub, priv });
+ });
+ });
+
+ const [rsa, ed] = await Promise.all([generateRSA(), generateEd()]);
+
+ return {
+ rsaPub: rsa.pub,
+ rsaPriv: rsa.priv,
+ edPub: ed.pub,
+ edPriv: ed.priv
+ };
+ });
+
+ ipcMain.handle('random-bytes', (event, size) => {
+ return crypto.randomBytes(size).toString('hex');
+ });
+
+ ipcMain.handle('sha256', (event, data) => {
+ return crypto.createHash('sha256').update(data).digest('hex');
+ });
+
+ ipcMain.handle('derive-auth-keys', (event, password, salt) => {
+ return new Promise((resolve, reject) => {
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
+ if (err) reject(err);
+ else {
+ const dak = derivedKey.subarray(0, 32).toString('hex');
+ const dek = derivedKey.subarray(32, 64);
+ resolve({ dak, dek });
+ }
+ });
+ });
+ });
+
+ ipcMain.handle('encrypt-data', (event, plaintext, key) => {
+ console.log('encrypt-data called with:', {
+ plaintextType: typeof plaintext,
+ isBuffer: Buffer.isBuffer(plaintext),
+ keyType: typeof key,
+ keyIsBuffer: Buffer.isBuffer(plaintext)
+ });
+
+ if (plaintext === undefined) throw new TypeError('plaintext is undefined');
+ if (key === undefined) throw new TypeError('key is undefined');
+
+ // Key can be hex string or buffer. 32 bytes for AES-256
+ const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
+ const iv = crypto.randomBytes(12); // 96-bit IV for GCM
+ const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
+
+ // Use Buffer concatenation to handle both string and Buffer inputs correctly without encoding confusion
+ const encryptedBuffer = Buffer.concat([
+ cipher.update(plaintext),
+ cipher.final()
+ ]);
+
+ const tag = cipher.getAuthTag().toString('hex');
+ return { content: encryptedBuffer.toString('hex'), iv: iv.toString('hex'), tag };
+ });
+
+ ipcMain.handle('decrypt-data', (event, ciphertext, key, iv, tag) => {
+ const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
+ const ivBuffer = Buffer.from(iv, 'hex');
+ const tagBuffer = Buffer.from(tag, 'hex');
+ const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer);
+ decipher.setAuthTag(tagBuffer);
+ let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+ return decrypted;
+ });
+});
diff --git a/Frontend/Electron/main.js b/Frontend/Electron/main.js
deleted file mode 100644
index ceadf1f..0000000
--- a/Frontend/Electron/main.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import { app, BrowserWindow, ipcMain } from 'electron';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import crypto from 'node:crypto';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-function createWindow() {
- const win = new BrowserWindow({
- width: 1200,
- height: 800,
- webPreferences: {
- preload: path.join(__dirname, 'preload.cjs'),
- contextIsolation: true,
- sandbox: true,
- nodeIntegration: false,
- },
- });
-
- if (process.env.NODE_ENV === 'development' || process.argv.includes('--dev')) {
- win.loadURL('http://localhost:5173');
- win.webContents.openDevTools();
- } else {
- win.loadFile(path.join(__dirname, 'dist', 'index.html'));
- }
-}
-
-app.whenReady().then(() => {
- // Crypto IPC Handlers
- ipcMain.handle('crypto:deriveAuthKeys', async (_, password, salt) => {
- return new Promise((resolve, reject) => {
- crypto.pbkdf2(password, salt, 100000, 32, 'sha512', (err, derived) => {
- if (err) reject(err);
- resolve({
- dek: derived.slice(0, 16).toString('hex'),
- dak: derived.slice(16, 32).toString('hex')
- });
- });
- });
- });
-
- ipcMain.handle('crypto:encryptData', (_, plaintext, keyHex, ivHex) => {
- const key = Buffer.from(keyHex, 'hex');
- const iv = ivHex ? Buffer.from(ivHex, 'hex') : crypto.randomBytes(12);
- const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
- let encrypted = cipher.update(plaintext, 'utf8', 'hex');
- encrypted += cipher.final('hex');
- return {
- content: encrypted,
- tag: cipher.getAuthTag().toString('hex'),
- iv: iv.toString('hex')
- };
- });
-
- ipcMain.handle('crypto:decryptData', (_, ciphertext, keyHex, ivHex, tagHex) => {
- const key = Buffer.from(keyHex, 'hex');
- const iv = Buffer.from(ivHex, 'hex');
- const tag = Buffer.from(tagHex, 'hex');
- const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv);
- decipher.setAuthTag(tag);
- let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
- decrypted += decipher.final('utf8');
- return decrypted;
- });
-
- ipcMain.handle('crypto:generateKeys', async () => {
- const { publicKey: rsaPub, privateKey: rsaPriv } = crypto.generateKeyPairSync('rsa', {
- modulusLength: 2048,
- publicKeyEncoding: { type: 'spki', format: 'pem' },
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
- });
- const { publicKey: edPub, privateKey: edPriv } = crypto.generateKeyPairSync('ed25519', {
- publicKeyEncoding: { type: 'spki', format: 'pem' },
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
- });
- return { rsaPub, rsaPriv, edPub, edPriv };
- });
-
- ipcMain.handle('crypto:randomBytes', (_, size) => {
- return crypto.randomBytes(size).toString('hex');
- });
-
- ipcMain.handle('crypto:sha256', (_, data) => {
- return crypto.createHash('sha256').update(data).digest('hex');
- });
-
- createWindow();
-
- app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- }
- });
-});
-
-app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') {
- app.quit();
- }
-});
diff --git a/Frontend/Electron/package.json b/Frontend/Electron/package.json
index de0766e..cf84f8f 100644
--- a/Frontend/Electron/package.json
+++ b/Frontend/Electron/package.json
@@ -3,7 +3,7 @@
"private": true,
"version": "0.0.0",
"type": "module",
- "main": "main.js",
+ "main": "main.cjs",
"homepage": "./",
"scripts": {
"dev": "vite",
@@ -37,4 +37,4 @@
"vite": "^7.2.4",
"wait-on": "^8.0.1"
}
-}
+}
\ No newline at end of file
diff --git a/Frontend/Electron/preload.cjs b/Frontend/Electron/preload.cjs
index ddf4383..9ee21fc 100644
--- a/Frontend/Electron/preload.cjs
+++ b/Frontend/Electron/preload.cjs
@@ -1,10 +1,12 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('cryptoAPI', {
- deriveAuthKeys: (password, salt) => ipcRenderer.invoke('crypto:deriveAuthKeys', password, salt),
- encryptData: (plaintext, keyHex, ivHex) => ipcRenderer.invoke('crypto:encryptData', plaintext, keyHex, ivHex),
- decryptData: (ciphertext, keyHex, ivHex, tagHex) => ipcRenderer.invoke('crypto:decryptData', ciphertext, keyHex, ivHex, tagHex),
- generateKeys: () => ipcRenderer.invoke('crypto:generateKeys'),
- randomBytes: (size) => ipcRenderer.invoke('crypto:randomBytes', size),
- sha256: (data) => ipcRenderer.invoke('crypto:sha256', data)
+ generateKeys: () => ipcRenderer.invoke('generate-keys'),
+ randomBytes: (size) => ipcRenderer.invoke('random-bytes', size),
+ sha256: (data) => ipcRenderer.invoke('sha256', data),
+ deriveAuthKeys: (password, salt) => ipcRenderer.invoke('derive-auth-keys', password, salt),
+ encryptData: (data, key) => ipcRenderer.invoke('encrypt-data', data, key),
+ decryptData: (encryptedData, key, iv, tag) => ipcRenderer.invoke('decrypt-data', encryptedData, key, iv, tag),
+ fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url),
+ openExternal: (url) => ipcRenderer.invoke('open-external', url),
});
diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx
index 5ca4a43..4859621 100644
--- a/Frontend/Electron/src/components/ChatArea.jsx
+++ b/Frontend/Electron/src/components/ChatArea.jsx
@@ -10,20 +10,26 @@ const ChatArea = ({ channelId, username }) => {
const [input, setInput] = useState('');
const [socket, setSocket] = useState(null);
const messagesEndRef = useRef(null);
+ const textareaRef = useRef(null);
- // Mock Key for demo (In real app, derive from Channel Key Bundle)
- const DEMO_CHANNEL_KEY = '000102030405060708090a0b0c0d0e0f';
+ // Mock Key for demo (32 bytes hex = 64 chars)
+ const DEMO_CHANNEL_KEY = '000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f';
+
+ // Helper to get consistent color for user
+ const getUserColor = (username) => {
+ const colors = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
+ let hash = 0;
+ for (let i = 0; i < username.length; i++) {
+ hash = username.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ return colors[Math.abs(hash) % colors.length];
+ };
// Helper to decrypt message
const decryptMessage = async (msg) => {
try {
- // Check if ciphertext has appended tag
- // Tag is 16 bytes = 32 hex chars
const TAG_LENGTH = 32;
if (!msg.ciphertext || msg.ciphertext.length < TAG_LENGTH) {
- // Try decrypting without tag if it was legacy (though we just started)
- // Or maybe it's just raw text if not encrypted? No, we always encrypt.
- console.warn('Message missing tag, trying raw decrypt or fail');
return '[Invalid Encrypted Message]';
}
@@ -38,6 +44,85 @@ const ChatArea = ({ channelId, username }) => {
}
};
+ // Helper to extract URLs
+ const extractUrls = (text) => {
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
+ return text.match(urlRegex) || [];
+ };
+
+ const LinkPreview = ({ url }) => {
+ const [metadata, setMetadata] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [playing, setPlaying] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+ const fetchMeta = async () => {
+ try {
+ const data = await window.cryptoAPI.fetchMetadata(url);
+ if (isMounted) {
+ setMetadata(data);
+ setLoading(false);
+ }
+ } catch (err) {
+ console.error("Failed to fetch metadata", err);
+ if (isMounted) setLoading(false);
+ }
+ };
+ fetchMeta();
+ return () => { isMounted = false; };
+ }, [url]);
+
+ // Helper to extract video ID
+ const getYouTubeId = (link) => {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
+ const match = link.match(regExp);
+ return (match && match[2].length === 11) ? match[2] : null;
+ };
+
+ const videoId = getYouTubeId(url);
+ const isYouTube = !!videoId;
+
+ if (loading || !metadata || (!metadata.title && !metadata.image)) return null;
+
+ return (
+
+
+
+ {metadata.image && (!isYouTube || !playing) && (
+
isYouTube && setPlaying(true)}
+ style={isYouTube ? { cursor: 'pointer' } : {}}
+ >
+

+ {isYouTube &&
▶
}
+
+ )}
+
+ );
+ };
+
useEffect(() => {
const newSocket = io('http://localhost:3000');
setSocket(newSocket);
@@ -64,32 +149,37 @@ const ChatArea = ({ channelId, username }) => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
+ useEffect(() => {
+ if (textareaRef.current) {
+ textareaRef.current.style.height = 'auto';
+ textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
+ }
+ }, [input]);
+
const handleSend = async (e) => {
e.preventDefault();
if (!input.trim()) return;
try {
- // Encrypt message
const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(input, DEMO_CHANNEL_KEY);
-
- // Append tag to ciphertext for storage
const ciphertext = encryptedContent + tag;
-
- // Sign message (placeholder)
const signature = 'placeholder_signature';
+ const senderId = localStorage.getItem('userId');
+ if (!senderId) {
+ console.error('No userId found in localStorage');
+ return;
+ }
+
const messageData = {
channelId,
- senderId: '8b105be1-981e-4200-bb07-68d0714870c2', // Placeholder default, gets overwritten below
+ senderId,
ciphertext,
nonce: iv,
signature,
keyVersion: 1
};
- const storedUserId = localStorage.getItem('userId');
- if (storedUserId) messageData.senderId = storedUserId;
-
socket.emit('send_message', messageData);
setInput('');
} catch (err) {
@@ -107,51 +197,114 @@ const ChatArea = ({ channelId, username }) => {
return (
- {messages.map((msg, idx) => (
-
-
-
{msg.username || 'Unknown'}
-
{new Date(msg.created_at).toLocaleTimeString()}
+ {messages.map((msg, idx) => {
+ const urls = extractUrls(msg.content);
+ return (
+
+
+
+ {(msg.username || '?').substring(0, 1).toUpperCase()}
+
+
+
+
+
+ {msg.username || 'Unknown'}
+
+
+ {new Date(msg.created_at).toLocaleDateString()} at {new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
+
+
(
+ {
+ e.preventDefault();
+ window.cryptoAPI.openExternal(props.href);
+ }}
+ style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }}
+ onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
+ onMouseOut={(e) => e.target.style.textDecoration = 'none'}
+ />
+ ),
+ code({ node, inline, className, children, ...props }) {
+ const match = /language-(\w+)/.exec(className || '')
+ return !inline && match ? (
+
+ {String(children).replace(/\n$/, '')}
+
+ ) : (
+
+ {children}
+
+ )
+ },
+ p: ({ node, ...props }) => ,
+ h1: ({ node, ...props }) => ,
+ h2: ({ node, ...props }) => ,
+ h3: ({ node, ...props }) => ,
+ ul: ({ node, ...props }) => ,
+ ol: ({ node, ...props }) =>
,
+ li: ({ node, ...props }) => ,
+ hr: ({ node, ...props }) =>
,
+ }}
+ >
+ {msg.content}
+
+ {urls.map((url, i) => (
+
+ ))}
+
+
-
-
- {String(children).replace(/\n$/, '')}
-
- ) : (
-
- {children}
-
- )
- }
- }}
- >
- {msg.content}
-
-
-
- ))}
+ );
+ })}
);
diff --git a/Frontend/Electron/src/index.css b/Frontend/Electron/src/index.css
index bd2c450..9379193 100644
--- a/Frontend/Electron/src/index.css
+++ b/Frontend/Electron/src/index.css
@@ -31,7 +31,6 @@ body {
justify-content: center;
height: 100vh;
background-image: url('https://discord.com/assets/f9e794909795f472.svg');
- /* Placeholder background */
background-size: cover;
background-position: center;
}
@@ -122,6 +121,7 @@ body {
/* Sidebar */
.sidebar {
width: 300px;
+ min-width: 300px;
background-color: var(--bg-secondary);
display: flex;
flex-direction: row;
@@ -201,28 +201,71 @@ body {
background-color: var(--bg-primary);
display: flex;
flex-direction: column;
+ position: relative;
}
.messages-list {
flex: 1;
overflow-y: auto;
- padding: 20px;
+ padding: 0 0 20px 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.messages-list::-webkit-scrollbar {
+ width: 8px;
+ background-color: #2b2d31;
+}
+
+.messages-list::-webkit-scrollbar-thumb {
+ background-color: #1a1b1e;
+ border-radius: 4px;
}
.message-item {
- margin-bottom: 20px;
+ display: flex;
+ padding: 2px 16px;
+ margin-top: 17px;
+}
+
+.message-item:hover {
+ background-color: rgba(2, 2, 2, 0.06);
+}
+
+.message-avatar-wrapper {
+ width: 40px;
+ margin-right: 16px;
+ margin-top: 2px;
+}
+
+.message-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ color: white;
+ font-size: 18px;
+ user-select: none;
+}
+
+.message-body {
+ flex: 1;
+ min-width: 0;
}
.message-header {
display: flex;
- align-items: baseline;
- margin-bottom: 4px;
+ align-items: center;
+ margin-bottom: 2px;
}
.username {
- color: var(--header-primary);
+ font-size: 16px;
font-weight: 500;
- margin-right: 8px;
+ margin-right: 0.25rem;
cursor: pointer;
}
@@ -232,54 +275,267 @@ body {
.timestamp {
color: var(--text-muted);
- font-size: 12px;
+ font-size: 0.75rem;
+ margin-left: 0.25rem;
+ font-weight: 400;
}
.message-content {
color: var(--text-normal);
+ font-size: 1rem;
+ line-height: 1.375rem;
white-space: pre-wrap;
word-wrap: break-word;
}
-.chat-input-form {
- padding: 0 16px 24px;
+/* Markdown Styles Tweaks */
+.message-content strong {
+ font-weight: 700;
}
-.chat-input-form textarea {
- width: 100%;
- padding: 11px 16px;
- background-color: #40444b;
- border: none;
- border-radius: 8px;
- color: var(--text-normal);
- font-size: 16px;
- font-family: inherit;
- height: auto;
- min-height: 44px;
+.message-content h1,
+.message-content h2,
+.message-content h3 {
+ border-bottom: none;
+ font-weight: 700;
+ color: var(--header-primary);
}
-.chat-input-form textarea:focus {
- outline: none;
+.message-content ul,
+.message-content ol {
+ list-style-type: disc;
}
-/* Markdown Styles */
-.message-content p {
- margin: 0;
+.message-content a {
+ color: #00b0f4;
+ text-decoration: none;
+}
+
+.message-content a:hover {
+ text-decoration: underline;
+}
+
+.message-content blockquote {
+ border-left: 4px solid #4f545c;
+ margin: 4px 0 4px 0;
+ padding: 0 8px 0 12px;
+ color: #b9bbbe;
+ background-color: transparent;
}
.message-content code {
background-color: #2f3136;
padding: 2px 4px;
border-radius: 3px;
- font-family: monospace;
+ font-family: Consolas, 'Courier New', monospace;
+ font-size: 0.875rem;
}
.message-content pre {
- margin: 6px 0;
+ background-color: #2f3136 !important;
+ border-radius: 4px;
+ border: 1px solid #202225;
+ padding: 8px !important;
+ margin: 8px 0 !important;
+ max-width: 100%;
+ overflow-x: auto;
}
-.message-content blockquote {
- border-left: 4px solid var(--interactive-normal);
- margin: 0;
- padding-left: 10px;
+.chat-input-form {
+ padding: 0 16px 24px;
+ margin-top: 8px;
+ background-color: var(--bg-primary);
+}
+
+.chat-input-wrapper {
+ background-color: #40444b;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ padding: 0 16px;
+ min-height: 44px;
+}
+
+.chat-input-file-btn {
+ background: none;
+ border: none;
+ color: #b9bbbe;
+ cursor: pointer;
+ padding: 0 16px 0 0;
+ display: flex;
+ align-items: center;
+ height: 24px;
+ align-self: center;
+}
+
+.chat-input-file-btn:hover {
+ color: var(--text-normal);
+}
+
+.chat-input-form textarea {
+ flex: 1;
+ padding: 11px 0;
+ background-color: transparent;
+ border: none;
+ color: var(--text-normal);
+ font-size: 16px;
+ font-family: inherit;
+ height: 44px;
+ resize: none;
+ overflow: hidden;
+ line-height: 1.375rem;
+ box-sizing: border-box;
+}
+
+.chat-input-form textarea:focus {
+ outline: none;
+}
+
+.chat-input-form textarea::placeholder {
+ color: #72767d;
+}
+
+.chat-input-icons {
+ display: flex;
+ align-items: center;
+ padding: 0 0 0 10px;
+ align-self: center;
+ height: 100%;
+}
+
+.chat-input-icon-btn {
+ background: none;
+ border: none;
+ color: #b9bbbe;
+ cursor: pointer;
+ padding: 4px;
+ margin-left: 4px;
+ display: flex;
+ align-items: center;
+}
+
+.chat-input-icon-btn:hover {
+ color: var(--text-normal);
+}
+
+/* Link Previews */
+.link-preview {
+ display: flex;
+ background-color: #2f3136;
+ border-left: 4px solid #202225;
+ border-radius: 4px;
+ padding: 10px;
+ margin-top: 8px;
+ max-width: 520px;
+ gap: 16px;
+}
+
+.preview-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-width: 0;
+}
+
+.preview-site-name {
+ font-size: 12px;
+ color: #b9bbbe;
+ margin-bottom: 4px;
+}
+
+.preview-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #00b0f4;
+ text-decoration: none;
+ margin-bottom: 4px;
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.preview-title:hover {
+ text-decoration: underline;
+}
+
+.preview-description {
+ font-size: 14px;
+ color: #dcddde;
+ line-height: 1.4;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.preview-image-container {
+ flex-shrink: 0;
+ position: relative;
+}
+
+.preview-image {
+ max-width: 150px;
+ max-height: 150px;
+ border-radius: 4px;
+ object-fit: cover;
+}
+
+.play-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: white;
+ background: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+}
+
+.youtube-preview {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+}
+
+.youtube-preview .preview-image-container {
+ width: 100%;
+ max-width: 400px;
+ position: relative;
+}
+
+.youtube-preview .preview-image {
+ width: 100%;
+ max-width: 100%;
+ max-height: none;
+ aspect-ratio: 16 / 9;
+}
+
+.youtube-video-wrapper {
+ margin-top: 8px;
+ position: relative;
+ width: 100%;
+ max-width: 560px;
+ /* Standard YouTube embed width or max for chat */
+ padding-bottom: 56.25%;
+ /* 16:9 Aspect Ratio */
+ height: 0;
+ overflow: hidden;
+ border-radius: 4px;
+ background-color: black;
+}
+
+.youtube-video-wrapper iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
}
\ No newline at end of file
diff --git a/Frontend/Electron/src/pages/Login.jsx b/Frontend/Electron/src/pages/Login.jsx
index c05efbe..eb0879f 100644
--- a/Frontend/Electron/src/pages/Login.jsx
+++ b/Frontend/Electron/src/pages/Login.jsx
@@ -42,11 +42,20 @@ const Login = () => {
throw new Error(verifyData.error || 'Login failed');
}
- console.log('Login verified');
+ console.log('Login verified. Response data:', verifyData);
+
if (verifyData.userId) {
+ console.log('Saving userId to localStorage:', verifyData.userId);
localStorage.setItem('userId', verifyData.userId);
+ } else {
+ console.error('MISSING USERID IN VERIFY RESPONSE!', verifyData);
}
+
localStorage.setItem('username', username);
+
+ // Verify immediate read back
+ console.log('Immediate localStorage read check:', localStorage.getItem('userId'));
+
navigate('/chat');
} catch (err) {
console.error('Login error:', err);
diff --git a/Frontend/Electron/src/pages/Register.jsx b/Frontend/Electron/src/pages/Register.jsx
index 0a34e64..15b51b5 100644
--- a/Frontend/Electron/src/pages/Register.jsx
+++ b/Frontend/Electron/src/pages/Register.jsx
@@ -18,7 +18,7 @@ const Register = () => {
// 1. Generate Salt and Master Key (MK)
const salt = await window.cryptoAPI.randomBytes(16);
- const mk = await window.cryptoAPI.randomBytes(16); // 128-bit MK
+ const mk = await window.cryptoAPI.randomBytes(32); // 256-bit MK for AES-256
console.log('Generated Salt and MK');