THEZYZSTUDIO commited on
Commit
bc8d45e
·
verified ·
1 Parent(s): 6f169ac

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +377 -183
static/index.html CHANGED
@@ -158,34 +158,6 @@ img{max-width:100%;display:block}
158
  .pact.liked{color:var(--acc3)}
159
  .pact .ai{font-size:17px}
160
 
161
- /* ── REELS ── */
162
- #reels-full{position:fixed;inset:0;z-index:200;background:#000;display:none}
163
- #reels-full.on{display:block}
164
- .reels-scroll{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;scrollbar-width:none}
165
- .reels-scroll::-webkit-scrollbar{display:none}
166
- .reel{height:100vh;width:100%;scroll-snap-align:start;position:relative;overflow:hidden;background:#000}
167
- .reel-bg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:100px}
168
- .reel-bg video{width:100%;height:100%;object-fit:cover;position:absolute;inset:0}
169
- .reel-grad{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.85) 0%,transparent 45%,rgba(0,0,0,.2) 100%)}
170
- .reel-info{position:absolute;bottom:0;left:0;right:64px;padding:16px 14px 90px}
171
- .reel-user{display:flex;align-items:center;gap:8px;margin-bottom:8px}
172
- .reel-av{width:38px;height:38px;border-radius:50%;border:2px solid #fff;overflow:hidden;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
173
- .reel-av img{width:100%;height:100%;object-fit:cover}
174
- .reel-uname{color:#fff;font-weight:700;font-size:14px;cursor:pointer}
175
- .reel-fol{background:transparent;border:1.5px solid #fff;border-radius:6px;padding:3px 10px;color:#fff;font-size:11px;font-weight:700;transition:all .15s}
176
- .reel-fol:hover{background:#fff;color:#000}
177
- .reel-cap{color:#fff;font-size:13px;line-height:1.6;margin-bottom:10px}
178
- .reel-mus{display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.8);font-size:12px}
179
- .reel-mdisc{width:28px;height:28px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:12px;animation:spin 3s linear infinite}
180
- .reel-acts{position:absolute;right:10px;bottom:90px;display:flex;flex-direction:column;align-items:center;gap:18px}
181
- .ract{display:flex;flex-direction:column;align-items:center;gap:3px;cursor:pointer}
182
- .ract-ic{width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,.12);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;font-size:20px;color:#fff;transition:transform .15s}
183
- .ract:hover .ract-ic{transform:scale(1.1)}
184
- .ract span{color:#fff;font-size:10px;font-weight:600}
185
- .reels-close{position:absolute;top:env(safe-area-inset-top,14px);left:14px;z-index:10;width:36px;height:36px;border-radius:50%;background:rgba(0,0,0,.5);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;cursor:pointer}
186
- .reel-video-wrap{position:absolute;inset:0}.reel-video{width:100%;height:100%;object-fit:cover;position:absolute;inset:0}.reel-sound,.reel-play,.sv-sound{position:absolute;z-index:11;width:36px;height:36px;border-radius:50%;background:rgba(0,0,0,.55);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;color:#fff;font-size:16px;cursor:pointer;border:none}.reel-play{top:14px;left:14px}.reel-sound{top:14px;right:14px}.sv-sound{top:env(safe-area-inset-top,14px);right:54px}.sv-sound.off{opacity:.55}
187
-
188
- /* ── STORY VIEWER ── */
189
  #sv{position:fixed;inset:0;z-index:300;background:#000;display:none;flex-direction:column}
190
  #sv.on{display:flex}
191
  .sv-bars{position:absolute;top:env(safe-area-inset-top,10px);left:0;right:0;display:flex;gap:3px;padding:10px 10px 0;z-index:5}
@@ -386,7 +358,6 @@ img{max-width:100%;display:block}
386
  .empty-state .eico{font-size:48px;margin-bottom:12px}
387
  .empty-state p{font-size:13px;text-align:center;line-height:1.6}
388
 
389
-
390
  /* ── STORY COMPOSER ── */
391
  .story-effects{display:flex;gap:8px;overflow-x:auto;padding:6px 0 0;scrollbar-width:none}
392
  .story-effects::-webkit-scrollbar{display:none}
@@ -415,6 +386,29 @@ img{max-width:100%;display:block}
415
  width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:11px;padding:11px 14px;color:var(--text);font-size:14px;outline:none;font-family:Cairo,sans-serif;
416
  }
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  /* ── TOAST ── */
419
  #toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(16px);background:var(--card);border:1px solid var(--border);border-radius:24px;padding:10px 20px;font-size:13px;font-weight:600;white-space:nowrap;opacity:0;transition:all .3s;z-index:9000;pointer-events:none;box-shadow:0 4px 20px rgba(0,0,0,.4)}
420
  #toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
@@ -499,8 +493,7 @@ img{max-width:100%;display:block}
499
  <div class="ecats" id="ecats">
500
  <button class="ecat on" data-cat="all">الكل</button>
501
  <button class="ecat" data-cat="video">🎬 فيديو</button>
502
- <button class="ecat" data-cat="reel">▶️ ريلز</button>
503
- <button class="ecat" data-cat="photo">📸 صور</button>
504
  <button class="ecat" data-cat="audio">🎵 صوتيات</button>
505
  <button class="ecat" data-cat="text">📝 نصوص</button>
506
  </div>
@@ -508,18 +501,8 @@ img{max-width:100%;display:block}
508
  <div id="explore-load" style="padding:20px;text-align:center"></div>
509
  </div>
510
 
511
- <!-- REELS -->
512
- <div class="page" id="page-reels">
513
- <div style="height:100%;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px;">
514
- <div style="font-size:48px">▶️</div>
515
- <div style="font-size:16px;font-weight:700">ريلز</div>
516
- <button class="btn-main" style="padding:12px 32px;border-radius:12px;width:auto" onclick="openReels()">افتح الريلز</button>
517
- </div>
518
- </div>
519
-
520
- <!-- MESSAGES -->
521
  <div class="page" id="page-messages">
522
- <div class="sec-head"><span class="sec-ttl">💬 الرسائل</span><button class="sec-lnk" onclick="showNewMsgModal()">+ جديد</button></div>
523
  <div class="mlist" id="conv-list"></div>
524
  </div>
525
 
@@ -543,7 +526,6 @@ img{max-width:100%;display:block}
543
  </div>
544
  <div class="ptabs">
545
  <div class="ptab-btn on" onclick="switchPTab(this,'posts')">منشورات</div>
546
- <div class="ptab-btn" onclick="switchPTab(this,'reels')">ريلز</div>
547
  <div class="ptab-btn" onclick="switchPTab(this,'saved')">محفوظ</div>
548
  </div>
549
  <div class="pgrid" id="prof-grid"></div>
@@ -567,18 +549,10 @@ img{max-width:100%;display:block}
567
  <button class="bnav-btn on" id="bn-home" onclick="showPage('home')"><span class="ic">🏠</span><span>الرئيسية</span></button>
568
  <button class="bnav-btn" id="bn-notif" onclick="showPage('notif')"><span class="ic">🔔</span><span>إشعارات</span></button>
569
  <div class="bnav-post" onclick="openPostModal()">+</div>
570
- <button class="bnav-btn" id="bn-reels" onclick="openReels()"><span class="ic">▶️</span><span>ريلز</span></button>
571
- <button class="bnav-btn" id="bn-messages" onclick="showPage('messages')"><span class="ic">💬</span><span>رسائل</span></button>
572
  </nav>
573
  </div>
574
 
575
- <!-- REELS FULLSCREEN -->
576
- <div id="reels-full">
577
- <div class="reels-close" onclick="closeReels()">✕</div>
578
- <div class="reels-scroll" id="reels-scroll"></div>
579
- </div>
580
-
581
- <!-- STORY VIEWER -->
582
  <div id="sv">
583
  <div class="sv-bars" id="sv-bars"></div>
584
  <div class="sv-head">
@@ -599,7 +573,6 @@ img{max-width:100%;display:block}
599
  </div>
600
  </div>
601
 
602
-
603
  <!-- STORY COMPOSER -->
604
  <div id="story-modal" class="moverlay" onclick="if(event.target===this)closeStoryComposer()">
605
  <div class="mbox">
@@ -651,8 +624,8 @@ img{max-width:100%;display:block}
651
  <div class="chat-st">● متصل</div>
652
  </div>
653
  <div class="chat-acts">
654
- <div class="chat-act" onclick="showToast('📞 مكالمة صوتية')">📞</div>
655
- <div class="chat-act" onclick="showToast('📹 مكالمة مرئية')">📹</div>
656
  </div>
657
  </div>
658
  <div class="chat-msgs" id="chat-msgs"></div>
@@ -668,6 +641,40 @@ img{max-width:100%;display:block}
668
  </div>
669
  </div>
670
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  <!-- POST MODAL — FULLSCREEN -->
672
  <div class="moverlay" id="post-modal">
673
  <div class="mbox">
@@ -728,7 +735,6 @@ img{max-width:100%;display:block}
728
  </div>
729
  </div>
730
 
731
-
732
  <!-- POST VIEWER -->
733
  <div id="post-viewer" class="moverlay" onclick="if(event.target===this)closePostViewer()">
734
  <div class="mbox">
@@ -804,6 +810,16 @@ let currentCommentPostId = null;
804
  let currentEditPostId = null;
805
  let currentMenuPostId = null;
806
  let currentMenuOwnerId = null;
 
 
 
 
 
 
 
 
 
 
807
 
808
  // ═══════════════════════════════════════════════
809
  // UTILS
@@ -1599,133 +1615,6 @@ document.getElementById('ecats').addEventListener('click', e => {
1599
  loadExplore();
1600
  });
1601
 
1602
- // ═══════════════════════════════════════════════
1603
- // REELS — FIXED (video display)
1604
- // ═══════════════════════════════════════════════
1605
- async function openReels() {
1606
- const full = document.getElementById('reels-full');
1607
- full.classList.add('on');
1608
- document.body.style.overflow = 'hidden';
1609
- const scroll = document.getElementById('reels-scroll');
1610
- scroll.innerHTML = '<div style="height:100%;display:flex;align-items:center;justify-content:center"><div class="spin-sm"></div></div>';
1611
- try {
1612
- const { reels } = await api(`/api/reels?user_id=${ME.id}`);
1613
- scroll.innerHTML = '';
1614
- if (!reels || !reels.length) {
1615
- scroll.innerHTML = `<div class="reel" style="display:flex;align-items:center;justify-content:center;flex-direction:column;gap:16px;color:#fff">
1616
- <div style="font-size:48px">▶️</div><div>لا توجد ريلز بعد</div>
1617
- <button onclick="closeReels();openPostModal()" style="background:var(--g1);color:#fff;border:none;border-radius:12px;padding:12px 24px;font-family:Cairo,sans-serif;font-weight:700;font-size:14px;cursor:pointer">أضف أول ريل ⚡</button>
1618
- </div>`;
1619
- return;
1620
- }
1621
- reels.forEach(r => scroll.appendChild(buildReel(r)));
1622
- } catch(e) {
1623
- scroll.innerHTML = `<div class="reel" style="display:flex;align-items:center;justify-content:center;color:#fff;flex-direction:column;gap:10px"><div>⚠️ ${e.message}</div></div>`;
1624
- }
1625
- }
1626
-
1627
- function closeReels() {
1628
- document.getElementById('reels-full').classList.remove('on');
1629
- document.body.style.overflow = '';
1630
- document.querySelectorAll('#reels-scroll video').forEach(v=>{ v.pause(); v.src = ''; });
1631
- }
1632
-
1633
- function buildReel(r) {
1634
- const u = r.user || {};
1635
- const d = document.createElement('div');
1636
- d.className = 'reel';
1637
- const emojis = ['💃','🎸','🌊','🔥','🎨','😂','✈️','🎵','⚡','🌟'];
1638
- const em = emojis[Math.floor(Math.random()*emojis.length)];
1639
-
1640
- // Reels: autoplay muted on load, but with explicit play/pause and sound controls
1641
- const videoHtml = r.file_url
1642
- ? `<div class="reel-video-wrap">
1643
- <video class="reel-video" src="${API_URL}${r.file_url}" playsinline loop muted webkit-playsinline preload="metadata"></video>
1644
- <button type="button" class="reel-play" onclick="toggleReelPlay(this)" title="إيقاف/تشغيل">⏸</button>
1645
- <button type="button" class="reel-sound" onclick="toggleReelSound(this)" title="تشغيل الصوت">🔇</button>
1646
- </div>`
1647
- : `<span style="font-size:80px;position:relative;z-index:1">${em}</span>`;
1648
-
1649
- d.innerHTML = `
1650
- <div class="reel-bg" style="z-index:0">${videoHtml}</div>
1651
- <div class="reel-grad" style="z-index:1"></div>
1652
- <div class="reel-info" style="z-index:2">
1653
- <div class="reel-user">
1654
- <div class="reel-av">${avatarEl(u,38)}</div>
1655
- <div class="reel-uname" onclick="openUserProfile('${u.id}')">${u.name||'—'}</div>
1656
- <button class="reel-fol" onclick="toggleFollowUser('${u.id}',this)">${r.user?.is_following?'✓ متابَع':'+ تابع'}</button>
1657
- </div>
1658
- <div class="reel-cap">${r.caption||''}</div>
1659
- <div class="reel-mus"><div class="reel-mdisc">♪</div><span>THE Z CHAT</span></div>
1660
- </div>
1661
- <div class="reel-acts" style="z-index:2">
1662
- <div class="ract" onclick="toggleLikeReel(this,'${r.id}')">
1663
- <div class="ract-ic">${r.is_liked?'❤️':'🤍'}</div><span>${fmt(r.likes_count)}</span>
1664
- </div>
1665
- <div class="ract" onclick="openComments('${r.id}')">
1666
- <div class="ract-ic">💬</div><span>${fmt(r.comments_count)}</span>
1667
- </div>
1668
- <div class="ract" onclick="sharePost('${r.id}')">
1669
- <div class="ract-ic">↗️</div><span>شارك</span>
1670
- </div>
1671
- </div>`;
1672
-
1673
- // Auto-play / pause based on visibility, while keeping current sound state
1674
- if (r.file_url) {
1675
- const obs = new IntersectionObserver(entries => {
1676
- entries.forEach(e => {
1677
- const v = d.querySelector('video');
1678
- const playBtn = d.querySelector('.reel-play');
1679
- if (!v) return;
1680
- if (e.isIntersecting) {
1681
- v.play().then(() => {
1682
- if (playBtn) playBtn.textContent = '⏸';
1683
- }).catch(()=>{});
1684
- } else {
1685
- v.pause();
1686
- if (playBtn) playBtn.textContent = '▶️';
1687
- }
1688
- });
1689
- }, { threshold: 0.6 });
1690
- obs.observe(d);
1691
-
1692
- // Tap anywhere on the video to toggle pause/play
1693
- const v = d.querySelector('video');
1694
- v && v.addEventListener('click', () => toggleReelPlay(d.querySelector('.reel-play')));
1695
- }
1696
- return d;
1697
- }
1698
-
1699
- function toggleReelPlay(btn) {
1700
- const reel = btn.closest('.reel');
1701
- const vid = reel ? reel.querySelector('video') : null;
1702
- if (!vid) return;
1703
- if (vid.paused) {
1704
- vid.play().then(() => { btn.textContent = '⏸'; }).catch(() => {});
1705
- } else {
1706
- vid.pause();
1707
- btn.textContent = '▶️';
1708
- }
1709
- }
1710
-
1711
- function toggleReelSound(btn) {
1712
- const reel = btn.closest('.reel');
1713
- const vid = reel ? reel.querySelector('video') : null;
1714
- if (!vid) return;
1715
- vid.muted = !vid.muted;
1716
- vid.volume = 1;
1717
- if (!vid.muted && vid.paused) vid.play().catch(()=>{});
1718
- btn.textContent = vid.muted ? '🔇' : '🔊';
1719
- btn.title = vid.muted ? 'تشغيل الصوت' : 'كتم الصوت';
1720
- }
1721
- async function toggleLikeReel(el, postId) {
1722
- try {
1723
- const res = await api(`/api/posts/${postId}/like?user_id=${ME.id}`, 'POST');
1724
- el.querySelector('.ract-ic').textContent = res.liked ? '❤️' : '🤍';
1725
- el.querySelector('span').textContent = fmt(res.count);
1726
- } catch(e) {}
1727
- }
1728
-
1729
  // ═══════════════════════════════════════════════
1730
  // MESSAGES — FIXED (file sending)
1731
  // ═══════════════════════════════════════════════
@@ -1846,6 +1735,309 @@ async function sendChatFile(inp) {
1846
  }
1847
 
1848
  function closeChat() { document.getElementById('chat-win').classList.remove('on'); currentChat = null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1849
  function showNewMsgModal() {
1850
  const un = prompt('اكتب اسم مستخدم الشخص:');
1851
  if (!un) return;
@@ -2255,6 +2447,8 @@ function connectWS() {
2255
  showToast('💬 رسالة جديدة من ' + (msg.message.sender?.name || 'مستخدم'));
2256
  loadConversations();
2257
  }
 
 
2258
  }
2259
  } catch(e) {}
2260
  };
 
158
  .pact.liked{color:var(--acc3)}
159
  .pact .ai{font-size:17px}
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  #sv{position:fixed;inset:0;z-index:300;background:#000;display:none;flex-direction:column}
162
  #sv.on{display:flex}
163
  .sv-bars{position:absolute;top:env(safe-area-inset-top,10px);left:0;right:0;display:flex;gap:3px;padding:10px 10px 0;z-index:5}
 
358
  .empty-state .eico{font-size:48px;margin-bottom:12px}
359
  .empty-state p{font-size:13px;text-align:center;line-height:1.6}
360
 
 
361
  /* ── STORY COMPOSER ── */
362
  .story-effects{display:flex;gap:8px;overflow-x:auto;padding:6px 0 0;scrollbar-width:none}
363
  .story-effects::-webkit-scrollbar{display:none}
 
386
  width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:11px;padding:11px 14px;color:var(--text);font-size:14px;outline:none;font-family:Cairo,sans-serif;
387
  }
388
 
389
+
390
+ /* ── CALLS ── */
391
+ #call-modal{position:fixed;inset:0;z-index:720;background:rgba(0,0,0,.96);display:none;align-items:center;justify-content:center;padding:16px}
392
+ #call-modal.on{display:flex}
393
+ .call-shell{width:min(100%,540px);height:min(100%,760px);background:linear-gradient(180deg,rgba(18,18,28,.98),rgba(10,10,16,.98));border:1px solid var(--border);border-radius:28px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.45)}
394
+ .call-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 18px;border-bottom:1px solid var(--border);flex-shrink:0}
395
+ .call-peer{display:flex;align-items:center;gap:12px;min-width:0}
396
+ .call-peer-av{width:48px;height:48px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0;font-size:18px;font-weight:700}
397
+ .call-peer-name{font-size:16px;font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}
398
+ .call-status{font-size:12px;color:var(--text2);margin-top:2px}
399
+ .call-close{width:36px;height:36px;border-radius:50%;background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
400
+ .call-media{flex:1;position:relative;background:#000;min-height:300px;display:flex;align-items:center;justify-content:center}
401
+ .call-media video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#000}
402
+ #call-local-video{width:28%;height:28%;right:16px;left:auto;bottom:16px;top:auto;border:1px solid rgba(255,255,255,.25);border-radius:16px;overflow:hidden;object-fit:cover;z-index:2;display:none}
403
+ #call-remote-video{z-index:1}
404
+ .call-ring{position:relative;z-index:3;width:110px;height:110px;border-radius:50%;background:rgba(255,255,255,.08);backdrop-filter:blur(12px);display:flex;align-items:center;justify-content:center;font-size:40px;border:1px solid rgba(255,255,255,.12);animation:pulse 1.5s ease-in-out infinite}
405
+ .call-incoming,.call-controls{display:flex;gap:10px;padding:14px 16px;justify-content:center;flex-shrink:0}
406
+ .call-btn{min-width:92px;height:44px;border-radius:14px;background:var(--bg3);border:1px solid var(--border);color:var(--text);font-weight:800;font-size:13px;display:flex;align-items:center;justify-content:center}
407
+ .call-btn.accept{background:var(--acc2);border-color:var(--acc2);color:#fff}
408
+ .call-btn.danger{background:var(--acc3);border-color:var(--acc3);color:#fff}
409
+ .call-controls{padding-top:0;padding-bottom:18px}
410
+ .call-btn:active{transform:scale(.97)}
411
+
412
  /* ── TOAST ── */
413
  #toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(16px);background:var(--card);border:1px solid var(--border);border-radius:24px;padding:10px 20px;font-size:13px;font-weight:600;white-space:nowrap;opacity:0;transition:all .3s;z-index:9000;pointer-events:none;box-shadow:0 4px 20px rgba(0,0,0,.4)}
414
  #toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
 
493
  <div class="ecats" id="ecats">
494
  <button class="ecat on" data-cat="all">الكل</button>
495
  <button class="ecat" data-cat="video">🎬 فيديو</button>
496
+ <button class="ecat" data-cat="photo">📸 صور</button>
 
497
  <button class="ecat" data-cat="audio">🎵 صوتيات</button>
498
  <button class="ecat" data-cat="text">📝 نصوص</button>
499
  </div>
 
501
  <div id="explore-load" style="padding:20px;text-align:center"></div>
502
  </div>
503
 
 
 
 
 
 
 
 
 
 
 
504
  <div class="page" id="page-messages">
505
+ <div class="sec-head"><span class="sec-ttl">💬 المراسلات</span><button class="sec-lnk" onclick="showNewMsgModal()">+ جديد</button></div>
506
  <div class="mlist" id="conv-list"></div>
507
  </div>
508
 
 
526
  </div>
527
  <div class="ptabs">
528
  <div class="ptab-btn on" onclick="switchPTab(this,'posts')">منشورات</div>
 
529
  <div class="ptab-btn" onclick="switchPTab(this,'saved')">محفوظ</div>
530
  </div>
531
  <div class="pgrid" id="prof-grid"></div>
 
549
  <button class="bnav-btn on" id="bn-home" onclick="showPage('home')"><span class="ic">🏠</span><span>الرئيسية</span></button>
550
  <button class="bnav-btn" id="bn-notif" onclick="showPage('notif')"><span class="ic">🔔</span><span>إشعارات</span></button>
551
  <div class="bnav-post" onclick="openPostModal()">+</div>
552
+ <button class="bnav-btn" id="bn-messages" onclick="showPage('messages')"><span class="ic">💬</span><span>المراسلات</span></button>
 
553
  </nav>
554
  </div>
555
 
 
 
 
 
 
 
 
556
  <div id="sv">
557
  <div class="sv-bars" id="sv-bars"></div>
558
  <div class="sv-head">
 
573
  </div>
574
  </div>
575
 
 
576
  <!-- STORY COMPOSER -->
577
  <div id="story-modal" class="moverlay" onclick="if(event.target===this)closeStoryComposer()">
578
  <div class="mbox">
 
624
  <div class="chat-st">● متصل</div>
625
  </div>
626
  <div class="chat-acts">
627
+ <div class="chat-act" title="مكالمة صوتية" onclick="startCall('audio')">📞</div>
628
+ <div class="chat-act" title="مكالمة مرئية" onclick="startCall('video')">📹</div>
629
  </div>
630
  </div>
631
  <div class="chat-msgs" id="chat-msgs"></div>
 
641
  </div>
642
  </div>
643
 
644
+
645
+ <!-- CALL MODAL -->
646
+ <div id="call-modal">
647
+ <div class="call-shell">
648
+ <div class="call-head">
649
+ <div class="call-peer">
650
+ <div class="call-peer-av" id="call-peer-av">👤</div>
651
+ <div class="call-peer-meta">
652
+ <div class="call-peer-name" id="call-peer-name">—</div>
653
+ <div class="call-status" id="call-status">جاهز للمكالمة</div>
654
+ </div>
655
+ </div>
656
+ <button class="call-close" onclick="endCall(true)">✕</button>
657
+ </div>
658
+
659
+ <div class="call-media">
660
+ <video id="call-remote-video" autoplay playsinline></video>
661
+ <video id="call-local-video" autoplay muted playsinline></video>
662
+ <div class="call-ring" id="call-ring">📞</div>
663
+ </div>
664
+
665
+ <div class="call-incoming" id="call-incoming-actions" style="display:none">
666
+ <button class="call-btn accept" onclick="acceptIncomingCall()">قبول</button>
667
+ <button class="call-btn danger" onclick="rejectIncomingCall()">رفض</button>
668
+ </div>
669
+
670
+ <div class="call-controls">
671
+ <button class="call-btn" id="call-mic-btn" onclick="toggleCallMute()">🎤</button>
672
+ <button class="call-btn" id="call-cam-btn" onclick="toggleCallCamera()">📷</button>
673
+ <button class="call-btn danger" onclick="endCall(true)">إنهاء</button>
674
+ </div>
675
+ </div>
676
+ </div>
677
+
678
  <!-- POST MODAL — FULLSCREEN -->
679
  <div class="moverlay" id="post-modal">
680
  <div class="mbox">
 
735
  </div>
736
  </div>
737
 
 
738
  <!-- POST VIEWER -->
739
  <div id="post-viewer" class="moverlay" onclick="if(event.target===this)closePostViewer()">
740
  <div class="mbox">
 
810
  let currentEditPostId = null;
811
  let currentMenuPostId = null;
812
  let currentMenuOwnerId = null;
813
+ let callPeer = null;
814
+ let callStream = null;
815
+ let callType = '';
816
+ let callTargetId = '';
817
+ let callId = '';
818
+ let callMode = 'idle'; // idle | outgoing | incoming | connected
819
+ let callIncomingOffer = null;
820
+ let callIncomingFrom = null;
821
+ let callMute = false;
822
+ let callCameraOff = false;
823
 
824
  // ═══════════════════════════════════════════════
825
  // UTILS
 
1615
  loadExplore();
1616
  });
1617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1618
  // ═══════════════════════════════════════════════
1619
  // MESSAGES — FIXED (file sending)
1620
  // ═══════════════════════════════════════════════
 
1735
  }
1736
 
1737
  function closeChat() { document.getElementById('chat-win').classList.remove('on'); currentChat = null; }
1738
+
1739
+
1740
+ // ═══════════════════════════════════════════════
1741
+ // CALLS — WebRTC
1742
+ // ═══════════════════════════════════════════════
1743
+ const CALL_ICE = [{ urls: 'stun:stun.l.google.com:19302' }];
1744
+
1745
+ function uid() {
1746
+ try { return crypto.randomUUID(); } catch(e) { return 'call-' + Date.now() + '-' + Math.floor(Math.random()*1e6); }
1747
+ }
1748
+
1749
+ function showCallModal() {
1750
+ document.getElementById('call-modal').classList.add('on');
1751
+ }
1752
+
1753
+ function hideCallModal() {
1754
+ document.getElementById('call-modal').classList.remove('on');
1755
+ }
1756
+
1757
+ function setCallStatus(text) {
1758
+ const el = document.getElementById('call-status');
1759
+ if (el) el.textContent = text;
1760
+ }
1761
+
1762
+ function setCallPeer(user, incoming=false) {
1763
+ const nameEl = document.getElementById('call-peer-name');
1764
+ const avEl = document.getElementById('call-peer-av');
1765
+ if (nameEl) nameEl.textContent = user?.name || '—';
1766
+ if (avEl) avEl.innerHTML = avatarEl(user, 48);
1767
+ document.getElementById('call-incoming-actions').style.display = incoming ? 'flex' : 'none';
1768
+ }
1769
+
1770
+ function resetCallUI() {
1771
+ const remote = document.getElementById('call-remote-video');
1772
+ const local = document.getElementById('call-local-video');
1773
+ const ring = document.getElementById('call-ring');
1774
+ if (remote) remote.srcObject = null;
1775
+ if (local) local.srcObject = null;
1776
+ if (local) local.style.display = 'none';
1777
+ if (ring) ring.style.display = 'flex';
1778
+ document.getElementById('call-incoming-actions').style.display = 'none';
1779
+ document.getElementById('call-mic-btn').textContent = '🎤';
1780
+ document.getElementById('call-cam-btn').textContent = '📷';
1781
+ callMute = false;
1782
+ callCameraOff = false;
1783
+ }
1784
+
1785
+ async function prepareLocalStream(kind) {
1786
+ const constraints = { audio: true, video: kind === 'video' };
1787
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
1788
+ callStream = stream;
1789
+ const local = document.getElementById('call-local-video');
1790
+ local.srcObject = stream;
1791
+ local.style.display = kind === 'video' ? 'block' : 'none';
1792
+ const ring = document.getElementById('call-ring');
1793
+ if (ring) ring.style.display = 'none';
1794
+ return stream;
1795
+ }
1796
+
1797
+ function makePeer() {
1798
+ if (callPeer) {
1799
+ try { callPeer.close(); } catch(e) {}
1800
+ }
1801
+ callPeer = new RTCPeerConnection({ iceServers: CALL_ICE });
1802
+
1803
+ callPeer.onicecandidate = (e) => {
1804
+ if (e.candidate && WS?.readyState === 1) {
1805
+ WS.send(JSON.stringify({
1806
+ type: 'call_ice',
1807
+ to: callTargetId,
1808
+ call_id: callId,
1809
+ call_type: callType,
1810
+ candidate: e.candidate
1811
+ }));
1812
+ }
1813
+ };
1814
+
1815
+ callPeer.ontrack = (e) => {
1816
+ const remote = document.getElementById('call-remote-video');
1817
+ if (remote && e.streams && e.streams[0]) {
1818
+ remote.srcObject = e.streams[0];
1819
+ remote.play?.().catch(() => {});
1820
+ }
1821
+ setCallStatus('متصل');
1822
+ };
1823
+
1824
+ callPeer.onconnectionstatechange = () => {
1825
+ if (!callPeer) return;
1826
+ const st = callPeer.connectionState;
1827
+ if (st === 'connected') setCallStatus('متصل');
1828
+ else if (st === 'connecting') setCallStatus('جاري الاتصال...');
1829
+ else if (st === 'failed') {
1830
+ setCallStatus('فشل الاتصال');
1831
+ endCall(false);
1832
+ } else if (st === 'disconnected' || st === 'closed') {
1833
+ endCall(false);
1834
+ }
1835
+ };
1836
+ }
1837
+
1838
+ async function startCall(type) {
1839
+ if (!currentChat) return showToast('افتح محادثة أولاً');
1840
+ if (!WS || WS.readyState !== 1) return showToast('الاتصال غير جاهز');
1841
+ if (!navigator.mediaDevices?.getUserMedia) return showToast('المتصفح لا يدعم المكالمات');
1842
+ if (callMode !== 'idle') endCall(false);
1843
+
1844
+ callType = type;
1845
+ callTargetId = currentChat.user_id;
1846
+ callId = uid();
1847
+ callMode = 'outgoing';
1848
+ callIncomingOffer = null;
1849
+ callIncomingFrom = null;
1850
+
1851
+ setCallPeer(currentChat, false);
1852
+ setCallStatus(type === 'video' ? 'بدء مكالمة مرئية...' : 'بدء مكالمة صوتية...');
1853
+ showCallModal();
1854
+ resetCallUI();
1855
+ document.getElementById('call-ring').style.display = 'flex';
1856
+
1857
+ try {
1858
+ await prepareLocalStream(type);
1859
+ makePeer();
1860
+ callStream.getTracks().forEach(track => callPeer.addTrack(track, callStream));
1861
+
1862
+ const offer = await callPeer.createOffer();
1863
+ await callPeer.setLocalDescription(offer);
1864
+
1865
+ WS.send(JSON.stringify({
1866
+ type: 'call_offer',
1867
+ to: callTargetId,
1868
+ call_id: callId,
1869
+ call_type: callType,
1870
+ from_name: ME?.name || ''
1871
+ }));
1872
+ WS.send(JSON.stringify({
1873
+ type: 'call_offer_sdp',
1874
+ to: callTargetId,
1875
+ call_id: callId,
1876
+ call_type: callType,
1877
+ sdp: offer
1878
+ }));
1879
+ setCallStatus('جاري الاتصال...');
1880
+ } catch(e) {
1881
+ showToast('❌ ' + (e.message || 'تعذر بدء المكالمة'));
1882
+ endCall(false);
1883
+ }
1884
+ }
1885
+
1886
+ function handleIncomingCall(msg) {
1887
+ if (callMode !== 'idle') {
1888
+ try {
1889
+ WS.send(JSON.stringify({
1890
+ type: 'call_busy',
1891
+ to: msg.from,
1892
+ call_id: msg.call_id || '',
1893
+ call_type: msg.call_type || 'audio'
1894
+ }));
1895
+ } catch(e) {}
1896
+ return;
1897
+ }
1898
+
1899
+ callMode = 'incoming';
1900
+ callType = msg.call_type || 'audio';
1901
+ callTargetId = msg.from;
1902
+ callId = msg.call_id || uid();
1903
+ callIncomingFrom = { id: msg.from, name: msg.from_name || 'مستخدم' };
1904
+ callIncomingOffer = msg.sdp || null;
1905
+
1906
+ setCallPeer(callIncomingFrom, true);
1907
+ setCallStatus(callType === 'video' ? 'مكالمة مرئية واردة' : 'مكالمة صوتية واردة');
1908
+ showCallModal();
1909
+ resetCallUI();
1910
+ document.getElementById('call-ring').style.display = 'flex';
1911
+ }
1912
+
1913
+ async function acceptIncomingCall() {
1914
+ if (!callIncomingOffer) return;
1915
+ document.getElementById('call-incoming-actions').style.display = 'none';
1916
+ setCallStatus('جاري قبول المكالمة...');
1917
+ try {
1918
+ await prepareLocalStream(callType);
1919
+ makePeer();
1920
+ callStream.getTracks().forEach(track => callPeer.addTrack(track, callStream));
1921
+
1922
+ await callPeer.setRemoteDescription(new RTCSessionDescription(callIncomingOffer));
1923
+ const answer = await callPeer.createAnswer();
1924
+ await callPeer.setLocalDescription(answer);
1925
+
1926
+ WS.send(JSON.stringify({
1927
+ type: 'call_answer',
1928
+ to: callTargetId,
1929
+ call_id: callId,
1930
+ call_type: callType,
1931
+ sdp: answer
1932
+ }));
1933
+ callMode = 'connected';
1934
+ setCallStatus('متصل');
1935
+ } catch(e) {
1936
+ showToast('❌ ' + (e.message || 'تعذر قبول المكالمة'));
1937
+ endCall(false);
1938
+ }
1939
+ }
1940
+
1941
+ function rejectIncomingCall() {
1942
+ if (WS?.readyState === 1 && callTargetId) {
1943
+ WS.send(JSON.stringify({
1944
+ type: 'call_reject',
1945
+ to: callTargetId,
1946
+ call_id: callId,
1947
+ call_type: callType
1948
+ }));
1949
+ }
1950
+ endCall(false);
1951
+ }
1952
+
1953
+ function toggleCallMute() {
1954
+ if (!callStream) return;
1955
+ callMute = !callMute;
1956
+ callStream.getAudioTracks().forEach(t => t.enabled = !callMute);
1957
+ document.getElementById('call-mic-btn').textContent = callMute ? '🔇' : '🎤';
1958
+ }
1959
+
1960
+ function toggleCallCamera() {
1961
+ if (!callStream) return;
1962
+ const tracks = callStream.getVideoTracks();
1963
+ if (!tracks.length) return showToast('هذه مكالمة صوتية');
1964
+ callCameraOff = !callCameraOff;
1965
+ tracks.forEach(t => t.enabled = !callCameraOff);
1966
+ document.getElementById('call-cam-btn').textContent = callCameraOff ? '🚫📷' : '📷';
1967
+ }
1968
+
1969
+ function endCall(notify = true) {
1970
+ try {
1971
+ if (notify && WS?.readyState === 1 && callTargetId) {
1972
+ WS.send(JSON.stringify({
1973
+ type: 'call_end',
1974
+ to: callTargetId,
1975
+ call_id: callId,
1976
+ call_type: callType
1977
+ }));
1978
+ }
1979
+ } catch(e) {}
1980
+
1981
+ if (callPeer) {
1982
+ try { callPeer.ontrack = null; callPeer.onicecandidate = null; callPeer.close(); } catch(e) {}
1983
+ }
1984
+ if (callStream) {
1985
+ callStream.getTracks().forEach(t => { try { t.stop(); } catch(e) {} });
1986
+ }
1987
+ callPeer = null;
1988
+ callStream = null;
1989
+ callType = '';
1990
+ callTargetId = '';
1991
+ callId = '';
1992
+ callMode = 'idle';
1993
+ callIncomingOffer = null;
1994
+ callIncomingFrom = null;
1995
+ resetCallUI();
1996
+ hideCallModal();
1997
+ setCallStatus('جاهز للمكالمة');
1998
+ if (notify) showToast('تم إنهاء المكالمة');
1999
+ }
2000
+
2001
+ function handleCallSignal(msg) {
2002
+ if (!msg || !msg.type) return;
2003
+ if (msg.type === 'call_offer') {
2004
+ handleIncomingCall(msg);
2005
+ return;
2006
+ }
2007
+ if (msg.type === 'call_offer_sdp') {
2008
+ if (callMode === 'outgoing' && callPeer && msg.sdp) {
2009
+ callPeer.setRemoteDescription(new RTCSessionDescription(msg.sdp)).catch(() => {});
2010
+ document.getElementById('call-ring').style.display = 'none';
2011
+ document.getElementById('call-local-video').style.display = callType === 'video' ? 'block' : 'none';
2012
+ }
2013
+ return;
2014
+ }
2015
+ if (msg.type === 'call_answer') {
2016
+ if (callMode === 'outgoing' && callPeer && msg.sdp) {
2017
+ callPeer.setRemoteDescription(new RTCSessionDescription(msg.sdp)).catch(() => {});
2018
+ callMode = 'connected';
2019
+ setCallStatus('متصل');
2020
+ document.getElementById('call-ring').style.display = 'none';
2021
+ }
2022
+ return;
2023
+ }
2024
+ if (msg.type === 'call_ice' && callPeer && msg.candidate) {
2025
+ callPeer.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(() => {});
2026
+ return;
2027
+ }
2028
+ if (msg.type === 'call_reject') {
2029
+ showToast('تم رفض المكالمة');
2030
+ endCall(false);
2031
+ return;
2032
+ }
2033
+ if (msg.type === 'call_end' || msg.type === 'call_busy') {
2034
+ showToast(msg.type === 'call_busy' ? 'الشخص مشغول الآن' : 'انتهت المكالمة');
2035
+ endCall(false);
2036
+ return;
2037
+ }
2038
+ }
2039
+
2040
+
2041
  function showNewMsgModal() {
2042
  const un = prompt('اكتب اسم مستخدم الشخص:');
2043
  if (!un) return;
 
2447
  showToast('💬 رسالة جديدة من ' + (msg.message.sender?.name || 'مستخدم'));
2448
  loadConversations();
2449
  }
2450
+ } else if (msg.type && msg.type.startsWith('call_')) {
2451
+ handleCallSignal(msg);
2452
  }
2453
  } catch(e) {}
2454
  };