Spaces:
Sleeping
Sleeping
Update static/index.html
Browse files- 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 |
-
|
| 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">💬 الر
|
| 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-
|
| 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"
|
| 655 |
-
<div class="chat-act"
|
| 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 |
};
|