Dhurgh commited on
Commit
70a3190
·
1 Parent(s): e847ebb

release 2.3.4

Browse files
Files changed (3) hide show
  1. client.js +206 -66
  2. package.json +1 -1
  3. 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 peerConnection.addIceCandidate(new RTCIceCandidate(signal.candidate));
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.display = 'none';
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.right = '-500px';
656
  }
657
 
658
 
@@ -786,7 +790,14 @@ function toggleChannelSidebar() {
786
  }
787
 
788
  function openProfile() {
789
- document.getElementById('profilePanel').classList.add('show');
 
 
 
 
 
 
 
790
  loadUserProfile();
791
  }
792
 
@@ -807,14 +818,19 @@ function shareProfileLink() {
807
  }
808
 
809
  function closeProfile() {
810
- document.getElementById('profilePanel').classList.remove('show');
 
 
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
- event.target.classList.add('active');
 
 
 
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
- showToast('Item purchased!');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1985
  loadShopItems();
 
1986
  } else {
1987
  showToast(data.error, 'error');
1988
  }
@@ -2028,15 +2066,59 @@ async function loadInventory() {
2028
  }
2029
  }
2030
 
2031
- function applyShopItem(itemId, category) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2032
  const effectsMap = {
2033
  'Banners': () => {
2034
  document.documentElement.style.setProperty('--primary-color', '#00d4ff');
2035
  document.documentElement.style.setProperty('--primary-hover', '#00b8e6');
2036
- showToast('✨ Banner effect applied! Your profile now has a neon glow.', 'success');
2037
  },
2038
  'Badges': () => {
2039
- showToast('🏆 Badge equipped! It now appears next to your name.', 'success');
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
- showToast('🎨 Theme applied! Your accent color has changed.', 'success');
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
- showToast('✨ Message glow effect activated!', 'success');
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
- showToast('🌟 Avatar aura effect activated!', 'success');
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
- showToast('💎 Crystal nameplate effect activated!', 'success');
2101
  },
2102
  'Emotes': () => {
2103
- showToast('😎 Special emotes unlocked! Use them in your messages.', 'success');
2104
  }
2105
  };
2106
 
2107
  const applyEffect = effectsMap[category];
2108
  if (applyEffect) {
2109
  applyEffect();
 
 
 
 
2110
  } else {
2111
- showToast('Effect applied!', 'success');
2112
  }
2113
  }
2114
 
@@ -2140,7 +2226,10 @@ function showToast(message, type = 'success') {
2140
 
2141
 
2142
  function openSettings() {
2143
- document.getElementById('settingsPanel').classList.add('show');
 
 
 
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').classList.remove('show');
 
 
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
- event.target.classList.add('active');
 
 
 
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: { facingMode: 'user' }
4744
  });
4745
- showToast('📹 Camera and microphone enabled (default devices)', 'success');
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 && event.streams[0]) {
4805
- remoteVideo.srcObject = event.streams[0];
 
 
 
 
 
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
- 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
 
@@ -4869,14 +4989,8 @@ function createPeerConnection(peerId) {
4869
  startCallTimer();
4870
  }
4871
  } else if (peerConnection.connectionState === 'failed') {
4872
- showToast('Network issue detected, trying to recover call...', 'warning');
4873
- peerConnection.restartIce?.();
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.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.create({ userId: user.id, shopItemId: item.id });
 
 
 
 
 
 
 
 
 
 
 
 
693
 
694
- res.json({ message: 'Item purchased', coins: user.coins });
 
 
 
 
 
 
 
 
 
 
 
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
  }