Spaces:
Runtime error
Runtime error
release 2.3.4
Browse files- client.js +206 -66
- package.json +1 -1
- server.js +25 -2
client.js
CHANGED
|
@@ -10,6 +10,10 @@ let pendingMediaDrafts = [];
|
|
| 10 |
let activeCallState = null;
|
| 11 |
let peerConnection = null;
|
| 12 |
let localCallStream = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
let currentCallPeerId = null;
|
| 14 |
let currentCallPeerName = null;
|
| 15 |
let callTimerInterval = null;
|
|
@@ -21,6 +25,8 @@ let roomMessages = JSON.parse(localStorage.getItem('roomMessages') || '{}');
|
|
| 21 |
let pinnedChats = [];
|
| 22 |
let joinedCommunities = [];
|
| 23 |
let joinedGroups = [];
|
|
|
|
|
|
|
| 24 |
let communityProfiles = JSON.parse(localStorage.getItem('communityProfiles') || '{}');
|
| 25 |
let groupProfiles = JSON.parse(localStorage.getItem('groupProfiles') || '{}');
|
| 26 |
let chatSettings = {
|
|
@@ -590,6 +596,7 @@ function connectSocket() {
|
|
| 590 |
await startCallSession(fromName, fromId, false);
|
| 591 |
}
|
| 592 |
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.offer));
|
|
|
|
| 593 |
const answer = await peerConnection.createAnswer();
|
| 594 |
await peerConnection.setLocalDescription(answer);
|
| 595 |
|
|
@@ -602,10 +609,11 @@ function connectSocket() {
|
|
| 602 |
|
| 603 |
if (signal.type === 'answer' && peerConnection) {
|
| 604 |
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.answer));
|
|
|
|
| 605 |
}
|
| 606 |
|
| 607 |
if (signal.type === 'ice-candidate' && peerConnection && signal.candidate) {
|
| 608 |
-
await
|
| 609 |
}
|
| 610 |
} catch (error) {
|
| 611 |
console.error('Video signaling error:', error);
|
|
@@ -641,18 +649,14 @@ function showMainApp() {
|
|
| 641 |
const profilePanel = document.getElementById('profilePanel');
|
| 642 |
if (profilePanel) {
|
| 643 |
profilePanel.classList.remove('show');
|
| 644 |
-
profilePanel.style
|
| 645 |
-
profilePanel.style.visibility = 'hidden';
|
| 646 |
-
profilePanel.style.pointerEvents = 'none';
|
| 647 |
-
profilePanel.style.opacity = '0';
|
| 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
|
| 656 |
}
|
| 657 |
|
| 658 |
|
|
@@ -786,7 +790,14 @@ function toggleChannelSidebar() {
|
|
| 786 |
}
|
| 787 |
|
| 788 |
function openProfile() {
|
| 789 |
-
document.getElementById('profilePanel')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
loadUserProfile();
|
| 791 |
}
|
| 792 |
|
|
@@ -807,14 +818,19 @@ function shareProfileLink() {
|
|
| 807 |
}
|
| 808 |
|
| 809 |
function closeProfile() {
|
| 810 |
-
document.getElementById('profilePanel')
|
|
|
|
|
|
|
| 811 |
}
|
| 812 |
|
| 813 |
-
function switchProfileTab(tab) {
|
| 814 |
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
| 815 |
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
| 816 |
|
| 817 |
-
|
|
|
|
|
|
|
|
|
|
| 818 |
document.getElementById(tab + 'Tab').classList.add('active');
|
| 819 |
|
| 820 |
if (tab === 'inventory') {
|
|
@@ -863,6 +879,8 @@ async function loadUserProfile() {
|
|
| 863 |
document.getElementById('coinsDisplay').textContent = `💰 Coins: ${user.coins}`;
|
| 864 |
|
| 865 |
currentUser = user;
|
|
|
|
|
|
|
| 866 |
} catch (error) {
|
| 867 |
console.error('Failed to load profile:', error);
|
| 868 |
}
|
|
@@ -1936,6 +1954,11 @@ async function loadShopItems() {
|
|
| 1936 |
{ id: 'fallback_nameplate', name: 'Crystal Nameplate', description: 'Polished nameplate style in member list.', price: 410, category: 'Nameplates' }
|
| 1937 |
];
|
| 1938 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1939 |
const shopList = document.getElementById('shopList');
|
| 1940 |
shopList.innerHTML = '';
|
| 1941 |
|
|
@@ -1981,8 +2004,23 @@ async function buyItem(itemId) {
|
|
| 1981 |
|
| 1982 |
const data = await response.json();
|
| 1983 |
if (response.ok) {
|
| 1984 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1985 |
loadShopItems();
|
|
|
|
| 1986 |
} else {
|
| 1987 |
showToast(data.error, 'error');
|
| 1988 |
}
|
|
@@ -2028,15 +2066,59 @@ async function loadInventory() {
|
|
| 2028 |
}
|
| 2029 |
}
|
| 2030 |
|
| 2031 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2032 |
const effectsMap = {
|
| 2033 |
'Banners': () => {
|
| 2034 |
document.documentElement.style.setProperty('--primary-color', '#00d4ff');
|
| 2035 |
document.documentElement.style.setProperty('--primary-hover', '#00b8e6');
|
| 2036 |
-
|
| 2037 |
},
|
| 2038 |
'Badges': () => {
|
| 2039 |
-
|
| 2040 |
|
| 2041 |
},
|
| 2042 |
'Themes': () => {
|
|
@@ -2044,7 +2126,7 @@ function applyShopItem(itemId, category) {
|
|
| 2044 |
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
| 2045 |
document.documentElement.style.setProperty('--primary-color', randomColor);
|
| 2046 |
document.documentElement.style.setProperty('--primary-hover', randomColor + 'dd');
|
| 2047 |
-
|
| 2048 |
},
|
| 2049 |
'Effects': () => {
|
| 2050 |
|
|
@@ -2062,7 +2144,7 @@ function applyShopItem(itemId, category) {
|
|
| 2062 |
const existing = document.getElementById('message-glow-effect');
|
| 2063 |
if (existing) existing.remove();
|
| 2064 |
document.head.appendChild(style);
|
| 2065 |
-
|
| 2066 |
},
|
| 2067 |
'Avatar': () => {
|
| 2068 |
const style = document.createElement('style');
|
|
@@ -2080,7 +2162,7 @@ function applyShopItem(itemId, category) {
|
|
| 2080 |
const existing = document.getElementById('avatar-aura-effect');
|
| 2081 |
if (existing) existing.remove();
|
| 2082 |
document.head.appendChild(style);
|
| 2083 |
-
|
| 2084 |
},
|
| 2085 |
'Nameplates': () => {
|
| 2086 |
const style = document.createElement('style');
|
|
@@ -2097,18 +2179,22 @@ function applyShopItem(itemId, category) {
|
|
| 2097 |
const existing = document.getElementById('nameplate-effect');
|
| 2098 |
if (existing) existing.remove();
|
| 2099 |
document.head.appendChild(style);
|
| 2100 |
-
|
| 2101 |
},
|
| 2102 |
'Emotes': () => {
|
| 2103 |
-
|
| 2104 |
}
|
| 2105 |
};
|
| 2106 |
|
| 2107 |
const applyEffect = effectsMap[category];
|
| 2108 |
if (applyEffect) {
|
| 2109 |
applyEffect();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2110 |
} else {
|
| 2111 |
-
|
| 2112 |
}
|
| 2113 |
}
|
| 2114 |
|
|
@@ -2140,7 +2226,10 @@ function showToast(message, type = 'success') {
|
|
| 2140 |
|
| 2141 |
|
| 2142 |
function openSettings() {
|
| 2143 |
-
document.getElementById('settingsPanel')
|
|
|
|
|
|
|
|
|
|
| 2144 |
if (currentUser) {
|
| 2145 |
document.getElementById('settingsEmail').value = currentUser.email || '';
|
| 2146 |
document.getElementById('settingsUsername').value = '';
|
|
@@ -2150,19 +2239,24 @@ function openSettings() {
|
|
| 2150 |
}
|
| 2151 |
|
| 2152 |
function closeSettings() {
|
| 2153 |
-
document.getElementById('settingsPanel')
|
|
|
|
|
|
|
| 2154 |
const simpleTheme = localStorage.getItem('simpleTheme');
|
| 2155 |
if (simpleTheme) {
|
| 2156 |
showToast('✅ Settings saved!', 'success');
|
| 2157 |
}
|
| 2158 |
}
|
| 2159 |
|
| 2160 |
-
function switchSettingsTab(tab) {
|
| 2161 |
|
| 2162 |
document.querySelectorAll('.settings-nav-item').forEach(item => {
|
| 2163 |
item.classList.remove('active');
|
| 2164 |
});
|
| 2165 |
-
|
|
|
|
|
|
|
|
|
|
| 2166 |
|
| 2167 |
|
| 2168 |
document.querySelectorAll('.settings-section').forEach(section => {
|
|
@@ -4700,6 +4794,10 @@ function handleIncomingVideoCall(payload) {
|
|
| 4700 |
callId: activeCallState.callId,
|
| 4701 |
toId: activeCallState.peerId
|
| 4702 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4703 |
|
| 4704 |
} else {
|
| 4705 |
stopIncomingCallAlert();
|
|
@@ -4715,6 +4813,13 @@ function handleIncomingVideoCall(payload) {
|
|
| 4715 |
|
| 4716 |
async function ensureLocalCallStream() {
|
| 4717 |
if (localCallStream) return localCallStream;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4718 |
try {
|
| 4719 |
const selectedMic = audioSettings?.microphoneId;
|
| 4720 |
const constraints = {
|
|
@@ -4740,9 +4845,9 @@ async function ensureLocalCallStream() {
|
|
| 4740 |
try {
|
| 4741 |
localCallStream = await navigator.mediaDevices.getUserMedia({
|
| 4742 |
audio: true,
|
| 4743 |
-
video:
|
| 4744 |
});
|
| 4745 |
-
showToast('
|
| 4746 |
return localCallStream;
|
| 4747 |
} catch (fallbackError) {
|
| 4748 |
showToast(`❌ Cannot access camera/mic: ${fallbackError.message}`, 'error');
|
|
@@ -4782,6 +4887,18 @@ function createPeerConnection(peerId) {
|
|
| 4782 |
iceCandidatePoolSize: 10
|
| 4783 |
});
|
| 4784 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4785 |
if (localCallStream) {
|
| 4786 |
localCallStream.getTracks().forEach((track) => {
|
| 4787 |
peerConnection.addTrack(track, localCallStream);
|
|
@@ -4801,8 +4918,13 @@ function createPeerConnection(peerId) {
|
|
| 4801 |
peerConnection.ontrack = (event) => {
|
| 4802 |
console.log('📞 Remote track received:', event.track.kind, event.streams.length);
|
| 4803 |
const remoteVideo = document.getElementById('remoteCallVideo');
|
| 4804 |
-
if (remoteVideo
|
| 4805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4806 |
|
| 4807 |
|
| 4808 |
remoteVideo.muted = false;
|
|
@@ -4829,9 +4951,20 @@ function createPeerConnection(peerId) {
|
|
| 4829 |
}, 500);
|
| 4830 |
});
|
| 4831 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4832 |
}
|
| 4833 |
};
|
| 4834 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4835 |
peerConnection.onconnectionstatechange = () => {
|
| 4836 |
console.log('📡 Connection state:', peerConnection.connectionState);
|
| 4837 |
|
|
@@ -4840,13 +4973,6 @@ function createPeerConnection(peerId) {
|
|
| 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;
|
| 4848 |
-
}
|
| 4849 |
-
|
| 4850 |
if (peerConnection.connectionState === 'connecting') {
|
| 4851 |
showToast('📞 Connecting call...', 'info');
|
| 4852 |
|
|
@@ -4854,14 +4980,8 @@ function createPeerConnection(peerId) {
|
|
| 4854 |
startCallTimer();
|
| 4855 |
}
|
| 4856 |
} else if (peerConnection.connectionState === 'disconnected') {
|
| 4857 |
-
|
| 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 |
|
|
@@ -4869,14 +4989,8 @@ function createPeerConnection(peerId) {
|
|
| 4869 |
startCallTimer();
|
| 4870 |
}
|
| 4871 |
} else if (peerConnection.connectionState === 'failed') {
|
| 4872 |
-
|
| 4873 |
-
|
| 4874 |
-
window.callDisconnectTimeout = setTimeout(() => {
|
| 4875 |
-
if (peerConnection && peerConnection.connectionState === 'failed' && !window.userIntentionallEndedCall) {
|
| 4876 |
-
showToast('Call ended after network failure', 'error');
|
| 4877 |
-
cleanupCallSession(false);
|
| 4878 |
-
}
|
| 4879 |
-
}, 12000);
|
| 4880 |
} else if (peerConnection.connectionState === 'closed') {
|
| 4881 |
|
| 4882 |
cleanupCallSession(false);
|
|
@@ -4887,11 +5001,39 @@ function createPeerConnection(peerId) {
|
|
| 4887 |
if (!peerConnection) return;
|
| 4888 |
console.log('ICE connection state:', peerConnection.iceConnectionState);
|
| 4889 |
|
| 4890 |
-
if (peerConnection.iceConnectionState === 'failed') {
|
| 4891 |
-
|
| 4892 |
-
peerConnection.restartIce?.();
|
| 4893 |
}
|
| 4894 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4895 |
}
|
| 4896 |
|
| 4897 |
function openCallDialog(peerName) {
|
|
@@ -4934,7 +5076,10 @@ async function startCallSession(peerName, peerId, isCaller) {
|
|
| 4934 |
createPeerConnection(peerId);
|
| 4935 |
|
| 4936 |
if (isCaller && peerConnection) {
|
| 4937 |
-
const offer = await peerConnection.createOffer(
|
|
|
|
|
|
|
|
|
|
| 4938 |
await peerConnection.setLocalDescription(offer);
|
| 4939 |
socket.emit('video signal', {
|
| 4940 |
toId: peerId,
|
|
@@ -4949,11 +5094,6 @@ function endVideoCall() {
|
|
| 4949 |
window.userIntentionallEndedCall = true;
|
| 4950 |
|
| 4951 |
|
| 4952 |
-
if (window.callDisconnectTimeout) {
|
| 4953 |
-
clearTimeout(window.callDisconnectTimeout);
|
| 4954 |
-
window.callDisconnectTimeout = null;
|
| 4955 |
-
}
|
| 4956 |
-
|
| 4957 |
if (socket && socket.connected && currentCallPeerId) {
|
| 4958 |
socket.emit('end video call', {
|
| 4959 |
callId: activeCallState?.callId,
|
|
@@ -4989,11 +5129,6 @@ function cleanupCallSession(keepDialogOpen = false) {
|
|
| 4989 |
window.userIntentionallEndedCall = false;
|
| 4990 |
|
| 4991 |
|
| 4992 |
-
if (window.callDisconnectTimeout) {
|
| 4993 |
-
clearTimeout(window.callDisconnectTimeout);
|
| 4994 |
-
window.callDisconnectTimeout = null;
|
| 4995 |
-
}
|
| 4996 |
-
|
| 4997 |
stopIncomingCallAlert();
|
| 4998 |
stopCallTimer();
|
| 4999 |
|
|
@@ -5004,6 +5139,11 @@ function cleanupCallSession(keepDialogOpen = false) {
|
|
| 5004 |
peerConnection = null;
|
| 5005 |
}
|
| 5006 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5007 |
if (localCallStream) {
|
| 5008 |
localCallStream.getTracks().forEach((track) => track.stop());
|
| 5009 |
localCallStream = null;
|
|
|
|
| 10 |
let activeCallState = null;
|
| 11 |
let peerConnection = null;
|
| 12 |
let localCallStream = null;
|
| 13 |
+
let remoteCallStream = null;
|
| 14 |
+
let remoteIceCandidatesQueue = [];
|
| 15 |
+
let lastIceRecoveryAttemptAt = 0;
|
| 16 |
+
let lastCallWarningAt = 0;
|
| 17 |
let currentCallPeerId = null;
|
| 18 |
let currentCallPeerName = null;
|
| 19 |
let callTimerInterval = null;
|
|
|
|
| 25 |
let pinnedChats = [];
|
| 26 |
let joinedCommunities = [];
|
| 27 |
let joinedGroups = [];
|
| 28 |
+
let cachedShopItemsById = {};
|
| 29 |
+
let equippedShopItems = JSON.parse(localStorage.getItem('equippedShopItems') || '{}');
|
| 30 |
let communityProfiles = JSON.parse(localStorage.getItem('communityProfiles') || '{}');
|
| 31 |
let groupProfiles = JSON.parse(localStorage.getItem('groupProfiles') || '{}');
|
| 32 |
let chatSettings = {
|
|
|
|
| 596 |
await startCallSession(fromName, fromId, false);
|
| 597 |
}
|
| 598 |
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.offer));
|
| 599 |
+
await flushQueuedIceCandidates();
|
| 600 |
const answer = await peerConnection.createAnswer();
|
| 601 |
await peerConnection.setLocalDescription(answer);
|
| 602 |
|
|
|
|
| 609 |
|
| 610 |
if (signal.type === 'answer' && peerConnection) {
|
| 611 |
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.answer));
|
| 612 |
+
await flushQueuedIceCandidates();
|
| 613 |
}
|
| 614 |
|
| 615 |
if (signal.type === 'ice-candidate' && peerConnection && signal.candidate) {
|
| 616 |
+
await addOrQueueIceCandidate(signal.candidate);
|
| 617 |
}
|
| 618 |
} catch (error) {
|
| 619 |
console.error('Video signaling error:', error);
|
|
|
|
| 649 |
const profilePanel = document.getElementById('profilePanel');
|
| 650 |
if (profilePanel) {
|
| 651 |
profilePanel.classList.remove('show');
|
| 652 |
+
profilePanel.removeAttribute('style');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
}
|
| 654 |
|
| 655 |
|
| 656 |
const settingsPanel = document.getElementById('settingsPanel');
|
| 657 |
if (settingsPanel) {
|
| 658 |
settingsPanel.classList.remove('show');
|
| 659 |
+
settingsPanel.removeAttribute('style');
|
| 660 |
}
|
| 661 |
|
| 662 |
|
|
|
|
| 790 |
}
|
| 791 |
|
| 792 |
function openProfile() {
|
| 793 |
+
const panel = document.getElementById('profilePanel');
|
| 794 |
+
if (!panel) return;
|
| 795 |
+
panel.style.removeProperty('display');
|
| 796 |
+
panel.style.removeProperty('visibility');
|
| 797 |
+
panel.style.removeProperty('pointer-events');
|
| 798 |
+
panel.style.removeProperty('opacity');
|
| 799 |
+
panel.style.removeProperty('transform');
|
| 800 |
+
panel.classList.add('show');
|
| 801 |
loadUserProfile();
|
| 802 |
}
|
| 803 |
|
|
|
|
| 818 |
}
|
| 819 |
|
| 820 |
function closeProfile() {
|
| 821 |
+
const panel = document.getElementById('profilePanel');
|
| 822 |
+
if (!panel) return;
|
| 823 |
+
panel.classList.remove('show');
|
| 824 |
}
|
| 825 |
|
| 826 |
+
function switchProfileTab(tab, triggerEl) {
|
| 827 |
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
| 828 |
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
| 829 |
|
| 830 |
+
const tabTrigger = triggerEl || (typeof event !== 'undefined' ? event.target : null);
|
| 831 |
+
if (tabTrigger) {
|
| 832 |
+
tabTrigger.classList.add('active');
|
| 833 |
+
}
|
| 834 |
document.getElementById(tab + 'Tab').classList.add('active');
|
| 835 |
|
| 836 |
if (tab === 'inventory') {
|
|
|
|
| 879 |
document.getElementById('coinsDisplay').textContent = `💰 Coins: ${user.coins}`;
|
| 880 |
|
| 881 |
currentUser = user;
|
| 882 |
+
localStorage.setItem('user', JSON.stringify(user));
|
| 883 |
+
applyPersistedShopEffects();
|
| 884 |
} catch (error) {
|
| 885 |
console.error('Failed to load profile:', error);
|
| 886 |
}
|
|
|
|
| 1954 |
{ id: 'fallback_nameplate', name: 'Crystal Nameplate', description: 'Polished nameplate style in member list.', price: 410, category: 'Nameplates' }
|
| 1955 |
];
|
| 1956 |
|
| 1957 |
+
cachedShopItemsById = {};
|
| 1958 |
+
items.forEach((item) => {
|
| 1959 |
+
cachedShopItemsById[item.id] = item;
|
| 1960 |
+
});
|
| 1961 |
+
|
| 1962 |
const shopList = document.getElementById('shopList');
|
| 1963 |
shopList.innerHTML = '';
|
| 1964 |
|
|
|
|
| 2004 |
|
| 2005 |
const data = await response.json();
|
| 2006 |
if (response.ok) {
|
| 2007 |
+
if (typeof data?.coins === 'number') {
|
| 2008 |
+
currentUser.coins = data.coins;
|
| 2009 |
+
localStorage.setItem('user', JSON.stringify(currentUser));
|
| 2010 |
+
const coinsDisplay = document.getElementById('coinsDisplay');
|
| 2011 |
+
if (coinsDisplay) {
|
| 2012 |
+
coinsDisplay.textContent = `💰 Coins: ${data.coins}`;
|
| 2013 |
+
}
|
| 2014 |
+
}
|
| 2015 |
+
|
| 2016 |
+
const purchasedItem = data?.purchasedItem || cachedShopItemsById[itemId];
|
| 2017 |
+
if (purchasedItem?.category) {
|
| 2018 |
+
applyShopItem(purchasedItem.id || itemId, purchasedItem.category);
|
| 2019 |
+
}
|
| 2020 |
+
|
| 2021 |
+
showToast('Item purchased and applied!');
|
| 2022 |
loadShopItems();
|
| 2023 |
+
loadInventory();
|
| 2024 |
} else {
|
| 2025 |
showToast(data.error, 'error');
|
| 2026 |
}
|
|
|
|
| 2066 |
}
|
| 2067 |
}
|
| 2068 |
|
| 2069 |
+
function saveEquippedShopItems() {
|
| 2070 |
+
localStorage.setItem('equippedShopItems', JSON.stringify(equippedShopItems));
|
| 2071 |
+
}
|
| 2072 |
+
|
| 2073 |
+
function applyPersistedShopEffects() {
|
| 2074 |
+
if (!equippedShopItems || typeof equippedShopItems !== 'object') return;
|
| 2075 |
+
|
| 2076 |
+
Object.entries(equippedShopItems).forEach(([category, itemId]) => {
|
| 2077 |
+
applyShopItem(itemId, category, { skipPersist: true, silent: true });
|
| 2078 |
+
});
|
| 2079 |
+
}
|
| 2080 |
+
|
| 2081 |
+
async function flushQueuedIceCandidates() {
|
| 2082 |
+
if (!peerConnection || !remoteIceCandidatesQueue.length) return;
|
| 2083 |
+
|
| 2084 |
+
const queued = [...remoteIceCandidatesQueue];
|
| 2085 |
+
remoteIceCandidatesQueue = [];
|
| 2086 |
+
for (const candidate of queued) {
|
| 2087 |
+
try {
|
| 2088 |
+
await peerConnection.addIceCandidate(candidate);
|
| 2089 |
+
} catch (error) {
|
| 2090 |
+
console.warn('Failed to apply queued ICE candidate:', error);
|
| 2091 |
+
}
|
| 2092 |
+
}
|
| 2093 |
+
}
|
| 2094 |
+
|
| 2095 |
+
async function addOrQueueIceCandidate(candidateData) {
|
| 2096 |
+
if (!peerConnection || !candidateData) return;
|
| 2097 |
+
|
| 2098 |
+
const candidate = new RTCIceCandidate(candidateData);
|
| 2099 |
+
const hasRemoteDescription = Boolean(peerConnection.remoteDescription && peerConnection.remoteDescription.type);
|
| 2100 |
+
|
| 2101 |
+
if (hasRemoteDescription) {
|
| 2102 |
+
await peerConnection.addIceCandidate(candidate);
|
| 2103 |
+
} else {
|
| 2104 |
+
remoteIceCandidatesQueue.push(candidate);
|
| 2105 |
+
}
|
| 2106 |
+
}
|
| 2107 |
+
|
| 2108 |
+
function applyShopItem(itemId, category, options = {}) {
|
| 2109 |
+
const { skipPersist = false, silent = false } = options;
|
| 2110 |
+
const notify = (message, type = 'success') => {
|
| 2111 |
+
if (!silent) showToast(message, type);
|
| 2112 |
+
};
|
| 2113 |
+
|
| 2114 |
const effectsMap = {
|
| 2115 |
'Banners': () => {
|
| 2116 |
document.documentElement.style.setProperty('--primary-color', '#00d4ff');
|
| 2117 |
document.documentElement.style.setProperty('--primary-hover', '#00b8e6');
|
| 2118 |
+
notify('✨ Banner effect applied! Your profile now has a neon glow.', 'success');
|
| 2119 |
},
|
| 2120 |
'Badges': () => {
|
| 2121 |
+
notify('🏆 Badge equipped! It now appears next to your name.', 'success');
|
| 2122 |
|
| 2123 |
},
|
| 2124 |
'Themes': () => {
|
|
|
|
| 2126 |
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
| 2127 |
document.documentElement.style.setProperty('--primary-color', randomColor);
|
| 2128 |
document.documentElement.style.setProperty('--primary-hover', randomColor + 'dd');
|
| 2129 |
+
notify('🎨 Theme applied! Your accent color has changed.', 'success');
|
| 2130 |
},
|
| 2131 |
'Effects': () => {
|
| 2132 |
|
|
|
|
| 2144 |
const existing = document.getElementById('message-glow-effect');
|
| 2145 |
if (existing) existing.remove();
|
| 2146 |
document.head.appendChild(style);
|
| 2147 |
+
notify('✨ Message glow effect activated!', 'success');
|
| 2148 |
},
|
| 2149 |
'Avatar': () => {
|
| 2150 |
const style = document.createElement('style');
|
|
|
|
| 2162 |
const existing = document.getElementById('avatar-aura-effect');
|
| 2163 |
if (existing) existing.remove();
|
| 2164 |
document.head.appendChild(style);
|
| 2165 |
+
notify('🌟 Avatar aura effect activated!', 'success');
|
| 2166 |
},
|
| 2167 |
'Nameplates': () => {
|
| 2168 |
const style = document.createElement('style');
|
|
|
|
| 2179 |
const existing = document.getElementById('nameplate-effect');
|
| 2180 |
if (existing) existing.remove();
|
| 2181 |
document.head.appendChild(style);
|
| 2182 |
+
notify('💎 Crystal nameplate effect activated!', 'success');
|
| 2183 |
},
|
| 2184 |
'Emotes': () => {
|
| 2185 |
+
notify('😎 Special emotes unlocked! Use them in your messages.', 'success');
|
| 2186 |
}
|
| 2187 |
};
|
| 2188 |
|
| 2189 |
const applyEffect = effectsMap[category];
|
| 2190 |
if (applyEffect) {
|
| 2191 |
applyEffect();
|
| 2192 |
+
if (!skipPersist && category) {
|
| 2193 |
+
equippedShopItems[category] = itemId;
|
| 2194 |
+
saveEquippedShopItems();
|
| 2195 |
+
}
|
| 2196 |
} else {
|
| 2197 |
+
notify('Effect applied!', 'success');
|
| 2198 |
}
|
| 2199 |
}
|
| 2200 |
|
|
|
|
| 2226 |
|
| 2227 |
|
| 2228 |
function openSettings() {
|
| 2229 |
+
const panel = document.getElementById('settingsPanel');
|
| 2230 |
+
if (!panel) return;
|
| 2231 |
+
panel.style.removeProperty('right');
|
| 2232 |
+
panel.classList.add('show');
|
| 2233 |
if (currentUser) {
|
| 2234 |
document.getElementById('settingsEmail').value = currentUser.email || '';
|
| 2235 |
document.getElementById('settingsUsername').value = '';
|
|
|
|
| 2239 |
}
|
| 2240 |
|
| 2241 |
function closeSettings() {
|
| 2242 |
+
const panel = document.getElementById('settingsPanel');
|
| 2243 |
+
if (!panel) return;
|
| 2244 |
+
panel.classList.remove('show');
|
| 2245 |
const simpleTheme = localStorage.getItem('simpleTheme');
|
| 2246 |
if (simpleTheme) {
|
| 2247 |
showToast('✅ Settings saved!', 'success');
|
| 2248 |
}
|
| 2249 |
}
|
| 2250 |
|
| 2251 |
+
function switchSettingsTab(tab, triggerEl) {
|
| 2252 |
|
| 2253 |
document.querySelectorAll('.settings-nav-item').forEach(item => {
|
| 2254 |
item.classList.remove('active');
|
| 2255 |
});
|
| 2256 |
+
const navTrigger = triggerEl || (typeof event !== 'undefined' ? event.target : null);
|
| 2257 |
+
if (navTrigger) {
|
| 2258 |
+
navTrigger.classList.add('active');
|
| 2259 |
+
}
|
| 2260 |
|
| 2261 |
|
| 2262 |
document.querySelectorAll('.settings-section').forEach(section => {
|
|
|
|
| 4794 |
callId: activeCallState.callId,
|
| 4795 |
toId: activeCallState.peerId
|
| 4796 |
});
|
| 4797 |
+
startCallSession(callerName, activeCallState.peerId, false).catch((error) => {
|
| 4798 |
+
console.error('Failed to initialize incoming call session:', error);
|
| 4799 |
+
showToast('Unable to initialize call media devices', 'error');
|
| 4800 |
+
});
|
| 4801 |
|
| 4802 |
} else {
|
| 4803 |
stopIncomingCallAlert();
|
|
|
|
| 4813 |
|
| 4814 |
async function ensureLocalCallStream() {
|
| 4815 |
if (localCallStream) return localCallStream;
|
| 4816 |
+
|
| 4817 |
+
if (!window.isSecureContext && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
|
| 4818 |
+
const secureContextError = new Error('Camera and microphone require HTTPS on non-localhost devices.');
|
| 4819 |
+
showToast('❌ Call media blocked: open app over HTTPS on both devices.', 'error');
|
| 4820 |
+
throw secureContextError;
|
| 4821 |
+
}
|
| 4822 |
+
|
| 4823 |
try {
|
| 4824 |
const selectedMic = audioSettings?.microphoneId;
|
| 4825 |
const constraints = {
|
|
|
|
| 4845 |
try {
|
| 4846 |
localCallStream = await navigator.mediaDevices.getUserMedia({
|
| 4847 |
audio: true,
|
| 4848 |
+
video: false
|
| 4849 |
});
|
| 4850 |
+
showToast('🎤 Microphone enabled (camera unavailable on this device)', 'warning');
|
| 4851 |
return localCallStream;
|
| 4852 |
} catch (fallbackError) {
|
| 4853 |
showToast(`❌ Cannot access camera/mic: ${fallbackError.message}`, 'error');
|
|
|
|
| 4887 |
iceCandidatePoolSize: 10
|
| 4888 |
});
|
| 4889 |
|
| 4890 |
+
remoteIceCandidatesQueue = [];
|
| 4891 |
+
remoteCallStream = new MediaStream();
|
| 4892 |
+
|
| 4893 |
+
const remoteVideo = document.getElementById('remoteCallVideo');
|
| 4894 |
+
if (remoteVideo) {
|
| 4895 |
+
remoteVideo.srcObject = remoteCallStream;
|
| 4896 |
+
remoteVideo.autoplay = true;
|
| 4897 |
+
remoteVideo.playsInline = true;
|
| 4898 |
+
remoteVideo.muted = false;
|
| 4899 |
+
remoteVideo.volume = 1.0;
|
| 4900 |
+
}
|
| 4901 |
+
|
| 4902 |
if (localCallStream) {
|
| 4903 |
localCallStream.getTracks().forEach((track) => {
|
| 4904 |
peerConnection.addTrack(track, localCallStream);
|
|
|
|
| 4918 |
peerConnection.ontrack = (event) => {
|
| 4919 |
console.log('📞 Remote track received:', event.track.kind, event.streams.length);
|
| 4920 |
const remoteVideo = document.getElementById('remoteCallVideo');
|
| 4921 |
+
if (remoteVideo) {
|
| 4922 |
+
if (event.streams && event.streams[0]) {
|
| 4923 |
+
remoteVideo.srcObject = event.streams[0];
|
| 4924 |
+
} else if (remoteCallStream) {
|
| 4925 |
+
remoteCallStream.addTrack(event.track);
|
| 4926 |
+
remoteVideo.srcObject = remoteCallStream;
|
| 4927 |
+
}
|
| 4928 |
|
| 4929 |
|
| 4930 |
remoteVideo.muted = false;
|
|
|
|
| 4951 |
}, 500);
|
| 4952 |
});
|
| 4953 |
}
|
| 4954 |
+
|
| 4955 |
+
event.track.onunmute = () => {
|
| 4956 |
+
remoteVideo.play().catch(() => {});
|
| 4957 |
+
};
|
| 4958 |
}
|
| 4959 |
};
|
| 4960 |
|
| 4961 |
+
function showCallWarningThrottled(message) {
|
| 4962 |
+
const now = Date.now();
|
| 4963 |
+
if (now - lastCallWarningAt < 5000) return;
|
| 4964 |
+
lastCallWarningAt = now;
|
| 4965 |
+
showToast(message, 'warning');
|
| 4966 |
+
}
|
| 4967 |
+
|
| 4968 |
peerConnection.onconnectionstatechange = () => {
|
| 4969 |
console.log('📡 Connection state:', peerConnection.connectionState);
|
| 4970 |
|
|
|
|
| 4973 |
console.log('User ended call intentionally, skipping error messages');
|
| 4974 |
return;
|
| 4975 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4976 |
if (peerConnection.connectionState === 'connecting') {
|
| 4977 |
showToast('📞 Connecting call...', 'info');
|
| 4978 |
|
|
|
|
| 4980 |
startCallTimer();
|
| 4981 |
}
|
| 4982 |
} else if (peerConnection.connectionState === 'disconnected') {
|
| 4983 |
+
showCallWarningThrottled('⚠️ Connection unstable, trying to reconnect...');
|
| 4984 |
+
attemptIceRecovery(peerId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4985 |
} else if (peerConnection.connectionState === 'connected') {
|
| 4986 |
showToast('📞 Call connected!', 'success');
|
| 4987 |
|
|
|
|
| 4989 |
startCallTimer();
|
| 4990 |
}
|
| 4991 |
} else if (peerConnection.connectionState === 'failed') {
|
| 4992 |
+
showCallWarningThrottled('⚠️ Network issue detected, retrying call connection...');
|
| 4993 |
+
attemptIceRecovery(peerId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4994 |
} else if (peerConnection.connectionState === 'closed') {
|
| 4995 |
|
| 4996 |
cleanupCallSession(false);
|
|
|
|
| 5001 |
if (!peerConnection) return;
|
| 5002 |
console.log('ICE connection state:', peerConnection.iceConnectionState);
|
| 5003 |
|
| 5004 |
+
if (peerConnection.iceConnectionState === 'failed' || peerConnection.iceConnectionState === 'disconnected') {
|
| 5005 |
+
attemptIceRecovery(peerId);
|
|
|
|
| 5006 |
}
|
| 5007 |
};
|
| 5008 |
+
|
| 5009 |
+
async function attemptIceRecovery(recoveryPeerId) {
|
| 5010 |
+
if (!peerConnection || !socket || !socket.connected) return;
|
| 5011 |
+
|
| 5012 |
+
const now = Date.now();
|
| 5013 |
+
if (now - lastIceRecoveryAttemptAt < 4000) return;
|
| 5014 |
+
lastIceRecoveryAttemptAt = now;
|
| 5015 |
+
|
| 5016 |
+
try {
|
| 5017 |
+
peerConnection.restartIce?.();
|
| 5018 |
+
|
| 5019 |
+
if (peerConnection.signalingState === 'stable') {
|
| 5020 |
+
const recoveryOffer = await peerConnection.createOffer({
|
| 5021 |
+
iceRestart: true,
|
| 5022 |
+
offerToReceiveAudio: true,
|
| 5023 |
+
offerToReceiveVideo: true
|
| 5024 |
+
});
|
| 5025 |
+
await peerConnection.setLocalDescription(recoveryOffer);
|
| 5026 |
+
|
| 5027 |
+
socket.emit('video signal', {
|
| 5028 |
+
toId: recoveryPeerId,
|
| 5029 |
+
callId: activeCallState?.callId,
|
| 5030 |
+
signal: { type: 'offer', offer: recoveryOffer }
|
| 5031 |
+
});
|
| 5032 |
+
}
|
| 5033 |
+
} catch (recoveryError) {
|
| 5034 |
+
console.warn('ICE recovery attempt failed:', recoveryError);
|
| 5035 |
+
}
|
| 5036 |
+
}
|
| 5037 |
}
|
| 5038 |
|
| 5039 |
function openCallDialog(peerName) {
|
|
|
|
| 5076 |
createPeerConnection(peerId);
|
| 5077 |
|
| 5078 |
if (isCaller && peerConnection) {
|
| 5079 |
+
const offer = await peerConnection.createOffer({
|
| 5080 |
+
offerToReceiveAudio: true,
|
| 5081 |
+
offerToReceiveVideo: true
|
| 5082 |
+
});
|
| 5083 |
await peerConnection.setLocalDescription(offer);
|
| 5084 |
socket.emit('video signal', {
|
| 5085 |
toId: peerId,
|
|
|
|
| 5094 |
window.userIntentionallEndedCall = true;
|
| 5095 |
|
| 5096 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5097 |
if (socket && socket.connected && currentCallPeerId) {
|
| 5098 |
socket.emit('end video call', {
|
| 5099 |
callId: activeCallState?.callId,
|
|
|
|
| 5129 |
window.userIntentionallEndedCall = false;
|
| 5130 |
|
| 5131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5132 |
stopIncomingCallAlert();
|
| 5133 |
stopCallTimer();
|
| 5134 |
|
|
|
|
| 5139 |
peerConnection = null;
|
| 5140 |
}
|
| 5141 |
|
| 5142 |
+
remoteIceCandidatesQueue = [];
|
| 5143 |
+
remoteCallStream = null;
|
| 5144 |
+
lastIceRecoveryAttemptAt = 0;
|
| 5145 |
+
lastCallWarningAt = 0;
|
| 5146 |
+
|
| 5147 |
if (localCallStream) {
|
| 5148 |
localCallStream.getTracks().forEach((track) => track.stop());
|
| 5149 |
localCallStream = null;
|
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.4",
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|
server.js
CHANGED
|
@@ -689,9 +689,32 @@ app.post('/api/shop/buy', authenticateToken, async (req, res) => {
|
|
| 689 |
user.coins -= item.price;
|
| 690 |
await user.save();
|
| 691 |
|
| 692 |
-
await Inventory.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
-
res.json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
} catch (error) {
|
| 696 |
res.status(500).json({ error: error.message });
|
| 697 |
}
|
|
|
|
| 689 |
user.coins -= item.price;
|
| 690 |
await user.save();
|
| 691 |
|
| 692 |
+
let inventoryItem = await Inventory.findOne({
|
| 693 |
+
where: {
|
| 694 |
+
userId: user.id,
|
| 695 |
+
shopItemId: item.id
|
| 696 |
+
}
|
| 697 |
+
});
|
| 698 |
+
|
| 699 |
+
if (inventoryItem) {
|
| 700 |
+
inventoryItem.quantity += 1;
|
| 701 |
+
await inventoryItem.save();
|
| 702 |
+
} else {
|
| 703 |
+
inventoryItem = await Inventory.create({ userId: user.id, shopItemId: item.id, quantity: 1 });
|
| 704 |
+
}
|
| 705 |
|
| 706 |
+
res.json({
|
| 707 |
+
message: 'Item purchased',
|
| 708 |
+
coins: user.coins,
|
| 709 |
+
purchasedItem: {
|
| 710 |
+
id: item.id,
|
| 711 |
+
itemId: item.itemId,
|
| 712 |
+
name: item.name,
|
| 713 |
+
category: item.category,
|
| 714 |
+
description: item.description
|
| 715 |
+
},
|
| 716 |
+
quantity: inventoryItem.quantity
|
| 717 |
+
});
|
| 718 |
} catch (error) {
|
| 719 |
res.status(500).json({ error: error.message });
|
| 720 |
}
|