Spaces:
Runtime error
Runtime error
Deploy latest updates to FireTech-Messager-Server
Browse files- GETTING-STARTED.md +5 -5
- MOBILE-CONNECT-FIX.md +5 -6
- QUICKSTART.sh +3 -3
- README.md +2 -2
- SETUP-GUIDE.md +1 -1
- client.js +200 -210
- index.html +77 -81
- init-db.js +3 -3
- mobile-connect.ps1 +6 -7
- package-lock.json +2 -2
- package.json +1 -1
- server.js +14 -19
- setup-https.ps1 +11 -12
GETTING-STARTED.md
CHANGED
|
@@ -167,11 +167,11 @@ PORT=8080
|
|
| 167 |
## Command Reference
|
| 168 |
|
| 169 |
```powershell
|
| 170 |
-
npm install
|
| 171 |
-
npm start
|
| 172 |
-
npm run dev
|
| 173 |
-
npm run init-db
|
| 174 |
-
npm run https
|
| 175 |
```
|
| 176 |
|
| 177 |
## Environment Variables
|
|
|
|
| 167 |
## Command Reference
|
| 168 |
|
| 169 |
```powershell
|
| 170 |
+
npm install
|
| 171 |
+
npm start
|
| 172 |
+
npm run dev
|
| 173 |
+
npm run init-db
|
| 174 |
+
npm run https
|
| 175 |
```
|
| 176 |
|
| 177 |
## Environment Variables
|
MOBILE-CONNECT-FIX.md
CHANGED
|
@@ -36,13 +36,13 @@ cd C:\Users\dell\Downloads\message
|
|
| 36 |
taskkill /F /IM node.exe 2>$null
|
| 37 |
Start-Sleep -Seconds 2
|
| 38 |
|
| 39 |
-
|
| 40 |
$env:SSL_PFX_PATH = (Resolve-Path ".\certs\lan-localhost.pfx").Path
|
| 41 |
$env:SSL_PFX_PASSPHRASE = "123456"
|
| 42 |
$env:PORT = "3001"
|
| 43 |
$env:HOST = "0.0.0.0"
|
| 44 |
|
| 45 |
-
|
| 46 |
npm start
|
| 47 |
```
|
| 48 |
|
|
@@ -73,17 +73,16 @@ Wait for this message to appear:
|
|
| 73 |
|
| 74 |
### Check 1: Are you on the SAME Wi-Fi?
|
| 75 |
```powershell
|
| 76 |
-
|
| 77 |
ipconfig | findstr "Wi-Fi" -A 5
|
| 78 |
|
| 79 |
-
|
| 80 |
-
# They must match!
|
| 81 |
```
|
| 82 |
|
| 83 |
### Check 2: Is server actually listening?
|
| 84 |
```powershell
|
| 85 |
netstat -ano | findstr ":3001"
|
| 86 |
-
|
| 87 |
```
|
| 88 |
|
| 89 |
### Check 3: Firewall again (run as Admin)
|
|
|
|
| 36 |
taskkill /F /IM node.exe 2>$null
|
| 37 |
Start-Sleep -Seconds 2
|
| 38 |
|
| 39 |
+
|
| 40 |
$env:SSL_PFX_PATH = (Resolve-Path ".\certs\lan-localhost.pfx").Path
|
| 41 |
$env:SSL_PFX_PASSPHRASE = "123456"
|
| 42 |
$env:PORT = "3001"
|
| 43 |
$env:HOST = "0.0.0.0"
|
| 44 |
|
| 45 |
+
|
| 46 |
npm start
|
| 47 |
```
|
| 48 |
|
|
|
|
| 73 |
|
| 74 |
### Check 1: Are you on the SAME Wi-Fi?
|
| 75 |
```powershell
|
| 76 |
+
|
| 77 |
ipconfig | findstr "Wi-Fi" -A 5
|
| 78 |
|
| 79 |
+
|
|
|
|
| 80 |
```
|
| 81 |
|
| 82 |
### Check 2: Is server actually listening?
|
| 83 |
```powershell
|
| 84 |
netstat -ano | findstr ":3001"
|
| 85 |
+
|
| 86 |
```
|
| 87 |
|
| 88 |
### Check 3: Firewall again (run as Admin)
|
QUICKSTART.sh
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
-
|
| 3 |
|
| 4 |
echo "🚀 Discord-Like Chat App - Quick Start"
|
| 5 |
echo "======================================"
|
| 6 |
echo ""
|
| 7 |
|
| 8 |
-
|
| 9 |
if ! command -v node &> /dev/null; then
|
| 10 |
echo "❌ Node.js is not installed. Please install from https://nodejs.org/"
|
| 11 |
exit 1
|
|
@@ -15,7 +15,7 @@ echo "✅ Node.js: $(node --version)"
|
|
| 15 |
echo "✅ npm: $(npm --version)"
|
| 16 |
echo ""
|
| 17 |
|
| 18 |
-
|
| 19 |
if ! command -v mongod &> /dev/null; then
|
| 20 |
echo "⚠️ MongoDB not found in PATH"
|
| 21 |
echo " Make sure MongoDB is running separately!"
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
+
|
| 3 |
|
| 4 |
echo "🚀 Discord-Like Chat App - Quick Start"
|
| 5 |
echo "======================================"
|
| 6 |
echo ""
|
| 7 |
|
| 8 |
+
|
| 9 |
if ! command -v node &> /dev/null; then
|
| 10 |
echo "❌ Node.js is not installed. Please install from https://nodejs.org/"
|
| 11 |
exit 1
|
|
|
|
| 15 |
echo "✅ npm: $(npm --version)"
|
| 16 |
echo ""
|
| 17 |
|
| 18 |
+
|
| 19 |
if ! command -v mongod &> /dev/null; then
|
| 20 |
echo "⚠️ MongoDB not found in PATH"
|
| 21 |
echo " Make sure MongoDB is running separately!"
|
README.md
CHANGED
|
@@ -164,7 +164,7 @@ New-NetFirewallRule -DisplayName "Chat App HTTPS" -Direction Inbound -LocalPort
|
|
| 164 |
cd C:\Users\dell\Downloads\message
|
| 165 |
taskkill /F /IM node.exe
|
| 166 |
|
| 167 |
-
|
| 168 |
$cert = New-SelfSignedCertificate `
|
| 169 |
-Subject "CN=localhost" `
|
| 170 |
-CertStoreLocation "Cert:\CurrentUser\My" `
|
|
@@ -339,7 +339,7 @@ const HOST = process.env.HOST || '0.0.0.0';
|
|
| 339 |
### Adjust Message History
|
| 340 |
Edit `server.js`:
|
| 341 |
```javascript
|
| 342 |
-
const MAX_HISTORY = 50;
|
| 343 |
```
|
| 344 |
|
| 345 |
## 📝 Features in Detail
|
|
|
|
| 164 |
cd C:\Users\dell\Downloads\message
|
| 165 |
taskkill /F /IM node.exe
|
| 166 |
|
| 167 |
+
|
| 168 |
$cert = New-SelfSignedCertificate `
|
| 169 |
-Subject "CN=localhost" `
|
| 170 |
-CertStoreLocation "Cert:\CurrentUser\My" `
|
|
|
|
| 339 |
### Adjust Message History
|
| 340 |
Edit `server.js`:
|
| 341 |
```javascript
|
| 342 |
+
const MAX_HISTORY = 50;
|
| 343 |
```
|
| 344 |
|
| 345 |
## 📝 Features in Detail
|
SETUP-GUIDE.md
CHANGED
|
@@ -190,7 +190,7 @@ You can use either:
|
|
| 190 |
### JWT Secret
|
| 191 |
Change the `JWT_SECRET` in `.env` to a secure random string for production:
|
| 192 |
```bash
|
| 193 |
-
|
| 194 |
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
| 195 |
```
|
| 196 |
|
|
|
|
| 190 |
### JWT Secret
|
| 191 |
Change the `JWT_SECRET` in `.env` to a secure random string for production:
|
| 192 |
```bash
|
| 193 |
+
|
| 194 |
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
| 195 |
```
|
| 196 |
|
client.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
// ==================== GLOBAL STATE ====================
|
| 2 |
let socket;
|
| 3 |
let currentUser = null;
|
| 4 |
let currentToken = null;
|
|
@@ -55,8 +54,7 @@ let themeSettings = {
|
|
| 55 |
}
|
| 56 |
};
|
| 57 |
|
| 58 |
-
|
| 59 |
-
// Always use same-origin backend so it works on Railway, custom domains, LAN IPs, and localhost.
|
| 60 |
const SERVER_URL = window.location.origin;
|
| 61 |
const API_BASE = SERVER_URL + '/api';
|
| 62 |
|
|
@@ -76,7 +74,7 @@ async function syncBuildVersionBadge() {
|
|
| 76 |
badge.title = `Running backend version: ${data.version}`;
|
| 77 |
}
|
| 78 |
} catch (error) {
|
| 79 |
-
|
| 80 |
}
|
| 81 |
}
|
| 82 |
|
|
@@ -126,7 +124,7 @@ function startIncomingCallAlert() {
|
|
| 126 |
osc.start();
|
| 127 |
osc.stop(incomingCallAudioContext.currentTime + 0.15);
|
| 128 |
} catch (error) {
|
| 129 |
-
|
| 130 |
}
|
| 131 |
}, 900);
|
| 132 |
}
|
|
@@ -142,7 +140,7 @@ function stopIncomingCallAlert() {
|
|
| 142 |
}
|
| 143 |
}
|
| 144 |
|
| 145 |
-
|
| 146 |
|
| 147 |
function toggleAuthMode() {
|
| 148 |
isRegistering = !isRegistering;
|
|
@@ -335,7 +333,7 @@ function switchAccount() {
|
|
| 335 |
setTimeout(() => location.reload(), 400);
|
| 336 |
}
|
| 337 |
|
| 338 |
-
|
| 339 |
|
| 340 |
function connectSocket() {
|
| 341 |
socket = io(SERVER_URL, {
|
|
@@ -382,12 +380,12 @@ function connectSocket() {
|
|
| 382 |
|
| 383 |
const isCurrentRoom = currentChatContext?.type === roomType && String(currentChatContext?.name) === String(roomName);
|
| 384 |
if (isCurrentRoom) {
|
| 385 |
-
|
| 386 |
if (String(payload.from) !== String(currentUser.id)) {
|
| 387 |
addMessageToChat(payload);
|
| 388 |
}
|
| 389 |
} else if (payload.from !== currentUser.id && chatSettings.notifyOnReceived) {
|
| 390 |
-
|
| 391 |
const messagePreview = payload.mediaType === 'text' ? payload.content : `${payload.mediaType} message`;
|
| 392 |
const roomLabel = roomType === 'community' ? 'Community' : 'Group';
|
| 393 |
showToastClickable(
|
|
@@ -419,7 +417,7 @@ function connectSocket() {
|
|
| 419 |
const fromUserId = payload?.fromUserId;
|
| 420 |
if (!fromUserId) return;
|
| 421 |
|
| 422 |
-
|
| 423 |
if (currentChatContext?.type === 'dm' && String(currentChatContext?.id) === String(fromUserId)) {
|
| 424 |
document.getElementById('messages-container').innerHTML = '';
|
| 425 |
refreshChatScrollbar();
|
|
@@ -439,14 +437,14 @@ function connectSocket() {
|
|
| 439 |
);
|
| 440 |
|
| 441 |
if (isCurrentChat) {
|
| 442 |
-
|
| 443 |
if (!isSender) {
|
| 444 |
addMessageToChat(data);
|
| 445 |
}
|
| 446 |
}
|
| 447 |
|
| 448 |
if (isSender && chatSettings.notifyOnSent) {
|
| 449 |
-
|
| 450 |
}
|
| 451 |
|
| 452 |
if (!isSender && chatSettings.notifyOnReceived) {
|
|
@@ -468,17 +466,17 @@ function connectSocket() {
|
|
| 468 |
});
|
| 469 |
|
| 470 |
socket.on('dm user typing', (data) => {
|
| 471 |
-
|
| 472 |
showTypingIndicator(data.username);
|
| 473 |
});
|
| 474 |
|
| 475 |
socket.on('dm user stop typing', (data) => {
|
| 476 |
-
|
| 477 |
hideTypingIndicator(data.username);
|
| 478 |
});
|
| 479 |
|
| 480 |
socket.on('community user typing', (data) => {
|
| 481 |
-
|
| 482 |
if (currentChatContext.type === 'community' && currentChatContext.name === data.community) {
|
| 483 |
showTypingIndicator(data.username);
|
| 484 |
}
|
|
@@ -491,7 +489,7 @@ function connectSocket() {
|
|
| 491 |
});
|
| 492 |
|
| 493 |
socket.on('room user typing', (data) => {
|
| 494 |
-
|
| 495 |
if ((currentChatContext.type === 'group' || currentChatContext.type === 'room') && String(currentChatContext.id) === String(data.roomId)) {
|
| 496 |
showTypingIndicator(data.username);
|
| 497 |
}
|
|
@@ -632,15 +630,14 @@ function hideTypingIndicator() {
|
|
| 632 |
}
|
| 633 |
}
|
| 634 |
|
| 635 |
-
|
| 636 |
|
| 637 |
function showMainApp() {
|
| 638 |
document.getElementById('authContainer').style.display = 'none';
|
| 639 |
document.getElementById('mainApp').classList.remove('hidden');
|
| 640 |
|
| 641 |
-
// AGGRESSIVELY close all panels on startup (especially for mobile)
|
| 642 |
try {
|
| 643 |
-
|
| 644 |
const profilePanel = document.getElementById('profilePanel');
|
| 645 |
if (profilePanel) {
|
| 646 |
profilePanel.classList.remove('show');
|
|
@@ -651,26 +648,26 @@ function showMainApp() {
|
|
| 651 |
profilePanel.style.transform = 'translateX(500px)';
|
| 652 |
}
|
| 653 |
|
| 654 |
-
|
| 655 |
const settingsPanel = document.getElementById('settingsPanel');
|
| 656 |
if (settingsPanel) {
|
| 657 |
settingsPanel.classList.remove('show');
|
| 658 |
settingsPanel.style.right = '-500px';
|
| 659 |
}
|
| 660 |
|
| 661 |
-
|
| 662 |
const mobileMembersPanel = document.getElementById('mobileMembersPanel');
|
| 663 |
if (mobileMembersPanel) {
|
| 664 |
mobileMembersPanel.classList.remove('show');
|
| 665 |
}
|
| 666 |
|
| 667 |
-
|
| 668 |
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
| 669 |
if (sidebarOverlay) {
|
| 670 |
sidebarOverlay.classList.remove('show');
|
| 671 |
}
|
| 672 |
|
| 673 |
-
|
| 674 |
const channelSidebar = document.getElementById('channelSidebar');
|
| 675 |
if (channelSidebar) {
|
| 676 |
channelSidebar.classList.remove('show');
|
|
@@ -681,13 +678,12 @@ function showMainApp() {
|
|
| 681 |
|
| 682 |
loadUserProfile();
|
| 683 |
loadShopItems();
|
| 684 |
-
|
| 685 |
-
// Check if there's a friend request in the URL
|
| 686 |
const urlParams = new URLSearchParams(window.location.search);
|
| 687 |
const addFriendId = urlParams.get('addFriend');
|
| 688 |
if (addFriendId && addFriendId !== currentUser.id) {
|
| 689 |
sendFriendRequestFromLink(addFriendId);
|
| 690 |
-
|
| 691 |
window.history.replaceState({}, document.title, window.location.pathname);
|
| 692 |
}
|
| 693 |
}
|
|
@@ -723,31 +719,31 @@ function openDM(friendId, friendName) {
|
|
| 723 |
clearPendingMediaDraft();
|
| 724 |
updatePinButtonState();
|
| 725 |
|
| 726 |
-
|
| 727 |
const isSelfChat = String(friendId) === String(currentUser.id);
|
| 728 |
const displayName = isSelfChat ? `📝 ${currentUser.username} (Notes)` : `💬 ${friendName}`;
|
| 729 |
|
| 730 |
-
|
| 731 |
const chatTitleEl = document.getElementById('chatTitle');
|
| 732 |
chatTitleEl.textContent = displayName;
|
| 733 |
chatTitleEl.onclick = null;
|
| 734 |
document.getElementById('messages-container').innerHTML = '';
|
| 735 |
refreshChatScrollbar();
|
| 736 |
|
| 737 |
-
|
| 738 |
document.getElementById('closeChatBtn').style.display = 'block';
|
| 739 |
|
| 740 |
-
|
| 741 |
loadDMMessages(friendId);
|
| 742 |
|
| 743 |
-
|
| 744 |
if (socket && socket.connected) {
|
| 745 |
socket.emit('join dm', friendId);
|
| 746 |
}
|
| 747 |
|
| 748 |
showSection('chat');
|
| 749 |
|
| 750 |
-
|
| 751 |
if (window.innerWidth <= 480) {
|
| 752 |
closeMobileSidebar();
|
| 753 |
}
|
|
@@ -760,7 +756,7 @@ async function loadDMMessages(friendId) {
|
|
| 760 |
});
|
| 761 |
const messages = await response.json();
|
| 762 |
|
| 763 |
-
|
| 764 |
document.getElementById('messages-container').innerHTML = '';
|
| 765 |
messages.forEach(msg => {
|
| 766 |
addMessageToChat({
|
|
@@ -836,7 +832,7 @@ function closeShopModal() {
|
|
| 836 |
showSection('chat');
|
| 837 |
}
|
| 838 |
|
| 839 |
-
|
| 840 |
|
| 841 |
async function loadUserProfile() {
|
| 842 |
try {
|
|
@@ -900,7 +896,7 @@ async function saveProfile() {
|
|
| 900 |
}
|
| 901 |
}
|
| 902 |
|
| 903 |
-
|
| 904 |
|
| 905 |
function handleMessageKeypress(event) {
|
| 906 |
if (event.key === 'Enter' && chatSettings.enterToSend) {
|
|
@@ -980,7 +976,7 @@ function renderPendingMediaDraft() {
|
|
| 980 |
itemWrapper.appendChild(file);
|
| 981 |
}
|
| 982 |
|
| 983 |
-
|
| 984 |
const removeBtn = document.createElement('button');
|
| 985 |
removeBtn.innerHTML = '✕';
|
| 986 |
removeBtn.style.position = 'absolute';
|
|
@@ -1149,7 +1145,7 @@ async function sendMessage() {
|
|
| 1149 |
}
|
| 1150 |
|
| 1151 |
try {
|
| 1152 |
-
|
| 1153 |
if (content) {
|
| 1154 |
updateUserStats('messagesCount', 1);
|
| 1155 |
updateUserStats('totalChars', content.length);
|
|
@@ -1196,7 +1192,7 @@ async function sendMessage() {
|
|
| 1196 |
return;
|
| 1197 |
}
|
| 1198 |
|
| 1199 |
-
|
| 1200 |
const optimisticMessage = {
|
| 1201 |
id: Date.now(),
|
| 1202 |
from: currentUser.id,
|
|
@@ -1209,7 +1205,7 @@ async function sendMessage() {
|
|
| 1209 |
};
|
| 1210 |
addMessageToChat(optimisticMessage);
|
| 1211 |
|
| 1212 |
-
|
| 1213 |
socket.emit('send dm', {
|
| 1214 |
toUserId: targetId,
|
| 1215 |
content: content,
|
|
@@ -1274,18 +1270,18 @@ function addMessageToChat(data) {
|
|
| 1274 |
text.style.fontSize = '16px';
|
| 1275 |
}
|
| 1276 |
|
| 1277 |
-
|
| 1278 |
if (data.mediaType === 'sticker') {
|
| 1279 |
text.style.fontSize = '96px';
|
| 1280 |
text.style.lineHeight = '1';
|
| 1281 |
text.textContent = data.content;
|
| 1282 |
} else {
|
| 1283 |
-
|
| 1284 |
const emojiRegex = /^[\p{Emoji}\p{Emoji_Component}\s]+$/u;
|
| 1285 |
const isOnlyEmojis = data.content && emojiRegex.test(data.content.trim());
|
| 1286 |
|
| 1287 |
if (isOnlyEmojis && data.content.trim().length <= 20) {
|
| 1288 |
-
|
| 1289 |
text.style.fontSize = '64px';
|
| 1290 |
text.style.lineHeight = '1.2';
|
| 1291 |
}
|
|
@@ -1297,14 +1293,14 @@ function addMessageToChat(data) {
|
|
| 1297 |
content.appendChild(text);
|
| 1298 |
content.appendChild(time);
|
| 1299 |
|
| 1300 |
-
|
| 1301 |
messageDiv.oncontextmenu = (e) => {
|
| 1302 |
e.preventDefault();
|
| 1303 |
showMessageContextMenu(e, messageDiv, data);
|
| 1304 |
return false;
|
| 1305 |
};
|
| 1306 |
|
| 1307 |
-
|
| 1308 |
const actions = document.createElement('div');
|
| 1309 |
actions.className = 'message-actions';
|
| 1310 |
|
|
@@ -1361,7 +1357,7 @@ function addMessageToChat(data) {
|
|
| 1361 |
|
| 1362 |
content.appendChild(actions);
|
| 1363 |
|
| 1364 |
-
|
| 1365 |
const reactionsDiv = document.createElement('div');
|
| 1366 |
reactionsDiv.className = 'message-reactions';
|
| 1367 |
reactionsDiv.dataset.messageId = messageDiv.dataset.messageId;
|
|
@@ -1418,7 +1414,7 @@ function addMessageToChat(data) {
|
|
| 1418 |
audio.src = data.mediaUrl;
|
| 1419 |
audio.controls = true;
|
| 1420 |
audio.style.maxWidth = '300px';
|
| 1421 |
-
|
| 1422 |
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && audio.setSinkId) {
|
| 1423 |
audio.setSinkId(audioSettings.speakerId).catch(err => {
|
| 1424 |
console.warn('Could not set audio output device:', err);
|
|
@@ -1433,7 +1429,7 @@ function addMessageToChat(data) {
|
|
| 1433 |
messageDiv.appendChild(content);
|
| 1434 |
container.appendChild(messageDiv);
|
| 1435 |
|
| 1436 |
-
|
| 1437 |
setTimeout(() => {
|
| 1438 |
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
| 1439 |
refreshChatScrollbar();
|
|
@@ -1476,7 +1472,7 @@ function updateMembersList(users) {
|
|
| 1476 |
item.style.cursor = 'pointer';
|
| 1477 |
item.title = `Click to view ${user.username}'s profile`;
|
| 1478 |
|
| 1479 |
-
|
| 1480 |
const avatar = document.createElement('div');
|
| 1481 |
avatar.className = 'member-avatar';
|
| 1482 |
if (user.avatar) {
|
|
@@ -1493,11 +1489,11 @@ function updateMembersList(users) {
|
|
| 1493 |
const name = document.createElement('span');
|
| 1494 |
name.textContent = user.username || 'Unknown';
|
| 1495 |
|
| 1496 |
-
|
| 1497 |
const userId = user.id || user._id || user.userId;
|
| 1498 |
const username = user.username || 'User';
|
| 1499 |
|
| 1500 |
-
|
| 1501 |
item.onclick = () => {
|
| 1502 |
if (!userId) {
|
| 1503 |
showToast('❌ User ID not available', 'error');
|
|
@@ -1516,14 +1512,14 @@ function updateMembersList(users) {
|
|
| 1516 |
membersList.appendChild(item);
|
| 1517 |
});
|
| 1518 |
|
| 1519 |
-
|
| 1520 |
const mobileMembersList = document.getElementById('mobileMembersList');
|
| 1521 |
if (mobileMembersList) {
|
| 1522 |
mobileMembersList.innerHTML = membersList.innerHTML;
|
| 1523 |
}
|
| 1524 |
}
|
| 1525 |
|
| 1526 |
-
|
| 1527 |
|
| 1528 |
async function handleFileUpload(event) {
|
| 1529 |
const files = Array.from(event.target.files || []);
|
|
@@ -1542,8 +1538,7 @@ async function handleFileUpload(event) {
|
|
| 1542 |
let sentCount = 0;
|
| 1543 |
|
| 1544 |
for (const file of files) {
|
| 1545 |
-
|
| 1546 |
-
// This removes the extra "press send" step and feels instant like WhatsApp.
|
| 1547 |
const payload = await new Promise((resolve) => {
|
| 1548 |
const reader = new FileReader();
|
| 1549 |
reader.onload = (e) => {
|
|
@@ -1574,7 +1569,7 @@ async function handleFileUpload(event) {
|
|
| 1574 |
showToast(`Sent ${sentCount} media file(s)`, 'success');
|
| 1575 |
}
|
| 1576 |
|
| 1577 |
-
|
| 1578 |
event.target.value = '';
|
| 1579 |
}
|
| 1580 |
|
|
@@ -1610,11 +1605,11 @@ async function uploadProfileImage(event) {
|
|
| 1610 |
};
|
| 1611 |
reader.readAsDataURL(file);
|
| 1612 |
|
| 1613 |
-
|
| 1614 |
event.target.value = '';
|
| 1615 |
}
|
| 1616 |
|
| 1617 |
-
|
| 1618 |
|
| 1619 |
async function loadFriendsForDM() {
|
| 1620 |
try {
|
|
@@ -1626,7 +1621,7 @@ async function loadFriendsForDM() {
|
|
| 1626 |
const channelsList = document.getElementById('channelsList');
|
| 1627 |
channelsList.innerHTML = '';
|
| 1628 |
|
| 1629 |
-
|
| 1630 |
const pinnedFriends = [];
|
| 1631 |
const unpinnedFriends = [];
|
| 1632 |
|
|
@@ -1639,7 +1634,7 @@ async function loadFriendsForDM() {
|
|
| 1639 |
}
|
| 1640 |
});
|
| 1641 |
|
| 1642 |
-
|
| 1643 |
if (pinnedFriends.length > 0) {
|
| 1644 |
const pinnedTitle = document.createElement('div');
|
| 1645 |
pinnedTitle.className = 'channel-section-title';
|
|
@@ -1651,7 +1646,6 @@ async function loadFriendsForDM() {
|
|
| 1651 |
});
|
| 1652 |
}
|
| 1653 |
|
| 1654 |
-
// Show communities if any
|
| 1655 |
if (joinedCommunities.length > 0) {
|
| 1656 |
const communitiesTitle = document.createElement('div');
|
| 1657 |
communitiesTitle.className = 'channel-section-title';
|
|
@@ -1680,7 +1674,7 @@ async function loadFriendsForDM() {
|
|
| 1680 |
});
|
| 1681 |
}
|
| 1682 |
|
| 1683 |
-
|
| 1684 |
if (joinedGroups.length > 0) {
|
| 1685 |
const groupsTitle = document.createElement('div');
|
| 1686 |
groupsTitle.className = 'channel-section-title';
|
|
@@ -1714,7 +1708,7 @@ async function loadFriendsForDM() {
|
|
| 1714 |
directTitle.textContent = 'DIRECT MESSAGES';
|
| 1715 |
channelsList.appendChild(directTitle);
|
| 1716 |
|
| 1717 |
-
|
| 1718 |
const selfItem = document.createElement('div');
|
| 1719 |
selfItem.className = 'channel-item';
|
| 1720 |
|
|
@@ -1762,7 +1756,7 @@ function createFriendChannelItem(friend, container, isPinned) {
|
|
| 1762 |
item.classList.add('pinned');
|
| 1763 |
}
|
| 1764 |
|
| 1765 |
-
|
| 1766 |
const avatar = document.createElement('div');
|
| 1767 |
avatar.className = 'channel-avatar';
|
| 1768 |
if (friend.avatar) {
|
|
@@ -1773,7 +1767,7 @@ function createFriendChannelItem(friend, container, isPinned) {
|
|
| 1773 |
avatar.textContent = friend.username.charAt(0).toUpperCase();
|
| 1774 |
}
|
| 1775 |
|
| 1776 |
-
|
| 1777 |
const text = document.createElement('span');
|
| 1778 |
const status = friend.status === 'online' ? '🟢' : '⚫';
|
| 1779 |
text.textContent = `${status} ${friend.username}`;
|
|
@@ -1791,7 +1785,7 @@ function createFriendChannelItem(friend, container, isPinned) {
|
|
| 1791 |
item.style.cursor = 'pointer';
|
| 1792 |
item.onclick = () => openDM(friend.id, friend.username);
|
| 1793 |
|
| 1794 |
-
|
| 1795 |
item.oncontextmenu = (e) => {
|
| 1796 |
e.preventDefault();
|
| 1797 |
showFriendContextMenu(e, friend.id, friend.username);
|
|
@@ -1925,7 +1919,7 @@ async function sendFriendRequestFromLink(friendId) {
|
|
| 1925 |
}
|
| 1926 |
}
|
| 1927 |
|
| 1928 |
-
|
| 1929 |
|
| 1930 |
async function loadShopItems() {
|
| 1931 |
try {
|
|
@@ -1998,7 +1992,7 @@ async function buyItem(itemId) {
|
|
| 1998 |
}
|
| 1999 |
}
|
| 2000 |
|
| 2001 |
-
|
| 2002 |
|
| 2003 |
async function loadInventory() {
|
| 2004 |
try {
|
|
@@ -2043,7 +2037,7 @@ function applyShopItem(itemId, category) {
|
|
| 2043 |
},
|
| 2044 |
'Badges': () => {
|
| 2045 |
showToast('🏆 Badge equipped! It now appears next to your name.', 'success');
|
| 2046 |
-
|
| 2047 |
},
|
| 2048 |
'Themes': () => {
|
| 2049 |
const colors = ['#ff6b9d', '#c44569', '#4a69bd', '#6a89cc', '#60a3bc', '#78e08f', '#f6b93b', '#e55039'];
|
|
@@ -2053,7 +2047,7 @@ function applyShopItem(itemId, category) {
|
|
| 2053 |
showToast('🎨 Theme applied! Your accent color has changed.', 'success');
|
| 2054 |
},
|
| 2055 |
'Effects': () => {
|
| 2056 |
-
|
| 2057 |
const style = document.createElement('style');
|
| 2058 |
style.id = 'message-glow-effect';
|
| 2059 |
style.textContent = `
|
|
@@ -2119,7 +2113,6 @@ function applyShopItem(itemId, category) {
|
|
| 2119 |
}
|
| 2120 |
|
| 2121 |
|
| 2122 |
-
// ==================== TOAST NOTIFICATIONS ====================
|
| 2123 |
|
| 2124 |
function showToast(message, type = 'success') {
|
| 2125 |
const container = document.getElementById('toastContainer');
|
|
@@ -2144,7 +2137,7 @@ function showToast(message, type = 'success') {
|
|
| 2144 |
}, 3000);
|
| 2145 |
}
|
| 2146 |
|
| 2147 |
-
|
| 2148 |
|
| 2149 |
function openSettings() {
|
| 2150 |
document.getElementById('settingsPanel').classList.add('show');
|
|
@@ -2165,13 +2158,13 @@ function closeSettings() {
|
|
| 2165 |
}
|
| 2166 |
|
| 2167 |
function switchSettingsTab(tab) {
|
| 2168 |
-
|
| 2169 |
document.querySelectorAll('.settings-nav-item').forEach(item => {
|
| 2170 |
item.classList.remove('active');
|
| 2171 |
});
|
| 2172 |
event.target.classList.add('active');
|
| 2173 |
|
| 2174 |
-
|
| 2175 |
document.querySelectorAll('.settings-section').forEach(section => {
|
| 2176 |
section.classList.remove('active');
|
| 2177 |
});
|
|
@@ -2211,7 +2204,7 @@ async function updateUsername() {
|
|
| 2211 |
}
|
| 2212 |
}
|
| 2213 |
|
| 2214 |
-
|
| 2215 |
|
| 2216 |
function changeTheme(theme) {
|
| 2217 |
document.documentElement.setAttribute('data-theme', theme);
|
|
@@ -2228,7 +2221,7 @@ function loadTheme() {
|
|
| 2228 |
}
|
| 2229 |
}
|
| 2230 |
|
| 2231 |
-
|
| 2232 |
|
| 2233 |
function closeCurrentChat() {
|
| 2234 |
if (activeRoomSubscription && socket && socket.connected) {
|
|
@@ -2318,7 +2311,7 @@ function sendRoomMessage(payload) {
|
|
| 2318 |
return;
|
| 2319 |
}
|
| 2320 |
|
| 2321 |
-
|
| 2322 |
const optimisticMessage = {
|
| 2323 |
id: Date.now(),
|
| 2324 |
from: currentUser.id,
|
|
@@ -2357,7 +2350,7 @@ function ensureChatTarget() {
|
|
| 2357 |
return null;
|
| 2358 |
}
|
| 2359 |
|
| 2360 |
-
|
| 2361 |
|
| 2362 |
function requestNotificationPermission() {
|
| 2363 |
if ('Notification' in window && Notification.permission === 'default') {
|
|
@@ -2458,14 +2451,14 @@ function openCommunitiesModal() {
|
|
| 2458 |
const modal = document.getElementById('communitiesModal');
|
| 2459 |
const list = document.getElementById('communitiesList');
|
| 2460 |
|
| 2461 |
-
|
| 2462 |
const communities = [
|
| 2463 |
-
{ name: 'Gaming',
|
| 2464 |
-
{ name: 'Coding',
|
| 2465 |
-
{ name: 'Music',
|
| 2466 |
-
{ name: 'Art',
|
| 2467 |
-
{ name: 'Sports',
|
| 2468 |
-
{ name: 'Movies',
|
| 2469 |
];
|
| 2470 |
|
| 2471 |
list.innerHTML = communities.map(c => {
|
|
@@ -2475,7 +2468,7 @@ function openCommunitiesModal() {
|
|
| 2475 |
<div class="friend-item" style="flex-direction: column; align-items: flex-start; padding: 16px;">
|
| 2476 |
<div style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
|
| 2477 |
<div>
|
| 2478 |
-
<span class="friend-name" style="font-size: 16px;">${profile.icon || c.
|
| 2479 |
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">${c.description}</div>
|
| 2480 |
</div>
|
| 2481 |
<div style="display:flex; gap:6px;">
|
|
@@ -2502,7 +2495,7 @@ function openGroupsModal() {
|
|
| 2502 |
const modal = document.getElementById('groupsModal');
|
| 2503 |
const list = document.getElementById('groupsList');
|
| 2504 |
|
| 2505 |
-
|
| 2506 |
const groups = [
|
| 2507 |
{ name: 'Study Group', emoji: '📚', description: 'Study together and share notes' },
|
| 2508 |
{ name: 'Project Team', emoji: '💼', description: 'Collaborate on projects' },
|
|
@@ -2571,7 +2564,7 @@ function openCommunityChat(name) {
|
|
| 2571 |
document.getElementById('closeChatBtn').style.display = 'block';
|
| 2572 |
showSection('chat');
|
| 2573 |
|
| 2574 |
-
|
| 2575 |
if (window.innerWidth <= 480) {
|
| 2576 |
closeMobileSidebar();
|
| 2577 |
}
|
|
@@ -2593,7 +2586,7 @@ function openGroupChat(name) {
|
|
| 2593 |
document.getElementById('closeChatBtn').style.display = 'block';
|
| 2594 |
showSection('chat');
|
| 2595 |
|
| 2596 |
-
|
| 2597 |
if (window.innerWidth <= 480) {
|
| 2598 |
closeMobileSidebar();
|
| 2599 |
}
|
|
@@ -2636,7 +2629,7 @@ function joinGroup(name) {
|
|
| 2636 |
showToast(`Joined ${name} group!`);
|
| 2637 |
}
|
| 2638 |
|
| 2639 |
-
|
| 2640 |
|
| 2641 |
const stickers = [
|
| 2642 |
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃',
|
|
@@ -2684,7 +2677,7 @@ function loadPickerContent() {
|
|
| 2684 |
content.appendChild(item);
|
| 2685 |
});
|
| 2686 |
} else if (currentPickerTab === 'gifs') {
|
| 2687 |
-
|
| 2688 |
const gifs = [
|
| 2689 |
'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif',
|
| 2690 |
'https://media.giphy.com/media/l0MYt5jPR6QX5pnqM/giphy.gif',
|
|
@@ -2723,7 +2716,7 @@ function loadPickerContent() {
|
|
| 2723 |
}
|
| 2724 |
|
| 2725 |
function sendSticker(sticker) {
|
| 2726 |
-
|
| 2727 |
const messageInput = document.getElementById('messageInput');
|
| 2728 |
if (messageInput) {
|
| 2729 |
const currentText = messageInput.value;
|
|
@@ -2745,7 +2738,7 @@ function sendGif(gifUrl) {
|
|
| 2745 |
document.getElementById('pickerPanel').classList.remove('show');
|
| 2746 |
}
|
| 2747 |
|
| 2748 |
-
|
| 2749 |
|
| 2750 |
let mediaRecorder;
|
| 2751 |
let audioChunks = [];
|
|
@@ -2786,14 +2779,14 @@ async function toggleRecording() {
|
|
| 2786 |
|
| 2787 |
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
|
| 2788 |
try {
|
| 2789 |
-
|
| 2790 |
const stream = await getSelectedMicrophone();
|
| 2791 |
const mimeType = getSupportedAudioMimeType();
|
| 2792 |
recordedAudioMimeType = mimeType || 'audio/webm';
|
| 2793 |
mediaRecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
|
| 2794 |
audioChunks = [];
|
| 2795 |
|
| 2796 |
-
|
| 2797 |
if (previewContainer) previewContainer.style.display = 'none';
|
| 2798 |
if (previewAudio) {
|
| 2799 |
previewAudio.pause();
|
|
@@ -2813,22 +2806,22 @@ async function toggleRecording() {
|
|
| 2813 |
showToast('🎧 Recording ready! CLICK PLAY BUTTON to hear it before sending!', 'success');
|
| 2814 |
if (sendBtn) sendBtn.disabled = false;
|
| 2815 |
|
| 2816 |
-
|
| 2817 |
if (previewContainer) previewContainer.style.display = 'block';
|
| 2818 |
if (previewAudio) {
|
| 2819 |
previewAudio.src = reader.result;
|
| 2820 |
previewAudio.load();
|
| 2821 |
-
|
| 2822 |
previewAudio.volume = 1.0;
|
| 2823 |
|
| 2824 |
-
|
| 2825 |
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && previewAudio.setSinkId) {
|
| 2826 |
previewAudio.setSinkId(audioSettings.speakerId).catch(err => {
|
| 2827 |
console.warn('Could not set preview audio output:', err);
|
| 2828 |
});
|
| 2829 |
}
|
| 2830 |
|
| 2831 |
-
|
| 2832 |
previewAudio.play().catch(err => {
|
| 2833 |
console.log('Autoplay blocked, user must click play:', err);
|
| 2834 |
showToast('⚠️ Click the PLAY button ▶️ below to hear your recording!', 'warning');
|
|
@@ -2926,7 +2919,7 @@ function cancelRecording() {
|
|
| 2926 |
window.recordedAudio = null;
|
| 2927 |
recordingSeconds = 0;
|
| 2928 |
|
| 2929 |
-
|
| 2930 |
if (previewContainer) previewContainer.style.display = 'none';
|
| 2931 |
if (previewAudio) {
|
| 2932 |
previewAudio.pause();
|
|
@@ -2936,23 +2929,22 @@ function cancelRecording() {
|
|
| 2936 |
if (sendBtn) sendBtn.disabled = true;
|
| 2937 |
}
|
| 2938 |
|
| 2939 |
-
|
| 2940 |
|
| 2941 |
function handleChatWheel(event) {
|
| 2942 |
const container = document.getElementById('messages-container');
|
| 2943 |
if (!container) return;
|
| 2944 |
|
| 2945 |
-
// Allow smooth wheel scrolling with accelerated movement
|
| 2946 |
event.preventDefault();
|
| 2947 |
const delta = event.deltaY;
|
| 2948 |
container.scrollBy({ top: delta * 0.8, behavior: 'auto' });
|
| 2949 |
}
|
| 2950 |
|
| 2951 |
-
|
| 2952 |
|
| 2953 |
async function refreshAudioDevices() {
|
| 2954 |
try {
|
| 2955 |
-
|
| 2956 |
await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
|
| 2957 |
.then(stream => stream.getTracks().forEach(track => track.stop()))
|
| 2958 |
.catch(() => console.log('Microphone permission needed'));
|
|
@@ -3003,7 +2995,7 @@ function saveAudioSettings() {
|
|
| 3003 |
|
| 3004 |
localStorage.setItem('audioSettings', JSON.stringify(audioSettings));
|
| 3005 |
|
| 3006 |
-
|
| 3007 |
applySpeakerToAudioElements();
|
| 3008 |
|
| 3009 |
showToast('Audio settings saved', 'success');
|
|
@@ -3013,7 +3005,7 @@ function applySpeakerToAudioElements() {
|
|
| 3013 |
const speakerId = audioSettings.speakerId;
|
| 3014 |
if (!speakerId || speakerId === 'default') return;
|
| 3015 |
|
| 3016 |
-
|
| 3017 |
document.querySelectorAll('audio').forEach(audio => {
|
| 3018 |
if (audio.setSinkId) {
|
| 3019 |
audio.setSinkId(speakerId).catch(err => {
|
|
@@ -3022,7 +3014,7 @@ function applySpeakerToAudioElements() {
|
|
| 3022 |
}
|
| 3023 |
});
|
| 3024 |
|
| 3025 |
-
|
| 3026 |
const voicePreview = document.getElementById('voicePreview');
|
| 3027 |
if (voicePreview && voicePreview.setSinkId) {
|
| 3028 |
voicePreview.setSinkId(speakerId).catch(err => {
|
|
@@ -3052,7 +3044,7 @@ async function getSelectedMicrophone() {
|
|
| 3052 |
});
|
| 3053 |
}
|
| 3054 |
|
| 3055 |
-
|
| 3056 |
|
| 3057 |
function changeTheme(theme) {
|
| 3058 |
themeSettings.mode = theme;
|
|
@@ -3101,10 +3093,10 @@ function applySimpleCustomColors() {
|
|
| 3101 |
root.style.setProperty('--primary-color', icon);
|
| 3102 |
root.style.setProperty('--secondary-color', icon);
|
| 3103 |
root.style.setProperty('--icon-color', icon);
|
| 3104 |
-
|
| 3105 |
root.style.setProperty('--ui-text-color', '#ffffff');
|
| 3106 |
|
| 3107 |
-
|
| 3108 |
const simpleTheme = { bg, text, icon, mode: 'custom' };
|
| 3109 |
localStorage.setItem('simpleTheme', JSON.stringify(simpleTheme));
|
| 3110 |
}
|
|
@@ -3200,17 +3192,17 @@ function loadThemeSettings() {
|
|
| 3200 |
}
|
| 3201 |
}
|
| 3202 |
|
| 3203 |
-
// ==================== MESSAGE ACTIONS (EDIT, DELETE, REACT) ====================
|
| 3204 |
|
| 3205 |
-
|
|
|
|
| 3206 |
|
| 3207 |
function showQuickReactions(messageId, messageDiv) {
|
| 3208 |
const quickReactions = ['👍', '❤️', '😂', '😮', '😢', '🙏'];
|
| 3209 |
|
| 3210 |
-
|
| 3211 |
document.querySelectorAll('.reaction-picker').forEach(p => p.remove());
|
| 3212 |
|
| 3213 |
-
|
| 3214 |
const picker = document.createElement('div');
|
| 3215 |
picker.className = 'reaction-picker';
|
| 3216 |
picker.style.position = 'fixed';
|
|
@@ -3223,7 +3215,7 @@ function showQuickReactions(messageId, messageDiv) {
|
|
| 3223 |
picker.style.zIndex = '9999';
|
| 3224 |
picker.style.boxShadow = '0 8px 24px rgba(0,0,0,0.6)';
|
| 3225 |
|
| 3226 |
-
|
| 3227 |
const rect = messageDiv.getBoundingClientRect();
|
| 3228 |
picker.style.left = Math.max(20, rect.left) + 'px';
|
| 3229 |
picker.style.top = Math.max(20, rect.top - 60) + 'px';
|
|
@@ -3256,7 +3248,7 @@ function showQuickReactions(messageId, messageDiv) {
|
|
| 3256 |
|
| 3257 |
document.body.appendChild(picker);
|
| 3258 |
|
| 3259 |
-
|
| 3260 |
const closePickerHandler = (e) => {
|
| 3261 |
if (!picker.contains(e.target)) {
|
| 3262 |
picker.remove();
|
|
@@ -3282,13 +3274,13 @@ function addReaction(messageId, emoji) {
|
|
| 3282 |
const userIndex = messageReactions[messageId][emoji].indexOf(userId);
|
| 3283 |
|
| 3284 |
if (userIndex > -1) {
|
| 3285 |
-
|
| 3286 |
messageReactions[messageId][emoji].splice(userIndex, 1);
|
| 3287 |
if (messageReactions[messageId][emoji].length === 0) {
|
| 3288 |
delete messageReactions[messageId][emoji];
|
| 3289 |
}
|
| 3290 |
} else {
|
| 3291 |
-
|
| 3292 |
messageReactions[messageId][emoji].push(userId);
|
| 3293 |
updateUserStats('reactionsGiven', 1);
|
| 3294 |
}
|
|
@@ -3299,7 +3291,7 @@ function addReaction(messageId, emoji) {
|
|
| 3299 |
}
|
| 3300 |
|
| 3301 |
function showMessageContextMenu(e, messageDiv, data) {
|
| 3302 |
-
|
| 3303 |
document.querySelectorAll('.message-context-menu').forEach(m => m.remove());
|
| 3304 |
|
| 3305 |
const menu = document.createElement('div');
|
|
@@ -3337,7 +3329,7 @@ function showMessageContextMenu(e, messageDiv, data) {
|
|
| 3337 |
btn.style.fontWeight = '500';
|
| 3338 |
btn.style.borderBottom = '1px solid var(--border-color)';
|
| 3339 |
|
| 3340 |
-
|
| 3341 |
btn.onmouseover = () => {
|
| 3342 |
if (item.hoverClass === 'danger-hover') {
|
| 3343 |
btn.style.background = 'var(--danger)';
|
|
@@ -3365,18 +3357,18 @@ function showMessageContextMenu(e, messageDiv, data) {
|
|
| 3365 |
menu.appendChild(btn);
|
| 3366 |
});
|
| 3367 |
|
| 3368 |
-
|
| 3369 |
const lastBtn = menu.lastElementChild;
|
| 3370 |
if (lastBtn) lastBtn.style.borderBottom = 'none';
|
| 3371 |
|
| 3372 |
document.body.appendChild(menu);
|
| 3373 |
|
| 3374 |
-
|
| 3375 |
document.addEventListener('click', () => menu.remove(), { once: true });
|
| 3376 |
}
|
| 3377 |
|
| 3378 |
function showFriendContextMenu(e, friendId, friendName) {
|
| 3379 |
-
|
| 3380 |
document.querySelectorAll('.message-context-menu').forEach(m => m.remove());
|
| 3381 |
|
| 3382 |
const menu = document.createElement('div');
|
|
@@ -3408,7 +3400,7 @@ function showFriendContextMenu(e, friendId, friendName) {
|
|
| 3408 |
btn.style.fontWeight = '500';
|
| 3409 |
btn.style.borderBottom = '1px solid var(--border-color)';
|
| 3410 |
|
| 3411 |
-
|
| 3412 |
btn.onmouseover = () => {
|
| 3413 |
if (item.hoverClass === 'danger-hover') {
|
| 3414 |
btn.style.background = 'var(--danger)';
|
|
@@ -3436,13 +3428,13 @@ function showFriendContextMenu(e, friendId, friendName) {
|
|
| 3436 |
menu.appendChild(btn);
|
| 3437 |
});
|
| 3438 |
|
| 3439 |
-
|
| 3440 |
const lastBtn = menu.lastElementChild;
|
| 3441 |
if (lastBtn) lastBtn.style.borderBottom = 'none';
|
| 3442 |
|
| 3443 |
document.body.appendChild(menu);
|
| 3444 |
|
| 3445 |
-
|
| 3446 |
document.addEventListener('click', () => menu.remove(), { once: true });
|
| 3447 |
}
|
| 3448 |
|
|
@@ -3498,19 +3490,19 @@ function copyMessageText(messageId) {
|
|
| 3498 |
} else if (imgElement || vidElement || audioElement) {
|
| 3499 |
const url = imgElement?.src || vidElement?.src || audioElement?.src || '';
|
| 3500 |
if (url) {
|
| 3501 |
-
|
| 3502 |
if (imgElement) {
|
| 3503 |
addToMediaPreview('image', url);
|
| 3504 |
} else if (vidElement) {
|
| 3505 |
addToMediaPreview('video', url);
|
| 3506 |
}
|
| 3507 |
|
| 3508 |
-
|
| 3509 |
navigator.clipboard.writeText(url).then(() => {
|
| 3510 |
const mediaType = imgElement ? 'Image' : vidElement ? 'Video' : 'Audio';
|
| 3511 |
showToast(`✅ ${mediaType} copied! View in preview panel.`, 'success');
|
| 3512 |
|
| 3513 |
-
|
| 3514 |
if (copiedMediaItems.length > 0) {
|
| 3515 |
setTimeout(() => openMediaPreview(), 500);
|
| 3516 |
}
|
|
@@ -3542,8 +3534,7 @@ function editMessage(messageId, textElement) {
|
|
| 3542 |
|
| 3543 |
showToast('✅ Message edited', 'success');
|
| 3544 |
|
| 3545 |
-
|
| 3546 |
-
// socket.emit('edit message', { messageId, newText });
|
| 3547 |
}
|
| 3548 |
});
|
| 3549 |
}
|
|
@@ -3556,24 +3547,23 @@ function deleteMessage(messageId, messageDiv) {
|
|
| 3556 |
true
|
| 3557 |
).then(confirmed => {
|
| 3558 |
if (confirmed) {
|
| 3559 |
-
|
| 3560 |
messageDiv.style.transition = 'all 0.3s ease-out';
|
| 3561 |
messageDiv.style.opacity = '0';
|
| 3562 |
messageDiv.style.transform = 'translateX(-100%)';
|
| 3563 |
|
| 3564 |
-
|
| 3565 |
setTimeout(() => {
|
| 3566 |
messageDiv.remove();
|
| 3567 |
showToast('✅ Message deleted', 'success');
|
| 3568 |
}, 300);
|
| 3569 |
|
| 3570 |
-
|
| 3571 |
-
// socket.emit('delete message', { messageId });
|
| 3572 |
}
|
| 3573 |
});
|
| 3574 |
}
|
| 3575 |
|
| 3576 |
-
|
| 3577 |
|
| 3578 |
function createNewGroup() {
|
| 3579 |
customPrompt('Enter a name for your new group:', '👥 Create Group', 'My Awesome Group', '', '👥').then(groupName => {
|
|
@@ -3596,7 +3586,7 @@ function createNewGroup() {
|
|
| 3596 |
|
| 3597 |
const ids = memberIds.split(',').map(id => id.trim()).filter(Boolean);
|
| 3598 |
showToast(`✅ Group "${groupName}" created with ${ids.length} members!`, 'success');
|
| 3599 |
-
|
| 3600 |
});
|
| 3601 |
});
|
| 3602 |
}
|
|
@@ -3629,7 +3619,7 @@ function changeGroupPhoto() {
|
|
| 3629 |
input.click();
|
| 3630 |
}
|
| 3631 |
|
| 3632 |
-
|
| 3633 |
|
| 3634 |
function startVideoCall() {
|
| 3635 |
if (!currentChatContext || currentChatContext.type === 'none' || !currentChatContext.id) {
|
|
@@ -3646,11 +3636,11 @@ function startVideoCall() {
|
|
| 3646 |
const targetId = currentChatContext.id;
|
| 3647 |
const callType = 'Video Call';
|
| 3648 |
|
| 3649 |
-
|
| 3650 |
openVideoCallModal(targetName, targetId, callType);
|
| 3651 |
}
|
| 3652 |
|
| 3653 |
-
|
| 3654 |
|
| 3655 |
let archivedChats = JSON.parse(localStorage.getItem('archivedChats') || '[]');
|
| 3656 |
|
|
@@ -3667,14 +3657,14 @@ function toggleArchiveChat() {
|
|
| 3667 |
const archiveIndex = archivedChats.findIndex(c => c.id === chatId && c.type === chatType);
|
| 3668 |
|
| 3669 |
if (archiveIndex > -1) {
|
| 3670 |
-
|
| 3671 |
archivedChats.splice(archiveIndex, 1);
|
| 3672 |
showToast(`${chatName} unarchived`, 'success');
|
| 3673 |
} else {
|
| 3674 |
-
|
| 3675 |
archivedChats.push({ id: chatId, name: chatName, type: chatType });
|
| 3676 |
showToast(`${chatName} archived`, 'success');
|
| 3677 |
-
|
| 3678 |
if (chatType === 'dm') {
|
| 3679 |
const friendIndex = friends.findIndex(f => f.id === chatId);
|
| 3680 |
if (friendIndex > -1) {
|
|
@@ -3686,7 +3676,7 @@ function toggleArchiveChat() {
|
|
| 3686 |
}
|
| 3687 |
|
| 3688 |
localStorage.setItem('archivedChats', JSON.stringify(archivedChats));
|
| 3689 |
-
loadFriendsForDM();
|
| 3690 |
}
|
| 3691 |
|
| 3692 |
function showArchivedChats() {
|
|
@@ -3746,11 +3736,11 @@ function unarchiveChat(type, id) {
|
|
| 3746 |
showToast(`${chat.name} unarchived`, 'success');
|
| 3747 |
loadFriendsForDM();
|
| 3748 |
closeCustomDialog();
|
| 3749 |
-
showArchivedChats();
|
| 3750 |
}
|
| 3751 |
}
|
| 3752 |
|
| 3753 |
-
|
| 3754 |
|
| 3755 |
let userStats = JSON.parse(localStorage.getItem('userStats') || '{"messagesCount": 0, "totalChars": 0, "reactionsGiven": 0, "archiveCount": 0, "quizCorrect": 0}');
|
| 3756 |
|
|
@@ -3882,9 +3872,9 @@ function resetUserStats() {
|
|
| 3882 |
});
|
| 3883 |
}
|
| 3884 |
|
| 3885 |
-
|
| 3886 |
function showMobileSection(section) {
|
| 3887 |
-
|
| 3888 |
if (window.innerWidth <= 768) {
|
| 3889 |
closeMobileSidebar();
|
| 3890 |
closeSettings();
|
|
@@ -3893,7 +3883,7 @@ function showMobileSection(section) {
|
|
| 3893 |
closeGroupsModal();
|
| 3894 |
}
|
| 3895 |
|
| 3896 |
-
|
| 3897 |
document.querySelectorAll('.mobile-nav-item').forEach(item => {
|
| 3898 |
if (item.getAttribute('data-section') === section) {
|
| 3899 |
item.classList.add('active');
|
|
@@ -3902,7 +3892,7 @@ function showMobileSection(section) {
|
|
| 3902 |
}
|
| 3903 |
});
|
| 3904 |
|
| 3905 |
-
|
| 3906 |
switch(section) {
|
| 3907 |
case 'chats':
|
| 3908 |
toggleChannelSidebar();
|
|
@@ -3986,7 +3976,7 @@ function updateMobileMembersList() {
|
|
| 3986 |
const desktopList = document.getElementById('membersList');
|
| 3987 |
if (!mobileList || !desktopList) return;
|
| 3988 |
|
| 3989 |
-
|
| 3990 |
mobileList.innerHTML = desktopList.innerHTML;
|
| 3991 |
}
|
| 3992 |
|
|
@@ -3994,7 +3984,7 @@ function getCommunityProfile(name) {
|
|
| 3994 |
if (!communityProfiles[name]) {
|
| 3995 |
communityProfiles[name] = {
|
| 3996 |
name,
|
| 3997 |
-
icon: '
|
| 3998 |
image: '',
|
| 3999 |
members: [
|
| 4000 |
{
|
|
@@ -4117,7 +4107,7 @@ function setCommunityIcon(name, icon) {
|
|
| 4117 |
if (currentChatContext.type === 'community' && currentChatContext.name === name) {
|
| 4118 |
document.getElementById('chatTitle').textContent = `${profile.icon} Community: ${name}`;
|
| 4119 |
}
|
| 4120 |
-
loadFriendsForDM();
|
| 4121 |
showToast('Community icon updated! 🎉', 'success');
|
| 4122 |
}
|
| 4123 |
|
|
@@ -4145,7 +4135,7 @@ function changeCommunityImagePrompt(name) {
|
|
| 4145 |
communityProfiles[name] = profile;
|
| 4146 |
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
|
| 4147 |
|
| 4148 |
-
|
| 4149 |
if (currentChatContext?.type === 'community' && currentChatContext?.name === name) {
|
| 4150 |
const profileAvatar = document.getElementById('profileAvatar');
|
| 4151 |
if (profileAvatar) {
|
|
@@ -4158,7 +4148,7 @@ function changeCommunityImagePrompt(name) {
|
|
| 4158 |
if (profileName) {
|
| 4159 |
profileName.textContent = name;
|
| 4160 |
}
|
| 4161 |
-
|
| 4162 |
setTimeout(() => {
|
| 4163 |
const avatarDiv = document.getElementById('profileAvatar');
|
| 4164 |
if (avatarDiv && !avatarDiv.style.backgroundImage) {
|
|
@@ -4191,7 +4181,7 @@ function changeGroupImagePrompt(name) {
|
|
| 4191 |
groupProfiles[name] = profile;
|
| 4192 |
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
|
| 4193 |
|
| 4194 |
-
|
| 4195 |
if (currentChatContext?.type === 'group' && currentChatContext?.name === name) {
|
| 4196 |
const profileAvatar = document.getElementById('profileAvatar');
|
| 4197 |
if (profileAvatar) {
|
|
@@ -4204,7 +4194,7 @@ function changeGroupImagePrompt(name) {
|
|
| 4204 |
if (profileName) {
|
| 4205 |
profileName.textContent = name;
|
| 4206 |
}
|
| 4207 |
-
|
| 4208 |
setTimeout(() => {
|
| 4209 |
const avatarDiv = document.getElementById('profileAvatar');
|
| 4210 |
if (avatarDiv && !avatarDiv.style.backgroundImage) {
|
|
@@ -4222,7 +4212,7 @@ function changeGroupImagePrompt(name) {
|
|
| 4222 |
input.click();
|
| 4223 |
}
|
| 4224 |
|
| 4225 |
-
|
| 4226 |
function toggleChannelSidebarEnhanced() {
|
| 4227 |
const sidebar = document.getElementById('channelSidebar');
|
| 4228 |
const overlay = document.getElementById('sidebarOverlay');
|
|
@@ -4231,7 +4221,7 @@ function toggleChannelSidebarEnhanced() {
|
|
| 4231 |
overlay?.classList.toggle('show');
|
| 4232 |
}
|
| 4233 |
|
| 4234 |
-
|
| 4235 |
function togglePinCurrentChat() {
|
| 4236 |
if (!currentChatContext || currentChatContext.type === 'none') {
|
| 4237 |
showToast('No chat selected', 'warning');
|
|
@@ -4242,18 +4232,18 @@ function togglePinCurrentChat() {
|
|
| 4242 |
const index = pinnedChats.indexOf(chatId);
|
| 4243 |
|
| 4244 |
if (index > -1) {
|
| 4245 |
-
|
| 4246 |
pinnedChats.splice(index, 1);
|
| 4247 |
showToast('Chat unpinned', 'success');
|
| 4248 |
} else {
|
| 4249 |
-
|
| 4250 |
pinnedChats.push(chatId);
|
| 4251 |
showToast('Chat pinned to top', 'success');
|
| 4252 |
}
|
| 4253 |
|
| 4254 |
localStorage.setItem('pinnedChats', JSON.stringify(pinnedChats));
|
| 4255 |
updatePinButtonState();
|
| 4256 |
-
loadFriendsForDM();
|
| 4257 |
}
|
| 4258 |
|
| 4259 |
function updatePinButtonState() {
|
|
@@ -4272,7 +4262,7 @@ function isChatPinned(chatType, chatId) {
|
|
| 4272 |
return pinnedChats.includes(chatKey);
|
| 4273 |
}
|
| 4274 |
|
| 4275 |
-
|
| 4276 |
function showToastClickable(message, type = 'success', chatContext = null) {
|
| 4277 |
const container = document.getElementById('toastContainer');
|
| 4278 |
if (!container) return;
|
|
@@ -4299,7 +4289,7 @@ function showToastClickable(message, type = 'success', chatContext = null) {
|
|
| 4299 |
if (chatContext) {
|
| 4300 |
toast.style.cursor = 'pointer';
|
| 4301 |
toast.addEventListener('click', () => {
|
| 4302 |
-
|
| 4303 |
if (chatContext.type === 'dm') {
|
| 4304 |
openDM(chatContext.friendId, chatContext.friendName);
|
| 4305 |
} else if (chatContext.type === 'community') {
|
|
@@ -4308,10 +4298,10 @@ function showToastClickable(message, type = 'success', chatContext = null) {
|
|
| 4308 |
openRoomChat('group', chatContext.name);
|
| 4309 |
}
|
| 4310 |
|
| 4311 |
-
|
| 4312 |
toast.remove();
|
| 4313 |
|
| 4314 |
-
|
| 4315 |
if (window.innerWidth <= 480) {
|
| 4316 |
closeMobileSidebar();
|
| 4317 |
}
|
|
@@ -4325,7 +4315,7 @@ function showToastClickable(message, type = 'success', chatContext = null) {
|
|
| 4325 |
}, 5000);
|
| 4326 |
}
|
| 4327 |
|
| 4328 |
-
|
| 4329 |
|
| 4330 |
window.addEventListener('DOMContentLoaded', () => {
|
| 4331 |
loadTheme();
|
|
@@ -4341,14 +4331,14 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
| 4341 |
document.getElementById('autoplayGifs')?.addEventListener('change', saveChatSettings);
|
| 4342 |
document.getElementById('compactMode')?.addEventListener('change', saveAppearanceSettings);
|
| 4343 |
|
| 4344 |
-
|
| 4345 |
let typingTimeout = null;
|
| 4346 |
const typingInput = document.getElementById('messageInput');
|
| 4347 |
if (typingInput) {
|
| 4348 |
typingInput.addEventListener('input', () => {
|
| 4349 |
if (!chatSettings.showTypingIndicator) return;
|
| 4350 |
|
| 4351 |
-
|
| 4352 |
if (currentChatContext.type === 'community' && currentChatContext.name) {
|
| 4353 |
socket?.emit('community typing', { community: currentChatContext.name, username: currentUser?.username });
|
| 4354 |
clearTimeout(typingTimeout);
|
|
@@ -4391,13 +4381,13 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
| 4391 |
picker.classList.remove('show');
|
| 4392 |
});
|
| 4393 |
|
| 4394 |
-
|
| 4395 |
loadAudioSettings();
|
| 4396 |
loadThemeSettings();
|
| 4397 |
refreshAudioDevices();
|
| 4398 |
updateStatsDisplay();
|
| 4399 |
|
| 4400 |
-
|
| 4401 |
syncBuildVersionBadge();
|
| 4402 |
|
| 4403 |
const token = localStorage.getItem('token');
|
|
@@ -4413,7 +4403,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
| 4413 |
document.getElementById('mainApp').classList.add('hidden');
|
| 4414 |
}
|
| 4415 |
});
|
| 4416 |
-
|
| 4417 |
|
| 4418 |
function customAlert(message, title = 'Notice', icon = 'ℹ️') {
|
| 4419 |
return new Promise((resolve) => {
|
|
@@ -4502,7 +4492,7 @@ function resolveCustomDialog(value) {
|
|
| 4502 |
if (window.customDialogResolve) window.customDialogResolve(value);
|
| 4503 |
}
|
| 4504 |
|
| 4505 |
-
|
| 4506 |
|
| 4507 |
function showQRCodeModal() {
|
| 4508 |
const modal = document.getElementById('qrModal');
|
|
@@ -4514,10 +4504,10 @@ function showQRCodeModal() {
|
|
| 4514 |
return;
|
| 4515 |
}
|
| 4516 |
|
| 4517 |
-
|
| 4518 |
qrContainer.innerHTML = '';
|
| 4519 |
|
| 4520 |
-
|
| 4521 |
const qrDiv = document.createElement('div');
|
| 4522 |
qrDiv.id = 'qrCodeCanvas';
|
| 4523 |
qrDiv.style.backgroundColor = '#ffffff';
|
|
@@ -4526,13 +4516,13 @@ function showQRCodeModal() {
|
|
| 4526 |
qrDiv.style.display = 'inline-block';
|
| 4527 |
qrContainer.appendChild(qrDiv);
|
| 4528 |
|
| 4529 |
-
|
| 4530 |
userIdSpan.textContent = currentUser.id || currentUser._id || 'Unknown';
|
| 4531 |
|
| 4532 |
-
|
| 4533 |
const friendRequestData = `FRIEND_REQUEST:${currentUser.id || currentUser._id}:${currentUser.username || 'User'}`;
|
| 4534 |
|
| 4535 |
-
|
| 4536 |
if (typeof QRCode !== 'undefined') {
|
| 4537 |
try {
|
| 4538 |
new QRCode(qrDiv, {
|
|
@@ -4550,7 +4540,7 @@ function showQRCodeModal() {
|
|
| 4550 |
}
|
| 4551 |
} else {
|
| 4552 |
console.warn('⚠️ QRCode library not loaded, using fallback');
|
| 4553 |
-
|
| 4554 |
generateQRCodeFallback(qrDiv, friendRequestData);
|
| 4555 |
}
|
| 4556 |
|
|
@@ -4560,7 +4550,7 @@ function showQRCodeModal() {
|
|
| 4560 |
function generateQRCodeFallback(container, data) {
|
| 4561 |
console.log('📱 Using QR Code API fallback');
|
| 4562 |
|
| 4563 |
-
|
| 4564 |
const img = new Image();
|
| 4565 |
img.style.width = '200px';
|
| 4566 |
img.style.height = '200px';
|
|
@@ -4576,7 +4566,7 @@ function generateQRCodeFallback(container, data) {
|
|
| 4576 |
|
| 4577 |
img.onerror = () => {
|
| 4578 |
console.error('❌ QR Code API failed');
|
| 4579 |
-
|
| 4580 |
container.innerHTML = `
|
| 4581 |
<div style="width: 200px; height: 200px; background: white; border: 2px solid #000; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 20px; box-sizing: border-box;">
|
| 4582 |
<div style="font-size: 24px; margin-bottom: 10px;">📱</div>
|
|
@@ -4607,13 +4597,13 @@ function scanQRCodeForFriend() {
|
|
| 4607 |
let friendId = null;
|
| 4608 |
let friendName = 'User';
|
| 4609 |
|
| 4610 |
-
|
| 4611 |
if (qrData.startsWith('FRIEND_REQUEST:')) {
|
| 4612 |
const parts = qrData.split(':');
|
| 4613 |
friendId = parts[1];
|
| 4614 |
friendName = parts[2] || 'User';
|
| 4615 |
} else {
|
| 4616 |
-
|
| 4617 |
friendId = qrData.trim();
|
| 4618 |
}
|
| 4619 |
|
|
@@ -4622,7 +4612,7 @@ function scanQRCodeForFriend() {
|
|
| 4622 |
return;
|
| 4623 |
}
|
| 4624 |
|
| 4625 |
-
|
| 4626 |
customConfirm(
|
| 4627 |
`Send friend request to <strong>${friendName}</strong> (ID: ${friendId})?`,
|
| 4628 |
'👤 Friend Request',
|
|
@@ -4641,7 +4631,7 @@ function scanQRCodeForFriend() {
|
|
| 4641 |
});
|
| 4642 |
}
|
| 4643 |
|
| 4644 |
-
|
| 4645 |
|
| 4646 |
function openVideoCallModal(targetName, targetId, callType) {
|
| 4647 |
targetName = targetName || currentChatContext.name || 'User';
|
|
@@ -4674,7 +4664,7 @@ function openVideoCallModal(targetName, targetId, callType) {
|
|
| 4674 |
|
| 4675 |
customAlert(html, '📞 Video Call', '📞');
|
| 4676 |
|
| 4677 |
-
|
| 4678 |
if (socket && socket.connected && targetId) {
|
| 4679 |
socket.emit('start video call', {
|
| 4680 |
targetId: targetId,
|
|
@@ -4710,7 +4700,7 @@ function handleIncomingVideoCall(payload) {
|
|
| 4710 |
callId: activeCallState.callId,
|
| 4711 |
toId: activeCallState.peerId
|
| 4712 |
});
|
| 4713 |
-
|
| 4714 |
} else {
|
| 4715 |
stopIncomingCallAlert();
|
| 4716 |
socket.emit('reject video call', {
|
|
@@ -4746,7 +4736,7 @@ async function ensureLocalCallStream() {
|
|
| 4746 |
} catch (error) {
|
| 4747 |
console.error('Media access error:', error);
|
| 4748 |
|
| 4749 |
-
|
| 4750 |
try {
|
| 4751 |
localCallStream = await navigator.mediaDevices.getUserMedia({
|
| 4752 |
audio: true,
|
|
@@ -4772,7 +4762,7 @@ function createPeerConnection(peerId) {
|
|
| 4772 |
{ urls: 'stun:stun.l.google.com:19302' },
|
| 4773 |
{ urls: 'stun:stun1.l.google.com:19302' },
|
| 4774 |
{ urls: 'stun:stun2.l.google.com:19302' },
|
| 4775 |
-
|
| 4776 |
{
|
| 4777 |
urls: 'turn:openrelay.metered.ca:80',
|
| 4778 |
username: 'openrelayproject',
|
|
@@ -4814,18 +4804,18 @@ function createPeerConnection(peerId) {
|
|
| 4814 |
if (remoteVideo && event.streams[0]) {
|
| 4815 |
remoteVideo.srcObject = event.streams[0];
|
| 4816 |
|
| 4817 |
-
|
| 4818 |
remoteVideo.muted = false;
|
| 4819 |
remoteVideo.volume = 1.0;
|
| 4820 |
|
| 4821 |
-
|
| 4822 |
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && typeof remoteVideo.setSinkId === 'function') {
|
| 4823 |
remoteVideo.setSinkId(audioSettings.speakerId).catch((sinkError) => {
|
| 4824 |
console.warn('Could not apply selected speaker for call:', sinkError);
|
| 4825 |
});
|
| 4826 |
}
|
| 4827 |
|
| 4828 |
-
|
| 4829 |
const playPromise = remoteVideo.play();
|
| 4830 |
if (playPromise !== undefined) {
|
| 4831 |
playPromise.then(() => {
|
|
@@ -4833,7 +4823,7 @@ function createPeerConnection(peerId) {
|
|
| 4833 |
showToast('📞 Connected! Audio should be working now', 'success');
|
| 4834 |
}).catch((error) => {
|
| 4835 |
console.error('Remote video play error:', error);
|
| 4836 |
-
|
| 4837 |
setTimeout(() => {
|
| 4838 |
remoteVideo.play().catch(e => console.error('Retry play failed:', e));
|
| 4839 |
}, 500);
|
|
@@ -4845,13 +4835,13 @@ function createPeerConnection(peerId) {
|
|
| 4845 |
peerConnection.onconnectionstatechange = () => {
|
| 4846 |
console.log('📡 Connection state:', peerConnection.connectionState);
|
| 4847 |
|
| 4848 |
-
|
| 4849 |
if (window.userIntentionallEndedCall) {
|
| 4850 |
console.log('User ended call intentionally, skipping error messages');
|
| 4851 |
return;
|
| 4852 |
}
|
| 4853 |
|
| 4854 |
-
|
| 4855 |
if (window.callDisconnectTimeout) {
|
| 4856 |
clearTimeout(window.callDisconnectTimeout);
|
| 4857 |
window.callDisconnectTimeout = null;
|
|
@@ -4859,22 +4849,22 @@ function createPeerConnection(peerId) {
|
|
| 4859 |
|
| 4860 |
if (peerConnection.connectionState === 'connecting') {
|
| 4861 |
showToast('📞 Connecting call...', 'info');
|
| 4862 |
-
|
| 4863 |
if (!callTimerInterval) {
|
| 4864 |
startCallTimer();
|
| 4865 |
}
|
| 4866 |
} else if (peerConnection.connectionState === 'disconnected') {
|
| 4867 |
showToast('⚠️ Connection unstable, trying to reconnect...', 'warning');
|
| 4868 |
-
|
| 4869 |
window.callDisconnectTimeout = setTimeout(() => {
|
| 4870 |
if (peerConnection && peerConnection.connectionState === 'disconnected' && !window.userIntentionallEndedCall) {
|
| 4871 |
showToast('Call ended due to connection loss', 'error');
|
| 4872 |
cleanupCallSession(false);
|
| 4873 |
}
|
| 4874 |
-
}, 15000);
|
| 4875 |
} else if (peerConnection.connectionState === 'connected') {
|
| 4876 |
showToast('📞 Call connected!', 'success');
|
| 4877 |
-
|
| 4878 |
if (!callTimerInterval) {
|
| 4879 |
startCallTimer();
|
| 4880 |
}
|
|
@@ -4888,7 +4878,7 @@ function createPeerConnection(peerId) {
|
|
| 4888 |
}
|
| 4889 |
}, 12000);
|
| 4890 |
} else if (peerConnection.connectionState === 'closed') {
|
| 4891 |
-
|
| 4892 |
cleanupCallSession(false);
|
| 4893 |
}
|
| 4894 |
};
|
|
@@ -4898,7 +4888,7 @@ function createPeerConnection(peerId) {
|
|
| 4898 |
console.log('ICE connection state:', peerConnection.iceConnectionState);
|
| 4899 |
|
| 4900 |
if (peerConnection.iceConnectionState === 'failed') {
|
| 4901 |
-
|
| 4902 |
peerConnection.restartIce?.();
|
| 4903 |
}
|
| 4904 |
};
|
|
@@ -4955,10 +4945,10 @@ async function startCallSession(peerName, peerId, isCaller) {
|
|
| 4955 |
}
|
| 4956 |
|
| 4957 |
function endVideoCall() {
|
| 4958 |
-
|
| 4959 |
window.userIntentionallEndedCall = true;
|
| 4960 |
|
| 4961 |
-
|
| 4962 |
if (window.callDisconnectTimeout) {
|
| 4963 |
clearTimeout(window.callDisconnectTimeout);
|
| 4964 |
window.callDisconnectTimeout = null;
|
|
@@ -4995,10 +4985,10 @@ function toggleLocalMute() {
|
|
| 4995 |
}
|
| 4996 |
|
| 4997 |
function cleanupCallSession(keepDialogOpen = false) {
|
| 4998 |
-
|
| 4999 |
window.userIntentionallEndedCall = false;
|
| 5000 |
|
| 5001 |
-
|
| 5002 |
if (window.callDisconnectTimeout) {
|
| 5003 |
clearTimeout(window.callDisconnectTimeout);
|
| 5004 |
window.callDisconnectTimeout = null;
|
|
@@ -5029,13 +5019,13 @@ function cleanupCallSession(keepDialogOpen = false) {
|
|
| 5029 |
}
|
| 5030 |
|
| 5031 |
function visitUserProfile(userId, userName = 'User') {
|
| 5032 |
-
|
| 5033 |
if (!userId || userId === 'undefined') {
|
| 5034 |
showToast('❌ Invalid user ID', 'error');
|
| 5035 |
return;
|
| 5036 |
}
|
| 5037 |
|
| 5038 |
-
|
| 5039 |
const html = `
|
| 5040 |
<div style="text-align: center; padding: 20px 0;">
|
| 5041 |
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, var(--primary-color), #4752c4); border-radius: 50%; margin: 0 auto 20px; display: flex; align-items: center; justify-content: center; font-size: 48px; color: white;">
|
|
@@ -5102,7 +5092,7 @@ function sendFriendRequest(userId, userName = 'User') {
|
|
| 5102 |
});
|
| 5103 |
}
|
| 5104 |
|
| 5105 |
-
|
| 5106 |
|
| 5107 |
let copiedMediaItems = [];
|
| 5108 |
|
|
@@ -5144,7 +5134,7 @@ function closeMediaPreview() {
|
|
| 5144 |
document.getElementById('mediaPreviewPanel').classList.remove('show');
|
| 5145 |
}
|
| 5146 |
|
| 5147 |
-
|
| 5148 |
|
| 5149 |
function filterFriends(searchText) {
|
| 5150 |
const channelsList = document.getElementById('channelsList');
|
|
@@ -5162,7 +5152,7 @@ function filterFriends(searchText) {
|
|
| 5162 |
});
|
| 5163 |
}
|
| 5164 |
|
| 5165 |
-
|
| 5166 |
|
| 5167 |
let archivedMessages = JSON.parse(localStorage.getItem('archivedMessages') || '[]');
|
| 5168 |
|
|
@@ -5183,7 +5173,7 @@ function archiveSingleMessage(messageId, messageDiv) {
|
|
| 5183 |
archivedMessages.push(messageData);
|
| 5184 |
localStorage.setItem('archivedMessages', JSON.stringify(archivedMessages));
|
| 5185 |
|
| 5186 |
-
|
| 5187 |
messageDiv.style.transition = 'all 0.4s ease';
|
| 5188 |
messageDiv.style.opacity = '0.5';
|
| 5189 |
messageDiv.style.transform = 'translateX(-20px)';
|
|
|
|
|
|
|
| 1 |
let socket;
|
| 2 |
let currentUser = null;
|
| 3 |
let currentToken = null;
|
|
|
|
| 54 |
}
|
| 55 |
};
|
| 56 |
|
| 57 |
+
|
|
|
|
| 58 |
const SERVER_URL = window.location.origin;
|
| 59 |
const API_BASE = SERVER_URL + '/api';
|
| 60 |
|
|
|
|
| 74 |
badge.title = `Running backend version: ${data.version}`;
|
| 75 |
}
|
| 76 |
} catch (error) {
|
| 77 |
+
|
| 78 |
}
|
| 79 |
}
|
| 80 |
|
|
|
|
| 124 |
osc.start();
|
| 125 |
osc.stop(incomingCallAudioContext.currentTime + 0.15);
|
| 126 |
} catch (error) {
|
| 127 |
+
|
| 128 |
}
|
| 129 |
}, 900);
|
| 130 |
}
|
|
|
|
| 140 |
}
|
| 141 |
}
|
| 142 |
|
| 143 |
+
|
| 144 |
|
| 145 |
function toggleAuthMode() {
|
| 146 |
isRegistering = !isRegistering;
|
|
|
|
| 333 |
setTimeout(() => location.reload(), 400);
|
| 334 |
}
|
| 335 |
|
| 336 |
+
|
| 337 |
|
| 338 |
function connectSocket() {
|
| 339 |
socket = io(SERVER_URL, {
|
|
|
|
| 380 |
|
| 381 |
const isCurrentRoom = currentChatContext?.type === roomType && String(currentChatContext?.name) === String(roomName);
|
| 382 |
if (isCurrentRoom) {
|
| 383 |
+
|
| 384 |
if (String(payload.from) !== String(currentUser.id)) {
|
| 385 |
addMessageToChat(payload);
|
| 386 |
}
|
| 387 |
} else if (payload.from !== currentUser.id && chatSettings.notifyOnReceived) {
|
| 388 |
+
|
| 389 |
const messagePreview = payload.mediaType === 'text' ? payload.content : `${payload.mediaType} message`;
|
| 390 |
const roomLabel = roomType === 'community' ? 'Community' : 'Group';
|
| 391 |
showToastClickable(
|
|
|
|
| 417 |
const fromUserId = payload?.fromUserId;
|
| 418 |
if (!fromUserId) return;
|
| 419 |
|
| 420 |
+
|
| 421 |
if (currentChatContext?.type === 'dm' && String(currentChatContext?.id) === String(fromUserId)) {
|
| 422 |
document.getElementById('messages-container').innerHTML = '';
|
| 423 |
refreshChatScrollbar();
|
|
|
|
| 437 |
);
|
| 438 |
|
| 439 |
if (isCurrentChat) {
|
| 440 |
+
|
| 441 |
if (!isSender) {
|
| 442 |
addMessageToChat(data);
|
| 443 |
}
|
| 444 |
}
|
| 445 |
|
| 446 |
if (isSender && chatSettings.notifyOnSent) {
|
| 447 |
+
|
| 448 |
}
|
| 449 |
|
| 450 |
if (!isSender && chatSettings.notifyOnReceived) {
|
|
|
|
| 466 |
});
|
| 467 |
|
| 468 |
socket.on('dm user typing', (data) => {
|
| 469 |
+
|
| 470 |
showTypingIndicator(data.username);
|
| 471 |
});
|
| 472 |
|
| 473 |
socket.on('dm user stop typing', (data) => {
|
| 474 |
+
|
| 475 |
hideTypingIndicator(data.username);
|
| 476 |
});
|
| 477 |
|
| 478 |
socket.on('community user typing', (data) => {
|
| 479 |
+
|
| 480 |
if (currentChatContext.type === 'community' && currentChatContext.name === data.community) {
|
| 481 |
showTypingIndicator(data.username);
|
| 482 |
}
|
|
|
|
| 489 |
});
|
| 490 |
|
| 491 |
socket.on('room user typing', (data) => {
|
| 492 |
+
|
| 493 |
if ((currentChatContext.type === 'group' || currentChatContext.type === 'room') && String(currentChatContext.id) === String(data.roomId)) {
|
| 494 |
showTypingIndicator(data.username);
|
| 495 |
}
|
|
|
|
| 630 |
}
|
| 631 |
}
|
| 632 |
|
| 633 |
+
|
| 634 |
|
| 635 |
function showMainApp() {
|
| 636 |
document.getElementById('authContainer').style.display = 'none';
|
| 637 |
document.getElementById('mainApp').classList.remove('hidden');
|
| 638 |
|
|
|
|
| 639 |
try {
|
| 640 |
+
|
| 641 |
const profilePanel = document.getElementById('profilePanel');
|
| 642 |
if (profilePanel) {
|
| 643 |
profilePanel.classList.remove('show');
|
|
|
|
| 648 |
profilePanel.style.transform = 'translateX(500px)';
|
| 649 |
}
|
| 650 |
|
| 651 |
+
|
| 652 |
const settingsPanel = document.getElementById('settingsPanel');
|
| 653 |
if (settingsPanel) {
|
| 654 |
settingsPanel.classList.remove('show');
|
| 655 |
settingsPanel.style.right = '-500px';
|
| 656 |
}
|
| 657 |
|
| 658 |
+
|
| 659 |
const mobileMembersPanel = document.getElementById('mobileMembersPanel');
|
| 660 |
if (mobileMembersPanel) {
|
| 661 |
mobileMembersPanel.classList.remove('show');
|
| 662 |
}
|
| 663 |
|
| 664 |
+
|
| 665 |
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
| 666 |
if (sidebarOverlay) {
|
| 667 |
sidebarOverlay.classList.remove('show');
|
| 668 |
}
|
| 669 |
|
| 670 |
+
|
| 671 |
const channelSidebar = document.getElementById('channelSidebar');
|
| 672 |
if (channelSidebar) {
|
| 673 |
channelSidebar.classList.remove('show');
|
|
|
|
| 678 |
|
| 679 |
loadUserProfile();
|
| 680 |
loadShopItems();
|
| 681 |
+
|
|
|
|
| 682 |
const urlParams = new URLSearchParams(window.location.search);
|
| 683 |
const addFriendId = urlParams.get('addFriend');
|
| 684 |
if (addFriendId && addFriendId !== currentUser.id) {
|
| 685 |
sendFriendRequestFromLink(addFriendId);
|
| 686 |
+
|
| 687 |
window.history.replaceState({}, document.title, window.location.pathname);
|
| 688 |
}
|
| 689 |
}
|
|
|
|
| 719 |
clearPendingMediaDraft();
|
| 720 |
updatePinButtonState();
|
| 721 |
|
| 722 |
+
|
| 723 |
const isSelfChat = String(friendId) === String(currentUser.id);
|
| 724 |
const displayName = isSelfChat ? `📝 ${currentUser.username} (Notes)` : `💬 ${friendName}`;
|
| 725 |
|
| 726 |
+
|
| 727 |
const chatTitleEl = document.getElementById('chatTitle');
|
| 728 |
chatTitleEl.textContent = displayName;
|
| 729 |
chatTitleEl.onclick = null;
|
| 730 |
document.getElementById('messages-container').innerHTML = '';
|
| 731 |
refreshChatScrollbar();
|
| 732 |
|
| 733 |
+
|
| 734 |
document.getElementById('closeChatBtn').style.display = 'block';
|
| 735 |
|
| 736 |
+
|
| 737 |
loadDMMessages(friendId);
|
| 738 |
|
| 739 |
+
|
| 740 |
if (socket && socket.connected) {
|
| 741 |
socket.emit('join dm', friendId);
|
| 742 |
}
|
| 743 |
|
| 744 |
showSection('chat');
|
| 745 |
|
| 746 |
+
|
| 747 |
if (window.innerWidth <= 480) {
|
| 748 |
closeMobileSidebar();
|
| 749 |
}
|
|
|
|
| 756 |
});
|
| 757 |
const messages = await response.json();
|
| 758 |
|
| 759 |
+
|
| 760 |
document.getElementById('messages-container').innerHTML = '';
|
| 761 |
messages.forEach(msg => {
|
| 762 |
addMessageToChat({
|
|
|
|
| 832 |
showSection('chat');
|
| 833 |
}
|
| 834 |
|
| 835 |
+
|
| 836 |
|
| 837 |
async function loadUserProfile() {
|
| 838 |
try {
|
|
|
|
| 896 |
}
|
| 897 |
}
|
| 898 |
|
| 899 |
+
|
| 900 |
|
| 901 |
function handleMessageKeypress(event) {
|
| 902 |
if (event.key === 'Enter' && chatSettings.enterToSend) {
|
|
|
|
| 976 |
itemWrapper.appendChild(file);
|
| 977 |
}
|
| 978 |
|
| 979 |
+
|
| 980 |
const removeBtn = document.createElement('button');
|
| 981 |
removeBtn.innerHTML = '✕';
|
| 982 |
removeBtn.style.position = 'absolute';
|
|
|
|
| 1145 |
}
|
| 1146 |
|
| 1147 |
try {
|
| 1148 |
+
|
| 1149 |
if (content) {
|
| 1150 |
updateUserStats('messagesCount', 1);
|
| 1151 |
updateUserStats('totalChars', content.length);
|
|
|
|
| 1192 |
return;
|
| 1193 |
}
|
| 1194 |
|
| 1195 |
+
|
| 1196 |
const optimisticMessage = {
|
| 1197 |
id: Date.now(),
|
| 1198 |
from: currentUser.id,
|
|
|
|
| 1205 |
};
|
| 1206 |
addMessageToChat(optimisticMessage);
|
| 1207 |
|
| 1208 |
+
|
| 1209 |
socket.emit('send dm', {
|
| 1210 |
toUserId: targetId,
|
| 1211 |
content: content,
|
|
|
|
| 1270 |
text.style.fontSize = '16px';
|
| 1271 |
}
|
| 1272 |
|
| 1273 |
+
|
| 1274 |
if (data.mediaType === 'sticker') {
|
| 1275 |
text.style.fontSize = '96px';
|
| 1276 |
text.style.lineHeight = '1';
|
| 1277 |
text.textContent = data.content;
|
| 1278 |
} else {
|
| 1279 |
+
|
| 1280 |
const emojiRegex = /^[\p{Emoji}\p{Emoji_Component}\s]+$/u;
|
| 1281 |
const isOnlyEmojis = data.content && emojiRegex.test(data.content.trim());
|
| 1282 |
|
| 1283 |
if (isOnlyEmojis && data.content.trim().length <= 20) {
|
| 1284 |
+
|
| 1285 |
text.style.fontSize = '64px';
|
| 1286 |
text.style.lineHeight = '1.2';
|
| 1287 |
}
|
|
|
|
| 1293 |
content.appendChild(text);
|
| 1294 |
content.appendChild(time);
|
| 1295 |
|
| 1296 |
+
|
| 1297 |
messageDiv.oncontextmenu = (e) => {
|
| 1298 |
e.preventDefault();
|
| 1299 |
showMessageContextMenu(e, messageDiv, data);
|
| 1300 |
return false;
|
| 1301 |
};
|
| 1302 |
|
| 1303 |
+
|
| 1304 |
const actions = document.createElement('div');
|
| 1305 |
actions.className = 'message-actions';
|
| 1306 |
|
|
|
|
| 1357 |
|
| 1358 |
content.appendChild(actions);
|
| 1359 |
|
| 1360 |
+
|
| 1361 |
const reactionsDiv = document.createElement('div');
|
| 1362 |
reactionsDiv.className = 'message-reactions';
|
| 1363 |
reactionsDiv.dataset.messageId = messageDiv.dataset.messageId;
|
|
|
|
| 1414 |
audio.src = data.mediaUrl;
|
| 1415 |
audio.controls = true;
|
| 1416 |
audio.style.maxWidth = '300px';
|
| 1417 |
+
|
| 1418 |
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && audio.setSinkId) {
|
| 1419 |
audio.setSinkId(audioSettings.speakerId).catch(err => {
|
| 1420 |
console.warn('Could not set audio output device:', err);
|
|
|
|
| 1429 |
messageDiv.appendChild(content);
|
| 1430 |
container.appendChild(messageDiv);
|
| 1431 |
|
| 1432 |
+
|
| 1433 |
setTimeout(() => {
|
| 1434 |
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
| 1435 |
refreshChatScrollbar();
|
|
|
|
| 1472 |
item.style.cursor = 'pointer';
|
| 1473 |
item.title = `Click to view ${user.username}'s profile`;
|
| 1474 |
|
| 1475 |
+
|
| 1476 |
const avatar = document.createElement('div');
|
| 1477 |
avatar.className = 'member-avatar';
|
| 1478 |
if (user.avatar) {
|
|
|
|
| 1489 |
const name = document.createElement('span');
|
| 1490 |
name.textContent = user.username || 'Unknown';
|
| 1491 |
|
| 1492 |
+
|
| 1493 |
const userId = user.id || user._id || user.userId;
|
| 1494 |
const username = user.username || 'User';
|
| 1495 |
|
| 1496 |
+
|
| 1497 |
item.onclick = () => {
|
| 1498 |
if (!userId) {
|
| 1499 |
showToast('❌ User ID not available', 'error');
|
|
|
|
| 1512 |
membersList.appendChild(item);
|
| 1513 |
});
|
| 1514 |
|
| 1515 |
+
|
| 1516 |
const mobileMembersList = document.getElementById('mobileMembersList');
|
| 1517 |
if (mobileMembersList) {
|
| 1518 |
mobileMembersList.innerHTML = membersList.innerHTML;
|
| 1519 |
}
|
| 1520 |
}
|
| 1521 |
|
| 1522 |
+
|
| 1523 |
|
| 1524 |
async function handleFileUpload(event) {
|
| 1525 |
const files = Array.from(event.target.files || []);
|
|
|
|
| 1538 |
let sentCount = 0;
|
| 1539 |
|
| 1540 |
for (const file of files) {
|
| 1541 |
+
|
|
|
|
| 1542 |
const payload = await new Promise((resolve) => {
|
| 1543 |
const reader = new FileReader();
|
| 1544 |
reader.onload = (e) => {
|
|
|
|
| 1569 |
showToast(`Sent ${sentCount} media file(s)`, 'success');
|
| 1570 |
}
|
| 1571 |
|
| 1572 |
+
|
| 1573 |
event.target.value = '';
|
| 1574 |
}
|
| 1575 |
|
|
|
|
| 1605 |
};
|
| 1606 |
reader.readAsDataURL(file);
|
| 1607 |
|
| 1608 |
+
|
| 1609 |
event.target.value = '';
|
| 1610 |
}
|
| 1611 |
|
| 1612 |
+
|
| 1613 |
|
| 1614 |
async function loadFriendsForDM() {
|
| 1615 |
try {
|
|
|
|
| 1621 |
const channelsList = document.getElementById('channelsList');
|
| 1622 |
channelsList.innerHTML = '';
|
| 1623 |
|
| 1624 |
+
|
| 1625 |
const pinnedFriends = [];
|
| 1626 |
const unpinnedFriends = [];
|
| 1627 |
|
|
|
|
| 1634 |
}
|
| 1635 |
});
|
| 1636 |
|
| 1637 |
+
|
| 1638 |
if (pinnedFriends.length > 0) {
|
| 1639 |
const pinnedTitle = document.createElement('div');
|
| 1640 |
pinnedTitle.className = 'channel-section-title';
|
|
|
|
| 1646 |
});
|
| 1647 |
}
|
| 1648 |
|
|
|
|
| 1649 |
if (joinedCommunities.length > 0) {
|
| 1650 |
const communitiesTitle = document.createElement('div');
|
| 1651 |
communitiesTitle.className = 'channel-section-title';
|
|
|
|
| 1674 |
});
|
| 1675 |
}
|
| 1676 |
|
| 1677 |
+
|
| 1678 |
if (joinedGroups.length > 0) {
|
| 1679 |
const groupsTitle = document.createElement('div');
|
| 1680 |
groupsTitle.className = 'channel-section-title';
|
|
|
|
| 1708 |
directTitle.textContent = 'DIRECT MESSAGES';
|
| 1709 |
channelsList.appendChild(directTitle);
|
| 1710 |
|
| 1711 |
+
|
| 1712 |
const selfItem = document.createElement('div');
|
| 1713 |
selfItem.className = 'channel-item';
|
| 1714 |
|
|
|
|
| 1756 |
item.classList.add('pinned');
|
| 1757 |
}
|
| 1758 |
|
| 1759 |
+
|
| 1760 |
const avatar = document.createElement('div');
|
| 1761 |
avatar.className = 'channel-avatar';
|
| 1762 |
if (friend.avatar) {
|
|
|
|
| 1767 |
avatar.textContent = friend.username.charAt(0).toUpperCase();
|
| 1768 |
}
|
| 1769 |
|
| 1770 |
+
|
| 1771 |
const text = document.createElement('span');
|
| 1772 |
const status = friend.status === 'online' ? '🟢' : '⚫';
|
| 1773 |
text.textContent = `${status} ${friend.username}`;
|
|
|
|
| 1785 |
item.style.cursor = 'pointer';
|
| 1786 |
item.onclick = () => openDM(friend.id, friend.username);
|
| 1787 |
|
| 1788 |
+
|
| 1789 |
item.oncontextmenu = (e) => {
|
| 1790 |
e.preventDefault();
|
| 1791 |
showFriendContextMenu(e, friend.id, friend.username);
|
|
|
|
| 1919 |
}
|
| 1920 |
}
|
| 1921 |
|
| 1922 |
+
|
| 1923 |
|
| 1924 |
async function loadShopItems() {
|
| 1925 |
try {
|
|
|
|
| 1992 |
}
|
| 1993 |
}
|
| 1994 |
|
| 1995 |
+
|
| 1996 |
|
| 1997 |
async function loadInventory() {
|
| 1998 |
try {
|
|
|
|
| 2037 |
},
|
| 2038 |
'Badges': () => {
|
| 2039 |
showToast('🏆 Badge equipped! It now appears next to your name.', 'success');
|
| 2040 |
+
|
| 2041 |
},
|
| 2042 |
'Themes': () => {
|
| 2043 |
const colors = ['#ff6b9d', '#c44569', '#4a69bd', '#6a89cc', '#60a3bc', '#78e08f', '#f6b93b', '#e55039'];
|
|
|
|
| 2047 |
showToast('🎨 Theme applied! Your accent color has changed.', 'success');
|
| 2048 |
},
|
| 2049 |
'Effects': () => {
|
| 2050 |
+
|
| 2051 |
const style = document.createElement('style');
|
| 2052 |
style.id = 'message-glow-effect';
|
| 2053 |
style.textContent = `
|
|
|
|
| 2113 |
}
|
| 2114 |
|
| 2115 |
|
|
|
|
| 2116 |
|
| 2117 |
function showToast(message, type = 'success') {
|
| 2118 |
const container = document.getElementById('toastContainer');
|
|
|
|
| 2137 |
}, 3000);
|
| 2138 |
}
|
| 2139 |
|
| 2140 |
+
|
| 2141 |
|
| 2142 |
function openSettings() {
|
| 2143 |
document.getElementById('settingsPanel').classList.add('show');
|
|
|
|
| 2158 |
}
|
| 2159 |
|
| 2160 |
function switchSettingsTab(tab) {
|
| 2161 |
+
|
| 2162 |
document.querySelectorAll('.settings-nav-item').forEach(item => {
|
| 2163 |
item.classList.remove('active');
|
| 2164 |
});
|
| 2165 |
event.target.classList.add('active');
|
| 2166 |
|
| 2167 |
+
|
| 2168 |
document.querySelectorAll('.settings-section').forEach(section => {
|
| 2169 |
section.classList.remove('active');
|
| 2170 |
});
|
|
|
|
| 2204 |
}
|
| 2205 |
}
|
| 2206 |
|
| 2207 |
+
|
| 2208 |
|
| 2209 |
function changeTheme(theme) {
|
| 2210 |
document.documentElement.setAttribute('data-theme', theme);
|
|
|
|
| 2221 |
}
|
| 2222 |
}
|
| 2223 |
|
| 2224 |
+
|
| 2225 |
|
| 2226 |
function closeCurrentChat() {
|
| 2227 |
if (activeRoomSubscription && socket && socket.connected) {
|
|
|
|
| 2311 |
return;
|
| 2312 |
}
|
| 2313 |
|
| 2314 |
+
|
| 2315 |
const optimisticMessage = {
|
| 2316 |
id: Date.now(),
|
| 2317 |
from: currentUser.id,
|
|
|
|
| 2350 |
return null;
|
| 2351 |
}
|
| 2352 |
|
| 2353 |
+
|
| 2354 |
|
| 2355 |
function requestNotificationPermission() {
|
| 2356 |
if ('Notification' in window && Notification.permission === 'default') {
|
|
|
|
| 2451 |
const modal = document.getElementById('communitiesModal');
|
| 2452 |
const list = document.getElementById('communitiesList');
|
| 2453 |
|
| 2454 |
+
|
| 2455 |
const communities = [
|
| 2456 |
+
{ name: 'Gaming', iconId: '1F579', description: 'Talk about your favorite games' },
|
| 2457 |
+
{ name: 'Coding', iconId: '1F4BB', description: 'Share code and learn together' },
|
| 2458 |
+
{ name: 'Music', iconId: '1F3B5', description: 'Discuss and share music' },
|
| 2459 |
+
{ name: 'Art', iconId: '1F3A8', description: 'Share your creative works' },
|
| 2460 |
+
{ name: 'Sports', iconId: '26BD', description: 'Sports fans unite' },
|
| 2461 |
+
{ name: 'Movies', iconId: '1F3AC', description: 'Film enthusiasts' }
|
| 2462 |
];
|
| 2463 |
|
| 2464 |
list.innerHTML = communities.map(c => {
|
|
|
|
| 2468 |
<div class="friend-item" style="flex-direction: column; align-items: flex-start; padding: 16px;">
|
| 2469 |
<div style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
|
| 2470 |
<div>
|
| 2471 |
+
<span class="friend-name" style="font-size: 16px;">${profile.icon || String.fromCodePoint(parseInt(c.iconId, 16))} ${c.name}</span>
|
| 2472 |
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">${c.description}</div>
|
| 2473 |
</div>
|
| 2474 |
<div style="display:flex; gap:6px;">
|
|
|
|
| 2495 |
const modal = document.getElementById('groupsModal');
|
| 2496 |
const list = document.getElementById('groupsList');
|
| 2497 |
|
| 2498 |
+
|
| 2499 |
const groups = [
|
| 2500 |
{ name: 'Study Group', emoji: '📚', description: 'Study together and share notes' },
|
| 2501 |
{ name: 'Project Team', emoji: '💼', description: 'Collaborate on projects' },
|
|
|
|
| 2564 |
document.getElementById('closeChatBtn').style.display = 'block';
|
| 2565 |
showSection('chat');
|
| 2566 |
|
| 2567 |
+
|
| 2568 |
if (window.innerWidth <= 480) {
|
| 2569 |
closeMobileSidebar();
|
| 2570 |
}
|
|
|
|
| 2586 |
document.getElementById('closeChatBtn').style.display = 'block';
|
| 2587 |
showSection('chat');
|
| 2588 |
|
| 2589 |
+
|
| 2590 |
if (window.innerWidth <= 480) {
|
| 2591 |
closeMobileSidebar();
|
| 2592 |
}
|
|
|
|
| 2629 |
showToast(`Joined ${name} group!`);
|
| 2630 |
}
|
| 2631 |
|
| 2632 |
+
|
| 2633 |
|
| 2634 |
const stickers = [
|
| 2635 |
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃',
|
|
|
|
| 2677 |
content.appendChild(item);
|
| 2678 |
});
|
| 2679 |
} else if (currentPickerTab === 'gifs') {
|
| 2680 |
+
|
| 2681 |
const gifs = [
|
| 2682 |
'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif',
|
| 2683 |
'https://media.giphy.com/media/l0MYt5jPR6QX5pnqM/giphy.gif',
|
|
|
|
| 2716 |
}
|
| 2717 |
|
| 2718 |
function sendSticker(sticker) {
|
| 2719 |
+
|
| 2720 |
const messageInput = document.getElementById('messageInput');
|
| 2721 |
if (messageInput) {
|
| 2722 |
const currentText = messageInput.value;
|
|
|
|
| 2738 |
document.getElementById('pickerPanel').classList.remove('show');
|
| 2739 |
}
|
| 2740 |
|
| 2741 |
+
|
| 2742 |
|
| 2743 |
let mediaRecorder;
|
| 2744 |
let audioChunks = [];
|
|
|
|
| 2779 |
|
| 2780 |
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
|
| 2781 |
try {
|
| 2782 |
+
|
| 2783 |
const stream = await getSelectedMicrophone();
|
| 2784 |
const mimeType = getSupportedAudioMimeType();
|
| 2785 |
recordedAudioMimeType = mimeType || 'audio/webm';
|
| 2786 |
mediaRecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
|
| 2787 |
audioChunks = [];
|
| 2788 |
|
| 2789 |
+
|
| 2790 |
if (previewContainer) previewContainer.style.display = 'none';
|
| 2791 |
if (previewAudio) {
|
| 2792 |
previewAudio.pause();
|
|
|
|
| 2806 |
showToast('🎧 Recording ready! CLICK PLAY BUTTON to hear it before sending!', 'success');
|
| 2807 |
if (sendBtn) sendBtn.disabled = false;
|
| 2808 |
|
| 2809 |
+
|
| 2810 |
if (previewContainer) previewContainer.style.display = 'block';
|
| 2811 |
if (previewAudio) {
|
| 2812 |
previewAudio.src = reader.result;
|
| 2813 |
previewAudio.load();
|
| 2814 |
+
|
| 2815 |
previewAudio.volume = 1.0;
|
| 2816 |
|
| 2817 |
+
|
| 2818 |
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && previewAudio.setSinkId) {
|
| 2819 |
previewAudio.setSinkId(audioSettings.speakerId).catch(err => {
|
| 2820 |
console.warn('Could not set preview audio output:', err);
|
| 2821 |
});
|
| 2822 |
}
|
| 2823 |
|
| 2824 |
+
|
| 2825 |
previewAudio.play().catch(err => {
|
| 2826 |
console.log('Autoplay blocked, user must click play:', err);
|
| 2827 |
showToast('⚠️ Click the PLAY button ▶️ below to hear your recording!', 'warning');
|
|
|
|
| 2919 |
window.recordedAudio = null;
|
| 2920 |
recordingSeconds = 0;
|
| 2921 |
|
| 2922 |
+
|
| 2923 |
if (previewContainer) previewContainer.style.display = 'none';
|
| 2924 |
if (previewAudio) {
|
| 2925 |
previewAudio.pause();
|
|
|
|
| 2929 |
if (sendBtn) sendBtn.disabled = true;
|
| 2930 |
}
|
| 2931 |
|
| 2932 |
+
|
| 2933 |
|
| 2934 |
function handleChatWheel(event) {
|
| 2935 |
const container = document.getElementById('messages-container');
|
| 2936 |
if (!container) return;
|
| 2937 |
|
|
|
|
| 2938 |
event.preventDefault();
|
| 2939 |
const delta = event.deltaY;
|
| 2940 |
container.scrollBy({ top: delta * 0.8, behavior: 'auto' });
|
| 2941 |
}
|
| 2942 |
|
| 2943 |
+
|
| 2944 |
|
| 2945 |
async function refreshAudioDevices() {
|
| 2946 |
try {
|
| 2947 |
+
|
| 2948 |
await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
|
| 2949 |
.then(stream => stream.getTracks().forEach(track => track.stop()))
|
| 2950 |
.catch(() => console.log('Microphone permission needed'));
|
|
|
|
| 2995 |
|
| 2996 |
localStorage.setItem('audioSettings', JSON.stringify(audioSettings));
|
| 2997 |
|
| 2998 |
+
|
| 2999 |
applySpeakerToAudioElements();
|
| 3000 |
|
| 3001 |
showToast('Audio settings saved', 'success');
|
|
|
|
| 3005 |
const speakerId = audioSettings.speakerId;
|
| 3006 |
if (!speakerId || speakerId === 'default') return;
|
| 3007 |
|
| 3008 |
+
|
| 3009 |
document.querySelectorAll('audio').forEach(audio => {
|
| 3010 |
if (audio.setSinkId) {
|
| 3011 |
audio.setSinkId(speakerId).catch(err => {
|
|
|
|
| 3014 |
}
|
| 3015 |
});
|
| 3016 |
|
| 3017 |
+
|
| 3018 |
const voicePreview = document.getElementById('voicePreview');
|
| 3019 |
if (voicePreview && voicePreview.setSinkId) {
|
| 3020 |
voicePreview.setSinkId(speakerId).catch(err => {
|
|
|
|
| 3044 |
});
|
| 3045 |
}
|
| 3046 |
|
| 3047 |
+
|
| 3048 |
|
| 3049 |
function changeTheme(theme) {
|
| 3050 |
themeSettings.mode = theme;
|
|
|
|
| 3093 |
root.style.setProperty('--primary-color', icon);
|
| 3094 |
root.style.setProperty('--secondary-color', icon);
|
| 3095 |
root.style.setProperty('--icon-color', icon);
|
| 3096 |
+
|
| 3097 |
root.style.setProperty('--ui-text-color', '#ffffff');
|
| 3098 |
|
| 3099 |
+
|
| 3100 |
const simpleTheme = { bg, text, icon, mode: 'custom' };
|
| 3101 |
localStorage.setItem('simpleTheme', JSON.stringify(simpleTheme));
|
| 3102 |
}
|
|
|
|
| 3192 |
}
|
| 3193 |
}
|
| 3194 |
|
|
|
|
| 3195 |
|
| 3196 |
+
|
| 3197 |
+
let messageReactions = {};
|
| 3198 |
|
| 3199 |
function showQuickReactions(messageId, messageDiv) {
|
| 3200 |
const quickReactions = ['👍', '❤️', '😂', '😮', '😢', '🙏'];
|
| 3201 |
|
| 3202 |
+
|
| 3203 |
document.querySelectorAll('.reaction-picker').forEach(p => p.remove());
|
| 3204 |
|
| 3205 |
+
|
| 3206 |
const picker = document.createElement('div');
|
| 3207 |
picker.className = 'reaction-picker';
|
| 3208 |
picker.style.position = 'fixed';
|
|
|
|
| 3215 |
picker.style.zIndex = '9999';
|
| 3216 |
picker.style.boxShadow = '0 8px 24px rgba(0,0,0,0.6)';
|
| 3217 |
|
| 3218 |
+
|
| 3219 |
const rect = messageDiv.getBoundingClientRect();
|
| 3220 |
picker.style.left = Math.max(20, rect.left) + 'px';
|
| 3221 |
picker.style.top = Math.max(20, rect.top - 60) + 'px';
|
|
|
|
| 3248 |
|
| 3249 |
document.body.appendChild(picker);
|
| 3250 |
|
| 3251 |
+
|
| 3252 |
const closePickerHandler = (e) => {
|
| 3253 |
if (!picker.contains(e.target)) {
|
| 3254 |
picker.remove();
|
|
|
|
| 3274 |
const userIndex = messageReactions[messageId][emoji].indexOf(userId);
|
| 3275 |
|
| 3276 |
if (userIndex > -1) {
|
| 3277 |
+
|
| 3278 |
messageReactions[messageId][emoji].splice(userIndex, 1);
|
| 3279 |
if (messageReactions[messageId][emoji].length === 0) {
|
| 3280 |
delete messageReactions[messageId][emoji];
|
| 3281 |
}
|
| 3282 |
} else {
|
| 3283 |
+
|
| 3284 |
messageReactions[messageId][emoji].push(userId);
|
| 3285 |
updateUserStats('reactionsGiven', 1);
|
| 3286 |
}
|
|
|
|
| 3291 |
}
|
| 3292 |
|
| 3293 |
function showMessageContextMenu(e, messageDiv, data) {
|
| 3294 |
+
|
| 3295 |
document.querySelectorAll('.message-context-menu').forEach(m => m.remove());
|
| 3296 |
|
| 3297 |
const menu = document.createElement('div');
|
|
|
|
| 3329 |
btn.style.fontWeight = '500';
|
| 3330 |
btn.style.borderBottom = '1px solid var(--border-color)';
|
| 3331 |
|
| 3332 |
+
|
| 3333 |
btn.onmouseover = () => {
|
| 3334 |
if (item.hoverClass === 'danger-hover') {
|
| 3335 |
btn.style.background = 'var(--danger)';
|
|
|
|
| 3357 |
menu.appendChild(btn);
|
| 3358 |
});
|
| 3359 |
|
| 3360 |
+
|
| 3361 |
const lastBtn = menu.lastElementChild;
|
| 3362 |
if (lastBtn) lastBtn.style.borderBottom = 'none';
|
| 3363 |
|
| 3364 |
document.body.appendChild(menu);
|
| 3365 |
|
| 3366 |
+
|
| 3367 |
document.addEventListener('click', () => menu.remove(), { once: true });
|
| 3368 |
}
|
| 3369 |
|
| 3370 |
function showFriendContextMenu(e, friendId, friendName) {
|
| 3371 |
+
|
| 3372 |
document.querySelectorAll('.message-context-menu').forEach(m => m.remove());
|
| 3373 |
|
| 3374 |
const menu = document.createElement('div');
|
|
|
|
| 3400 |
btn.style.fontWeight = '500';
|
| 3401 |
btn.style.borderBottom = '1px solid var(--border-color)';
|
| 3402 |
|
| 3403 |
+
|
| 3404 |
btn.onmouseover = () => {
|
| 3405 |
if (item.hoverClass === 'danger-hover') {
|
| 3406 |
btn.style.background = 'var(--danger)';
|
|
|
|
| 3428 |
menu.appendChild(btn);
|
| 3429 |
});
|
| 3430 |
|
| 3431 |
+
|
| 3432 |
const lastBtn = menu.lastElementChild;
|
| 3433 |
if (lastBtn) lastBtn.style.borderBottom = 'none';
|
| 3434 |
|
| 3435 |
document.body.appendChild(menu);
|
| 3436 |
|
| 3437 |
+
|
| 3438 |
document.addEventListener('click', () => menu.remove(), { once: true });
|
| 3439 |
}
|
| 3440 |
|
|
|
|
| 3490 |
} else if (imgElement || vidElement || audioElement) {
|
| 3491 |
const url = imgElement?.src || vidElement?.src || audioElement?.src || '';
|
| 3492 |
if (url) {
|
| 3493 |
+
|
| 3494 |
if (imgElement) {
|
| 3495 |
addToMediaPreview('image', url);
|
| 3496 |
} else if (vidElement) {
|
| 3497 |
addToMediaPreview('video', url);
|
| 3498 |
}
|
| 3499 |
|
| 3500 |
+
|
| 3501 |
navigator.clipboard.writeText(url).then(() => {
|
| 3502 |
const mediaType = imgElement ? 'Image' : vidElement ? 'Video' : 'Audio';
|
| 3503 |
showToast(`✅ ${mediaType} copied! View in preview panel.`, 'success');
|
| 3504 |
|
| 3505 |
+
|
| 3506 |
if (copiedMediaItems.length > 0) {
|
| 3507 |
setTimeout(() => openMediaPreview(), 500);
|
| 3508 |
}
|
|
|
|
| 3534 |
|
| 3535 |
showToast('✅ Message edited', 'success');
|
| 3536 |
|
| 3537 |
+
|
|
|
|
| 3538 |
}
|
| 3539 |
});
|
| 3540 |
}
|
|
|
|
| 3547 |
true
|
| 3548 |
).then(confirmed => {
|
| 3549 |
if (confirmed) {
|
| 3550 |
+
|
| 3551 |
messageDiv.style.transition = 'all 0.3s ease-out';
|
| 3552 |
messageDiv.style.opacity = '0';
|
| 3553 |
messageDiv.style.transform = 'translateX(-100%)';
|
| 3554 |
|
| 3555 |
+
|
| 3556 |
setTimeout(() => {
|
| 3557 |
messageDiv.remove();
|
| 3558 |
showToast('✅ Message deleted', 'success');
|
| 3559 |
}, 300);
|
| 3560 |
|
| 3561 |
+
|
|
|
|
| 3562 |
}
|
| 3563 |
});
|
| 3564 |
}
|
| 3565 |
|
| 3566 |
+
|
| 3567 |
|
| 3568 |
function createNewGroup() {
|
| 3569 |
customPrompt('Enter a name for your new group:', '👥 Create Group', 'My Awesome Group', '', '👥').then(groupName => {
|
|
|
|
| 3586 |
|
| 3587 |
const ids = memberIds.split(',').map(id => id.trim()).filter(Boolean);
|
| 3588 |
showToast(`✅ Group "${groupName}" created with ${ids.length} members!`, 'success');
|
| 3589 |
+
|
| 3590 |
});
|
| 3591 |
});
|
| 3592 |
}
|
|
|
|
| 3619 |
input.click();
|
| 3620 |
}
|
| 3621 |
|
| 3622 |
+
|
| 3623 |
|
| 3624 |
function startVideoCall() {
|
| 3625 |
if (!currentChatContext || currentChatContext.type === 'none' || !currentChatContext.id) {
|
|
|
|
| 3636 |
const targetId = currentChatContext.id;
|
| 3637 |
const callType = 'Video Call';
|
| 3638 |
|
| 3639 |
+
|
| 3640 |
openVideoCallModal(targetName, targetId, callType);
|
| 3641 |
}
|
| 3642 |
|
| 3643 |
+
|
| 3644 |
|
| 3645 |
let archivedChats = JSON.parse(localStorage.getItem('archivedChats') || '[]');
|
| 3646 |
|
|
|
|
| 3657 |
const archiveIndex = archivedChats.findIndex(c => c.id === chatId && c.type === chatType);
|
| 3658 |
|
| 3659 |
if (archiveIndex > -1) {
|
| 3660 |
+
|
| 3661 |
archivedChats.splice(archiveIndex, 1);
|
| 3662 |
showToast(`${chatName} unarchived`, 'success');
|
| 3663 |
} else {
|
| 3664 |
+
|
| 3665 |
archivedChats.push({ id: chatId, name: chatName, type: chatType });
|
| 3666 |
showToast(`${chatName} archived`, 'success');
|
| 3667 |
+
|
| 3668 |
if (chatType === 'dm') {
|
| 3669 |
const friendIndex = friends.findIndex(f => f.id === chatId);
|
| 3670 |
if (friendIndex > -1) {
|
|
|
|
| 3676 |
}
|
| 3677 |
|
| 3678 |
localStorage.setItem('archivedChats', JSON.stringify(archivedChats));
|
| 3679 |
+
loadFriendsForDM();
|
| 3680 |
}
|
| 3681 |
|
| 3682 |
function showArchivedChats() {
|
|
|
|
| 3736 |
showToast(`${chat.name} unarchived`, 'success');
|
| 3737 |
loadFriendsForDM();
|
| 3738 |
closeCustomDialog();
|
| 3739 |
+
showArchivedChats();
|
| 3740 |
}
|
| 3741 |
}
|
| 3742 |
|
| 3743 |
+
|
| 3744 |
|
| 3745 |
let userStats = JSON.parse(localStorage.getItem('userStats') || '{"messagesCount": 0, "totalChars": 0, "reactionsGiven": 0, "archiveCount": 0, "quizCorrect": 0}');
|
| 3746 |
|
|
|
|
| 3872 |
});
|
| 3873 |
}
|
| 3874 |
|
| 3875 |
+
|
| 3876 |
function showMobileSection(section) {
|
| 3877 |
+
|
| 3878 |
if (window.innerWidth <= 768) {
|
| 3879 |
closeMobileSidebar();
|
| 3880 |
closeSettings();
|
|
|
|
| 3883 |
closeGroupsModal();
|
| 3884 |
}
|
| 3885 |
|
| 3886 |
+
|
| 3887 |
document.querySelectorAll('.mobile-nav-item').forEach(item => {
|
| 3888 |
if (item.getAttribute('data-section') === section) {
|
| 3889 |
item.classList.add('active');
|
|
|
|
| 3892 |
}
|
| 3893 |
});
|
| 3894 |
|
| 3895 |
+
|
| 3896 |
switch(section) {
|
| 3897 |
case 'chats':
|
| 3898 |
toggleChannelSidebar();
|
|
|
|
| 3976 |
const desktopList = document.getElementById('membersList');
|
| 3977 |
if (!mobileList || !desktopList) return;
|
| 3978 |
|
| 3979 |
+
|
| 3980 |
mobileList.innerHTML = desktopList.innerHTML;
|
| 3981 |
}
|
| 3982 |
|
|
|
|
| 3984 |
if (!communityProfiles[name]) {
|
| 3985 |
communityProfiles[name] = {
|
| 3986 |
name,
|
| 3987 |
+
icon: '',
|
| 3988 |
image: '',
|
| 3989 |
members: [
|
| 3990 |
{
|
|
|
|
| 4107 |
if (currentChatContext.type === 'community' && currentChatContext.name === name) {
|
| 4108 |
document.getElementById('chatTitle').textContent = `${profile.icon} Community: ${name}`;
|
| 4109 |
}
|
| 4110 |
+
loadFriendsForDM();
|
| 4111 |
showToast('Community icon updated! 🎉', 'success');
|
| 4112 |
}
|
| 4113 |
|
|
|
|
| 4135 |
communityProfiles[name] = profile;
|
| 4136 |
localStorage.setItem('communityProfiles', JSON.stringify(communityProfiles));
|
| 4137 |
|
| 4138 |
+
|
| 4139 |
if (currentChatContext?.type === 'community' && currentChatContext?.name === name) {
|
| 4140 |
const profileAvatar = document.getElementById('profileAvatar');
|
| 4141 |
if (profileAvatar) {
|
|
|
|
| 4148 |
if (profileName) {
|
| 4149 |
profileName.textContent = name;
|
| 4150 |
}
|
| 4151 |
+
|
| 4152 |
setTimeout(() => {
|
| 4153 |
const avatarDiv = document.getElementById('profileAvatar');
|
| 4154 |
if (avatarDiv && !avatarDiv.style.backgroundImage) {
|
|
|
|
| 4181 |
groupProfiles[name] = profile;
|
| 4182 |
localStorage.setItem('groupProfiles', JSON.stringify(groupProfiles));
|
| 4183 |
|
| 4184 |
+
|
| 4185 |
if (currentChatContext?.type === 'group' && currentChatContext?.name === name) {
|
| 4186 |
const profileAvatar = document.getElementById('profileAvatar');
|
| 4187 |
if (profileAvatar) {
|
|
|
|
| 4194 |
if (profileName) {
|
| 4195 |
profileName.textContent = name;
|
| 4196 |
}
|
| 4197 |
+
|
| 4198 |
setTimeout(() => {
|
| 4199 |
const avatarDiv = document.getElementById('profileAvatar');
|
| 4200 |
if (avatarDiv && !avatarDiv.style.backgroundImage) {
|
|
|
|
| 4212 |
input.click();
|
| 4213 |
}
|
| 4214 |
|
| 4215 |
+
|
| 4216 |
function toggleChannelSidebarEnhanced() {
|
| 4217 |
const sidebar = document.getElementById('channelSidebar');
|
| 4218 |
const overlay = document.getElementById('sidebarOverlay');
|
|
|
|
| 4221 |
overlay?.classList.toggle('show');
|
| 4222 |
}
|
| 4223 |
|
| 4224 |
+
|
| 4225 |
function togglePinCurrentChat() {
|
| 4226 |
if (!currentChatContext || currentChatContext.type === 'none') {
|
| 4227 |
showToast('No chat selected', 'warning');
|
|
|
|
| 4232 |
const index = pinnedChats.indexOf(chatId);
|
| 4233 |
|
| 4234 |
if (index > -1) {
|
| 4235 |
+
|
| 4236 |
pinnedChats.splice(index, 1);
|
| 4237 |
showToast('Chat unpinned', 'success');
|
| 4238 |
} else {
|
| 4239 |
+
|
| 4240 |
pinnedChats.push(chatId);
|
| 4241 |
showToast('Chat pinned to top', 'success');
|
| 4242 |
}
|
| 4243 |
|
| 4244 |
localStorage.setItem('pinnedChats', JSON.stringify(pinnedChats));
|
| 4245 |
updatePinButtonState();
|
| 4246 |
+
loadFriendsForDM();
|
| 4247 |
}
|
| 4248 |
|
| 4249 |
function updatePinButtonState() {
|
|
|
|
| 4262 |
return pinnedChats.includes(chatKey);
|
| 4263 |
}
|
| 4264 |
|
| 4265 |
+
|
| 4266 |
function showToastClickable(message, type = 'success', chatContext = null) {
|
| 4267 |
const container = document.getElementById('toastContainer');
|
| 4268 |
if (!container) return;
|
|
|
|
| 4289 |
if (chatContext) {
|
| 4290 |
toast.style.cursor = 'pointer';
|
| 4291 |
toast.addEventListener('click', () => {
|
| 4292 |
+
|
| 4293 |
if (chatContext.type === 'dm') {
|
| 4294 |
openDM(chatContext.friendId, chatContext.friendName);
|
| 4295 |
} else if (chatContext.type === 'community') {
|
|
|
|
| 4298 |
openRoomChat('group', chatContext.name);
|
| 4299 |
}
|
| 4300 |
|
| 4301 |
+
|
| 4302 |
toast.remove();
|
| 4303 |
|
| 4304 |
+
|
| 4305 |
if (window.innerWidth <= 480) {
|
| 4306 |
closeMobileSidebar();
|
| 4307 |
}
|
|
|
|
| 4315 |
}, 5000);
|
| 4316 |
}
|
| 4317 |
|
| 4318 |
+
|
| 4319 |
|
| 4320 |
window.addEventListener('DOMContentLoaded', () => {
|
| 4321 |
loadTheme();
|
|
|
|
| 4331 |
document.getElementById('autoplayGifs')?.addEventListener('change', saveChatSettings);
|
| 4332 |
document.getElementById('compactMode')?.addEventListener('change', saveAppearanceSettings);
|
| 4333 |
|
| 4334 |
+
|
| 4335 |
let typingTimeout = null;
|
| 4336 |
const typingInput = document.getElementById('messageInput');
|
| 4337 |
if (typingInput) {
|
| 4338 |
typingInput.addEventListener('input', () => {
|
| 4339 |
if (!chatSettings.showTypingIndicator) return;
|
| 4340 |
|
| 4341 |
+
|
| 4342 |
if (currentChatContext.type === 'community' && currentChatContext.name) {
|
| 4343 |
socket?.emit('community typing', { community: currentChatContext.name, username: currentUser?.username });
|
| 4344 |
clearTimeout(typingTimeout);
|
|
|
|
| 4381 |
picker.classList.remove('show');
|
| 4382 |
});
|
| 4383 |
|
| 4384 |
+
|
| 4385 |
loadAudioSettings();
|
| 4386 |
loadThemeSettings();
|
| 4387 |
refreshAudioDevices();
|
| 4388 |
updateStatsDisplay();
|
| 4389 |
|
| 4390 |
+
|
| 4391 |
syncBuildVersionBadge();
|
| 4392 |
|
| 4393 |
const token = localStorage.getItem('token');
|
|
|
|
| 4403 |
document.getElementById('mainApp').classList.add('hidden');
|
| 4404 |
}
|
| 4405 |
});
|
| 4406 |
+
|
| 4407 |
|
| 4408 |
function customAlert(message, title = 'Notice', icon = 'ℹ️') {
|
| 4409 |
return new Promise((resolve) => {
|
|
|
|
| 4492 |
if (window.customDialogResolve) window.customDialogResolve(value);
|
| 4493 |
}
|
| 4494 |
|
| 4495 |
+
|
| 4496 |
|
| 4497 |
function showQRCodeModal() {
|
| 4498 |
const modal = document.getElementById('qrModal');
|
|
|
|
| 4504 |
return;
|
| 4505 |
}
|
| 4506 |
|
| 4507 |
+
|
| 4508 |
qrContainer.innerHTML = '';
|
| 4509 |
|
| 4510 |
+
|
| 4511 |
const qrDiv = document.createElement('div');
|
| 4512 |
qrDiv.id = 'qrCodeCanvas';
|
| 4513 |
qrDiv.style.backgroundColor = '#ffffff';
|
|
|
|
| 4516 |
qrDiv.style.display = 'inline-block';
|
| 4517 |
qrContainer.appendChild(qrDiv);
|
| 4518 |
|
| 4519 |
+
|
| 4520 |
userIdSpan.textContent = currentUser.id || currentUser._id || 'Unknown';
|
| 4521 |
|
| 4522 |
+
|
| 4523 |
const friendRequestData = `FRIEND_REQUEST:${currentUser.id || currentUser._id}:${currentUser.username || 'User'}`;
|
| 4524 |
|
| 4525 |
+
|
| 4526 |
if (typeof QRCode !== 'undefined') {
|
| 4527 |
try {
|
| 4528 |
new QRCode(qrDiv, {
|
|
|
|
| 4540 |
}
|
| 4541 |
} else {
|
| 4542 |
console.warn('⚠️ QRCode library not loaded, using fallback');
|
| 4543 |
+
|
| 4544 |
generateQRCodeFallback(qrDiv, friendRequestData);
|
| 4545 |
}
|
| 4546 |
|
|
|
|
| 4550 |
function generateQRCodeFallback(container, data) {
|
| 4551 |
console.log('📱 Using QR Code API fallback');
|
| 4552 |
|
| 4553 |
+
|
| 4554 |
const img = new Image();
|
| 4555 |
img.style.width = '200px';
|
| 4556 |
img.style.height = '200px';
|
|
|
|
| 4566 |
|
| 4567 |
img.onerror = () => {
|
| 4568 |
console.error('❌ QR Code API failed');
|
| 4569 |
+
|
| 4570 |
container.innerHTML = `
|
| 4571 |
<div style="width: 200px; height: 200px; background: white; border: 2px solid #000; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 20px; box-sizing: border-box;">
|
| 4572 |
<div style="font-size: 24px; margin-bottom: 10px;">📱</div>
|
|
|
|
| 4597 |
let friendId = null;
|
| 4598 |
let friendName = 'User';
|
| 4599 |
|
| 4600 |
+
|
| 4601 |
if (qrData.startsWith('FRIEND_REQUEST:')) {
|
| 4602 |
const parts = qrData.split(':');
|
| 4603 |
friendId = parts[1];
|
| 4604 |
friendName = parts[2] || 'User';
|
| 4605 |
} else {
|
| 4606 |
+
|
| 4607 |
friendId = qrData.trim();
|
| 4608 |
}
|
| 4609 |
|
|
|
|
| 4612 |
return;
|
| 4613 |
}
|
| 4614 |
|
| 4615 |
+
|
| 4616 |
customConfirm(
|
| 4617 |
`Send friend request to <strong>${friendName}</strong> (ID: ${friendId})?`,
|
| 4618 |
'👤 Friend Request',
|
|
|
|
| 4631 |
});
|
| 4632 |
}
|
| 4633 |
|
| 4634 |
+
|
| 4635 |
|
| 4636 |
function openVideoCallModal(targetName, targetId, callType) {
|
| 4637 |
targetName = targetName || currentChatContext.name || 'User';
|
|
|
|
| 4664 |
|
| 4665 |
customAlert(html, '📞 Video Call', '📞');
|
| 4666 |
|
| 4667 |
+
|
| 4668 |
if (socket && socket.connected && targetId) {
|
| 4669 |
socket.emit('start video call', {
|
| 4670 |
targetId: targetId,
|
|
|
|
| 4700 |
callId: activeCallState.callId,
|
| 4701 |
toId: activeCallState.peerId
|
| 4702 |
});
|
| 4703 |
+
|
| 4704 |
} else {
|
| 4705 |
stopIncomingCallAlert();
|
| 4706 |
socket.emit('reject video call', {
|
|
|
|
| 4736 |
} catch (error) {
|
| 4737 |
console.error('Media access error:', error);
|
| 4738 |
|
| 4739 |
+
|
| 4740 |
try {
|
| 4741 |
localCallStream = await navigator.mediaDevices.getUserMedia({
|
| 4742 |
audio: true,
|
|
|
|
| 4762 |
{ urls: 'stun:stun.l.google.com:19302' },
|
| 4763 |
{ urls: 'stun:stun1.l.google.com:19302' },
|
| 4764 |
{ urls: 'stun:stun2.l.google.com:19302' },
|
| 4765 |
+
|
| 4766 |
{
|
| 4767 |
urls: 'turn:openrelay.metered.ca:80',
|
| 4768 |
username: 'openrelayproject',
|
|
|
|
| 4804 |
if (remoteVideo && event.streams[0]) {
|
| 4805 |
remoteVideo.srcObject = event.streams[0];
|
| 4806 |
|
| 4807 |
+
|
| 4808 |
remoteVideo.muted = false;
|
| 4809 |
remoteVideo.volume = 1.0;
|
| 4810 |
|
| 4811 |
+
|
| 4812 |
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && typeof remoteVideo.setSinkId === 'function') {
|
| 4813 |
remoteVideo.setSinkId(audioSettings.speakerId).catch((sinkError) => {
|
| 4814 |
console.warn('Could not apply selected speaker for call:', sinkError);
|
| 4815 |
});
|
| 4816 |
}
|
| 4817 |
|
| 4818 |
+
|
| 4819 |
const playPromise = remoteVideo.play();
|
| 4820 |
if (playPromise !== undefined) {
|
| 4821 |
playPromise.then(() => {
|
|
|
|
| 4823 |
showToast('📞 Connected! Audio should be working now', 'success');
|
| 4824 |
}).catch((error) => {
|
| 4825 |
console.error('Remote video play error:', error);
|
| 4826 |
+
|
| 4827 |
setTimeout(() => {
|
| 4828 |
remoteVideo.play().catch(e => console.error('Retry play failed:', e));
|
| 4829 |
}, 500);
|
|
|
|
| 4835 |
peerConnection.onconnectionstatechange = () => {
|
| 4836 |
console.log('📡 Connection state:', peerConnection.connectionState);
|
| 4837 |
|
| 4838 |
+
|
| 4839 |
if (window.userIntentionallEndedCall) {
|
| 4840 |
console.log('User ended call intentionally, skipping error messages');
|
| 4841 |
return;
|
| 4842 |
}
|
| 4843 |
|
| 4844 |
+
|
| 4845 |
if (window.callDisconnectTimeout) {
|
| 4846 |
clearTimeout(window.callDisconnectTimeout);
|
| 4847 |
window.callDisconnectTimeout = null;
|
|
|
|
| 4849 |
|
| 4850 |
if (peerConnection.connectionState === 'connecting') {
|
| 4851 |
showToast('📞 Connecting call...', 'info');
|
| 4852 |
+
|
| 4853 |
if (!callTimerInterval) {
|
| 4854 |
startCallTimer();
|
| 4855 |
}
|
| 4856 |
} else if (peerConnection.connectionState === 'disconnected') {
|
| 4857 |
showToast('⚠️ Connection unstable, trying to reconnect...', 'warning');
|
| 4858 |
+
|
| 4859 |
window.callDisconnectTimeout = setTimeout(() => {
|
| 4860 |
if (peerConnection && peerConnection.connectionState === 'disconnected' && !window.userIntentionallEndedCall) {
|
| 4861 |
showToast('Call ended due to connection loss', 'error');
|
| 4862 |
cleanupCallSession(false);
|
| 4863 |
}
|
| 4864 |
+
}, 15000);
|
| 4865 |
} else if (peerConnection.connectionState === 'connected') {
|
| 4866 |
showToast('📞 Call connected!', 'success');
|
| 4867 |
+
|
| 4868 |
if (!callTimerInterval) {
|
| 4869 |
startCallTimer();
|
| 4870 |
}
|
|
|
|
| 4878 |
}
|
| 4879 |
}, 12000);
|
| 4880 |
} else if (peerConnection.connectionState === 'closed') {
|
| 4881 |
+
|
| 4882 |
cleanupCallSession(false);
|
| 4883 |
}
|
| 4884 |
};
|
|
|
|
| 4888 |
console.log('ICE connection state:', peerConnection.iceConnectionState);
|
| 4889 |
|
| 4890 |
if (peerConnection.iceConnectionState === 'failed') {
|
| 4891 |
+
|
| 4892 |
peerConnection.restartIce?.();
|
| 4893 |
}
|
| 4894 |
};
|
|
|
|
| 4945 |
}
|
| 4946 |
|
| 4947 |
function endVideoCall() {
|
| 4948 |
+
|
| 4949 |
window.userIntentionallEndedCall = true;
|
| 4950 |
|
| 4951 |
+
|
| 4952 |
if (window.callDisconnectTimeout) {
|
| 4953 |
clearTimeout(window.callDisconnectTimeout);
|
| 4954 |
window.callDisconnectTimeout = null;
|
|
|
|
| 4985 |
}
|
| 4986 |
|
| 4987 |
function cleanupCallSession(keepDialogOpen = false) {
|
| 4988 |
+
|
| 4989 |
window.userIntentionallEndedCall = false;
|
| 4990 |
|
| 4991 |
+
|
| 4992 |
if (window.callDisconnectTimeout) {
|
| 4993 |
clearTimeout(window.callDisconnectTimeout);
|
| 4994 |
window.callDisconnectTimeout = null;
|
|
|
|
| 5019 |
}
|
| 5020 |
|
| 5021 |
function visitUserProfile(userId, userName = 'User') {
|
| 5022 |
+
|
| 5023 |
if (!userId || userId === 'undefined') {
|
| 5024 |
showToast('❌ Invalid user ID', 'error');
|
| 5025 |
return;
|
| 5026 |
}
|
| 5027 |
|
| 5028 |
+
|
| 5029 |
const html = `
|
| 5030 |
<div style="text-align: center; padding: 20px 0;">
|
| 5031 |
<div style="width: 100px; height: 100px; background: linear-gradient(135deg, var(--primary-color), #4752c4); border-radius: 50%; margin: 0 auto 20px; display: flex; align-items: center; justify-content: center; font-size: 48px; color: white;">
|
|
|
|
| 5092 |
});
|
| 5093 |
}
|
| 5094 |
|
| 5095 |
+
|
| 5096 |
|
| 5097 |
let copiedMediaItems = [];
|
| 5098 |
|
|
|
|
| 5134 |
document.getElementById('mediaPreviewPanel').classList.remove('show');
|
| 5135 |
}
|
| 5136 |
|
| 5137 |
+
|
| 5138 |
|
| 5139 |
function filterFriends(searchText) {
|
| 5140 |
const channelsList = document.getElementById('channelsList');
|
|
|
|
| 5152 |
});
|
| 5153 |
}
|
| 5154 |
|
| 5155 |
+
|
| 5156 |
|
| 5157 |
let archivedMessages = JSON.parse(localStorage.getItem('archivedMessages') || '[]');
|
| 5158 |
|
|
|
|
| 5173 |
archivedMessages.push(messageData);
|
| 5174 |
localStorage.setItem('archivedMessages', JSON.stringify(archivedMessages));
|
| 5175 |
|
| 5176 |
+
|
| 5177 |
messageDiv.style.transition = 'all 0.4s ease';
|
| 5178 |
messageDiv.style.opacity = '0.5';
|
| 5179 |
messageDiv.style.transform = 'translateX(-20px)';
|
index.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
| 27 |
--danger: #f04747;
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
| 31 |
[data-theme="light"] {
|
| 32 |
--primary-color: #5865f2;
|
| 33 |
--secondary-color: #99aab5;
|
|
@@ -50,7 +50,7 @@
|
|
| 50 |
overflow: hidden;
|
| 51 |
}
|
| 52 |
|
| 53 |
-
|
| 54 |
.auth-container {
|
| 55 |
position: fixed;
|
| 56 |
top: 0;
|
|
@@ -187,7 +187,7 @@
|
|
| 187 |
cursor: pointer;
|
| 188 |
}
|
| 189 |
|
| 190 |
-
|
| 191 |
.main-container {
|
| 192 |
display: grid;
|
| 193 |
grid-template-columns: 72px 240px 1fr 240px;
|
|
@@ -199,7 +199,7 @@
|
|
| 199 |
display: none;
|
| 200 |
}
|
| 201 |
|
| 202 |
-
|
| 203 |
.guild-sidebar {
|
| 204 |
background: var(--sidebar-bg);
|
| 205 |
display: flex;
|
|
@@ -241,7 +241,7 @@
|
|
| 241 |
margin: 8px 0;
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
.channel-sidebar {
|
| 246 |
background: var(--sidebar-bg);
|
| 247 |
border-right: 1px solid var(--border-color);
|
|
@@ -348,7 +348,7 @@
|
|
| 348 |
font-weight: bold;
|
| 349 |
}
|
| 350 |
|
| 351 |
-
|
| 352 |
.chat-area {
|
| 353 |
position: relative;
|
| 354 |
display: flex;
|
|
@@ -431,7 +431,7 @@
|
|
| 431 |
-webkit-overflow-scrolling: touch;
|
| 432 |
}
|
| 433 |
|
| 434 |
-
|
| 435 |
#messages-container::-webkit-scrollbar {
|
| 436 |
width: 0;
|
| 437 |
height: 0;
|
|
@@ -682,7 +682,7 @@
|
|
| 682 |
cursor: pointer;
|
| 683 |
}
|
| 684 |
|
| 685 |
-
|
| 686 |
|
| 687 |
.input-area {
|
| 688 |
padding: 16px 24px;
|
|
@@ -735,7 +735,7 @@
|
|
| 735 |
display: none;
|
| 736 |
}
|
| 737 |
|
| 738 |
-
|
| 739 |
.members-sidebar {
|
| 740 |
background: var(--sidebar-bg);
|
| 741 |
border-left: 1px solid var(--border-color);
|
|
@@ -784,7 +784,7 @@
|
|
| 784 |
background: var(--text-secondary);
|
| 785 |
}
|
| 786 |
|
| 787 |
-
|
| 788 |
.profile-panel {
|
| 789 |
position: fixed;
|
| 790 |
right: 0;
|
|
@@ -1076,7 +1076,7 @@
|
|
| 1076 |
to { opacity: 0; transform: translateX(100%); }
|
| 1077 |
}
|
| 1078 |
|
| 1079 |
-
|
| 1080 |
.picker-panel {
|
| 1081 |
position: absolute;
|
| 1082 |
bottom: 60px;
|
|
@@ -1147,7 +1147,7 @@
|
|
| 1147 |
object-fit: cover;
|
| 1148 |
}
|
| 1149 |
|
| 1150 |
-
|
| 1151 |
.voice-recorder {
|
| 1152 |
position: absolute;
|
| 1153 |
bottom: 70px;
|
|
@@ -1216,7 +1216,7 @@
|
|
| 1216 |
color: var(--danger);
|
| 1217 |
}
|
| 1218 |
|
| 1219 |
-
|
| 1220 |
.settings-panel {
|
| 1221 |
position: fixed;
|
| 1222 |
top: 0;
|
|
@@ -1367,7 +1367,7 @@
|
|
| 1367 |
transform: translateX(20px);
|
| 1368 |
}
|
| 1369 |
|
| 1370 |
-
|
| 1371 |
.modal {
|
| 1372 |
position: fixed;
|
| 1373 |
top: 0;
|
|
@@ -1396,7 +1396,7 @@
|
|
| 1396 |
overflow-y: auto;
|
| 1397 |
}
|
| 1398 |
|
| 1399 |
-
|
| 1400 |
::-webkit-scrollbar {
|
| 1401 |
width: 8px;
|
| 1402 |
}
|
|
@@ -1414,7 +1414,7 @@
|
|
| 1414 |
background: #5a5d60;
|
| 1415 |
}
|
| 1416 |
|
| 1417 |
-
|
| 1418 |
.mobile-bottom-nav {
|
| 1419 |
display: none;
|
| 1420 |
position: fixed;
|
|
@@ -1430,7 +1430,7 @@
|
|
| 1430 |
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
|
| 1431 |
}
|
| 1432 |
|
| 1433 |
-
|
| 1434 |
.mobile-members-panel {
|
| 1435 |
position: fixed;
|
| 1436 |
right: -100%;
|
|
@@ -1512,7 +1512,7 @@
|
|
| 1512 |
text-align: center;
|
| 1513 |
}
|
| 1514 |
|
| 1515 |
-
|
| 1516 |
@media (max-width: 1200px) {
|
| 1517 |
.main-container {
|
| 1518 |
grid-template-columns: 72px 240px 1fr;
|
|
@@ -1532,7 +1532,7 @@
|
|
| 1532 |
}
|
| 1533 |
}
|
| 1534 |
|
| 1535 |
-
|
| 1536 |
@media (max-width: 768px) {
|
| 1537 |
body {
|
| 1538 |
overflow: hidden;
|
|
@@ -1545,7 +1545,7 @@
|
|
| 1545 |
max-height: -webkit-fill-available;
|
| 1546 |
}
|
| 1547 |
|
| 1548 |
-
|
| 1549 |
.guild-sidebar {
|
| 1550 |
padding: 2px 0;
|
| 1551 |
width: 45px;
|
|
@@ -1581,7 +1581,7 @@
|
|
| 1581 |
display: flex;
|
| 1582 |
}
|
| 1583 |
|
| 1584 |
-
|
| 1585 |
.channel-sidebar.show::before {
|
| 1586 |
content: '';
|
| 1587 |
position: fixed;
|
|
@@ -1593,7 +1593,7 @@
|
|
| 1593 |
z-index: -1;
|
| 1594 |
}
|
| 1595 |
|
| 1596 |
-
|
| 1597 |
.chat-area {
|
| 1598 |
height: 100vh;
|
| 1599 |
height: 100dvh;
|
|
@@ -1641,7 +1641,7 @@
|
|
| 1641 |
display: none;
|
| 1642 |
}
|
| 1643 |
|
| 1644 |
-
|
| 1645 |
#messages-container {
|
| 1646 |
flex: 1;
|
| 1647 |
min-height: 0;
|
|
@@ -1674,7 +1674,7 @@
|
|
| 1674 |
max-width: 90%;
|
| 1675 |
}
|
| 1676 |
|
| 1677 |
-
|
| 1678 |
.input-area {
|
| 1679 |
padding: 12px 16px;
|
| 1680 |
gap: 8px;
|
|
@@ -1694,7 +1694,7 @@
|
|
| 1694 |
flex-shrink: 0;
|
| 1695 |
}
|
| 1696 |
|
| 1697 |
-
|
| 1698 |
.chat-scroll-rail {
|
| 1699 |
right: 4px;
|
| 1700 |
top: 64px;
|
|
@@ -1706,7 +1706,7 @@
|
|
| 1706 |
min-height: 40px;
|
| 1707 |
}
|
| 1708 |
|
| 1709 |
-
|
| 1710 |
.picker-panel {
|
| 1711 |
width: 100%;
|
| 1712 |
left: 0;
|
|
@@ -1727,7 +1727,7 @@
|
|
| 1727 |
padding: 8px;
|
| 1728 |
}
|
| 1729 |
|
| 1730 |
-
|
| 1731 |
.settings-panel {
|
| 1732 |
width: 100%;
|
| 1733 |
right: -100%;
|
|
@@ -1762,7 +1762,7 @@
|
|
| 1762 |
border-bottom-color: var(--primary-color);
|
| 1763 |
}
|
| 1764 |
|
| 1765 |
-
|
| 1766 |
.toast-container {
|
| 1767 |
top: 10px;
|
| 1768 |
right: 10px;
|
|
@@ -1774,18 +1774,17 @@
|
|
| 1774 |
padding: 12px 16px;
|
| 1775 |
}
|
| 1776 |
|
| 1777 |
-
|
| 1778 |
.profile-panel {
|
| 1779 |
width: 100%;
|
| 1780 |
}
|
| 1781 |
|
| 1782 |
-
/* Hide members sidebar completely on mobile */
|
| 1783 |
.members-sidebar {
|
| 1784 |
display: none !important;
|
| 1785 |
}
|
| 1786 |
}
|
| 1787 |
|
| 1788 |
-
|
| 1789 |
.custom-dialog {
|
| 1790 |
position: fixed;
|
| 1791 |
top: 0;
|
|
@@ -1903,7 +1902,7 @@
|
|
| 1903 |
transform: translateY(-2px);
|
| 1904 |
}
|
| 1905 |
|
| 1906 |
-
|
| 1907 |
.community-modal {
|
| 1908 |
position: fixed;
|
| 1909 |
top: 0;
|
|
@@ -1975,7 +1974,7 @@
|
|
| 1975 |
background: rgba(255, 193, 7, 0.1);
|
| 1976 |
}
|
| 1977 |
|
| 1978 |
-
|
| 1979 |
.friend-search-container {
|
| 1980 |
padding: 12px 16px;
|
| 1981 |
border-bottom: 1px solid var(--border-color);
|
|
@@ -2011,7 +2010,7 @@
|
|
| 2011 |
font-size: 14px;
|
| 2012 |
}
|
| 2013 |
|
| 2014 |
-
|
| 2015 |
.qr-modal {
|
| 2016 |
position: fixed;
|
| 2017 |
top: 0;
|
|
@@ -2082,7 +2081,7 @@
|
|
| 2082 |
transform: translateY(-2px);
|
| 2083 |
}
|
| 2084 |
|
| 2085 |
-
|
| 2086 |
.media-preview-panel {
|
| 2087 |
position: fixed;
|
| 2088 |
bottom: -100%;
|
|
@@ -2165,14 +2164,14 @@
|
|
| 2165 |
object-fit: cover;
|
| 2166 |
}
|
| 2167 |
|
| 2168 |
-
|
| 2169 |
.danger-hover:hover {
|
| 2170 |
color: var(--danger) !important;
|
| 2171 |
transform: scale(1.08);
|
| 2172 |
filter: drop-shadow(0 0 8px rgba(240, 71, 71, 0.6));
|
| 2173 |
}
|
| 2174 |
|
| 2175 |
-
|
| 2176 |
.save-btn.danger-hover:hover {
|
| 2177 |
color: white !important;
|
| 2178 |
transform: scale(1.02) !important;
|
|
@@ -2190,7 +2189,7 @@
|
|
| 2190 |
filter: drop-shadow(0 0 8px rgba(88, 101, 242, 0.6));
|
| 2191 |
}
|
| 2192 |
|
| 2193 |
-
|
| 2194 |
@media (max-width: 768px) {
|
| 2195 |
.custom-dialog-content {
|
| 2196 |
min-width: 90%;
|
|
@@ -2213,7 +2212,7 @@
|
|
| 2213 |
}
|
| 2214 |
|
| 2215 |
@media (max-width: 480px) {
|
| 2216 |
-
|
| 2217 |
.main-container {
|
| 2218 |
grid-template-columns: 1fr;
|
| 2219 |
height: calc(100vh - 68px);
|
|
@@ -2271,7 +2270,7 @@
|
|
| 2271 |
display: inline-block;
|
| 2272 |
}
|
| 2273 |
|
| 2274 |
-
|
| 2275 |
.sidebar-overlay {
|
| 2276 |
display: none;
|
| 2277 |
position: fixed;
|
|
@@ -2296,7 +2295,7 @@
|
|
| 2296 |
max-width: 100%;
|
| 2297 |
}
|
| 2298 |
|
| 2299 |
-
|
| 2300 |
.chat-header {
|
| 2301 |
padding: 12px 16px;
|
| 2302 |
flex-shrink: 0;
|
|
@@ -2328,7 +2327,7 @@
|
|
| 2328 |
display: none;
|
| 2329 |
}
|
| 2330 |
|
| 2331 |
-
|
| 2332 |
#messages-container {
|
| 2333 |
flex: 1;
|
| 2334 |
min-height: 0;
|
|
@@ -2350,7 +2349,7 @@
|
|
| 2350 |
min-width: 32px;
|
| 2351 |
}
|
| 2352 |
|
| 2353 |
-
|
| 2354 |
.message-input-area {
|
| 2355 |
flex-shrink: 0;
|
| 2356 |
padding: 12px 16px;
|
|
@@ -2372,7 +2371,6 @@
|
|
| 2372 |
font-size: 14px;
|
| 2373 |
}
|
| 2374 |
|
| 2375 |
-
/* Scrollbar adjustment */
|
| 2376 |
.chat-scroll-rail {
|
| 2377 |
right: 4px;
|
| 2378 |
width: 8px;
|
|
@@ -2391,7 +2389,7 @@
|
|
| 2391 |
width: 100%;
|
| 2392 |
}
|
| 2393 |
|
| 2394 |
-
|
| 2395 |
.profile-panel {
|
| 2396 |
position: fixed;
|
| 2397 |
left: 0;
|
|
@@ -2407,7 +2405,6 @@
|
|
| 2407 |
border-radius: 0;
|
| 2408 |
}
|
| 2409 |
|
| 2410 |
-
/* Friends modal adjustments */
|
| 2411 |
.friends-modal {
|
| 2412 |
position: fixed;
|
| 2413 |
left: 0;
|
|
@@ -2444,7 +2441,7 @@
|
|
| 2444 |
</style>
|
| 2445 |
</head>
|
| 2446 |
<body>
|
| 2447 |
-
|
| 2448 |
<div class="auth-container" id="authContainer">
|
| 2449 |
<div class="auth-box">
|
| 2450 |
<h2 id="authTitle">Login</h2>
|
|
@@ -2485,9 +2482,9 @@
|
|
| 2485 |
</div>
|
| 2486 |
</div>
|
| 2487 |
|
| 2488 |
-
|
| 2489 |
<div class="main-container" id="mainApp">
|
| 2490 |
-
|
| 2491 |
<div class="guild-sidebar">
|
| 2492 |
<div class="guild-item active" onclick="showSection('chat')" title="Chat">
|
| 2493 |
<i class="fas fa-comment"></i>
|
|
@@ -2510,7 +2507,7 @@
|
|
| 2510 |
</div>
|
| 2511 |
</div>
|
| 2512 |
|
| 2513 |
-
|
| 2514 |
<div class="channel-sidebar" id="channelSidebar">
|
| 2515 |
<div class="sidebar-header">
|
| 2516 |
<h3>FireTech</h3>
|
|
@@ -2531,11 +2528,11 @@
|
|
| 2531 |
</div>
|
| 2532 |
</div>
|
| 2533 |
<div class="channels-list" id="channelsList">
|
| 2534 |
-
|
| 2535 |
</div>
|
| 2536 |
</div>
|
| 2537 |
|
| 2538 |
-
|
| 2539 |
<div class="chat-area">
|
| 2540 |
<div class="chat-header">
|
| 2541 |
<div class="chat-header-left">
|
|
@@ -2545,7 +2542,7 @@
|
|
| 2545 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 2546 |
</div>
|
| 2547 |
<div class="chat-header-actions">
|
| 2548 |
-
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.3.
|
| 2549 |
<button class="header-btn" onclick="toggleArchiveChat()" title="Archive chat">
|
| 2550 |
<i class="fas fa-archive"></i>
|
| 2551 |
</button>
|
|
@@ -2617,14 +2614,14 @@
|
|
| 2617 |
</div>
|
| 2618 |
</div>
|
| 2619 |
|
| 2620 |
-
|
| 2621 |
<div class="members-sidebar">
|
| 2622 |
<div class="members-header">👥 Online Members</div>
|
| 2623 |
<div class="members-list" id="membersList"></div>
|
| 2624 |
</div>
|
| 2625 |
</div>
|
| 2626 |
|
| 2627 |
-
|
| 2628 |
<div class="profile-panel" id="profilePanel">
|
| 2629 |
<button class="profile-close" onclick="closeProfile()"><i class="fas fa-times"></i></button>
|
| 2630 |
<div class="profile-header">
|
|
@@ -2638,7 +2635,7 @@
|
|
| 2638 |
<button class="tab-btn" onclick="switchProfileTab('inventory')">Inventory</button>
|
| 2639 |
</div>
|
| 2640 |
|
| 2641 |
-
|
| 2642 |
<div class="tab-content active" id="profileTab">
|
| 2643 |
<div class="form-field">
|
| 2644 |
<label>Profile Picture</label>
|
|
@@ -2666,14 +2663,13 @@
|
|
| 2666 |
<button class="save-btn" onclick="saveProfile()">Save Profile</button>
|
| 2667 |
</div>
|
| 2668 |
|
| 2669 |
-
|
| 2670 |
<div class="tab-content" id="inventoryTab">
|
| 2671 |
<div id="inventoryList"></div>
|
| 2672 |
</div>
|
| 2673 |
</div>
|
| 2674 |
</div>
|
| 2675 |
|
| 2676 |
-
<!-- FRIENDS MODAL -->
|
| 2677 |
<div class="modal hidden" id="friendsModal">
|
| 2678 |
<div class="modal-content">
|
| 2679 |
<h2><i class="fas fa-users"></i> Friends & Friend Requests</h2>
|
|
@@ -2685,7 +2681,7 @@
|
|
| 2685 |
</div>
|
| 2686 |
</div>
|
| 2687 |
|
| 2688 |
-
|
| 2689 |
<div class="modal hidden" id="shopModal">
|
| 2690 |
<div class="modal-content">
|
| 2691 |
<h2><i class="fas fa-store"></i> Shop</h2>
|
|
@@ -2695,21 +2691,21 @@
|
|
| 2695 |
</div>
|
| 2696 |
</div>
|
| 2697 |
|
| 2698 |
-
|
| 2699 |
<div class="toast-container" id="toastContainer"></div>
|
| 2700 |
|
| 2701 |
-
|
| 2702 |
<div class="picker-panel" id="pickerPanel">
|
| 2703 |
<div class="picker-tabs">
|
| 2704 |
<button class="picker-tab active" onclick="switchPickerTab('stickers')">😀 Stickers</button>
|
| 2705 |
<button class="picker-tab" onclick="switchPickerTab('gifs')">🎬 GIFs</button>
|
| 2706 |
</div>
|
| 2707 |
<div class="picker-content" id="pickerContent">
|
| 2708 |
-
|
| 2709 |
</div>
|
| 2710 |
</div>
|
| 2711 |
|
| 2712 |
-
|
| 2713 |
<div class="voice-recorder" id="voiceRecorder">
|
| 2714 |
<div class="record-timer" id="recordTimer">00:00</div>
|
| 2715 |
<button class="record-button" id="recordButton" onclick="toggleRecording()">
|
|
@@ -2725,7 +2721,7 @@
|
|
| 2725 |
</div>
|
| 2726 |
</div>
|
| 2727 |
|
| 2728 |
-
|
| 2729 |
<div class="settings-panel" id="settingsPanel">
|
| 2730 |
<div class="settings-header">
|
| 2731 |
<h2>⚙️ Settings</h2>
|
|
@@ -2741,7 +2737,7 @@
|
|
| 2741 |
<div class="settings-nav-item" onclick="switchSettingsTab('stats')">📊 Statistics</div>
|
| 2742 |
</div>
|
| 2743 |
<div class="settings-content">
|
| 2744 |
-
|
| 2745 |
<div class="settings-section active" id="accountSettings">
|
| 2746 |
<h3>Account Settings</h3>
|
| 2747 |
<div class="settings-item">
|
|
@@ -2768,7 +2764,7 @@
|
|
| 2768 |
</div>
|
| 2769 |
</div>
|
| 2770 |
|
| 2771 |
-
|
| 2772 |
<div class="settings-section" id="privacySettings">
|
| 2773 |
<h3>Privacy & Safety</h3>
|
| 2774 |
<div class="settings-item">
|
|
@@ -2795,7 +2791,7 @@
|
|
| 2795 |
</div>
|
| 2796 |
</div>
|
| 2797 |
|
| 2798 |
-
|
| 2799 |
<div class="settings-section" id="chatSettings">
|
| 2800 |
<h3>Chat Settings</h3>
|
| 2801 |
<div class="settings-item">
|
|
@@ -2837,7 +2833,7 @@
|
|
| 2837 |
</div>
|
| 2838 |
</div>
|
| 2839 |
|
| 2840 |
-
|
| 2841 |
<div class="settings-section" id="notificationSettings">
|
| 2842 |
<h3>Notifications</h3>
|
| 2843 |
<div class="settings-item">
|
|
@@ -2879,7 +2875,7 @@
|
|
| 2879 |
</div>
|
| 2880 |
</div>
|
| 2881 |
|
| 2882 |
-
|
| 2883 |
<div class="settings-section" id="appearanceSettings">
|
| 2884 |
<h3>Appearance</h3>
|
| 2885 |
<div class="settings-item">
|
|
@@ -2939,7 +2935,7 @@
|
|
| 2939 |
</div>
|
| 2940 |
</div>
|
| 2941 |
|
| 2942 |
-
|
| 2943 |
<div class="settings-section" id="statsSettings">
|
| 2944 |
<h3>📊 Your Statistics</h3>
|
| 2945 |
<div class="settings-item" style="padding: 20px; background: var(--bg-color); border-radius: 8px;">
|
|
@@ -2973,7 +2969,7 @@
|
|
| 2973 |
</div>
|
| 2974 |
</div>
|
| 2975 |
|
| 2976 |
-
|
| 2977 |
<div class="modal hidden" id="communitiesModal">
|
| 2978 |
<div class="modal-content">
|
| 2979 |
<h2><i class="fas fa-globe"></i> Communities</h2>
|
|
@@ -2983,7 +2979,7 @@
|
|
| 2983 |
</div>
|
| 2984 |
</div>
|
| 2985 |
|
| 2986 |
-
|
| 2987 |
<div class="modal hidden" id="groupsModal">
|
| 2988 |
<div class="modal-content">
|
| 2989 |
<h2><i class="fas fa-user-group"></i> Groups</h2>
|
|
@@ -2994,7 +2990,7 @@
|
|
| 2994 |
|
| 2995 |
</div>
|
| 2996 |
|
| 2997 |
-
|
| 2998 |
<div class="community-modal" id="communityDetailsModal">
|
| 2999 |
<div class="community-modal-content">
|
| 3000 |
<div class="community-modal-header">
|
|
@@ -3005,7 +3001,7 @@
|
|
| 3005 |
</div>
|
| 3006 |
</div>
|
| 3007 |
|
| 3008 |
-
|
| 3009 |
<div class="qr-modal" id="qrModal">
|
| 3010 |
<div class="qr-modal-content">
|
| 3011 |
<h2 class="qr-modal-title">📱 Friend Request QR Code</h2>
|
|
@@ -3018,7 +3014,7 @@
|
|
| 3018 |
</div>
|
| 3019 |
</div>
|
| 3020 |
|
| 3021 |
-
|
| 3022 |
<div class="media-preview-panel" id="mediaPreviewPanel">
|
| 3023 |
<div class="media-preview-header">
|
| 3024 |
<h3 class="media-preview-title">📸 Copied Media</h3>
|
|
@@ -3027,11 +3023,11 @@
|
|
| 3027 |
</button>
|
| 3028 |
</div>
|
| 3029 |
<div class="media-preview-content" id="mediaPreviewContent">
|
| 3030 |
-
|
| 3031 |
</div>
|
| 3032 |
</div>
|
| 3033 |
|
| 3034 |
-
|
| 3035 |
<div class="mobile-bottom-nav" id="mobileBottomNav">
|
| 3036 |
<button class="mobile-nav-item active" onclick="showMobileSection('chats')" data-section="chats">
|
| 3037 |
<i class="fas fa-comment"></i>
|
|
@@ -3055,10 +3051,10 @@
|
|
| 3055 |
</button>
|
| 3056 |
</div>
|
| 3057 |
|
| 3058 |
-
|
| 3059 |
<div class="sidebar-overlay" id="sidebarOverlay" onclick="closeMobileSidebar()"></div>
|
| 3060 |
|
| 3061 |
-
|
| 3062 |
<div class="mobile-members-panel" id="mobileMembersPanel">
|
| 3063 |
<div class="mobile-members-header">
|
| 3064 |
<h3>👥 Online Members</h3>
|
|
@@ -3067,11 +3063,11 @@
|
|
| 3067 |
</button>
|
| 3068 |
</div>
|
| 3069 |
<div class="mobile-members-list" id="mobileMembersList">
|
| 3070 |
-
|
| 3071 |
</div>
|
| 3072 |
</div>
|
| 3073 |
|
| 3074 |
-
|
| 3075 |
<div id="customDialog" style="display: none;"></div>
|
| 3076 |
|
| 3077 |
<script src="/socket.io/socket.io.js"></script>
|
|
|
|
| 27 |
--danger: #f04747;
|
| 28 |
}
|
| 29 |
|
| 30 |
+
|
| 31 |
[data-theme="light"] {
|
| 32 |
--primary-color: #5865f2;
|
| 33 |
--secondary-color: #99aab5;
|
|
|
|
| 50 |
overflow: hidden;
|
| 51 |
}
|
| 52 |
|
| 53 |
+
|
| 54 |
.auth-container {
|
| 55 |
position: fixed;
|
| 56 |
top: 0;
|
|
|
|
| 187 |
cursor: pointer;
|
| 188 |
}
|
| 189 |
|
| 190 |
+
|
| 191 |
.main-container {
|
| 192 |
display: grid;
|
| 193 |
grid-template-columns: 72px 240px 1fr 240px;
|
|
|
|
| 199 |
display: none;
|
| 200 |
}
|
| 201 |
|
| 202 |
+
|
| 203 |
.guild-sidebar {
|
| 204 |
background: var(--sidebar-bg);
|
| 205 |
display: flex;
|
|
|
|
| 241 |
margin: 8px 0;
|
| 242 |
}
|
| 243 |
|
| 244 |
+
|
| 245 |
.channel-sidebar {
|
| 246 |
background: var(--sidebar-bg);
|
| 247 |
border-right: 1px solid var(--border-color);
|
|
|
|
| 348 |
font-weight: bold;
|
| 349 |
}
|
| 350 |
|
| 351 |
+
|
| 352 |
.chat-area {
|
| 353 |
position: relative;
|
| 354 |
display: flex;
|
|
|
|
| 431 |
-webkit-overflow-scrolling: touch;
|
| 432 |
}
|
| 433 |
|
| 434 |
+
|
| 435 |
#messages-container::-webkit-scrollbar {
|
| 436 |
width: 0;
|
| 437 |
height: 0;
|
|
|
|
| 682 |
cursor: pointer;
|
| 683 |
}
|
| 684 |
|
| 685 |
+
|
| 686 |
|
| 687 |
.input-area {
|
| 688 |
padding: 16px 24px;
|
|
|
|
| 735 |
display: none;
|
| 736 |
}
|
| 737 |
|
| 738 |
+
|
| 739 |
.members-sidebar {
|
| 740 |
background: var(--sidebar-bg);
|
| 741 |
border-left: 1px solid var(--border-color);
|
|
|
|
| 784 |
background: var(--text-secondary);
|
| 785 |
}
|
| 786 |
|
| 787 |
+
|
| 788 |
.profile-panel {
|
| 789 |
position: fixed;
|
| 790 |
right: 0;
|
|
|
|
| 1076 |
to { opacity: 0; transform: translateX(100%); }
|
| 1077 |
}
|
| 1078 |
|
| 1079 |
+
|
| 1080 |
.picker-panel {
|
| 1081 |
position: absolute;
|
| 1082 |
bottom: 60px;
|
|
|
|
| 1147 |
object-fit: cover;
|
| 1148 |
}
|
| 1149 |
|
| 1150 |
+
|
| 1151 |
.voice-recorder {
|
| 1152 |
position: absolute;
|
| 1153 |
bottom: 70px;
|
|
|
|
| 1216 |
color: var(--danger);
|
| 1217 |
}
|
| 1218 |
|
| 1219 |
+
|
| 1220 |
.settings-panel {
|
| 1221 |
position: fixed;
|
| 1222 |
top: 0;
|
|
|
|
| 1367 |
transform: translateX(20px);
|
| 1368 |
}
|
| 1369 |
|
| 1370 |
+
|
| 1371 |
.modal {
|
| 1372 |
position: fixed;
|
| 1373 |
top: 0;
|
|
|
|
| 1396 |
overflow-y: auto;
|
| 1397 |
}
|
| 1398 |
|
| 1399 |
+
|
| 1400 |
::-webkit-scrollbar {
|
| 1401 |
width: 8px;
|
| 1402 |
}
|
|
|
|
| 1414 |
background: #5a5d60;
|
| 1415 |
}
|
| 1416 |
|
| 1417 |
+
|
| 1418 |
.mobile-bottom-nav {
|
| 1419 |
display: none;
|
| 1420 |
position: fixed;
|
|
|
|
| 1430 |
box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
|
| 1431 |
}
|
| 1432 |
|
| 1433 |
+
|
| 1434 |
.mobile-members-panel {
|
| 1435 |
position: fixed;
|
| 1436 |
right: -100%;
|
|
|
|
| 1512 |
text-align: center;
|
| 1513 |
}
|
| 1514 |
|
| 1515 |
+
|
| 1516 |
@media (max-width: 1200px) {
|
| 1517 |
.main-container {
|
| 1518 |
grid-template-columns: 72px 240px 1fr;
|
|
|
|
| 1532 |
}
|
| 1533 |
}
|
| 1534 |
|
| 1535 |
+
|
| 1536 |
@media (max-width: 768px) {
|
| 1537 |
body {
|
| 1538 |
overflow: hidden;
|
|
|
|
| 1545 |
max-height: -webkit-fill-available;
|
| 1546 |
}
|
| 1547 |
|
| 1548 |
+
|
| 1549 |
.guild-sidebar {
|
| 1550 |
padding: 2px 0;
|
| 1551 |
width: 45px;
|
|
|
|
| 1581 |
display: flex;
|
| 1582 |
}
|
| 1583 |
|
| 1584 |
+
|
| 1585 |
.channel-sidebar.show::before {
|
| 1586 |
content: '';
|
| 1587 |
position: fixed;
|
|
|
|
| 1593 |
z-index: -1;
|
| 1594 |
}
|
| 1595 |
|
| 1596 |
+
|
| 1597 |
.chat-area {
|
| 1598 |
height: 100vh;
|
| 1599 |
height: 100dvh;
|
|
|
|
| 1641 |
display: none;
|
| 1642 |
}
|
| 1643 |
|
| 1644 |
+
|
| 1645 |
#messages-container {
|
| 1646 |
flex: 1;
|
| 1647 |
min-height: 0;
|
|
|
|
| 1674 |
max-width: 90%;
|
| 1675 |
}
|
| 1676 |
|
| 1677 |
+
|
| 1678 |
.input-area {
|
| 1679 |
padding: 12px 16px;
|
| 1680 |
gap: 8px;
|
|
|
|
| 1694 |
flex-shrink: 0;
|
| 1695 |
}
|
| 1696 |
|
| 1697 |
+
|
| 1698 |
.chat-scroll-rail {
|
| 1699 |
right: 4px;
|
| 1700 |
top: 64px;
|
|
|
|
| 1706 |
min-height: 40px;
|
| 1707 |
}
|
| 1708 |
|
| 1709 |
+
|
| 1710 |
.picker-panel {
|
| 1711 |
width: 100%;
|
| 1712 |
left: 0;
|
|
|
|
| 1727 |
padding: 8px;
|
| 1728 |
}
|
| 1729 |
|
| 1730 |
+
|
| 1731 |
.settings-panel {
|
| 1732 |
width: 100%;
|
| 1733 |
right: -100%;
|
|
|
|
| 1762 |
border-bottom-color: var(--primary-color);
|
| 1763 |
}
|
| 1764 |
|
| 1765 |
+
|
| 1766 |
.toast-container {
|
| 1767 |
top: 10px;
|
| 1768 |
right: 10px;
|
|
|
|
| 1774 |
padding: 12px 16px;
|
| 1775 |
}
|
| 1776 |
|
| 1777 |
+
|
| 1778 |
.profile-panel {
|
| 1779 |
width: 100%;
|
| 1780 |
}
|
| 1781 |
|
|
|
|
| 1782 |
.members-sidebar {
|
| 1783 |
display: none !important;
|
| 1784 |
}
|
| 1785 |
}
|
| 1786 |
|
| 1787 |
+
|
| 1788 |
.custom-dialog {
|
| 1789 |
position: fixed;
|
| 1790 |
top: 0;
|
|
|
|
| 1902 |
transform: translateY(-2px);
|
| 1903 |
}
|
| 1904 |
|
| 1905 |
+
|
| 1906 |
.community-modal {
|
| 1907 |
position: fixed;
|
| 1908 |
top: 0;
|
|
|
|
| 1974 |
background: rgba(255, 193, 7, 0.1);
|
| 1975 |
}
|
| 1976 |
|
| 1977 |
+
|
| 1978 |
.friend-search-container {
|
| 1979 |
padding: 12px 16px;
|
| 1980 |
border-bottom: 1px solid var(--border-color);
|
|
|
|
| 2010 |
font-size: 14px;
|
| 2011 |
}
|
| 2012 |
|
| 2013 |
+
|
| 2014 |
.qr-modal {
|
| 2015 |
position: fixed;
|
| 2016 |
top: 0;
|
|
|
|
| 2081 |
transform: translateY(-2px);
|
| 2082 |
}
|
| 2083 |
|
| 2084 |
+
|
| 2085 |
.media-preview-panel {
|
| 2086 |
position: fixed;
|
| 2087 |
bottom: -100%;
|
|
|
|
| 2164 |
object-fit: cover;
|
| 2165 |
}
|
| 2166 |
|
| 2167 |
+
|
| 2168 |
.danger-hover:hover {
|
| 2169 |
color: var(--danger) !important;
|
| 2170 |
transform: scale(1.08);
|
| 2171 |
filter: drop-shadow(0 0 8px rgba(240, 71, 71, 0.6));
|
| 2172 |
}
|
| 2173 |
|
| 2174 |
+
|
| 2175 |
.save-btn.danger-hover:hover {
|
| 2176 |
color: white !important;
|
| 2177 |
transform: scale(1.02) !important;
|
|
|
|
| 2189 |
filter: drop-shadow(0 0 8px rgba(88, 101, 242, 0.6));
|
| 2190 |
}
|
| 2191 |
|
| 2192 |
+
|
| 2193 |
@media (max-width: 768px) {
|
| 2194 |
.custom-dialog-content {
|
| 2195 |
min-width: 90%;
|
|
|
|
| 2212 |
}
|
| 2213 |
|
| 2214 |
@media (max-width: 480px) {
|
| 2215 |
+
|
| 2216 |
.main-container {
|
| 2217 |
grid-template-columns: 1fr;
|
| 2218 |
height: calc(100vh - 68px);
|
|
|
|
| 2270 |
display: inline-block;
|
| 2271 |
}
|
| 2272 |
|
| 2273 |
+
|
| 2274 |
.sidebar-overlay {
|
| 2275 |
display: none;
|
| 2276 |
position: fixed;
|
|
|
|
| 2295 |
max-width: 100%;
|
| 2296 |
}
|
| 2297 |
|
| 2298 |
+
|
| 2299 |
.chat-header {
|
| 2300 |
padding: 12px 16px;
|
| 2301 |
flex-shrink: 0;
|
|
|
|
| 2327 |
display: none;
|
| 2328 |
}
|
| 2329 |
|
| 2330 |
+
|
| 2331 |
#messages-container {
|
| 2332 |
flex: 1;
|
| 2333 |
min-height: 0;
|
|
|
|
| 2349 |
min-width: 32px;
|
| 2350 |
}
|
| 2351 |
|
| 2352 |
+
|
| 2353 |
.message-input-area {
|
| 2354 |
flex-shrink: 0;
|
| 2355 |
padding: 12px 16px;
|
|
|
|
| 2371 |
font-size: 14px;
|
| 2372 |
}
|
| 2373 |
|
|
|
|
| 2374 |
.chat-scroll-rail {
|
| 2375 |
right: 4px;
|
| 2376 |
width: 8px;
|
|
|
|
| 2389 |
width: 100%;
|
| 2390 |
}
|
| 2391 |
|
| 2392 |
+
|
| 2393 |
.profile-panel {
|
| 2394 |
position: fixed;
|
| 2395 |
left: 0;
|
|
|
|
| 2405 |
border-radius: 0;
|
| 2406 |
}
|
| 2407 |
|
|
|
|
| 2408 |
.friends-modal {
|
| 2409 |
position: fixed;
|
| 2410 |
left: 0;
|
|
|
|
| 2441 |
</style>
|
| 2442 |
</head>
|
| 2443 |
<body>
|
| 2444 |
+
|
| 2445 |
<div class="auth-container" id="authContainer">
|
| 2446 |
<div class="auth-box">
|
| 2447 |
<h2 id="authTitle">Login</h2>
|
|
|
|
| 2482 |
</div>
|
| 2483 |
</div>
|
| 2484 |
|
| 2485 |
+
|
| 2486 |
<div class="main-container" id="mainApp">
|
| 2487 |
+
|
| 2488 |
<div class="guild-sidebar">
|
| 2489 |
<div class="guild-item active" onclick="showSection('chat')" title="Chat">
|
| 2490 |
<i class="fas fa-comment"></i>
|
|
|
|
| 2507 |
</div>
|
| 2508 |
</div>
|
| 2509 |
|
| 2510 |
+
|
| 2511 |
<div class="channel-sidebar" id="channelSidebar">
|
| 2512 |
<div class="sidebar-header">
|
| 2513 |
<h3>FireTech</h3>
|
|
|
|
| 2528 |
</div>
|
| 2529 |
</div>
|
| 2530 |
<div class="channels-list" id="channelsList">
|
| 2531 |
+
|
| 2532 |
</div>
|
| 2533 |
</div>
|
| 2534 |
|
| 2535 |
+
|
| 2536 |
<div class="chat-area">
|
| 2537 |
<div class="chat-header">
|
| 2538 |
<div class="chat-header-left">
|
|
|
|
| 2542 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 2543 |
</div>
|
| 2544 |
<div class="chat-header-actions">
|
| 2545 |
+
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.3.1</span>
|
| 2546 |
<button class="header-btn" onclick="toggleArchiveChat()" title="Archive chat">
|
| 2547 |
<i class="fas fa-archive"></i>
|
| 2548 |
</button>
|
|
|
|
| 2614 |
</div>
|
| 2615 |
</div>
|
| 2616 |
|
| 2617 |
+
|
| 2618 |
<div class="members-sidebar">
|
| 2619 |
<div class="members-header">👥 Online Members</div>
|
| 2620 |
<div class="members-list" id="membersList"></div>
|
| 2621 |
</div>
|
| 2622 |
</div>
|
| 2623 |
|
| 2624 |
+
|
| 2625 |
<div class="profile-panel" id="profilePanel">
|
| 2626 |
<button class="profile-close" onclick="closeProfile()"><i class="fas fa-times"></i></button>
|
| 2627 |
<div class="profile-header">
|
|
|
|
| 2635 |
<button class="tab-btn" onclick="switchProfileTab('inventory')">Inventory</button>
|
| 2636 |
</div>
|
| 2637 |
|
| 2638 |
+
|
| 2639 |
<div class="tab-content active" id="profileTab">
|
| 2640 |
<div class="form-field">
|
| 2641 |
<label>Profile Picture</label>
|
|
|
|
| 2663 |
<button class="save-btn" onclick="saveProfile()">Save Profile</button>
|
| 2664 |
</div>
|
| 2665 |
|
| 2666 |
+
|
| 2667 |
<div class="tab-content" id="inventoryTab">
|
| 2668 |
<div id="inventoryList"></div>
|
| 2669 |
</div>
|
| 2670 |
</div>
|
| 2671 |
</div>
|
| 2672 |
|
|
|
|
| 2673 |
<div class="modal hidden" id="friendsModal">
|
| 2674 |
<div class="modal-content">
|
| 2675 |
<h2><i class="fas fa-users"></i> Friends & Friend Requests</h2>
|
|
|
|
| 2681 |
</div>
|
| 2682 |
</div>
|
| 2683 |
|
| 2684 |
+
|
| 2685 |
<div class="modal hidden" id="shopModal">
|
| 2686 |
<div class="modal-content">
|
| 2687 |
<h2><i class="fas fa-store"></i> Shop</h2>
|
|
|
|
| 2691 |
</div>
|
| 2692 |
</div>
|
| 2693 |
|
| 2694 |
+
|
| 2695 |
<div class="toast-container" id="toastContainer"></div>
|
| 2696 |
|
| 2697 |
+
|
| 2698 |
<div class="picker-panel" id="pickerPanel">
|
| 2699 |
<div class="picker-tabs">
|
| 2700 |
<button class="picker-tab active" onclick="switchPickerTab('stickers')">😀 Stickers</button>
|
| 2701 |
<button class="picker-tab" onclick="switchPickerTab('gifs')">🎬 GIFs</button>
|
| 2702 |
</div>
|
| 2703 |
<div class="picker-content" id="pickerContent">
|
| 2704 |
+
|
| 2705 |
</div>
|
| 2706 |
</div>
|
| 2707 |
|
| 2708 |
+
|
| 2709 |
<div class="voice-recorder" id="voiceRecorder">
|
| 2710 |
<div class="record-timer" id="recordTimer">00:00</div>
|
| 2711 |
<button class="record-button" id="recordButton" onclick="toggleRecording()">
|
|
|
|
| 2721 |
</div>
|
| 2722 |
</div>
|
| 2723 |
|
| 2724 |
+
|
| 2725 |
<div class="settings-panel" id="settingsPanel">
|
| 2726 |
<div class="settings-header">
|
| 2727 |
<h2>⚙️ Settings</h2>
|
|
|
|
| 2737 |
<div class="settings-nav-item" onclick="switchSettingsTab('stats')">📊 Statistics</div>
|
| 2738 |
</div>
|
| 2739 |
<div class="settings-content">
|
| 2740 |
+
|
| 2741 |
<div class="settings-section active" id="accountSettings">
|
| 2742 |
<h3>Account Settings</h3>
|
| 2743 |
<div class="settings-item">
|
|
|
|
| 2764 |
</div>
|
| 2765 |
</div>
|
| 2766 |
|
| 2767 |
+
|
| 2768 |
<div class="settings-section" id="privacySettings">
|
| 2769 |
<h3>Privacy & Safety</h3>
|
| 2770 |
<div class="settings-item">
|
|
|
|
| 2791 |
</div>
|
| 2792 |
</div>
|
| 2793 |
|
| 2794 |
+
|
| 2795 |
<div class="settings-section" id="chatSettings">
|
| 2796 |
<h3>Chat Settings</h3>
|
| 2797 |
<div class="settings-item">
|
|
|
|
| 2833 |
</div>
|
| 2834 |
</div>
|
| 2835 |
|
| 2836 |
+
|
| 2837 |
<div class="settings-section" id="notificationSettings">
|
| 2838 |
<h3>Notifications</h3>
|
| 2839 |
<div class="settings-item">
|
|
|
|
| 2875 |
</div>
|
| 2876 |
</div>
|
| 2877 |
|
| 2878 |
+
|
| 2879 |
<div class="settings-section" id="appearanceSettings">
|
| 2880 |
<h3>Appearance</h3>
|
| 2881 |
<div class="settings-item">
|
|
|
|
| 2935 |
</div>
|
| 2936 |
</div>
|
| 2937 |
|
| 2938 |
+
|
| 2939 |
<div class="settings-section" id="statsSettings">
|
| 2940 |
<h3>📊 Your Statistics</h3>
|
| 2941 |
<div class="settings-item" style="padding: 20px; background: var(--bg-color); border-radius: 8px;">
|
|
|
|
| 2969 |
</div>
|
| 2970 |
</div>
|
| 2971 |
|
| 2972 |
+
|
| 2973 |
<div class="modal hidden" id="communitiesModal">
|
| 2974 |
<div class="modal-content">
|
| 2975 |
<h2><i class="fas fa-globe"></i> Communities</h2>
|
|
|
|
| 2979 |
</div>
|
| 2980 |
</div>
|
| 2981 |
|
| 2982 |
+
|
| 2983 |
<div class="modal hidden" id="groupsModal">
|
| 2984 |
<div class="modal-content">
|
| 2985 |
<h2><i class="fas fa-user-group"></i> Groups</h2>
|
|
|
|
| 2990 |
|
| 2991 |
</div>
|
| 2992 |
|
| 2993 |
+
|
| 2994 |
<div class="community-modal" id="communityDetailsModal">
|
| 2995 |
<div class="community-modal-content">
|
| 2996 |
<div class="community-modal-header">
|
|
|
|
| 3001 |
</div>
|
| 3002 |
</div>
|
| 3003 |
|
| 3004 |
+
|
| 3005 |
<div class="qr-modal" id="qrModal">
|
| 3006 |
<div class="qr-modal-content">
|
| 3007 |
<h2 class="qr-modal-title">📱 Friend Request QR Code</h2>
|
|
|
|
| 3014 |
</div>
|
| 3015 |
</div>
|
| 3016 |
|
| 3017 |
+
|
| 3018 |
<div class="media-preview-panel" id="mediaPreviewPanel">
|
| 3019 |
<div class="media-preview-header">
|
| 3020 |
<h3 class="media-preview-title">📸 Copied Media</h3>
|
|
|
|
| 3023 |
</button>
|
| 3024 |
</div>
|
| 3025 |
<div class="media-preview-content" id="mediaPreviewContent">
|
| 3026 |
+
|
| 3027 |
</div>
|
| 3028 |
</div>
|
| 3029 |
|
| 3030 |
+
|
| 3031 |
<div class="mobile-bottom-nav" id="mobileBottomNav">
|
| 3032 |
<button class="mobile-nav-item active" onclick="showMobileSection('chats')" data-section="chats">
|
| 3033 |
<i class="fas fa-comment"></i>
|
|
|
|
| 3051 |
</button>
|
| 3052 |
</div>
|
| 3053 |
|
| 3054 |
+
|
| 3055 |
<div class="sidebar-overlay" id="sidebarOverlay" onclick="closeMobileSidebar()"></div>
|
| 3056 |
|
| 3057 |
+
|
| 3058 |
<div class="mobile-members-panel" id="mobileMembersPanel">
|
| 3059 |
<div class="mobile-members-header">
|
| 3060 |
<h3>👥 Online Members</h3>
|
|
|
|
| 3063 |
</button>
|
| 3064 |
</div>
|
| 3065 |
<div class="mobile-members-list" id="mobileMembersList">
|
| 3066 |
+
|
| 3067 |
</div>
|
| 3068 |
</div>
|
| 3069 |
|
| 3070 |
+
|
| 3071 |
<div id="customDialog" style="display: none;"></div>
|
| 3072 |
|
| 3073 |
<script src="/socket.io/socket.io.js"></script>
|
init-db.js
CHANGED
|
@@ -66,15 +66,15 @@ const defaultItems = [
|
|
| 66 |
|
| 67 |
async function initializeDatabase() {
|
| 68 |
try {
|
| 69 |
-
|
| 70 |
await ShopItem.deleteMany({});
|
| 71 |
console.log('Cleared existing shop items');
|
| 72 |
|
| 73 |
-
|
| 74 |
await ShopItem.insertMany(defaultItems);
|
| 75 |
console.log(`✅ Successfully added ${defaultItems.length} shop items to database`);
|
| 76 |
|
| 77 |
-
|
| 78 |
const items = await ShopItem.find();
|
| 79 |
console.log('\n📦 Shop Items:');
|
| 80 |
items.forEach(item => {
|
|
|
|
| 66 |
|
| 67 |
async function initializeDatabase() {
|
| 68 |
try {
|
| 69 |
+
|
| 70 |
await ShopItem.deleteMany({});
|
| 71 |
console.log('Cleared existing shop items');
|
| 72 |
|
| 73 |
+
|
| 74 |
await ShopItem.insertMany(defaultItems);
|
| 75 |
console.log(`✅ Successfully added ${defaultItems.length} shop items to database`);
|
| 76 |
|
| 77 |
+
|
| 78 |
const items = await ShopItem.find();
|
| 79 |
console.log('\n📦 Shop Items:');
|
| 80 |
items.forEach(item => {
|
mobile-connect.ps1
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
-
|
| 2 |
-
# Run as Administrator
|
| 3 |
|
| 4 |
Write-Host "╔════════════════════════════════════════╗" -ForegroundColor Cyan
|
| 5 |
Write-Host "║ Mobile Connection Setup - Complete ║" -ForegroundColor Cyan
|
| 6 |
Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Cyan
|
| 7 |
Write-Host ""
|
| 8 |
|
| 9 |
-
|
| 10 |
Write-Host "Finding your PC Wi-Fi IP address..." -ForegroundColor Yellow
|
| 11 |
$wifiIP = $null
|
| 12 |
try {
|
|
@@ -26,14 +25,14 @@ if (!$wifiIP) {
|
|
| 26 |
Write-Host "Found Wi-Fi IP: $wifiIP" -ForegroundColor Green
|
| 27 |
Write-Host ""
|
| 28 |
|
| 29 |
-
|
| 30 |
Write-Host "Stopping existing servers..." -ForegroundColor Yellow
|
| 31 |
taskkill /F /IM node.exe 2>$null | Out-Null
|
| 32 |
Start-Sleep -Seconds 2
|
| 33 |
Write-Host "OK - Stopped" -ForegroundColor Green
|
| 34 |
Write-Host ""
|
| 35 |
|
| 36 |
-
|
| 37 |
Write-Host "Checking certificate..." -ForegroundColor Yellow
|
| 38 |
$pfxPath = "C:\Users\dell\Downloads\message\certs\lan-localhost.pfx"
|
| 39 |
if (!(Test-Path $pfxPath)) {
|
|
@@ -44,7 +43,7 @@ if (!(Test-Path $pfxPath)) {
|
|
| 44 |
Write-Host "OK - Certificate ready" -ForegroundColor Green
|
| 45 |
Write-Host ""
|
| 46 |
|
| 47 |
-
|
| 48 |
Write-Host "Adding firewall rule..." -ForegroundColor Yellow
|
| 49 |
try {
|
| 50 |
Remove-NetFirewallRule -DisplayName "Chat App 3001" -ErrorAction SilentlyContinue
|
|
@@ -55,7 +54,7 @@ try {
|
|
| 55 |
}
|
| 56 |
Write-Host ""
|
| 57 |
|
| 58 |
-
|
| 59 |
Write-Host "Starting server..." -ForegroundColor Yellow
|
| 60 |
Write-Host ""
|
| 61 |
|
|
|
|
| 1 |
+
|
|
|
|
| 2 |
|
| 3 |
Write-Host "╔════════════════════════════════════════╗" -ForegroundColor Cyan
|
| 4 |
Write-Host "║ Mobile Connection Setup - Complete ║" -ForegroundColor Cyan
|
| 5 |
Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Cyan
|
| 6 |
Write-Host ""
|
| 7 |
|
| 8 |
+
|
| 9 |
Write-Host "Finding your PC Wi-Fi IP address..." -ForegroundColor Yellow
|
| 10 |
$wifiIP = $null
|
| 11 |
try {
|
|
|
|
| 25 |
Write-Host "Found Wi-Fi IP: $wifiIP" -ForegroundColor Green
|
| 26 |
Write-Host ""
|
| 27 |
|
| 28 |
+
|
| 29 |
Write-Host "Stopping existing servers..." -ForegroundColor Yellow
|
| 30 |
taskkill /F /IM node.exe 2>$null | Out-Null
|
| 31 |
Start-Sleep -Seconds 2
|
| 32 |
Write-Host "OK - Stopped" -ForegroundColor Green
|
| 33 |
Write-Host ""
|
| 34 |
|
| 35 |
+
|
| 36 |
Write-Host "Checking certificate..." -ForegroundColor Yellow
|
| 37 |
$pfxPath = "C:\Users\dell\Downloads\message\certs\lan-localhost.pfx"
|
| 38 |
if (!(Test-Path $pfxPath)) {
|
|
|
|
| 43 |
Write-Host "OK - Certificate ready" -ForegroundColor Green
|
| 44 |
Write-Host ""
|
| 45 |
|
| 46 |
+
|
| 47 |
Write-Host "Adding firewall rule..." -ForegroundColor Yellow
|
| 48 |
try {
|
| 49 |
Remove-NetFirewallRule -DisplayName "Chat App 3001" -ErrorAction SilentlyContinue
|
|
|
|
| 54 |
}
|
| 55 |
Write-Host ""
|
| 56 |
|
| 57 |
+
|
| 58 |
Write-Host "Starting server..." -ForegroundColor Yellow
|
| 59 |
Write-Host ""
|
| 60 |
|
package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
-
"version": "2.
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "discord-like-app",
|
| 9 |
-
"version": "2.
|
| 10 |
"dependencies": {
|
| 11 |
"bcryptjs": "^2.4.3",
|
| 12 |
"body-parser": "^1.20.2",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
+
"version": "2.3.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "discord-like-app",
|
| 9 |
+
"version": "2.3.0",
|
| 10 |
"dependencies": {
|
| 11 |
"bcryptjs": "^2.4.3",
|
| 12 |
"body-parser": "^1.20.2",
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
-
"version": "2.3.
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
+
"version": "2.3.1",
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|
server.js
CHANGED
|
@@ -14,7 +14,7 @@ require('dotenv').config();
|
|
| 14 |
|
| 15 |
const app = express();
|
| 16 |
|
| 17 |
-
|
| 18 |
const cors = require('cors');
|
| 19 |
const allowedOrigins = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map(o => o.trim()).filter(Boolean) : ['*'];
|
| 20 |
|
|
@@ -42,7 +42,6 @@ app.use(cors({
|
|
| 42 |
allowedHeaders: ['Content-Type', 'Authorization']
|
| 43 |
}));
|
| 44 |
|
| 45 |
-
// Middleware
|
| 46 |
app.use(bodyParser.json({ limit: '50mb' }));
|
| 47 |
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
| 48 |
|
|
@@ -67,8 +66,6 @@ app.get('/api/version', (req, res) => {
|
|
| 67 |
}
|
| 68 |
});
|
| 69 |
|
| 70 |
-
// ======================== DATABASE CONNECTION ========================
|
| 71 |
-
|
| 72 |
const connectionString = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/discord-app';
|
| 73 |
const isLocalConnection = connectionString.includes('localhost') || connectionString.includes('127.0.0.1');
|
| 74 |
|
|
@@ -79,7 +76,7 @@ const sequelize = new Sequelize(connectionString, {
|
|
| 79 |
dialectOptions: isLocalConnection ? {} : { ssl: { require: true, rejectUnauthorized: false } }
|
| 80 |
});
|
| 81 |
|
| 82 |
-
|
| 83 |
|
| 84 |
const User = sequelize.define('User', {
|
| 85 |
id: {
|
|
@@ -215,7 +212,7 @@ const DirectMessage = sequelize.define('DirectMessage', {
|
|
| 215 |
mediaUrl: DataTypes.TEXT
|
| 216 |
});
|
| 217 |
|
| 218 |
-
|
| 219 |
|
| 220 |
User.belongsToMany(User, {
|
| 221 |
as: 'friends',
|
|
@@ -245,7 +242,7 @@ FriendRequest.belongsTo(User, { as: 'to', foreignKey: 'toId' });
|
|
| 245 |
DirectMessage.belongsTo(User, { as: 'sender', foreignKey: 'fromId' });
|
| 246 |
DirectMessage.belongsTo(User, { as: 'recipient', foreignKey: 'toId' });
|
| 247 |
|
| 248 |
-
|
| 249 |
|
| 250 |
const authenticateToken = (req, res, next) => {
|
| 251 |
const authHeader = req.headers['authorization'];
|
|
@@ -260,7 +257,7 @@ const authenticateToken = (req, res, next) => {
|
|
| 260 |
});
|
| 261 |
};
|
| 262 |
|
| 263 |
-
|
| 264 |
|
| 265 |
app.post('/api/register', async (req, res) => {
|
| 266 |
try {
|
|
@@ -355,7 +352,6 @@ app.post('/api/reset-password', async (req, res) => {
|
|
| 355 |
}
|
| 356 |
});
|
| 357 |
|
| 358 |
-
// ======================== PROFILE ROUTES ========================
|
| 359 |
|
| 360 |
app.get('/api/profile/:userId', authenticateToken, async (req, res) => {
|
| 361 |
try {
|
|
@@ -457,7 +453,7 @@ app.post('/api/profile/upload', authenticateToken, async (req, res) => {
|
|
| 457 |
}
|
| 458 |
});
|
| 459 |
|
| 460 |
-
|
| 461 |
|
| 462 |
app.post('/api/friends/request', authenticateToken, async (req, res) => {
|
| 463 |
try {
|
|
@@ -583,7 +579,7 @@ app.get('/api/friends', authenticateToken, async (req, res) => {
|
|
| 583 |
}
|
| 584 |
});
|
| 585 |
|
| 586 |
-
|
| 587 |
|
| 588 |
app.get('/api/dms/:friendId', authenticateToken, async (req, res) => {
|
| 589 |
try {
|
|
@@ -658,7 +654,7 @@ app.delete('/api/dms/:friendId', authenticateToken, async (req, res) => {
|
|
| 658 |
}
|
| 659 |
});
|
| 660 |
|
| 661 |
-
|
| 662 |
emitToUser(friendId, 'dm history cleared', {
|
| 663 |
fromUserId: req.user.userId,
|
| 664 |
clearedAt: new Date().toISOString()
|
|
@@ -670,7 +666,7 @@ app.delete('/api/dms/:friendId', authenticateToken, async (req, res) => {
|
|
| 670 |
}
|
| 671 |
});
|
| 672 |
|
| 673 |
-
|
| 674 |
|
| 675 |
app.get('/api/shop', authenticateToken, async (req, res) => {
|
| 676 |
try {
|
|
@@ -713,7 +709,7 @@ app.get('/api/inventory', authenticateToken, async (req, res) => {
|
|
| 713 |
}
|
| 714 |
});
|
| 715 |
|
| 716 |
-
|
| 717 |
|
| 718 |
function createServer(appInstance) {
|
| 719 |
const pfxPath = process.env.SSL_PFX_PATH;
|
|
@@ -862,7 +858,7 @@ io.on('connection', (socket) => {
|
|
| 862 |
timestamp: dm.createdAt
|
| 863 |
};
|
| 864 |
|
| 865 |
-
|
| 866 |
emitToUser(socket.userId, 'dm message', dmPayload);
|
| 867 |
const deliveredToRecipient = emitToUser(toUserId, 'dm message', dmPayload);
|
| 868 |
|
|
@@ -1176,7 +1172,6 @@ io.on('connection', (socket) => {
|
|
| 1176 |
});
|
| 1177 |
});
|
| 1178 |
|
| 1179 |
-
// ======================== INITIALIZATION ========================
|
| 1180 |
|
| 1181 |
function getLocalNetworkAddress() {
|
| 1182 |
const interfaces = os.networkInterfaces();
|
|
@@ -1193,10 +1188,10 @@ function getLocalNetworkAddress() {
|
|
| 1193 |
const PORT = process.env.PORT || 3000;
|
| 1194 |
const HOST = process.env.HOST || '0.0.0.0';
|
| 1195 |
|
| 1196 |
-
|
| 1197 |
async function migrateMediaTypeEnum() {
|
| 1198 |
try {
|
| 1199 |
-
|
| 1200 |
const [results] = await sequelize.query(`
|
| 1201 |
SELECT EXISTS (
|
| 1202 |
SELECT 1 FROM pg_type t
|
|
@@ -1209,7 +1204,7 @@ async function migrateMediaTypeEnum() {
|
|
| 1209 |
if (!results[0].has_voice) {
|
| 1210 |
console.log('📦 Migrating mediaType ENUM to include sticker, gif, voice...');
|
| 1211 |
|
| 1212 |
-
|
| 1213 |
await sequelize.query(`
|
| 1214 |
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'sticker';
|
| 1215 |
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'gif';
|
|
|
|
| 14 |
|
| 15 |
const app = express();
|
| 16 |
|
| 17 |
+
|
| 18 |
const cors = require('cors');
|
| 19 |
const allowedOrigins = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map(o => o.trim()).filter(Boolean) : ['*'];
|
| 20 |
|
|
|
|
| 42 |
allowedHeaders: ['Content-Type', 'Authorization']
|
| 43 |
}));
|
| 44 |
|
|
|
|
| 45 |
app.use(bodyParser.json({ limit: '50mb' }));
|
| 46 |
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
| 47 |
|
|
|
|
| 66 |
}
|
| 67 |
});
|
| 68 |
|
|
|
|
|
|
|
| 69 |
const connectionString = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/discord-app';
|
| 70 |
const isLocalConnection = connectionString.includes('localhost') || connectionString.includes('127.0.0.1');
|
| 71 |
|
|
|
|
| 76 |
dialectOptions: isLocalConnection ? {} : { ssl: { require: true, rejectUnauthorized: false } }
|
| 77 |
});
|
| 78 |
|
| 79 |
+
|
| 80 |
|
| 81 |
const User = sequelize.define('User', {
|
| 82 |
id: {
|
|
|
|
| 212 |
mediaUrl: DataTypes.TEXT
|
| 213 |
});
|
| 214 |
|
| 215 |
+
|
| 216 |
|
| 217 |
User.belongsToMany(User, {
|
| 218 |
as: 'friends',
|
|
|
|
| 242 |
DirectMessage.belongsTo(User, { as: 'sender', foreignKey: 'fromId' });
|
| 243 |
DirectMessage.belongsTo(User, { as: 'recipient', foreignKey: 'toId' });
|
| 244 |
|
| 245 |
+
|
| 246 |
|
| 247 |
const authenticateToken = (req, res, next) => {
|
| 248 |
const authHeader = req.headers['authorization'];
|
|
|
|
| 257 |
});
|
| 258 |
};
|
| 259 |
|
| 260 |
+
|
| 261 |
|
| 262 |
app.post('/api/register', async (req, res) => {
|
| 263 |
try {
|
|
|
|
| 352 |
}
|
| 353 |
});
|
| 354 |
|
|
|
|
| 355 |
|
| 356 |
app.get('/api/profile/:userId', authenticateToken, async (req, res) => {
|
| 357 |
try {
|
|
|
|
| 453 |
}
|
| 454 |
});
|
| 455 |
|
| 456 |
+
|
| 457 |
|
| 458 |
app.post('/api/friends/request', authenticateToken, async (req, res) => {
|
| 459 |
try {
|
|
|
|
| 579 |
}
|
| 580 |
});
|
| 581 |
|
| 582 |
+
|
| 583 |
|
| 584 |
app.get('/api/dms/:friendId', authenticateToken, async (req, res) => {
|
| 585 |
try {
|
|
|
|
| 654 |
}
|
| 655 |
});
|
| 656 |
|
| 657 |
+
|
| 658 |
emitToUser(friendId, 'dm history cleared', {
|
| 659 |
fromUserId: req.user.userId,
|
| 660 |
clearedAt: new Date().toISOString()
|
|
|
|
| 666 |
}
|
| 667 |
});
|
| 668 |
|
| 669 |
+
|
| 670 |
|
| 671 |
app.get('/api/shop', authenticateToken, async (req, res) => {
|
| 672 |
try {
|
|
|
|
| 709 |
}
|
| 710 |
});
|
| 711 |
|
| 712 |
+
|
| 713 |
|
| 714 |
function createServer(appInstance) {
|
| 715 |
const pfxPath = process.env.SSL_PFX_PATH;
|
|
|
|
| 858 |
timestamp: dm.createdAt
|
| 859 |
};
|
| 860 |
|
| 861 |
+
|
| 862 |
emitToUser(socket.userId, 'dm message', dmPayload);
|
| 863 |
const deliveredToRecipient = emitToUser(toUserId, 'dm message', dmPayload);
|
| 864 |
|
|
|
|
| 1172 |
});
|
| 1173 |
});
|
| 1174 |
|
|
|
|
| 1175 |
|
| 1176 |
function getLocalNetworkAddress() {
|
| 1177 |
const interfaces = os.networkInterfaces();
|
|
|
|
| 1188 |
const PORT = process.env.PORT || 3000;
|
| 1189 |
const HOST = process.env.HOST || '0.0.0.0';
|
| 1190 |
|
| 1191 |
+
|
| 1192 |
async function migrateMediaTypeEnum() {
|
| 1193 |
try {
|
| 1194 |
+
|
| 1195 |
const [results] = await sequelize.query(`
|
| 1196 |
SELECT EXISTS (
|
| 1197 |
SELECT 1 FROM pg_type t
|
|
|
|
| 1204 |
if (!results[0].has_voice) {
|
| 1205 |
console.log('📦 Migrating mediaType ENUM to include sticker, gif, voice...');
|
| 1206 |
|
| 1207 |
+
|
| 1208 |
await sequelize.query(`
|
| 1209 |
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'sticker';
|
| 1210 |
ALTER TYPE "enum_DirectMessages_mediaType" ADD VALUE IF NOT EXISTS 'gif';
|
setup-https.ps1
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
-
|
| 2 |
-
# Run this in PowerShell from the project directory
|
| 3 |
|
| 4 |
Write-Host "🚀 Ultimate Chat App - HTTPS LAN Setup" -ForegroundColor Cyan
|
| 5 |
Write-Host "=======================================" -ForegroundColor Cyan
|
| 6 |
Write-Host ""
|
| 7 |
|
| 8 |
-
|
| 9 |
$lanIP = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceAlias -notlike "*Loopback*" -and $_.IPAddress -notlike "169.254.*"}).IPAddress | Select-Object -First 1
|
| 10 |
|
| 11 |
if (!$lanIP) {
|
|
@@ -16,15 +15,15 @@ if (!$lanIP) {
|
|
| 16 |
Write-Host "📡 Detected LAN IP: $lanIP" -ForegroundColor Green
|
| 17 |
Write-Host ""
|
| 18 |
|
| 19 |
-
|
| 20 |
Write-Host "🛑 Stopping existing Node processes..." -ForegroundColor Yellow
|
| 21 |
taskkill /F /IM node.exe 2>$null | Out-Null
|
| 22 |
|
| 23 |
-
|
| 24 |
Write-Host "📁 Creating certs directory..." -ForegroundColor Yellow
|
| 25 |
New-Item -ItemType Directory -Force .\certs | Out-Null
|
| 26 |
|
| 27 |
-
|
| 28 |
Write-Host "🔥 Adding Windows Firewall rule..." -ForegroundColor Yellow
|
| 29 |
$existingRule = Get-NetFirewallRule -DisplayName "Chat App HTTPS" -ErrorAction SilentlyContinue
|
| 30 |
if (!$existingRule) {
|
|
@@ -34,7 +33,7 @@ if (!$existingRule) {
|
|
| 34 |
Write-Host "✅ Firewall rule already exists" -ForegroundColor Green
|
| 35 |
}
|
| 36 |
|
| 37 |
-
|
| 38 |
Write-Host "🔐 Generating HTTPS certificate for localhost and $lanIP..." -ForegroundColor Yellow
|
| 39 |
$cert = New-SelfSignedCertificate `
|
| 40 |
-Subject "CN=localhost" `
|
|
@@ -44,24 +43,24 @@ $cert = New-SelfSignedCertificate `
|
|
| 44 |
-NotAfter (Get-Date).AddYears(2) `
|
| 45 |
-TextExtension @("2.5.29.17={text}DNS=localhost&IPAddress=$lanIP")
|
| 46 |
|
| 47 |
-
|
| 48 |
$pwd = ConvertTo-SecureString "123456" -AsPlainText -Force
|
| 49 |
Export-PfxCertificate -Cert $cert -FilePath ".\certs\lan-localhost.pfx" -Password $pwd | Out-Null
|
| 50 |
Export-Certificate -Cert $cert -FilePath ".\certs\lan-localhost.cer" -Force | Out-Null
|
| 51 |
Write-Host "✅ Certificate generated" -ForegroundColor Green
|
| 52 |
|
| 53 |
-
|
| 54 |
Write-Host "🔒 Trusting certificate..." -ForegroundColor Yellow
|
| 55 |
Import-Certificate -FilePath ".\certs\lan-localhost.cer" -CertStoreLocation "Cert:\CurrentUser\Root" | Out-Null
|
| 56 |
Write-Host "✅ Certificate trusted" -ForegroundColor Green
|
| 57 |
Write-Host ""
|
| 58 |
|
| 59 |
-
|
| 60 |
$env:SSL_PFX_PATH = (Resolve-Path ".\certs\lan-localhost.pfx").Path
|
| 61 |
$env:SSL_PFX_PASSPHRASE = "123456"
|
| 62 |
$env:PORT = "3001"
|
| 63 |
|
| 64 |
-
|
| 65 |
Write-Host "╔═══════════════════════════════════════╗" -ForegroundColor Cyan
|
| 66 |
Write-Host "║ 🎉 Setup Complete! ║" -ForegroundColor Cyan
|
| 67 |
Write-Host "╚═══════════════════════════════════════╝" -ForegroundColor Cyan
|
|
@@ -76,5 +75,5 @@ Write-Host ""
|
|
| 76 |
Write-Host "Starting server..." -ForegroundColor Yellow
|
| 77 |
Write-Host ""
|
| 78 |
|
| 79 |
-
|
| 80 |
npm start
|
|
|
|
| 1 |
+
|
|
|
|
| 2 |
|
| 3 |
Write-Host "🚀 Ultimate Chat App - HTTPS LAN Setup" -ForegroundColor Cyan
|
| 4 |
Write-Host "=======================================" -ForegroundColor Cyan
|
| 5 |
Write-Host ""
|
| 6 |
|
| 7 |
+
|
| 8 |
$lanIP = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceAlias -notlike "*Loopback*" -and $_.IPAddress -notlike "169.254.*"}).IPAddress | Select-Object -First 1
|
| 9 |
|
| 10 |
if (!$lanIP) {
|
|
|
|
| 15 |
Write-Host "📡 Detected LAN IP: $lanIP" -ForegroundColor Green
|
| 16 |
Write-Host ""
|
| 17 |
|
| 18 |
+
|
| 19 |
Write-Host "🛑 Stopping existing Node processes..." -ForegroundColor Yellow
|
| 20 |
taskkill /F /IM node.exe 2>$null | Out-Null
|
| 21 |
|
| 22 |
+
|
| 23 |
Write-Host "📁 Creating certs directory..." -ForegroundColor Yellow
|
| 24 |
New-Item -ItemType Directory -Force .\certs | Out-Null
|
| 25 |
|
| 26 |
+
|
| 27 |
Write-Host "🔥 Adding Windows Firewall rule..." -ForegroundColor Yellow
|
| 28 |
$existingRule = Get-NetFirewallRule -DisplayName "Chat App HTTPS" -ErrorAction SilentlyContinue
|
| 29 |
if (!$existingRule) {
|
|
|
|
| 33 |
Write-Host "✅ Firewall rule already exists" -ForegroundColor Green
|
| 34 |
}
|
| 35 |
|
| 36 |
+
|
| 37 |
Write-Host "🔐 Generating HTTPS certificate for localhost and $lanIP..." -ForegroundColor Yellow
|
| 38 |
$cert = New-SelfSignedCertificate `
|
| 39 |
-Subject "CN=localhost" `
|
|
|
|
| 43 |
-NotAfter (Get-Date).AddYears(2) `
|
| 44 |
-TextExtension @("2.5.29.17={text}DNS=localhost&IPAddress=$lanIP")
|
| 45 |
|
| 46 |
+
|
| 47 |
$pwd = ConvertTo-SecureString "123456" -AsPlainText -Force
|
| 48 |
Export-PfxCertificate -Cert $cert -FilePath ".\certs\lan-localhost.pfx" -Password $pwd | Out-Null
|
| 49 |
Export-Certificate -Cert $cert -FilePath ".\certs\lan-localhost.cer" -Force | Out-Null
|
| 50 |
Write-Host "✅ Certificate generated" -ForegroundColor Green
|
| 51 |
|
| 52 |
+
|
| 53 |
Write-Host "🔒 Trusting certificate..." -ForegroundColor Yellow
|
| 54 |
Import-Certificate -FilePath ".\certs\lan-localhost.cer" -CertStoreLocation "Cert:\CurrentUser\Root" | Out-Null
|
| 55 |
Write-Host "✅ Certificate trusted" -ForegroundColor Green
|
| 56 |
Write-Host ""
|
| 57 |
|
| 58 |
+
|
| 59 |
$env:SSL_PFX_PATH = (Resolve-Path ".\certs\lan-localhost.pfx").Path
|
| 60 |
$env:SSL_PFX_PASSPHRASE = "123456"
|
| 61 |
$env:PORT = "3001"
|
| 62 |
|
| 63 |
+
|
| 64 |
Write-Host "╔═══════════════════════════════════════╗" -ForegroundColor Cyan
|
| 65 |
Write-Host "║ 🎉 Setup Complete! ║" -ForegroundColor Cyan
|
| 66 |
Write-Host "╚═══════════════════════════════════════╝" -ForegroundColor Cyan
|
|
|
|
| 75 |
Write-Host "Starting server..." -ForegroundColor Yellow
|
| 76 |
Write-Host ""
|
| 77 |
|
| 78 |
+
|
| 79 |
npm start
|