Thezchat / static /index.html
THEZYZSTUDIO's picture
Update static/index.html
bc8d45e verified
Raw
History Blame Contribute Delete
136 kB
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>THE Z CHAT — by The ZYZ Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700;900&family=Tajawal:wght@300;400;700;900&display=swap" rel="stylesheet">
<style>
:root {
--bg:#08080e;--bg2:#0f0f18;--bg3:#171724;--card:#131320;--border:#242436;
--acc:#8b5cf6;--acc2:#06d6a0;--acc3:#ff4f6d;--acc4:#f59e0b;
--text:#eeeeff;--text2:#8888aa;--text3:#44445a;
--g1:linear-gradient(135deg,#8b5cf6,#ec4899);
--g2:linear-gradient(135deg,#06d6a0,#0ea5e9);
--g3:linear-gradient(135deg,#f59e0b,#ef4444);
--r:14px;--rs:10px;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%;overflow:hidden;-webkit-overflow-scrolling:touch}
body{font-family:'Cairo',sans-serif;background:var(--bg);color:var(--text);overscroll-behavior:none}
button{font-family:'Cairo',sans-serif;cursor:pointer;border:none;background:none}
input,textarea{font-family:'Cairo',sans-serif}
a{text-decoration:none;color:inherit}
img{max-width:100%;display:block}
::-webkit-scrollbar{width:3px;height:3px}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:99px}
::-webkit-scrollbar-track{background:transparent}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes fup{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
@keyframes bin{from{opacity:0;transform:scale(.88)}to{opacity:1;transform:none}}
@keyframes mup{from{transform:translateY(100%)}to{transform:none}}
@keyframes sfill{from{width:0}to{width:100%}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
@keyframes slideIn{from{transform:translateX(100%)}to{transform:translateX(0)}}
/* ── SPINNER ── */
.spin-sm{width:22px;height:22px;border-radius:50%;border:2.5px solid var(--border);border-top-color:var(--acc);animation:spin .7s linear infinite;display:inline-block}
/* ── SPLASH ── */
#splash{position:fixed;inset:0;z-index:9999;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;transition:opacity .4s}
.sp-logo{font-family:'Tajawal',sans-serif;font-size:56px;font-weight:900;background:var(--g1);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-2px}
.sp-by{color:var(--text3);font-size:12px;letter-spacing:3px;text-transform:uppercase}
.sp-ring{width:44px;height:44px;border-radius:50%;border:3px solid var(--border);border-top-color:var(--acc);animation:spin .8s linear infinite;margin-top:12px}
/* ── AUTH ── */
#auth{position:fixed;inset:0;z-index:500;background:var(--bg);display:none;align-items:center;justify-content:center;padding:20px;overflow-y:auto}
.auth-card{background:var(--card);border:1px solid var(--border);border-radius:24px;padding:32px 24px;width:100%;max-width:400px;position:relative;overflow:hidden}
.auth-card::before{content:'';position:absolute;top:-60px;right:-60px;width:180px;height:180px;border-radius:50%;background:radial-gradient(circle,rgba(139,92,246,.1),transparent 70%);pointer-events:none}
.auth-logo{font-family:'Tajawal',sans-serif;font-size:36px;font-weight:900;background:var(--g1);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;margin-bottom:2px}
.auth-by{color:var(--text3);font-size:10px;text-align:center;margin-bottom:24px;letter-spacing:3px;text-transform:uppercase}
.atabs{display:flex;background:var(--bg3);border-radius:12px;padding:4px;margin-bottom:20px;gap:4px}
.atab{flex:1;padding:9px;border-radius:9px;color:var(--text2);font-size:13px;font-weight:700;transition:all .2s}
.atab.on{background:var(--acc);color:#fff}
.aform{display:flex;flex-direction:column;gap:11px}
.aform.hide{display:none}
.fg{display:flex;flex-direction:column;gap:4px}
.fg label{font-size:11px;color:var(--text2);font-weight:700}
.pw-wrap{position:relative;display:flex;align-items:center}
.pw-wrap input{width:100%;padding-left:44px !important}
.pw-eye{position:absolute;left:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text3);font-size:16px;padding:4px;display:flex;align-items:center;justify-content:center;transition:color .2s;z-index:2}
.pw-eye:hover{color:var(--text2)}
.fg input,.fg textarea{background:var(--bg3);border:1px solid var(--border);border-radius:11px;padding:11px 14px;color:var(--text);font-size:14px;outline:none;transition:border-color .2s;width:100%}
.fg input:focus{border-color:var(--acc)}
.fg input::placeholder{color:var(--text3)}
.err{color:var(--acc3);font-size:12px;min-height:18px;text-align:center;padding:2px 0}
.btn-main{background:var(--g1);border-radius:12px;padding:13px;color:#fff;font-size:14px;font-weight:700;transition:opacity .2s,transform .1s;margin-top:2px;width:100%;display:flex;align-items:center;justify-content:center;gap:8px}
.btn-main:hover{opacity:.88}
.btn-main:active{transform:scale(.97)}
.btn-main:disabled{opacity:.5;cursor:not-allowed}
.btn-hf{background:linear-gradient(135deg,#ff9a00,#ffcc00);border-radius:12px;padding:11px;color:#000;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:8px;transition:opacity .2s;width:100%}
.btn-hf:hover{opacity:.88}
.btn-google{background:var(--bg3);border:1px solid var(--border);border-radius:12px;padding:11px;color:var(--text2);font-size:13px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:8px;transition:border-color .2s;width:100%}
.btn-google:hover{border-color:var(--acc);color:var(--text)}
.divider{display:flex;align-items:center;gap:10px;color:var(--text3);font-size:11px}
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--border)}
/* ── APP SHELL ── */
/* FIX: use position:fixed+inset instead of height:100vh — works in ALL mobile browsers & HF Space iframes */
#app{display:none;position:fixed;inset:0;flex-direction:column}
#app.on{display:flex}
.tnav{flex-shrink:0;height:52px;background:rgba(8,8,14,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 12px;gap:10px;position:relative;z-index:50}
.tnav-logo{font-family:'Tajawal',sans-serif;font-size:22px;font-weight:900;background:var(--g1);-webkit-background-clip:text;-webkit-text-fill-color:transparent;flex-shrink:0}
.tnav-search{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:99px;height:34px;display:flex;align-items:center;gap:6px;padding:0 12px}
.tnav-search input{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-size:13px}
.tnav-search input::placeholder{color:var(--text3)}
.tnav-btn{width:34px;height:34px;border-radius:50%;background:var(--bg3);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--text2);cursor:pointer;transition:all .15s;flex-shrink:0;position:relative}
.tnav-btn:hover{background:var(--acc);border-color:var(--acc);color:#fff}
.badge{position:absolute;top:-3px;right:-3px;width:16px;height:16px;border-radius:50%;background:var(--acc3);color:#fff;font-size:9px;font-weight:700;display:flex;align-items:center;justify-content:center;border:2px solid var(--bg)}
.av-btn{width:34px;height:34px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;cursor:pointer;flex-shrink:0;overflow:hidden}
.av-btn img{width:100%;height:100%;object-fit:cover;border-radius:50%}
/* FIX: min-height:0 prevents flex children from overflowing in Safari/Firefox */
.content{flex:1;overflow:hidden;position:relative;min-height:0}
.page{position:absolute;inset:0;overflow-y:auto;display:none;padding-bottom:80px}
.page.on{display:block}
.bnav{flex-shrink:0;background:rgba(8,8,14,.96);backdrop-filter:blur(20px);border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-around;padding:6px 0 env(safe-area-inset-bottom,6px);height:62px}
.bnav-btn{display:flex;flex-direction:column;align-items:center;gap:3px;color:var(--text3);font-size:9px;font-weight:700;padding:4px 12px;transition:color .15s}
.bnav-btn .ic{font-size:20px;transition:transform .15s}
.bnav-btn.on{color:var(--acc)}
.bnav-btn.on .ic{transform:scale(1.15)}
.bnav-post{width:46px;height:46px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:26px;color:#fff;cursor:pointer;box-shadow:0 0 20px rgba(139,92,246,.4);transition:transform .15s,box-shadow .15s}
.bnav-post:hover{transform:scale(1.08);box-shadow:0 0 30px rgba(139,92,246,.6)}
/* ── SECTION HEAD ── */
.sec-head{display:flex;align-items:center;justify-content:space-between;padding:8px 14px 2px}
.sec-ttl{font-size:14px;font-weight:700}
.sec-lnk{font-size:12px;color:var(--acc);cursor:pointer}
/* ── SUGGESTED STRIP ── */
.sug-strip{display:flex;gap:10px;padding:6px 12px 12px;overflow-x:auto;scrollbar-width:none}
.sug-strip::-webkit-scrollbar{display:none}
.sugcard{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:14px 12px;min-width:130px;display:flex;flex-direction:column;align-items:center;gap:6px;flex-shrink:0;cursor:pointer;transition:border-color .2s}
.sugcard:hover{border-color:var(--acc)}
.sugav{width:52px;height:52px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:22px;overflow:hidden}
.sugav img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.sugname{font-size:12px;font-weight:700;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:110px}
.sughand{font-size:10px;color:var(--text3);text-align:center}
.sug-fol{background:var(--acc);color:#fff;border:none;border-radius:8px;padding:5px 12px;font-size:11px;font-weight:700;cursor:pointer;font-family:'Cairo',sans-serif;transition:opacity .15s}
.sug-fol:hover{opacity:.8}
.sug-fol.following{background:var(--bg3);color:var(--text2);border:1px solid var(--border)}
/* ── STORIES ── */
.stories{display:flex;gap:10px;padding:12px;overflow-x:auto;scrollbar-width:none}
.stories::-webkit-scrollbar{display:none}
.sitem{display:flex;flex-direction:column;align-items:center;gap:5px;cursor:pointer;flex-shrink:0}
.sring{width:60px;height:60px;border-radius:50%;padding:2px;background:var(--g1);transition:transform .15s}
.sring:hover{transform:scale(1.06)}
.sring.seen{background:var(--bg3);border:2px solid var(--border);padding:0}
.sav{width:100%;height:100%;border-radius:50%;background:var(--bg3);border:2px solid var(--bg);display:flex;align-items:center;justify-content:center;font-size:20px;overflow:hidden}
.sav img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.sname{font-size:10px;color:var(--text2);max-width:60px;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sadd .sring{background:var(--bg3);border:2px dashed var(--acc)}
.sadd .sav{color:var(--acc);font-size:24px}
/* ── POST CARD ── */
.pcard{background:var(--card);border:1px solid var(--border);border-radius:var(--r);margin:0 10px 14px;overflow:hidden;animation:fup .35s ease both}
.phead{display:flex;align-items:center;gap:10px;padding:12px 14px}
.pav{width:40px;height:40px;border-radius:50%;background:var(--g1);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px;overflow:hidden;cursor:pointer}
.pav img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.puinfo{flex:1;cursor:pointer}
.puname{font-weight:700;font-size:14px}
.puhandle{color:var(--text3);font-size:11px}
.pmore{color:var(--text3);padding:6px;font-size:20px;cursor:pointer;line-height:1;border-radius:50%;transition:background .15s}
.pmore:hover{background:var(--bg3)}
.ptxt{padding:0 14px 10px;font-size:13px;line-height:1.7}
.ptxt .ht{color:var(--acc);cursor:pointer}
.pmedia{width:100%;display:block;background:var(--bg3)}
/* ── FIX: Video container ── */
.pvideo{position:relative;cursor:pointer;background:#000;min-height:200px;display:flex;align-items:center;justify-content:center}
.pvideo video{width:100%;max-height:420px;display:block;object-fit:contain}
.pvo{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.3);transition:opacity .2s}
.pplay{width:60px;height:60px;border-radius:50%;background:rgba(255,255,255,.2);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;font-size:26px;color:#fff;border:2px solid rgba(255,255,255,.4)}
.paudio{margin:0 14px 10px;background:var(--bg3);border-radius:12px;padding:14px;display:flex;align-items:center;gap:12px;cursor:pointer}
.adisc{width:44px;height:44px;border-radius:12px;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:20px;animation:spin 4s linear infinite paused;flex-shrink:0}
.adisc.playing{animation-play-state:running}
.pactions{display:flex;border-top:1px solid var(--border)}
.pact{display:flex;align-items:center;justify-content:center;gap:5px;flex:1;padding:10px 6px;font-size:12px;font-weight:600;color:var(--text2);transition:all .15s;border-radius:0}
.pact:hover{background:var(--bg3);color:var(--text)}
.pact.liked{color:var(--acc3)}
.pact .ai{font-size:17px}
#sv{position:fixed;inset:0;z-index:300;background:#000;display:none;flex-direction:column}
#sv.on{display:flex}
.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}
.sv-bar{flex:1;height:2px;background:rgba(255,255,255,.25);border-radius:99px;overflow:hidden}
.sv-fill{height:100%;background:#fff;border-radius:99px;width:0}
.sv-fill.done{width:100%}
.sv-fill.active{animation:sfill 5s linear forwards}
.sv-head{position:absolute;top:28px;left:0;right:0;display:flex;align-items:center;gap:10px;padding:10px 14px;z-index:5}
.sv-uname{color:#fff;font-weight:700;flex:1}
.sv-time{color:rgba(255,255,255,.6);font-size:12px}
.sv-close{color:#fff;font-size:22px;cursor:pointer;padding:4px}
.sv-body{flex:1;display:flex;align-items:center;justify-content:center;position:relative}
.sv-bg{position:absolute;inset:0}
.sv-emoji{font-size:80px;position:relative;z-index:1}
.sv-txt{position:absolute;bottom:100px;left:0;right:0;text-align:center;color:#fff;font-size:20px;font-weight:700;text-shadow:0 2px 8px rgba(0,0,0,.9);padding:0 24px;z-index:2}
.sv-reply{position:absolute;bottom:30px;left:14px;right:14px;z-index:5;display:flex;gap:8px}
.sv-reply input{flex:1;background:rgba(255,255,255,.15);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.3);border-radius:24px;padding:10px 16px;color:#fff;font-size:13px;outline:none}
.sv-reply input::placeholder{color:rgba(255,255,255,.6)}
.sv-reply button{width:42px;height:42px;border-radius:50%;background:var(--acc);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px}
/* ── EXPLORE ── */
.ecats{display:flex;gap:8px;padding:12px;overflow-x:auto;scrollbar-width:none}
.ecats::-webkit-scrollbar{display:none}
.ecat{padding:7px 16px;border-radius:99px;background:var(--bg3);border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:700;white-space:nowrap;transition:all .15s}
.ecat.on{background:var(--acc);border-color:var(--acc);color:#fff}
.egrid{display:grid;grid-template-columns:repeat(3,1fr);gap:2px;padding:2px}
.egrid-item{aspect-ratio:1;background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:36px;cursor:pointer;overflow:hidden;position:relative}
.egrid-item:nth-child(7n+1){grid-column:span 2;aspect-ratio:2}
.egrid-ov{position:absolute;inset:0;background:rgba(0,0,0,.5);opacity:0;transition:opacity .15s;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;gap:4px;font-size:12px;font-weight:700}
.egrid-item:hover .egrid-ov{opacity:1}
.egrid-item video{width:100%;height:100%;object-fit:cover;position:absolute;inset:0}
/* ── MESSAGES ── */
.mlist{padding:10px}
.mitem{display:flex;align-items:center;gap:10px;padding:12px 10px;border-radius:var(--r);cursor:pointer;transition:background .15s}
.mitem:hover{background:var(--card)}
.mav-wrap{position:relative;flex-shrink:0}
.mav{width:50px;height:50px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:20px;overflow:hidden}
.mav img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.mdot{position:absolute;bottom:2px;right:2px;width:11px;height:11px;border-radius:50%;background:var(--acc2);border:2px solid var(--bg)}
.mprev{flex:1;min-width:0}
.mpname{font-weight:700;font-size:14px;margin-bottom:2px}
.mpmsg{color:var(--text2);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mpmsg.unread{color:var(--text);font-weight:600}
.mmeta{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0}
.mtime{color:var(--text3);font-size:11px}
.munread{background:var(--acc);color:#fff;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700}
/* ── CHAT ── */
#chat-win{position:fixed;inset:0;z-index:400;background:var(--bg);display:none;flex-direction:column}
#chat-win.on{display:flex}
.chat-head{background:rgba(8,8,14,.95);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;padding:10px 12px;flex-shrink:0}
.chat-back{font-size:22px;cursor:pointer;color:var(--text2);padding:4px}
.chat-av{width:38px;height:38px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:16px;overflow:hidden;flex-shrink:0}
.chat-av img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.chat-info{flex:1}
.chat-name{font-weight:700;font-size:15px}
.chat-st{color:var(--acc2);font-size:11px}
.chat-acts{display:flex;gap:6px}
.chat-act{width:34px;height:34px;border-radius:50%;background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:16px;cursor:pointer;color:var(--text2);transition:all .15s}
.chat-act:hover{background:var(--acc);color:#fff}
.chat-msgs{flex:1;overflow-y:auto;padding:12px 10px;display:flex;flex-direction:column;gap:8px}
.bubble{max-width:75%;padding:9px 13px;border-radius:16px;font-size:13px;line-height:1.5;animation:bin .2s ease;word-break:break-word}
.bubble.sent{background:var(--acc);color:#fff;align-self:flex-end;border-bottom-left-radius:4px}
.bubble.recv{background:var(--card);border:1px solid var(--border);align-self:flex-start;border-bottom-right-radius:4px}
.btime{font-size:10px;opacity:.55;margin-top:3px}
.bubble.sent .btime{text-align:right}
.bubble-file{display:flex;flex-direction:column;gap:6px}
.bubble-file video{max-width:220px;border-radius:10px}
.bubble-file img{max-width:220px;border-radius:10px}
.bubble-file audio{width:220px}
.chat-bar{background:rgba(8,8,14,.95);backdrop-filter:blur(20px);border-top:1px solid var(--border);display:flex;align-items:center;gap:8px;padding:8px 10px env(safe-area-inset-bottom,10px);flex-shrink:0}
.chat-inp{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:22px;padding:9px 14px;color:var(--text);font-size:13px;outline:none;transition:border-color .2s;resize:none;max-height:100px}
.chat-inp:focus{border-color:var(--acc)}
.chat-send{width:40px;height:40px;border-radius:50%;background:var(--acc);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;transition:opacity .15s,transform .1s;flex-shrink:0}
.chat-send:hover{opacity:.85}
.chat-send:active{transform:scale(.88)}
.chat-att{width:36px;height:36px;border-radius:50%;background:var(--bg3);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--text2);cursor:pointer;flex-shrink:0;transition:all .15s}
.chat-att:hover{background:var(--acc);border-color:var(--acc);color:#fff}
/* ── PROFILE — FIXED ── */
.prof-cover{width:100%;height:150px;background:var(--g1);position:relative;overflow:hidden;flex-shrink:0}
.prof-cover-art{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:50px;opacity:.15;pointer-events:none}
/* FIX: prof-section needs solid background so it sits above cover */
.prof-section{padding:0 14px;background:var(--bg);position:relative;z-index:1}
.prof-pic{width:78px;height:78px;border-radius:50%;background:var(--g1);border:3px solid var(--bg);margin-top:-36px;display:flex;align-items:center;justify-content:center;font-size:30px;overflow:hidden;cursor:pointer;flex-shrink:0;position:relative;z-index:2}
.prof-pic img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.prof-name{font-size:19px;font-weight:900;margin-top:8px}
.prof-handle{color:var(--text3);font-size:12px;margin-bottom:6px}
.prof-bio{color:var(--text2);font-size:12px;line-height:1.6;margin-bottom:12px}
.prof-stats{display:flex;gap:20px;margin-bottom:14px}
.pstat{text-align:center}
.pstat-n{font-size:17px;font-weight:900}
.pstat-l{font-size:10px;color:var(--text2)}
.prof-btns{display:flex;gap:8px;padding-bottom:14px}
.btn-fol{flex:1;background:var(--g1);border-radius:10px;padding:9px;color:#fff;font-size:13px;font-weight:700;transition:opacity .15s}
.btn-fol:hover{opacity:.85}
.btn-fol.followed{background:var(--bg3);border:1px solid var(--border);color:var(--text2)}
.btn-dm{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:10px;padding:9px;color:var(--text);font-size:13px;font-weight:600}
.ptabs{display:flex;border-top:1px solid var(--border);background:var(--bg);position:sticky;top:0;z-index:3}
.ptab-btn{flex:1;padding:12px;text-align:center;font-size:12px;font-weight:700;color:var(--text2);border-bottom:2px solid transparent;cursor:pointer;transition:all .15s}
.ptab-btn.on{color:var(--acc);border-bottom-color:var(--acc)}
.pgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:2px;padding:2px}
.pgitem{aspect-ratio:1;background:var(--bg3);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:32px;overflow:hidden;position:relative;transition:opacity .15s}
.pgitem:hover{opacity:.8}
.pgitem video{width:100%;height:100%;object-fit:cover;position:absolute;inset:0}
.pgitem-ov{position:absolute;inset:0;background:rgba(0,0,0,.45);opacity:0;transition:opacity .15s;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700;gap:4px}
.pgitem:hover .pgitem-ov{opacity:1}
/* ── NOTIF ── */
.nitem{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
.nitem:hover{background:var(--card)}
.nic{width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.nic.like{background:rgba(255,79,109,.12)}
.nic.follow{background:rgba(139,92,246,.12)}
.nic.comment{background:rgba(6,214,160,.12)}
.nic.share{background:rgba(245,158,11,.12)}
.ntxt{flex:1;font-size:12px;color:var(--text2);line-height:1.6}
.ntxt strong{color:var(--text)}
.ntime{font-size:10px;color:var(--text3);margin-top:3px}
.nunread{background:var(--acc);width:6px;height:6px;border-radius:50%;margin-top:4px;flex-shrink:0}
/* ── POST MODAL — FULLSCREEN ── */
.moverlay{position:fixed;inset:0;z-index:600;background:var(--bg);display:none;flex-direction:column}
.moverlay.on{display:flex}
.mbox{flex:1;display:flex;flex-direction:column;overflow:hidden}
.mbox-header{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border);flex-shrink:0;background:var(--bg)}
.mbox-title{font-size:16px;font-weight:900;flex:1}
.mclose{font-size:22px;cursor:pointer;color:var(--text2);padding:4px;border-radius:50%;transition:background .15s}
.mclose:hover{background:var(--bg3)}
.mbox-body{flex:1;overflow-y:auto;padding:16px}
.ptypes{display:flex;gap:6px;margin-bottom:16px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
.ptypes::-webkit-scrollbar{display:none}
.ptype{padding:7px 14px;border-radius:99px;background:var(--bg3);border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:700;white-space:nowrap;transition:all .15s}
.ptype.on{background:var(--acc);border-color:var(--acc);color:#fff}
.compose{display:flex;gap:10px;align-items:flex-start;margin-bottom:10px}
.compose-av{width:40px;height:40px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0;overflow:hidden}
.compose-av img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.compose-ta{flex:1;background:transparent;border:none;color:var(--text);font-size:15px;resize:none;outline:none;min-height:100px;line-height:1.6}
.compose-ta::placeholder{color:var(--text3)}
.ctoolbar{display:flex;gap:6px;padding:10px 0;border-top:1px solid var(--border);margin-top:6px;flex-wrap:wrap;align-items:center}
.ctool{width:38px;height:38px;border-radius:10px;background:var(--bg3);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:17px;cursor:pointer;color:var(--text2);transition:all .15s}
.ctool:hover{background:var(--acc);border-color:var(--acc);color:#fff}
.file-preview{margin-top:12px;border-radius:var(--rs);overflow:hidden;background:var(--bg3);display:flex;align-items:center;justify-content:center;position:relative}
.file-preview img{max-width:100%;max-height:260px;object-fit:contain;display:block}
.file-preview video{max-width:100%;max-height:260px;display:block}
.file-preview.audio-prev{flex-direction:column;gap:8px;padding:24px;color:var(--text2);font-size:13px}
.fp-remove{position:absolute;top:8px;left:8px;width:28px;height:28px;border-radius:50%;background:rgba(0,0,0,.7);color:#fff;display:flex;align-items:center;justify-content:center;font-size:14px;cursor:pointer;z-index:2}
.mbox-footer{padding:12px 16px;border-top:1px solid var(--border);flex-shrink:0;background:var(--bg)}
/* ── EDIT POST MODAL ── */
#edit-modal{position:fixed;inset:0;z-index:700;background:rgba(0,0,0,.8);backdrop-filter:blur(12px);display:none;align-items:center;justify-content:center;padding:20px}
#edit-modal.on{display:flex}
.edit-box{background:var(--card);border:1px solid var(--border);border-radius:20px;padding:24px;width:100%;max-width:480px;animation:bin .2s ease}
.edit-box h3{font-size:16px;font-weight:900;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between}
/* ── COMMENTS MODAL ── */
#comments-modal{position:fixed;inset:0;z-index:700;background:rgba(0,0,0,.7);backdrop-filter:blur(10px);display:none;align-items:flex-end}
#comments-modal.on{display:flex}
.cm-box{background:var(--card);border:1px solid var(--border);border-radius:24px 24px 0 0;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:mup .3s ease}
.cm-head{display:flex;align-items:center;padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
.cm-title{font-size:15px;font-weight:900;flex:1}
.cm-close{font-size:20px;cursor:pointer;color:var(--text2)}
.cm-list{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:12px}
.cm-item{display:flex;gap:10px}
.cm-av{width:32px;height:32px;border-radius:50%;background:var(--g1);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:13px;overflow:hidden}
.cm-av img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.cm-body{flex:1}
.cm-name{font-size:12px;font-weight:700;margin-bottom:2px}
.cm-text{font-size:13px;color:var(--text2);line-height:1.5}
.cm-time{font-size:10px;color:var(--text3);margin-top:2px}
.cm-bar{display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border);flex-shrink:0}
.cm-inp{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:22px;padding:9px 14px;color:var(--text);font-size:13px;outline:none;transition:border-color .2s}
.cm-inp:focus{border-color:var(--acc)}
.cm-send{width:38px;height:38px;border-radius:50%;background:var(--acc);display:flex;align-items:center;justify-content:center;color:#fff;font-size:16px;flex-shrink:0}
/* ── POST MENU SHEET ── */
#post-menu-sheet{position:fixed;inset:0;z-index:650;background:rgba(0,0,0,.6);backdrop-filter:blur(8px);display:none;align-items:flex-end}
#post-menu-sheet.on{display:flex}
.pmenu-box{background:var(--card);border:1px solid var(--border);border-radius:20px 20px 0 0;width:100%;padding:8px 0 env(safe-area-inset-bottom,12px);animation:mup .25s ease}
.pmenu-item{display:flex;align-items:center;gap:12px;padding:14px 20px;font-size:14px;font-weight:600;cursor:pointer;transition:background .15s}
.pmenu-item:hover{background:var(--bg3)}
.pmenu-item.danger{color:var(--acc3)}
.pmenu-sep{height:1px;background:var(--border);margin:4px 0}
/* ── SEARCH ── */
.search-results{padding:10px}
.sr-user{display:flex;align-items:center;gap:10px;padding:10px;border-radius:var(--r);cursor:pointer;transition:background .15s}
.sr-user:hover{background:var(--card)}
.sr-av{width:44px;height:44px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:18px;overflow:hidden;flex-shrink:0}
.sr-av img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.sr-info{flex:1}
.sr-name{font-size:14px;font-weight:700}
.sr-handle{color:var(--text3);font-size:12px}
/* ── EMPTY STATE ── */
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:var(--text2)}
.empty-state .eico{font-size:48px;margin-bottom:12px}
.empty-state p{font-size:13px;text-align:center;line-height:1.6}
/* ── STORY COMPOSER ── */
.story-effects{display:flex;gap:8px;overflow-x:auto;padding:6px 0 0;scrollbar-width:none}
.story-effects::-webkit-scrollbar{display:none}
.story-eff{padding:8px 12px;border-radius:999px;background:var(--bg3);border:1px solid var(--border);color:var(--text2);font-size:12px;font-weight:700;white-space:nowrap}
.story-eff.on{background:var(--acc);border-color:var(--acc);color:#fff}
#story-preview-area .file-preview img,#story-preview-area .file-preview video{max-height:320px}
.story-filter-sepia{filter:sepia(1) saturate(1.15)}
.story-filter-grayscale{filter:grayscale(1)}
.story-filter-contrast{filter:contrast(1.25)}
.story-filter-blur{filter:blur(1.5px) saturate(1.1)}
.story-filter-none{filter:none}
/* ── POST VIEWER ── */
#post-viewer .mbox-body{padding:0}
.pv-head{padding:14px 16px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--border)}
.pv-author{display:flex;align-items:center;gap:10px;flex:1}
.pv-meta{font-size:12px;color:var(--text3)}
.pv-media{background:#000;min-height:240px;display:flex;align-items:center;justify-content:center;position:relative}
.pv-media img,.pv-media video{width:100%;max-height:60vh;object-fit:contain;display:block}
.pv-caption{padding:14px 16px;font-size:14px;line-height:1.7;color:var(--text)}
.pv-actions{display:flex;gap:8px;padding:0 16px 16px}
.pv-actions .btn-main,.pv-actions .btn-dm{flex:1}
/* ── EDIT PROFILE ── */
#profile-edit-modal .edit-box input,#profile-edit-modal .edit-box textarea{
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;
}
/* ── CALLS ── */
#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}
#call-modal.on{display:flex}
.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)}
.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}
.call-peer{display:flex;align-items:center;gap:12px;min-width:0}
.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}
.call-peer-name{font-size:16px;font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}
.call-status{font-size:12px;color:var(--text2);margin-top:2px}
.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}
.call-media{flex:1;position:relative;background:#000;min-height:300px;display:flex;align-items:center;justify-content:center}
.call-media video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#000}
#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}
#call-remote-video{z-index:1}
.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}
.call-incoming,.call-controls{display:flex;gap:10px;padding:14px 16px;justify-content:center;flex-shrink:0}
.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}
.call-btn.accept{background:var(--acc2);border-color:var(--acc2);color:#fff}
.call-btn.danger{background:var(--acc3);border-color:var(--acc3);color:#fff}
.call-controls{padding-top:0;padding-bottom:18px}
.call-btn:active{transform:scale(.97)}
/* ── TOAST ── */
#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)}
#toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
</style>
</head>
<body>
<!-- SPLASH -->
<div id="splash">
<div class="sp-logo">Z</div>
<div class="sp-by">BY THE ZYZ STUDIO</div>
<div class="sp-ring"></div>
</div>
<!-- AUTH -->
<div id="auth">
<div class="auth-card">
<div class="auth-logo">THE Z CHAT</div>
<div class="auth-by">BY THE ZYZ STUDIO</div>
<div class="atabs">
<button class="atab on" onclick="switchAuth('login')">دخول</button>
<button class="atab" onclick="switchAuth('reg')">حساب جديد</button>
</div>
<!-- LOGIN -->
<div class="aform" id="form-login">
<div class="fg"><label>البريد الإلكتروني</label><input type="email" id="l-email" placeholder="email@example.com" autocomplete="email"></div>
<div class="fg">
<label>كلمة المرور</label>
<div class="pw-wrap">
<input type="password" id="l-pass" placeholder="••••••••" autocomplete="current-password" onkeydown="if(event.key==='Enter')doLogin()">
<button class="pw-eye" type="button" onclick="togglePW('l-pass',this)" tabindex="-1">👁</button>
</div>
</div>
<div class="err" id="l-err"></div>
<button class="btn-main" id="l-btn" onclick="doLogin()"><span>دخول</span><span></span></button>
<div class="divider">أو</div>
</div>
<!-- REGISTER -->
<div class="aform hide" id="form-reg">
<div class="fg"><label>الاسم الكامل</label><input type="text" id="r-name" placeholder="محمد أحمد" autocomplete="name"></div>
<div class="fg"><label>اسم المستخدم</label><input type="text" id="r-user" placeholder="@username" autocomplete="username"></div>
<div class="fg"><label>البريد الإلكتروني</label><input type="email" id="r-email" placeholder="email@example.com" autocomplete="email"></div>
<div class="fg">
<label>كلمة المرور</label>
<div class="pw-wrap">
<input type="password" id="r-pass" placeholder="6 أحرف على الأقل" autocomplete="new-password">
<button class="pw-eye" type="button" onclick="togglePW('r-pass',this)" tabindex="-1">👁</button>
</div>
</div>
<div class="err" id="r-err"></div>
<button class="btn-main" id="r-btn" onclick="doRegister()"><span>إنشاء الحساب</span><span></span></button>
</div>
</div>
</div>
<!-- APP -->
<div id="app">
<nav class="tnav">
<div class="tnav-logo">Z</div>
<div class="tnav-search">
<span style="color:var(--text3)">🔍</span>
<input id="search-inp" placeholder="ابحث..." oninput="onSearch(this.value)" onfocus="showPage('search')">
</div>
<div class="tnav-btn" onclick="showPage('notif')">
🔔<span class="badge" id="notif-badge" style="display:none">0</span>
</div>
<div class="av-btn" id="nav-av" onclick="showPage('profile')">م</div>
</nav>
<div class="content">
<!-- HOME -->
<div class="page on" id="page-home">
<div class="stories" id="home-stories"></div>
<div class="sec-head"><span class="sec-ttl">✨ مقترحون</span></div>
<div class="sug-strip" id="sug-strip"></div>
<div class="sec-head"><span class="sec-ttl">📰 الخلاصة</span></div>
<div id="feed"></div>
</div>
<!-- EXPLORE -->
<div class="page" id="page-explore">
<div class="ecats" id="ecats">
<button class="ecat on" data-cat="all">الكل</button>
<button class="ecat" data-cat="video">🎬 فيديو</button>
<button class="ecat" data-cat="photo">📸 صور</button>
<button class="ecat" data-cat="audio">🎵 صوتيات</button>
<button class="ecat" data-cat="text">📝 نصوص</button>
</div>
<div class="egrid" id="explore-grid"></div>
<div id="explore-load" style="padding:20px;text-align:center"></div>
</div>
<div class="page" id="page-messages">
<div class="sec-head"><span class="sec-ttl">💬 المراسلات</span><button class="sec-lnk" onclick="showNewMsgModal()">+ جديد</button></div>
<div class="mlist" id="conv-list"></div>
</div>
<!-- PROFILE -->
<div class="page" id="page-profile">
<div class="prof-cover"><div class="prof-cover-art"></div></div>
<div class="prof-section">
<div class="prof-pic" id="prof-pic" onclick="changeAvatar()">😊</div>
<div class="prof-name" id="prof-name"></div>
<div class="prof-handle" id="prof-handle">@—</div>
<div class="prof-bio" id="prof-bio">اضغط لتعديل النبذة</div>
<div class="prof-stats">
<div class="pstat"><div class="pstat-n" id="ps-posts">0</div><div class="pstat-l">منشور</div></div>
<div class="pstat"><div class="pstat-n" id="ps-followers">0</div><div class="pstat-l">متابع</div></div>
<div class="pstat"><div class="pstat-n" id="ps-following">0</div><div class="pstat-l">أتابع</div></div>
</div>
<div class="prof-btns" id="prof-btns">
<button class="btn-fol" onclick="editProfile()">✏️ تعديل الملف</button>
<button class="btn-dm" onclick="doLogout()">خروج 🚪</button>
</div>
</div>
<div class="ptabs">
<div class="ptab-btn on" onclick="switchPTab(this,'posts')">منشورات</div>
<div class="ptab-btn" onclick="switchPTab(this,'saved')">محفوظ</div>
</div>
<div class="pgrid" id="prof-grid"></div>
</div>
<!-- NOTIFICATIONS -->
<div class="page" id="page-notif">
<div class="sec-head"><span class="sec-ttl">🔔 الإشعارات</span></div>
<div id="notif-list"></div>
</div>
<!-- SEARCH -->
<div class="page" id="page-search">
<div class="search-results" id="search-results">
<div class="empty-state"><div class="eico">🔍</div><p>ابحث عن أشخاص أو محتوى</p></div>
</div>
</div>
</div>
<nav class="bnav">
<button class="bnav-btn on" id="bn-home" onclick="showPage('home')"><span class="ic">🏠</span><span>الرئيسية</span></button>
<button class="bnav-btn" id="bn-notif" onclick="showPage('notif')"><span class="ic">🔔</span><span>إشعارات</span></button>
<div class="bnav-post" onclick="openPostModal()">+</div>
<button class="bnav-btn" id="bn-messages" onclick="showPage('messages')"><span class="ic">💬</span><span>المراسلات</span></button>
</nav>
</div>
<div id="sv">
<div class="sv-bars" id="sv-bars"></div>
<div class="sv-head">
<div style="width:32px;height:32px;border-radius:50%;background:var(--g1);display:flex;align-items:center;justify-content:center;font-size:14px;border:2px solid #fff;overflow:hidden" id="sv-av">👤</div>
<div class="sv-uname" id="sv-uname"></div>
<div class="sv-time" id="sv-time">الآن</div>
<button class="sv-sound" id="sv-sound" type="button" onclick="toggleStorySound(this)" style="display:none">🔇</button>
<div class="sv-close" onclick="closeSV()"></div>
</div>
<div class="sv-body">
<div class="sv-bg" id="sv-bg"></div>
<div class="sv-emoji" id="sv-emoji">🌟</div>
<div class="sv-txt" id="sv-txt"></div>
</div>
<div class="sv-reply">
<input id="sv-rep-inp" placeholder="رد على القصة...">
<button onclick="sendStoryReply()"></button>
</div>
</div>
<!-- STORY COMPOSER -->
<div id="story-modal" class="moverlay" onclick="if(event.target===this)closeStoryComposer()">
<div class="mbox">
<div class="mbox-header">
<button class="mclose" onclick="closeStoryComposer()"></button>
<div class="mbox-title">✦ قصة جديدة</div>
<button style="background:none;border:none;color:var(--text3);font-size:12px;cursor:pointer" onclick="resetStoryComposer()">مسح</button>
</div>
<div class="mbox-body">
<div class="ptypes" id="story-ptypes">
<button class="ptype on" data-s="text">📝 نص</button>
<button class="ptype" data-s="photo">📸 صورة</button>
<button class="ptype" data-s="video">🎬 فيديو</button>
<button class="ptype" data-s="product">🛍️ منتج</button>
</div>
<div class="compose">
<div class="compose-av" id="story-av">😊</div>
<textarea class="compose-ta" id="story-text" placeholder="اكتب قصتك، أو أضف صورة / فيديو..." rows="5"></textarea>
</div>
<div id="story-preview-area"></div>
<div class="ctoolbar">
<label class="ctool" title="صورة" style="cursor:pointer">📸<input type="file" accept="image/*" style="display:none" onchange="previewStoryFile(this,'image')"></label>
<label class="ctool" title="فيديو" style="cursor:pointer">🎬<input type="file" accept="video/*" style="display:none" onchange="previewStoryFile(this,'video')"></label>
<div class="ctool" onclick="insertStoryEmoji()">😊</div>
<div style="flex:1"></div>
<div style="color:var(--text3);font-size:11px;display:flex;align-items:center;padding:0 4px">فلاتر</div>
</div>
<div class="story-effects" id="story-effects">
<button class="story-eff on" data-eff="none">عادي</button>
<button class="story-eff" data-eff="sepia">سيبيا</button>
<button class="story-eff" data-eff="grayscale">رمادي</button>
<button class="story-eff" data-eff="contrast">تباين</button>
<button class="story-eff" data-eff="blur">ضبابي</button>
</div>
</div>
<div class="mbox-footer">
<button class="btn-main" id="story-pub-btn" onclick="publishStory()">نشر القصة ✨</button>
</div>
</div>
</div>
<!-- CHAT WINDOW -->
<div id="chat-win">
<div class="chat-head">
<div class="chat-back" onclick="closeChat()"></div>
<div class="chat-av" id="chat-av">👤</div>
<div class="chat-info">
<div class="chat-name" id="chat-name"></div>
<div class="chat-st">● متصل</div>
</div>
<div class="chat-acts">
<div class="chat-act" title="مكالمة صوتية" onclick="startCall('audio')">📞</div>
<div class="chat-act" title="مكالمة مرئية" onclick="startCall('video')">📹</div>
</div>
</div>
<div class="chat-msgs" id="chat-msgs"></div>
<div class="chat-bar">
<label class="chat-att" title="إرفاق ملف">
📎
<input type="file" accept="image/*,video/*,audio/*" style="display:none" onchange="sendChatFile(this)">
</label>
<textarea class="chat-inp" id="chat-inp" placeholder="اكتب رسالة..." rows="1"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"
oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea>
<button class="chat-send" onclick="sendMsg()"></button>
</div>
</div>
<!-- CALL MODAL -->
<div id="call-modal">
<div class="call-shell">
<div class="call-head">
<div class="call-peer">
<div class="call-peer-av" id="call-peer-av">👤</div>
<div class="call-peer-meta">
<div class="call-peer-name" id="call-peer-name"></div>
<div class="call-status" id="call-status">جاهز للمكالمة</div>
</div>
</div>
<button class="call-close" onclick="endCall(true)"></button>
</div>
<div class="call-media">
<video id="call-remote-video" autoplay playsinline></video>
<video id="call-local-video" autoplay muted playsinline></video>
<div class="call-ring" id="call-ring">📞</div>
</div>
<div class="call-incoming" id="call-incoming-actions" style="display:none">
<button class="call-btn accept" onclick="acceptIncomingCall()">قبول</button>
<button class="call-btn danger" onclick="rejectIncomingCall()">رفض</button>
</div>
<div class="call-controls">
<button class="call-btn" id="call-mic-btn" onclick="toggleCallMute()">🎤</button>
<button class="call-btn" id="call-cam-btn" onclick="toggleCallCamera()">📷</button>
<button class="call-btn danger" onclick="endCall(true)">إنهاء</button>
</div>
</div>
</div>
<!-- POST MODAL — FULLSCREEN -->
<div class="moverlay" id="post-modal">
<div class="mbox">
<div class="mbox-header">
<button class="mclose" onclick="closePostModal()"></button>
<div class="mbox-title">✦ منشور جديد</div>
<button style="background:none;border:none;color:var(--text3);font-size:12px;cursor:pointer" onclick="resetPostModal()">مسح</button>
</div>
<div class="mbox-body">
<div class="ptypes" id="ptypes">
<button class="ptype on" data-t="text">📝 نص</button>
<button class="ptype" data-t="photo">📸 صورة</button>
<button class="ptype" data-t="video">🎬 فيديو</button>
<button class="ptype" data-t="reel">▶️ ريل</button>
<button class="ptype" data-t="story">⭕ قصة</button>
<button class="ptype" data-t="audio">🎵 صوت</button>
</div>
<div class="compose">
<div class="compose-av" id="compose-av">😊</div>
<textarea class="compose-ta" id="compose-txt" placeholder="ماذا تريد أن تشارك؟ ✨" rows="4"></textarea>
</div>
<div id="file-preview-area"></div>
<div class="ctoolbar">
<label class="ctool" title="صورة" style="cursor:pointer">
📸<input type="file" accept="image/*" style="display:none" onchange="previewFile(this,'image')">
</label>
<label class="ctool" title="فيديو" style="cursor:pointer">
🎬<input type="file" accept="video/*" style="display:none" onchange="previewFile(this,'video')">
</label>
<label class="ctool" title="صوت" style="cursor:pointer">
🎵<input type="file" accept="audio/*" style="display:none" onchange="previewFile(this,'audio')">
</label>
<div class="ctool" onclick="insertEmoji()">😊</div>
<div style="flex:1"></div>
<div style="color:var(--text3);font-size:11px;display:flex;align-items:center;padding:0 4px">🌍 عام</div>
</div>
</div>
<div class="mbox-footer">
<button class="btn-main" id="pub-btn" onclick="publishPost()">نشر ⚡</button>
</div>
</div>
</div>
<!-- COMMENTS MODAL -->
<div id="comments-modal">
<div class="cm-box">
<div class="cm-head">
<div class="cm-title">💬 التعليقات</div>
<div class="cm-close" onclick="closeComments()"></div>
</div>
<div class="cm-list" id="cm-list">
<div class="empty-state"><div class="eico">💬</div><p>لا توجد تعليقات بعد</p></div>
</div>
<div class="cm-bar">
<input class="cm-inp" id="cm-inp" placeholder="أضف تعليقاً..." onkeydown="if(event.key==='Enter')submitComment()">
<button class="cm-send" onclick="submitComment()"></button>
</div>
</div>
</div>
<!-- POST VIEWER -->
<div id="post-viewer" class="moverlay" onclick="if(event.target===this)closePostViewer()">
<div class="mbox">
<div class="mbox-header">
<button class="mclose" onclick="closePostViewer()"></button>
<div class="mbox-title" id="pv-title">✦ المنشور</div>
<button style="background:none;border:none;color:var(--text3);font-size:12px;cursor:pointer" onclick="if(currentViewerPost)postMenu(currentViewerPost.id,currentViewerPost.user_id,currentViewerPost.caption||'')"></button>
</div>
<div class="mbox-body" id="pv-body"></div>
</div>
</div>
<!-- EDIT PROFILE MODAL -->
<div id="profile-edit-modal" class="moverlay" onclick="if(event.target===this)closeProfileEditModal()">
<div class="edit-box" style="max-width:520px;width:100%">
<h3><span>✏️ تعديل الملف الشخصي</span><span class="mclose" onclick="closeProfileEditModal()"></span></h3>
<div class="fg" style="margin-bottom:12px">
<label>الاسم</label>
<input id="pe-name" type="text" placeholder="الاسم الكامل">
</div>
<div class="fg" style="margin-bottom:12px">
<label>النبذة</label>
<textarea id="pe-bio" rows="4" placeholder="نبذة قصيرة..."></textarea>
</div>
<div class="fg" style="margin-bottom:14px">
<label>لون الغلاف</label>
<input id="pe-cover" type="text" placeholder="linear-gradient(135deg,#8b5cf6,#ec4899)">
</div>
<button class="btn-main" onclick="saveProfileEdit()">حفظ التعديلات ✓</button>
</div>
</div>
<!-- POST MENU SHEET -->
<div id="post-menu-sheet" onclick="if(event.target===this)closePostMenu()">
<div class="pmenu-box" id="pmenu-box"></div>
</div>
<!-- EDIT POST MODAL -->
<div id="edit-modal" onclick="if(event.target===this)closeEditModal()">
<div class="edit-box">
<h3>
<span>✏️ تعديل المنشور</span>
<span class="mclose" onclick="closeEditModal()"></span>
</h3>
<div class="fg" style="margin-bottom:14px">
<label>الوصف / التعليق</label>
<textarea id="edit-caption" rows="4" style="background:var(--bg3);border:1px solid var(--border);border-radius:11px;padding:11px 14px;color:var(--text);font-size:14px;outline:none;resize:vertical;width:100%;font-family:Cairo,sans-serif"></textarea>
</div>
<button class="btn-main" onclick="saveEditPost()">حفظ التعديلات ✓</button>
</div>
</div>
<div id="toast"></div>
<script>
// ═══════════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════════
const API_URL = window.location.origin;
// ═══════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════
let ME = null;
let WS = null;
let currentChat = null;
let postType = 'text';
let pendingFile = null;
let svIdx = 0, svList = [], svTimer = null;
let feedPage = 1, feedLoading = false, feedDone = false;
let explorePage = 1, exploreLoading = false;
let currentCommentPostId = null;
let currentEditPostId = null;
let currentMenuPostId = null;
let currentMenuOwnerId = null;
let callPeer = null;
let callStream = null;
let callType = '';
let callTargetId = '';
let callId = '';
let callMode = 'idle'; // idle | outgoing | incoming | connected
let callIncomingOffer = null;
let callIncomingFrom = null;
let callMute = false;
let callCameraOff = false;
// ═══════════════════════════════════════════════
// UTILS
// ═══════════════════════════════════════════════
async function api(path, method = 'GET', body = null) {
try {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(API_URL + path, opts);
const data = await r.json();
if (!r.ok) throw new Error(data.detail || 'خطأ في الخادم');
return data;
} catch(e) {
if (e.message === 'Failed to fetch') throw new Error('لا يمكن الاتصال بالخادم');
throw e;
}
}
async function uploadFile(file, kind) {
const fd = new FormData();
fd.append('file', file);
fd.append('kind', kind);
const r = await fetch(API_URL + '/api/upload', { method: 'POST', body: fd });
if (!r.ok) throw new Error('فشل رفع الملف');
return (await r.json()).url;
}
function fmt(n) {
if (!n && n !== 0) return '0';
n = Number(n);
if (n >= 1e6) return (n/1e6).toFixed(1)+'M';
if (n >= 1e3) return (n/1e3).toFixed(1)+'K';
return String(n);
}
function timeAgo(iso) {
if (!iso) return '';
const d = (Date.now() - new Date(iso + (iso.includes('Z') ? '' : 'Z')).getTime()) / 1000;
if (isNaN(d) || d < 0) return 'الآن';
if (d < 60) return 'الآن';
if (d < 3600) return `${Math.floor(d/60)}د`;
if (d < 86400) return `${Math.floor(d/3600)}س`;
return `${Math.floor(d/86400)}ي`;
}
function avatarEl(user, size = 40) {
if (!user) return '<span>👤</span>';
if (user.avatar) return `<img src="${API_URL}${user.avatar}" alt="" onerror="this.style.display='none'">`;
const initial = (user.name || '؟')[0];
return `<span style="font-size:${Math.round(size*0.38)}px;font-weight:700">${initial}</span>`;
}
function togglePW(inputId, btn) {
const inp = document.getElementById(inputId);
if (inp.type === 'password') {
inp.type = 'text'; btn.textContent = '🙈'; btn.style.color = 'var(--acc)';
} else {
inp.type = 'password'; btn.textContent = '👁'; btn.style.color = '';
}
}
// ═══════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════
window.addEventListener('load', async () => {
const params = new URLSearchParams(window.location.search);
if (params.get('hf_oauth') === '1') {
const userData = params.get('user');
if (userData) {
try { ME = JSON.parse(decodeURIComponent(userData)); localStorage.setItem('zchat_user', JSON.stringify(ME)); window.history.replaceState({}, '', '/'); } catch(e) {}
}
}
setTimeout(() => {
const splash = document.getElementById('splash');
splash.style.opacity = '0';
setTimeout(() => {
splash.style.display = 'none';
const saved = localStorage.getItem('zchat_user');
if (saved) {
try { ME = JSON.parse(saved); startApp(); } catch(e) { localStorage.removeItem('zchat_user'); showAuthScreen(); }
} else { showAuthScreen(); }
}, 400);
}, 1400);
});
function showAuthScreen() { document.getElementById('auth').style.display = 'flex'; }
function startApp() {
document.getElementById('auth').style.display = 'none';
document.getElementById('app').classList.add('on');
updateNavAvatar();
loadHome();
loadConversations();
loadNotifs();
connectWS();
document.getElementById('page-home').addEventListener('scroll', function() {
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 300) loadMoreFeed();
});
document.getElementById('page-explore').addEventListener('scroll', function() {
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 200) loadExplore();
});
}
function updateNavAvatar() {
const el = document.getElementById('nav-av');
if (ME?.avatar) el.innerHTML = `<img src="${API_URL}${ME.avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else el.textContent = (ME?.name || 'م')[0];
const cav = document.getElementById('compose-av');
if (cav) cav.innerHTML = ME?.avatar ? `<img src="${API_URL}${ME.avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">` : (ME?.name||'م')[0];
}
// ═══════════════════════════════════════════════
// AUTH
// ═══════════════════════════════════════════════
function switchAuth(tab) {
document.querySelectorAll('.atab').forEach((t,i) => t.classList.toggle('on', (i===0)===(tab==='login')));
document.getElementById('form-login').classList.toggle('hide', tab !== 'login');
document.getElementById('form-reg').classList.toggle('hide', tab !== 'reg');
}
function setAuthLoading(formType, loading) {
const btn = document.getElementById(formType === 'login' ? 'l-btn' : 'r-btn');
if (!btn) return;
btn.disabled = loading;
btn.innerHTML = loading ? '<div class="spin-sm"></div><span>جاري...</span>'
: (formType === 'login' ? '<span>دخول</span><span>⚡</span>' : '<span>إنشاء الحساب</span><span>⚡</span>');
}
async function doLogin() {
const email = document.getElementById('l-email').value.trim();
const pass = document.getElementById('l-pass').value;
const err = document.getElementById('l-err');
err.textContent = '';
if (!email) { err.textContent = '⚠️ أدخل البريد الإلكتروني'; return; }
if (!pass) { err.textContent = '⚠️ أدخل كلمة المرور'; return; }
setAuthLoading('login', true);
try {
const res = await api('/api/login', 'POST', { email, password: pass });
ME = res.user;
localStorage.setItem('zchat_user', JSON.stringify(ME));
startApp();
showToast('✅ أهلاً ' + ME.name + '!');
} catch(e) { err.textContent = '❌ ' + e.message; }
setAuthLoading('login', false);
}
async function doRegister() {
const name = document.getElementById('r-name').value.trim();
const user = document.getElementById('r-user').value.trim().replace('@','').replace(/\s/g,'');
const email = document.getElementById('r-email').value.trim();
const pass = document.getElementById('r-pass').value;
const err = document.getElementById('r-err');
err.textContent = '';
if (!name) { err.textContent = '⚠️ أدخل الاسم الكامل'; return; }
if (!user) { err.textContent = '⚠️ أدخل اسم المستخدم'; return; }
if (user.length < 3) { err.textContent = '⚠️ اسم المستخدم قصير جداً (3 أحرف على الأقل)'; return; }
if (!/^[a-zA-Z0-9_]+$/.test(user)) { err.textContent = '⚠️ اسم المستخدم أحرف وأرقام فقط'; return; }
if (!email) { err.textContent = '⚠️ أدخل البريد الإلكتروني'; return; }
if (!/\S+@\S+\.\S+/.test(email)) { err.textContent = '⚠️ البريد غير صحيح'; return; }
if (!pass) { err.textContent = '⚠️ أدخل كلمة المرور'; return; }
if (pass.length < 6) { err.textContent = '⚠️ كلمة المرور 6 أحرف على الأقل'; return; }
setAuthLoading('reg', true);
try {
const res = await api('/api/register', 'POST', { name, username: user, email, password: pass });
ME = res.user;
localStorage.setItem('zchat_user', JSON.stringify(ME));
startApp();
showToast('🎉 مرحباً ' + ME.name + '!');
} catch(e) { err.textContent = '❌ ' + e.message; }
setAuthLoading('reg', false);
}
function doHFLogin() {
const clientId = 'YOUR_HF_CLIENT_ID';
if (clientId === 'YOUR_HF_CLIENT_ID') {
showToast('🤗 وضع تجريبي — إنشاء حساب...');
const rnd = Math.floor(Math.random()*9999);
document.getElementById('r-name').value = 'HF User ' + rnd;
document.getElementById('r-user').value = 'hfuser' + rnd;
document.getElementById('r-email').value = 'hf' + rnd + '@huggingface.co';
document.getElementById('r-pass').value = 'hfpass' + rnd;
switchAuth('reg');
setTimeout(doRegister, 300);
} else {
const redirect = encodeURIComponent(API_URL + '/api/hf-callback');
window.location.href = `https://huggingface.co/oauth/authorize?client_id=${clientId}&redirect_uri=${redirect}&response_type=code&scope=openid%20profile`;
}
}
function doLogout() {
if (!confirm('هل تريد تسجيل الخروج؟')) return;
localStorage.removeItem('zchat_user');
if (WS) WS.close();
location.reload();
}
// ═══════════════════════════════════════════════
// WEBSOCKET
// ═══════════════════════════════════════════════
function connectWS() {
try {
const wsURL = API_URL.replace('https','wss').replace('http','ws');
WS = new WebSocket(`${wsURL}/ws/${ME.id}`);
WS.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'new_message') {
if (currentChat?.user_id === msg.message.sender_id) {
appendBubble(msg.message);
} else {
showToast('💬 رسالة من ' + (msg.message.sender?.name || '؟'));
loadConversations();
}
}
} catch(e) {}
};
WS.onclose = () => { setTimeout(connectWS, 4000); };
WS.onerror = () => {};
} catch(e) {}
}
// ═══════════════════════════════════════════════
// NAVIGATION
// ═══════════════════════════════════════════════
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('on'));
document.querySelectorAll('.bnav-btn').forEach(b => b.classList.remove('on'));
const page = document.getElementById('page-' + name);
if (page) page.classList.add('on');
const btn = document.getElementById('bn-' + name);
if (btn) btn.classList.add('on');
if (name === 'profile') loadProfilePage(ME.id);
if (name === 'explore' && !document.getElementById('explore-grid').children.length) loadExplore();
if (name === 'messages') loadConversations();
if (name === 'notif') loadNotifs();
}
// ═══════════════════════════════════════════════
// HOME
// ═══════════════════════════════════════════════
async function loadHome() {
feedPage = 1; feedDone = false;
document.getElementById('feed').innerHTML = '';
await Promise.all([loadStories(), loadSuggested(), loadFeed()]);
}
async function loadFeed() {
if (feedLoading || feedDone) return;
feedLoading = true;
const feed = document.getElementById('feed');
if (feedPage === 1) feed.innerHTML = '<div style="padding:20px;text-align:center"><div class="spin-sm"></div></div>';
try {
const { posts } = await api(`/api/feed?user_id=${ME.id}&page=${feedPage}`);
if (feedPage === 1) feed.innerHTML = '';
if (!posts || !posts.length) {
if (feedPage === 1) feed.innerHTML = '<div class="empty-state"><div class="eico">📰</div><p>لا توجد منشورات — تابع أشخاصاً أو انشر أول منشور!</p></div>';
feedDone = true; feedLoading = false; return;
}
posts.forEach(p => feed.appendChild(buildPostCard(p)));
feedPage++;
} catch(e) {
if (feedPage === 1) feed.innerHTML = `<div class="empty-state"><div class="eico">⚠️</div><p>${e.message}</p></div>`;
}
feedLoading = false;
}
async function loadMoreFeed() { await loadFeed(); }
// ═══════════════════════════════════════════════
// STORIES
// ═══════════════════════════════════════════════
async function loadStories() {
const bar = document.getElementById('home-stories');
bar.innerHTML = `<div class="sitem sadd" onclick="addStory()"><div class="sring"><div class="sav">+</div></div><div class="sname">قصتي</div></div>`;
try {
svList = await api(`/api/stories?user_id=${ME.id}`);
svList.forEach((s, i) => {
const d = document.createElement('div');
d.className = 'sitem';
const avHtml = s.avatar ? `<img src="${API_URL}${s.avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">` : (s.name||'?')[0];
d.innerHTML = `<div class="sring"><div class="sav">${avHtml}</div></div><div class="sname">${(s.name||'').split(' ')[0]}</div>`;
d.onclick = () => openSV(i);
bar.appendChild(d);
});
} catch(e) {}
}
function openSV(idx) {
svIdx = idx;
const s = svList[idx];
if (!s) return;
const sv = document.getElementById('sv');
const svBg = document.getElementById('sv-bg');
const svSound = document.getElementById('sv-sound');
document.getElementById('sv-uname').textContent = s.name || '—';
if (s.avatar) document.getElementById('sv-av').innerHTML = `<img src="${API_URL}${s.avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else document.getElementById('sv-av').textContent = (s.name||'?')[0];
document.getElementById('sv-emoji').textContent = s.text ? '' : '🌟';
document.getElementById('sv-txt').textContent = s.text || '';
const bars = document.getElementById('sv-bars');
bars.innerHTML = svList.map((_,i) => `<div class="sv-bar"><div class="sv-fill ${i<idx?'done':i===idx?'active':''}"></div></div>`).join('');
const isVideo = !!s.file_url && /\.(mp4|webm|mov|m4v)(\?|$)/i.test(s.file_url) || (s.file_url || '').includes('/videos/');
if (s.file_url && isVideo) {
svBg.innerHTML = `<video id="sv-video" src="${API_URL}${s.file_url}" autoplay muted playsinline webkit-playsinline loop style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0"></video>`;
svBg.style.background = '#000';
svSound.style.display = 'flex';
svSound.classList.add('off');
svSound.textContent = '🔇';
svSound.title = 'تشغيل الصوت';
} else if (s.file_url) {
svBg.innerHTML = `<img src="${API_URL}${s.file_url}" style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0">`;
svBg.style.background = '#000';
svSound.style.display = 'none';
} else {
svBg.innerHTML = '';
svBg.style.background = s.bg_color || 'linear-gradient(135deg,#8b5cf6,#ec4899)';
svSound.style.display = 'none';
}
sv.classList.add('on');
clearTimeout(svTimer);
svTimer = setTimeout(() => { if (svIdx < svList.length-1) openSV(svIdx+1); else closeSV(); }, 5200);
}
function toggleStorySound(btn) {
const vid = document.getElementById('sv-video');
if (!vid) return;
vid.muted = !vid.muted;
vid.volume = 1;
if (!vid.muted && vid.paused) vid.play().catch(()=>{});
btn.textContent = vid.muted ? '🔇' : '🔊';
btn.title = vid.muted ? 'تشغيل الصوت' : 'كتم الصوت';
btn.classList.toggle('off', vid.muted);
}
function closeSV() { clearTimeout(svTimer); document.getElementById('sv').classList.remove('on'); }
function sendStoryReply() {
const v = document.getElementById('sv-rep-inp').value.trim();
if (v) { showToast('↗️ تم الرد'); document.getElementById('sv-rep-inp').value = ''; }
}
async function addStory() {
const text = prompt('نص القصة:') ?? '';
const colors = ['linear-gradient(135deg,#8b5cf6,#ec4899)','linear-gradient(135deg,#06d6a0,#0ea5e9)','linear-gradient(135deg,#f59e0b,#ef4444)'];
const bg = colors[Math.floor(Math.random()*colors.length)];
try {
await api('/api/stories','POST',{user_id:ME.id, text, bg_color:bg});
await loadStories();
showToast('✅ تم نشر القصة!');
} catch(e) { showToast('❌ '+e.message); }
}
// ═══════════════════════════════════════════════
// SUGGESTED
// ═══════════════════════════════════════════════
async function loadSuggested() {
try {
const users = await api(`/api/users/suggested?current=${ME.id}&limit=8`);
const strip = document.getElementById('sug-strip');
strip.innerHTML = '';
if (!users.length) return;
users.forEach(u => {
const d = document.createElement('div');
d.className = 'sugcard';
d.innerHTML = `
<div class="sugav">${avatarEl(u,52)}</div>
<div class="sugname">${u.name}</div>
<div class="sughand">@${u.username}</div>
<button class="sug-fol ${u.is_following?'following':''}" onclick="event.stopPropagation();toggleFollowUser('${u.id}',this)">
${u.is_following ? '✓ متابَع' : '+ تابع'}
</button>`;
d.onclick = () => openUserProfile(u.id);
strip.appendChild(d);
});
} catch(e) {}
}
async function toggleFollowUser(uid, btn) {
try {
const res = await api(`/api/follow?follower_id=${ME.id}&followee_id=${uid}`, 'POST');
btn.textContent = res.following ? '✓ متابَع' : '+ تابع';
btn.classList.toggle('following', res.following);
showToast(res.following ? '✅ تم المتابعة!' : 'إلغاء المتابعة');
} catch(e) { showToast('❌ '+e.message); }
}
// ═══════════════════════════════════════════════
// POST CARD — FIXED
// ═══════════════════════════════════════════════
function buildPostCard(p) {
const u = p.user || {};
const el = document.createElement('div');
el.className = 'pcard';
el.id = 'pc-' + p.id;
// ── Media (safe — no user-supplied data in event attrs) ──
let mediaEl = null;
if (p.file_url) {
const url = API_URL + p.file_url;
if (p.type === 'video' || p.type === 'reel') {
const wrap = document.createElement('div');
wrap.className = 'pvideo';
wrap.id = 'pv-' + p.id;
const vid = document.createElement('video');
vid.src = url; vid.preload = 'metadata'; vid.loop = true;
vid.controls = true; vid.muted = false; vid.volume = 1;
vid.setAttribute('playsinline',''); vid.setAttribute('webkit-playsinline','');
vid.setAttribute('controlsList','nodownload');
vid.style.cssText = 'width:100%;max-height:420px;display:block;object-fit:contain;background:#000';
vid.onclick = () => playVideo(vid);
const ov = document.createElement('div');
ov.className = 'pvo'; ov.id = 'pvo-' + p.id;
ov.innerHTML = '<div class="pplay">▶</div>';
ov.onclick = () => playVideoById(p.id);
wrap.appendChild(vid); wrap.appendChild(ov);
mediaEl = wrap;
} else if (p.type === 'photo') {
const img = document.createElement('img');
img.className = 'pmedia'; img.src = url; img.loading = 'lazy';
img.style.cssText = 'width:100%;display:block;max-height:500px;object-fit:cover';
mediaEl = img;
} else if (p.type === 'audio') {
const wrap = document.createElement('div');
wrap.className = 'paudio';
const disc = document.createElement('div');
disc.className = 'adisc'; disc.textContent = '🎵';
const info = document.createElement('div');
info.style.flex = '1';
info.innerHTML = '<div style="font-weight:700;font-size:13px">▶ تشغيل الصوت</div>';
const aud = document.createElement('audio');
aud.src = url; aud.preload = 'none';
wrap.onclick = () => { if(aud.paused){aud.play();disc.classList.add('playing');}else{aud.pause();disc.classList.remove('playing');} };
wrap.appendChild(disc); wrap.appendChild(info); wrap.appendChild(aud);
mediaEl = wrap;
}
}
// ── Static HTML (no user-data in event attributes) ──
const safeName = (u.name||'مستخدم').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const safeUser = (u.username||'user').replace(/</g,'&lt;');
const safeCaption = (p.caption||'').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/#(\S+)/g,'<span class="ht">#$1</span>');
el.innerHTML = `
<div class="phead">
<div class="pav">${avatarEl(u,40)}</div>
<div class="puinfo">
<div class="puname">${safeName}</div>
<div class="puhandle">@${safeUser} · ${timeAgo(p.created_at)}</div>
</div>
<div class="pmore">⋯</div>
</div>
${safeCaption ? `<div class="ptxt">${safeCaption}</div>` : ''}
<div class="pactions">
<button class="pact ${p.is_liked?'liked':''}" id="like-${p.id}">
<span class="ai">${p.is_liked?'❤️':'🤍'}</span>
<span id="lc-${p.id}">${fmt(p.likes_count)}</span>
</button>
<button class="pact">
<span class="ai">💬</span><span id="cc-${p.id}">${fmt(p.comments_count)}</span>
</button>
<button class="pact">
<span class="ai">↗️</span><span>مشاركة</span>
</button>
<button class="pact ${p.is_saved?'saved':''}" id="save-${p.id}">
<span class="ai">🔖</span>
</button>
</div>
`;
// ── Insert media between caption and actions ──
if (mediaEl) {
const actions = el.querySelector('.pactions');
el.insertBefore(mediaEl, actions);
}
// ── Safe event listeners (zero HTML-injection risk) ──
el.querySelector('.pav').onclick = () => openUserProfile(u.id);
el.querySelector('.puinfo').onclick = () => openUserProfile(u.id);
el.querySelector('.pmore').onclick = () => postMenu(p.id, u.id || '', p.caption || '');
const [likBtn, cmtBtn, shrBtn, savBtn] = el.querySelectorAll('.pact');
likBtn.onclick = function() { toggleLike(p.id, this); };
cmtBtn.onclick = () => openComments(p.id);
shrBtn.onclick = () => sharePost(p.id);
savBtn.onclick = function() { toggleSave(p.id, this); };
return el;
}
// FIX: Play video by post ID (targets the overlay and video)
function playVideoById(postId) {
const wrap = document.getElementById('pv-' + postId);
if (!wrap) return;
const vid = wrap.querySelector('video');
const ov = document.getElementById('pvo-' + postId);
if (!vid) return;
if (vid.paused) {
vid.muted = false;
vid.volume = 1;
vid.play().then(() => { if (ov) ov.style.display = 'none'; }).catch(() => { showToast('⚠️ لا يمكن تشغيل الفيديو'); });
} else {
vid.pause();
if (ov) ov.style.display = 'flex';
}
}
// Allow clicking directly on video element too
function playVideo(vid) {
const ov = vid.parentElement.querySelector('.pvo');
if (vid.paused) {
vid.muted = false;
vid.volume = 1;
vid.play().then(() => { if (ov) ov.style.display = 'none'; }).catch(() => {});
} else {
vid.pause();
if (ov) ov.style.display = 'flex';
}
}
function toggleReelSound(btn) {
const reel = btn.closest('.reel');
const vid = reel ? reel.querySelector('video') : null;
if (!vid) return;
vid.muted = !vid.muted;
vid.volume = 1;
if (!vid.muted && vid.paused) vid.play().catch(()=>{});
btn.textContent = vid.muted ? '🔇' : '🔊';
btn.title = vid.muted ? 'تشغيل الصوت' : 'كتم الصوت';
}
function toggleAudio(wrap) {
const aud = wrap.querySelector('audio');
const disc = wrap.querySelector('.adisc');
if (!aud) return;
if (aud.paused) { aud.play(); disc.classList.add('playing'); }
else { aud.pause(); disc.classList.remove('playing'); }
}
async function toggleLike(postId, btn) {
try {
const res = await api(`/api/posts/${postId}/like?user_id=${ME.id}`, 'POST');
btn.classList.toggle('liked', res.liked);
btn.querySelector('.ai').textContent = res.liked ? '❤️' : '🤍';
document.getElementById('lc-'+postId).textContent = fmt(res.count);
} catch(e) {}
}
async function toggleSave(postId, btn) {
try {
const res = await api(`/api/posts/${postId}/save?user_id=${ME.id}`, 'POST');
showToast(res.saved ? '🔖 تم الحفظ!' : 'إلغاء الحفظ');
btn.classList.toggle('saved', res.saved);
} catch(e) {}
}
function sharePost(id) {
navigator.clipboard?.writeText(window.location.origin + '/post/' + id).catch(()=>{});
showToast('↗️ تم نسخ الرابط!');
}
// ═══════════════════════════════════════════════
// POST MENU — SHEET (safe DOM creation, no HTML injection)
// ═══════════════════════════════════════════════
function postMenu(postId, ownerId, caption) {
currentMenuPostId = postId;
currentMenuOwnerId = ownerId;
const box = document.getElementById('pmenu-box');
box.innerHTML = '';
// Helper: create a menu item safely (no innerHTML with user data)
const mkItem = (label, action, danger) => {
const d = document.createElement('div');
d.className = 'pmenu-item' + (danger ? ' danger' : '');
d.textContent = label;
d.addEventListener('click', action);
return d;
};
const mkSep = () => { const s = document.createElement('div'); s.className = 'pmenu-sep'; return s; };
if (ownerId && ownerId !== 'undefined' && ownerId === ME.id) {
// OWN post: edit + delete
box.appendChild(mkItem('✏️ تعديل المنشور', () => { closePostMenu(); openEditPost(postId, caption); }));
box.appendChild(mkSep());
box.appendChild(mkItem('🗑️ حذف المنشور', () => {
closePostMenu();
if (confirm('هل تريد حذف هذا المنشور؟')) deletePost(postId);
}, true));
} else {
// OTHER's post: message + report
box.appendChild(mkItem('💬 إرسال رسالة', () => {
closePostMenu();
// Fetch user info then open chat
if (ownerId && ownerId !== 'undefined') {
api(`/api/user/${ownerId}?current=${ME.id}`)
.then(u => openChat(u))
.catch(() => showToast('❌ لا يمكن فتح الدردشة'));
}
}));
box.appendChild(mkItem('🚩 الإبلاغ عن المنشور', () => { closePostMenu(); reportPost(postId); }, true));
}
box.appendChild(mkSep());
box.appendChild(mkItem('إلغاء', closePostMenu));
document.getElementById('post-menu-sheet').classList.add('on');
}
function closePostMenu() { document.getElementById('post-menu-sheet').classList.remove('on'); }
// Report a post — auto-deleted after 2 unique reports
async function reportPost(postId) {
if (!confirm('هل تريد الإبلاغ عن هذا المنشور؟')) return;
try {
const res = await api(`/api/posts/${postId}/report?user_id=${ME.id}`, 'POST');
if (res.deleted) {
document.getElementById('pc-' + postId)?.remove();
showToast('🚫 تم حذف المنشور بعد تعدد البلاغات');
} else {
showToast(`✅ تم الإبلاغ (${res.count}/2)`);
}
} catch(e) {
showToast('⚠️ ' + e.message);
}
}
function openEditPost(postId, caption) {
currentEditPostId = postId;
document.getElementById('edit-caption').value = caption || '';
document.getElementById('edit-modal').classList.add('on');
}
function closeEditModal() { document.getElementById('edit-modal').classList.remove('on'); currentEditPostId = null; }
async function saveEditPost() {
if (!currentEditPostId) return;
const caption = document.getElementById('edit-caption').value.trim();
try {
const updated = await api(`/api/posts/${currentEditPostId}`, 'PUT', { user_id: ME.id, caption });
// Update the card in the DOM
const card = document.getElementById('pc-' + currentEditPostId);
if (card) {
const txtEl = card.querySelector('.ptxt');
if (caption) {
const newTxt = caption.replace(/#(\S+)/g,'<span class="ht">#$1</span>');
if (txtEl) txtEl.innerHTML = newTxt;
else {
const head = card.querySelector('.phead');
const div = document.createElement('div');
div.className = 'ptxt';
div.innerHTML = newTxt;
head.insertAdjacentElement('afterend', div);
}
} else if (txtEl) txtEl.remove();
}
closeEditModal();
showToast('✅ تم التحديث!');
} catch(e) { showToast('❌ '+e.message); }
}
function confirmDeletePost(postId) {
closePostMenu();
if (confirm('هل تريد حذف هذا المنشور؟')) deletePost(postId);
}
async function deletePost(postId) {
try {
await api(`/api/posts/${postId}?user_id=${ME.id}`, 'DELETE');
document.getElementById('pc-'+postId)?.remove();
showToast('🗑️ تم الحذف');
} catch(e) { showToast('❌ '+e.message); }
}
// ═══════════════════════════════════════════════
// COMMENTS MODAL — FIXED (no more prompt)
// ═══════════════════════════════════════════════
async function openComments(postId) {
currentCommentPostId = postId;
const list = document.getElementById('cm-list');
list.innerHTML = '<div style="padding:20px;text-align:center"><div class="spin-sm"></div></div>';
document.getElementById('comments-modal').classList.add('on');
document.getElementById('cm-inp').value = '';
try {
const comments = await api(`/api/posts/${postId}/comments`);
renderComments(comments);
} catch(e) {
list.innerHTML = `<div class="empty-state"><div class="eico">⚠️</div><p>${e.message}</p></div>`;
}
setTimeout(() => document.getElementById('cm-inp').focus(), 300);
}
function renderComments(comments) {
const list = document.getElementById('cm-list');
if (!comments.length) {
list.innerHTML = '<div class="empty-state"><div class="eico">💬</div><p>كن أول من يعلق!</p></div>';
return;
}
list.innerHTML = '';
comments.forEach(c => {
const u = c.user || {};
const d = document.createElement('div');
d.className = 'cm-item';
d.innerHTML = `
<div class="cm-av">${avatarEl(u,32)}</div>
<div class="cm-body">
<div class="cm-name">${u.name||'مستخدم'}</div>
<div class="cm-text">${c.text}</div>
<div class="cm-time">${timeAgo(c.created_at)}</div>
</div>`;
list.appendChild(d);
});
list.scrollTop = list.scrollHeight;
}
function closeComments() {
document.getElementById('comments-modal').classList.remove('on');
currentCommentPostId = null;
}
async function submitComment() {
if (!currentCommentPostId) return;
const inp = document.getElementById('cm-inp');
const text = inp.value.trim();
if (!text) return;
inp.value = '';
try {
const comment = await api('/api/comments','POST',{user_id:ME.id, post_id:currentCommentPostId, text});
// Add new comment to modal
const list = document.getElementById('cm-list');
const empty = list.querySelector('.empty-state');
if (empty) empty.remove();
const u = ME;
const d = document.createElement('div');
d.className = 'cm-item';
d.innerHTML = `
<div class="cm-av">${avatarEl(u,32)}</div>
<div class="cm-body">
<div class="cm-name">${u.name||'مستخدم'}</div>
<div class="cm-text">${text}</div>
<div class="cm-time">الآن</div>
</div>`;
list.appendChild(d);
list.scrollTop = list.scrollHeight;
// Update comment count on card
const countEl = document.getElementById('cc-' + currentCommentPostId);
if (countEl) countEl.textContent = fmt(parseInt(countEl.textContent||'0') + 1);
showToast('✅ تم نشر التعليق!');
} catch(e) { showToast('❌ '+e.message); inp.value = text; }
}
// ═══════════════════════════════════════════════
// EXPLORE
// ═══════════════════════════════════════════════
async function loadExplore() {
if (exploreLoading) return;
exploreLoading = true;
const load = document.getElementById('explore-load');
load.innerHTML = '<div class="spin-sm"></div>';
try {
const { posts } = await api(`/api/explore?user_id=${ME.id}&page=${explorePage}`);
const grid = document.getElementById('explore-grid');
if (!posts || !posts.length) { load.innerHTML = ''; exploreLoading = false; return; }
posts.forEach(p => {
const d = document.createElement('div');
d.className = 'egrid-item';
d.style.background = `hsl(${Math.random()*360},25%,12%)`;
d.innerHTML = `<div class="egrid-ov">❤️ ${fmt(p.likes_count)} 💬 ${fmt(p.comments_count)}</div>`;
if (p.file_url) {
if (p.type==='photo') {
const img = document.createElement('img');
img.src = API_URL + p.file_url;
img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
d.appendChild(img);
} else if (p.type==='video'||p.type==='reel') {
const v = document.createElement('video');
v.src = API_URL + p.file_url; v.muted = true; v.preload = 'none'; v.loop = true;
d.appendChild(v);
d.addEventListener('mouseenter',()=>v.play().catch(()=>{}));
d.addEventListener('mouseleave',()=>v.pause());
}
} else {
const ic = {text:'📝',audio:'🎵',reel:'▶️',video:'🎬',photo:'📸',story:'⭕'}[p.type]||'✦';
d.innerHTML += `<span style="font-size:36px;opacity:.5">${ic}</span>`;
}
d.onclick = () => openComments(p.id);
grid.appendChild(d);
});
explorePage++;
load.innerHTML = '';
} catch(e) { load.innerHTML = ''; }
exploreLoading = false;
}
document.getElementById('ecats').addEventListener('click', e => {
const btn = e.target.closest('.ecat');
if (!btn) return;
document.querySelectorAll('.ecat').forEach(b=>b.classList.remove('on'));
btn.classList.add('on');
explorePage = 1;
document.getElementById('explore-grid').innerHTML = '';
loadExplore();
});
// ═══════════════════════════════════════════════
// MESSAGES — FIXED (file sending)
// ═══════════════════════════════════════════════
async function loadConversations() {
const list = document.getElementById('conv-list');
list.innerHTML = '<div style="padding:20px;text-align:center"><div class="spin-sm"></div></div>';
try {
const convs = await api(`/api/conversations/${ME.id}`);
list.innerHTML = '';
if (!convs.length) {
list.innerHTML = '<div class="empty-state"><div class="eico">💬</div><p>لا توجد رسائل — ابدأ محادثة جديدة!</p></div>';
return;
}
convs.forEach(c => {
const u = c.user;
const lm = c.last_message;
const d = document.createElement('div');
d.className = 'mitem';
d.innerHTML = `
<div class="mav-wrap">
<div class="mav">${avatarEl(u,50)}</div>
<div class="mdot"></div>
</div>
<div class="mprev">
<div class="mpname">${u.name}</div>
<div class="mpmsg ${c.unread_count?'unread':''}">${lm?.text||'...'}</div>
</div>
<div class="mmeta">
<div class="mtime">${lm?timeAgo(lm.created_at):''}</div>
${c.unread_count ? `<div class="munread">${c.unread_count}</div>` : ''}
</div>`;
d.onclick = () => openChat(u);
list.appendChild(d);
});
} catch(e) {
list.innerHTML = `<div class="empty-state"><div class="eico">⚠️</div><p>${e.message}</p></div>`;
}
}
function openChat(user) {
currentChat = { user_id: user.id, name: user.name };
document.getElementById('chat-av').innerHTML = avatarEl(user, 38);
document.getElementById('chat-name').textContent = user.name;
document.getElementById('chat-win').classList.add('on');
loadMessages(user.id);
}
async function loadMessages(uid) {
const msgs = document.getElementById('chat-msgs');
msgs.innerHTML = '<div style="text-align:center;padding:20px"><div class="spin-sm"></div></div>';
try {
const list = await api(`/api/messages/${ME.id}/${uid}`);
msgs.innerHTML = '';
if (!list.length) {
msgs.innerHTML = '<div class="empty-state"><div class="eico">👋</div><p>ابدأ المحادثة</p></div>';
return;
}
list.forEach(m => appendBubble(m));
msgs.scrollTop = msgs.scrollHeight;
} catch(e) {
msgs.innerHTML = `<div class="empty-state"><div class="eico">⚠️</div><p>${e.message}</p></div>`;
}
}
function appendBubble(msg) {
const msgs = document.getElementById('chat-msgs');
const empty = msgs.querySelector('.empty-state');
if (empty) empty.remove();
const sent = msg.sender_id === ME.id;
const d = document.createElement('div');
d.className = 'bubble ' + (sent ? 'sent' : 'recv');
let content = '';
if (msg.file_url) {
const url = API_URL + msg.file_url;
const ft = msg.file_type || '';
if (ft.startsWith('image')) content += `<div class="bubble-file"><img src="${url}" onclick="window.open('${url}')"></div>`;
else if (ft.startsWith('video')) content += `<div class="bubble-file"><video src="${url}" controls playsinline></video></div>`;
else if (ft.startsWith('audio')) content += `<div class="bubble-file"><audio src="${url}" controls></audio></div>`;
else content += `<a href="${url}" target="_blank" style="color:inherit;text-decoration:underline">📎 ملف</a>`;
}
if (msg.text) content += `<span>${msg.text}</span>`;
d.innerHTML = `${content}<div class="btime">${timeAgo(msg.created_at)}${sent?' ✓✓':''}</div>`;
msgs.appendChild(d);
msgs.scrollTop = msgs.scrollHeight;
}
async function sendMsg() {
const inp = document.getElementById('chat-inp');
const txt = inp.value.trim();
if (!txt || !currentChat) return;
inp.value = '';
inp.style.height = 'auto';
const fakeMsg = { sender_id: ME.id, receiver_id: currentChat.user_id, text: txt, file_url: '', file_type: '', created_at: new Date().toISOString() };
appendBubble(fakeMsg);
try {
await api('/api/messages','POST',{sender_id:ME.id, receiver_id:currentChat.user_id, text:txt});
} catch(e) {}
}
async function sendChatFile(inp) {
if (!inp.files[0] || !currentChat) return;
const file = inp.files[0];
const ft = file.type;
let kind = 'image';
if (ft.startsWith('video')) kind = 'video';
else if (ft.startsWith('audio')) kind = 'audio';
showToast('⏳ جاري رفع الملف...');
try {
const url = await uploadFile(file, kind);
const msg = await api('/api/messages','POST',{
sender_id: ME.id, receiver_id: currentChat.user_id,
text: '', file_url: url, file_type: ft
});
appendBubble(msg);
showToast('✅ تم الإرسال!');
} catch(e) { showToast('❌ '+e.message); }
inp.value = '';
}
function closeChat() { document.getElementById('chat-win').classList.remove('on'); currentChat = null; }
// ═══════════════════════════════════════════════
// CALLS — WebRTC
// ═══════════════════════════════════════════════
const CALL_ICE = [{ urls: 'stun:stun.l.google.com:19302' }];
function uid() {
try { return crypto.randomUUID(); } catch(e) { return 'call-' + Date.now() + '-' + Math.floor(Math.random()*1e6); }
}
function showCallModal() {
document.getElementById('call-modal').classList.add('on');
}
function hideCallModal() {
document.getElementById('call-modal').classList.remove('on');
}
function setCallStatus(text) {
const el = document.getElementById('call-status');
if (el) el.textContent = text;
}
function setCallPeer(user, incoming=false) {
const nameEl = document.getElementById('call-peer-name');
const avEl = document.getElementById('call-peer-av');
if (nameEl) nameEl.textContent = user?.name || '—';
if (avEl) avEl.innerHTML = avatarEl(user, 48);
document.getElementById('call-incoming-actions').style.display = incoming ? 'flex' : 'none';
}
function resetCallUI() {
const remote = document.getElementById('call-remote-video');
const local = document.getElementById('call-local-video');
const ring = document.getElementById('call-ring');
if (remote) remote.srcObject = null;
if (local) local.srcObject = null;
if (local) local.style.display = 'none';
if (ring) ring.style.display = 'flex';
document.getElementById('call-incoming-actions').style.display = 'none';
document.getElementById('call-mic-btn').textContent = '🎤';
document.getElementById('call-cam-btn').textContent = '📷';
callMute = false;
callCameraOff = false;
}
async function prepareLocalStream(kind) {
const constraints = { audio: true, video: kind === 'video' };
const stream = await navigator.mediaDevices.getUserMedia(constraints);
callStream = stream;
const local = document.getElementById('call-local-video');
local.srcObject = stream;
local.style.display = kind === 'video' ? 'block' : 'none';
const ring = document.getElementById('call-ring');
if (ring) ring.style.display = 'none';
return stream;
}
function makePeer() {
if (callPeer) {
try { callPeer.close(); } catch(e) {}
}
callPeer = new RTCPeerConnection({ iceServers: CALL_ICE });
callPeer.onicecandidate = (e) => {
if (e.candidate && WS?.readyState === 1) {
WS.send(JSON.stringify({
type: 'call_ice',
to: callTargetId,
call_id: callId,
call_type: callType,
candidate: e.candidate
}));
}
};
callPeer.ontrack = (e) => {
const remote = document.getElementById('call-remote-video');
if (remote && e.streams && e.streams[0]) {
remote.srcObject = e.streams[0];
remote.play?.().catch(() => {});
}
setCallStatus('متصل');
};
callPeer.onconnectionstatechange = () => {
if (!callPeer) return;
const st = callPeer.connectionState;
if (st === 'connected') setCallStatus('متصل');
else if (st === 'connecting') setCallStatus('جاري الاتصال...');
else if (st === 'failed') {
setCallStatus('فشل الاتصال');
endCall(false);
} else if (st === 'disconnected' || st === 'closed') {
endCall(false);
}
};
}
async function startCall(type) {
if (!currentChat) return showToast('افتح محادثة أولاً');
if (!WS || WS.readyState !== 1) return showToast('الاتصال غير جاهز');
if (!navigator.mediaDevices?.getUserMedia) return showToast('المتصفح لا يدعم المكالمات');
if (callMode !== 'idle') endCall(false);
callType = type;
callTargetId = currentChat.user_id;
callId = uid();
callMode = 'outgoing';
callIncomingOffer = null;
callIncomingFrom = null;
setCallPeer(currentChat, false);
setCallStatus(type === 'video' ? 'بدء مكالمة مرئية...' : 'بدء مكالمة صوتية...');
showCallModal();
resetCallUI();
document.getElementById('call-ring').style.display = 'flex';
try {
await prepareLocalStream(type);
makePeer();
callStream.getTracks().forEach(track => callPeer.addTrack(track, callStream));
const offer = await callPeer.createOffer();
await callPeer.setLocalDescription(offer);
WS.send(JSON.stringify({
type: 'call_offer',
to: callTargetId,
call_id: callId,
call_type: callType,
from_name: ME?.name || ''
}));
WS.send(JSON.stringify({
type: 'call_offer_sdp',
to: callTargetId,
call_id: callId,
call_type: callType,
sdp: offer
}));
setCallStatus('جاري الاتصال...');
} catch(e) {
showToast('❌ ' + (e.message || 'تعذر بدء المكالمة'));
endCall(false);
}
}
function handleIncomingCall(msg) {
if (callMode !== 'idle') {
try {
WS.send(JSON.stringify({
type: 'call_busy',
to: msg.from,
call_id: msg.call_id || '',
call_type: msg.call_type || 'audio'
}));
} catch(e) {}
return;
}
callMode = 'incoming';
callType = msg.call_type || 'audio';
callTargetId = msg.from;
callId = msg.call_id || uid();
callIncomingFrom = { id: msg.from, name: msg.from_name || 'مستخدم' };
callIncomingOffer = msg.sdp || null;
setCallPeer(callIncomingFrom, true);
setCallStatus(callType === 'video' ? 'مكالمة مرئية واردة' : 'مكالمة صوتية واردة');
showCallModal();
resetCallUI();
document.getElementById('call-ring').style.display = 'flex';
}
async function acceptIncomingCall() {
if (!callIncomingOffer) return;
document.getElementById('call-incoming-actions').style.display = 'none';
setCallStatus('جاري قبول المكالمة...');
try {
await prepareLocalStream(callType);
makePeer();
callStream.getTracks().forEach(track => callPeer.addTrack(track, callStream));
await callPeer.setRemoteDescription(new RTCSessionDescription(callIncomingOffer));
const answer = await callPeer.createAnswer();
await callPeer.setLocalDescription(answer);
WS.send(JSON.stringify({
type: 'call_answer',
to: callTargetId,
call_id: callId,
call_type: callType,
sdp: answer
}));
callMode = 'connected';
setCallStatus('متصل');
} catch(e) {
showToast('❌ ' + (e.message || 'تعذر قبول المكالمة'));
endCall(false);
}
}
function rejectIncomingCall() {
if (WS?.readyState === 1 && callTargetId) {
WS.send(JSON.stringify({
type: 'call_reject',
to: callTargetId,
call_id: callId,
call_type: callType
}));
}
endCall(false);
}
function toggleCallMute() {
if (!callStream) return;
callMute = !callMute;
callStream.getAudioTracks().forEach(t => t.enabled = !callMute);
document.getElementById('call-mic-btn').textContent = callMute ? '🔇' : '🎤';
}
function toggleCallCamera() {
if (!callStream) return;
const tracks = callStream.getVideoTracks();
if (!tracks.length) return showToast('هذه مكالمة صوتية');
callCameraOff = !callCameraOff;
tracks.forEach(t => t.enabled = !callCameraOff);
document.getElementById('call-cam-btn').textContent = callCameraOff ? '🚫📷' : '📷';
}
function endCall(notify = true) {
try {
if (notify && WS?.readyState === 1 && callTargetId) {
WS.send(JSON.stringify({
type: 'call_end',
to: callTargetId,
call_id: callId,
call_type: callType
}));
}
} catch(e) {}
if (callPeer) {
try { callPeer.ontrack = null; callPeer.onicecandidate = null; callPeer.close(); } catch(e) {}
}
if (callStream) {
callStream.getTracks().forEach(t => { try { t.stop(); } catch(e) {} });
}
callPeer = null;
callStream = null;
callType = '';
callTargetId = '';
callId = '';
callMode = 'idle';
callIncomingOffer = null;
callIncomingFrom = null;
resetCallUI();
hideCallModal();
setCallStatus('جاهز للمكالمة');
if (notify) showToast('تم إنهاء المكالمة');
}
function handleCallSignal(msg) {
if (!msg || !msg.type) return;
if (msg.type === 'call_offer') {
handleIncomingCall(msg);
return;
}
if (msg.type === 'call_offer_sdp') {
if (callMode === 'outgoing' && callPeer && msg.sdp) {
callPeer.setRemoteDescription(new RTCSessionDescription(msg.sdp)).catch(() => {});
document.getElementById('call-ring').style.display = 'none';
document.getElementById('call-local-video').style.display = callType === 'video' ? 'block' : 'none';
}
return;
}
if (msg.type === 'call_answer') {
if (callMode === 'outgoing' && callPeer && msg.sdp) {
callPeer.setRemoteDescription(new RTCSessionDescription(msg.sdp)).catch(() => {});
callMode = 'connected';
setCallStatus('متصل');
document.getElementById('call-ring').style.display = 'none';
}
return;
}
if (msg.type === 'call_ice' && callPeer && msg.candidate) {
callPeer.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(() => {});
return;
}
if (msg.type === 'call_reject') {
showToast('تم رفض المكالمة');
endCall(false);
return;
}
if (msg.type === 'call_end' || msg.type === 'call_busy') {
showToast(msg.type === 'call_busy' ? 'الشخص مشغول الآن' : 'انتهت المكالمة');
endCall(false);
return;
}
}
function showNewMsgModal() {
const un = prompt('اكتب اسم مستخدم الشخص:');
if (!un) return;
api('/api/user/by-username/'+un.replace('@','')+'?current='+ME.id)
.then(u => openChat(u))
.catch(() => showToast('❌ المستخدم غير موجود'));
}
// ═══════════════════════════════════════════════
// PROFILE — FIXED (background on section)
// ═══════════════════════════════════════════════
async function loadProfilePage(uid) {
const isMe = uid === ME.id;
// Reset profile pic click only for own profile
const pic = document.getElementById('prof-pic');
pic.onclick = isMe ? changeAvatar : null;
try {
const u = await api(`/api/user/${uid}?current=${ME.id}`);
pic.innerHTML = avatarEl(u, 78);
document.getElementById('prof-name').textContent = u.name;
document.getElementById('prof-handle').textContent = '@' + u.username;
document.getElementById('prof-bio').textContent = u.bio || (isMe ? 'أضف نبذة عنك...' : '');
document.getElementById('ps-posts').textContent = fmt(u.posts_count||0);
document.getElementById('ps-followers').textContent = fmt(u.followers_count||0);
document.getElementById('ps-following').textContent = fmt(u.following_count||0);
const btns = document.getElementById('prof-btns');
if (!isMe) {
btns.innerHTML = `
<button class="btn-fol ${u.is_following?'followed':''}" id="prof-fol" onclick="toggleFollowProfile('${u.id}')">
${u.is_following?'✓ متابَع':'+ متابعة'}
</button>
<button class="btn-dm" onclick="openChat({id:'${u.id}',name:'${u.name}'})">رسالة 💬</button>`;
} else {
btns.innerHTML = `
<button class="btn-fol" onclick="editProfile()">✏️ تعديل الملف</button>
<button class="btn-dm" onclick="doLogout()">خروج 🚪</button>`;
}
loadProfilePosts(uid);
} catch(e) {}
}
async function loadProfilePosts(uid) {
const grid = document.getElementById('prof-grid');
grid.innerHTML = '';
try {
const { posts } = await api(`/api/user/${uid}/posts?current=${ME.id}`);
if (!posts || !posts.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:span 3"><div class="eico">📷</div><p>لا توجد منشورات</p></div>';
return;
}
posts.forEach(p => {
const d = document.createElement('div');
d.className = 'pgitem';
d.style.background = `hsl(${Math.random()*360},20%,10%)`;
if (p.file_url && p.type==='photo') {
const img = document.createElement('img');
img.src = API_URL + p.file_url;
img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
d.appendChild(img);
} else if (p.file_url && (p.type==='video'||p.type==='reel')) {
const v = document.createElement('video');
v.src = API_URL + p.file_url; v.muted = true; v.preload = 'none'; v.loop = true;
v.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
d.appendChild(v);
d.addEventListener('mouseenter',()=>v.play().catch(()=>{}));
d.addEventListener('mouseleave',()=>v.pause());
} else {
const ic = {text:'📝',audio:'🎵',reel:'▶️',video:'🎬',photo:'📸',story:'⭕'}[p.type]||'✦';
d.innerHTML += `<span style="font-size:28px;opacity:.5">${ic}</span>`;
}
d.innerHTML += `<div class="pgitem-ov">❤️ ${fmt(p.likes_count)} 💬 ${fmt(p.comments_count)}</div>`;
d.onclick = () => openComments(p.id);
grid.appendChild(d);
});
} catch(e) {}
}
async function toggleFollowProfile(uid) {
try {
const res = await api(`/api/follow?follower_id=${ME.id}&followee_id=${uid}`,'POST');
const btn = document.getElementById('prof-fol');
if (btn) { btn.textContent = res.following?'✓ متابَع':'+ متابعة'; btn.classList.toggle('followed',res.following); }
document.getElementById('ps-followers').textContent = fmt(res.followers_count);
showToast(res.following?'✅ تم المتابعة!':'إلغاء المتابعة');
} catch(e) {}
}
function editProfile() {
const newName = prompt('الاسم:', ME.name);
if (newName===null) return;
const newBio = prompt('النبذة:', ME.bio||'');
if (newBio===null) return;
api('/api/user/update','PUT',{user_id:ME.id, name:newName||ME.name, bio:newBio})
.then(u => {
ME = {...ME, ...u};
localStorage.setItem('zchat_user', JSON.stringify(ME));
loadProfilePage(ME.id);
updateNavAvatar();
showToast('✅ تم التحديث!');
}).catch(e => showToast('❌ '+e.message));
}
function changeAvatar() {
const inp = document.createElement('input');
inp.type = 'file'; inp.accept = 'image/*';
inp.onchange = async () => {
if (!inp.files[0]) return;
showToast('⏳ جاري رفع الصورة...');
try {
const url = await uploadFile(inp.files[0], 'avatar');
await api('/api/user/update','PUT',{user_id:ME.id, name:ME.name});
ME.avatar = url;
localStorage.setItem('zchat_user', JSON.stringify(ME));
document.getElementById('prof-pic').innerHTML = `<img src="${API_URL}${url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
updateNavAvatar();
showToast('✅ تم تحديث الصورة!');
} catch(e) { showToast('❌ '+e.message); }
};
inp.click();
}
function switchPTab(el, tab) {
document.querySelectorAll('.ptab-btn').forEach(b=>b.classList.remove('on'));
el.classList.add('on');
if (tab==='saved') loadSavedPosts();
else loadProfilePosts(ME.id);
}
async function loadSavedPosts() {
const grid = document.getElementById('prof-grid');
grid.innerHTML = '';
try {
const posts = await api(`/api/user/${ME.id}/saved`);
if (!posts.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:span 3"><div class="eico">🔖</div><p>لا توجد منشورات محفوظة</p></div>';
return;
}
posts.forEach(p => {
const d = document.createElement('div');
d.className = 'pgitem';
d.style.background = `hsl(${Math.random()*360},20%,10%)`;
const ic = {text:'📝',audio:'🎵',reel:'▶️',video:'🎬',photo:'📸',story:'⭕'}[p.type]||'✦';
d.innerHTML = `<span style="font-size:28px">${ic}</span><div class="pgitem-ov">❤️ ${fmt(p.likes_count)}</div>`;
d.onclick = () => openComments(p.id);
grid.appendChild(d);
});
} catch(e) {}
}
function openUserProfile(uid) {
if (!uid || uid === 'undefined') return;
showPage('profile');
loadProfilePage(uid);
}
// ═══════════════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════════════
async function loadNotifs() {
try {
const notifs = await api(`/api/notifications/${ME.id}`);
const list = document.getElementById('notif-list');
list.innerHTML = '';
if (!notifs.length) {
list.innerHTML = '<div class="empty-state"><div class="eico">🔔</div><p>لا توجد إشعارات</p></div>';
return;
}
const icons = {like:'❤️',follow:'👤',comment:'💬',share:'↗️'};
const labels = {like:'أعجبه منشورك',follow:'بدأ في متابعتك',comment:'علق على منشورك',share:'شارك منشورك'};
notifs.forEach(n => {
const u = n.from_user||{};
const d = document.createElement('div');
d.className = 'nitem';
d.innerHTML = `
<div class="nic ${n.type}">${icons[n.type]||'🔔'}</div>
<div style="flex:1">
<div class="ntxt"><strong>${u.name||'—'}</strong> ${labels[n.type]||''}</div>
<div class="ntime">${timeAgo(n.created_at)}</div>
</div>
${!n.seen ? '<div class="nunread"></div>' : ''}`;
d.onclick = () => openUserProfile(u.id);
list.appendChild(d);
});
const unread = notifs.filter(n=>!n.seen).length;
const badge = document.getElementById('notif-badge');
badge.textContent = unread;
badge.style.display = unread ? 'flex' : 'none';
} catch(e) {}
}
// ═══════════════════════════════════════════════
// POST MODAL — FIXED (auto type + fullscreen)
// ═══════════════════════════════════════════════
function openPostModal() {
document.getElementById('post-modal').classList.add('on');
updateNavAvatar();
}
function closePostModal() {
document.getElementById('post-modal').classList.remove('on');
resetPostModal();
}
function resetPostModal() {
document.getElementById('compose-txt').value = '';
document.getElementById('file-preview-area').innerHTML = '';
pendingFile = null;
postType = 'text';
document.querySelectorAll('.ptype').forEach((b,i) => b.classList.toggle('on', i===0));
}
document.getElementById('ptypes').addEventListener('click', e => {
const btn = e.target.closest('.ptype');
if (!btn) return;
document.querySelectorAll('.ptype').forEach(b=>b.classList.remove('on'));
btn.classList.add('on');
postType = btn.dataset.t;
});
// FIX: Auto-update postType when file is selected
function previewFile(inp, kind) {
const file = inp.files[0];
if (!file) return;
pendingFile = { file, kind };
// Auto-set postType based on file kind
const typeMap = { image: 'photo', video: 'video', audio: 'audio' };
const newType = typeMap[kind] || postType;
postType = newType;
document.querySelectorAll('.ptype').forEach(b => {
b.classList.toggle('on', b.dataset.t === newType);
});
const area = document.getElementById('file-preview-area');
area.innerHTML = '';
const wrap = document.createElement('div');
wrap.className = 'file-preview';
wrap.style.position = 'relative';
if (kind === 'image') {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
wrap.appendChild(img);
} else if (kind === 'video') {
const v = document.createElement('video');
v.src = URL.createObjectURL(file);
v.controls = true;
v.style.maxWidth = '100%';
v.style.maxHeight = '260px';
wrap.appendChild(v);
} else {
wrap.classList.add('audio-prev');
wrap.innerHTML = `<span style="font-size:40px">🎵</span><span style="font-weight:700">${file.name}</span><span style="font-size:11px;color:var(--text3)">${(file.size/1024/1024).toFixed(2)} MB</span>`;
}
// Remove button
const removeBtn = document.createElement('div');
removeBtn.className = 'fp-remove';
removeBtn.textContent = '✕';
removeBtn.onclick = () => {
area.innerHTML = '';
pendingFile = null;
inp.value = '';
};
wrap.appendChild(removeBtn);
area.appendChild(wrap);
}
function insertEmoji() {
const ta = document.getElementById('compose-txt');
const emojis = ['😊','😂','❤️','🔥','✨','👏','💯','🎉','🙏','😍'];
ta.value += emojis[Math.floor(Math.random() * emojis.length)];
ta.focus();
}
async function publishPost() {
const txt = document.getElementById('compose-txt').value.trim();
if (!txt && !pendingFile) { showToast('⚠️ أضف نصاً أو ملفاً'); return; }
const btn = document.getElementById('pub-btn');
btn.disabled = true;
btn.innerHTML = '<div class="spin-sm"></div> جاري النشر...';
try {
let fileUrl = '';
if (pendingFile) {
showToast('⏳ جاري رفع الملف...');
fileUrl = await uploadFile(pendingFile.file, pendingFile.kind);
}
const post = await api('/api/posts','POST',{user_id:ME.id, type:postType, caption:txt, file_url:fileUrl});
closePostModal();
showToast('✅ تم النشر بنجاح!');
const feed = document.getElementById('feed');
const empty = feed.querySelector('.empty-state');
if (empty) empty.remove();
feed.insertBefore(buildPostCard(post), feed.firstChild);
const psP = document.getElementById('ps-posts');
if (psP) psP.textContent = fmt((parseInt(psP.textContent)||0) + 1);
} catch(e) { showToast('❌ '+e.message); }
btn.disabled = false;
btn.innerHTML = 'نشر ⚡';
}
// ═══════════════════════════════════════════════
// SEARCH
// ═══════════════════════════════════════════════
let searchTimer = null;
function onSearch(q) {
clearTimeout(searchTimer);
const res = document.getElementById('search-results');
if (!q.trim()) {
res.innerHTML = '<div class="empty-state"><div class="eico">🔍</div><p>ابحث عن أشخاص أو محتوى</p></div>';
return;
}
res.innerHTML = '<div style="padding:20px;text-align:center"><div class="spin-sm"></div></div>';
searchTimer = setTimeout(async () => {
try {
const users = await api(`/api/search/users?q=${encodeURIComponent(q)}&current=${ME.id}`);
res.innerHTML = '';
if (!users.length) {
res.innerHTML = `<div class="empty-state"><div class="eico">🔍</div><p>لا توجد نتائج لـ "${q}"</p></div>`;
return;
}
users.forEach(u => {
const d = document.createElement('div');
d.className = 'sr-user';
d.innerHTML = `
<div class="sr-av">${avatarEl(u,44)}</div>
<div class="sr-info"><div class="sr-name">${u.name}</div><div class="sr-handle">@${u.username}</div></div>
<button style="background:var(--acc);color:#fff;border:none;border-radius:8px;padding:6px 12px;font-family:Cairo,sans-serif;font-size:12px;font-weight:700;cursor:pointer;flex-shrink:0"
onclick="event.stopPropagation();toggleFollowUser('${u.id}',this)">${u.is_following?'✓ متابَع':'+ تابع'}</button>`;
d.onclick = () => openUserProfile(u.id);
res.appendChild(d);
});
} catch(e) {
res.innerHTML = `<div class="empty-state"><div class="eico">⚠️</div><p>${e.message}</p></div>`;
}
}, 450);
}
// ═══════════════════════════════════════════════
// TOAST
// ═══════════════════════════════════════════════
let toastTimer = null;
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('on');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('on'), 2600);
}
// ── ENHANCEMENTS & FIXES ───────────────────────────
let storyPendingFile = null;
let storyEffect = 'none';
let storyType = 'text';
let currentViewerPost = null;
let inboxTimer = null;
// Safer app start with periodic inbox refresh
function startApp() {
document.getElementById('auth').style.display = 'none';
document.getElementById('app').classList.add('on');
updateNavAvatar();
loadHome();
loadConversations();
loadNotifs();
connectWS();
clearInterval(inboxTimer);
inboxTimer = setInterval(() => {
loadConversations();
if (document.getElementById('page-notif')?.classList.contains('on')) loadNotifs();
}, 12000);
document.getElementById('page-home').addEventListener('scroll', function() {
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 300) loadMoreFeed();
});
document.getElementById('page-explore').addEventListener('scroll', function() {
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 200) loadExplore();
});
}
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('on'));
document.querySelectorAll('.bnav-btn').forEach(b => b.classList.remove('on'));
const page = document.getElementById('page-' + name);
if (page) page.classList.add('on');
const btn = document.getElementById('bn-' + name);
if (btn) btn.classList.add('on');
if (name === 'profile') loadProfilePage(ME.id);
if (name === 'messages') loadConversations();
if (name === 'notif') loadNotifs();
if (name === 'search') document.getElementById('search-inp')?.focus();
}
function connectWS() {
try {
const wsURL = API_URL.replace('https','wss').replace('http','ws');
if (WS) { try { WS.close(); } catch(e) {} }
WS = new WebSocket(`${wsURL}/ws/${ME.id}`);
WS.onopen = () => { try { WS.send(JSON.stringify({type:'ping'})); } catch(e) {} };
WS.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'new_message') {
if (currentChat?.user_id === msg.message.sender_id) {
appendBubble(msg.message);
} else {
showToast('💬 رسالة جديدة من ' + (msg.message.sender?.name || 'مستخدم'));
loadConversations();
}
} else if (msg.type && msg.type.startsWith('call_')) {
handleCallSignal(msg);
}
} catch(e) {}
};
WS.onclose = () => { setTimeout(() => { if (ME) connectWS(); }, 3000); };
WS.onerror = () => {};
} catch(e) {}
}
async function loadConversations() {
const list = document.getElementById('conv-list');
if (!list) return;
list.innerHTML = '<div style="padding:20px;text-align:center"><div class="spin-sm"></div></div>';
try {
const convs = await api(`/api/conversations/${ME.id}`);
list.innerHTML = '';
if (!convs.length) {
list.innerHTML = '<div class="empty-state"><div class="eico">💬</div><p>لا توجد رسائل — ابدأ محادثة جديدة!</p></div>';
return;
}
convs.forEach(c => {
const u = c.user || {};
const lm = c.last_message || {};
const d = document.createElement('div');
d.className = 'mitem';
const preview = lm.file_url
? (lm.file_type?.startsWith('image') ? '📷 صورة' : lm.file_type?.startsWith('video') ? '🎬 فيديو' : lm.file_type?.startsWith('audio') ? '🎵 صوت' : '📎 ملف')
: (lm.text || '...');
d.innerHTML = `
<div class="mav-wrap">
<div class="mav">${avatarEl(u,50)}</div>
${c.unread_count ? '<div class="mdot"></div>' : ''}
</div>
<div class="mprev">
<div class="mpname">${u.name || 'مستخدم'}</div>
<div class="mpmsg ${c.unread_count?'unread':''}">${preview}</div>
</div>
<div class="mmeta">
<div class="mtime">${lm.created_at ? timeAgo(lm.created_at) : ''}</div>
${c.unread_count ? `<div class="munread">${c.unread_count}</div>` : ''}
</div>`;
d.onclick = () => openChat(u);
list.appendChild(d);
});
} catch(e) {
list.innerHTML = `<div class="empty-state"><div class="eico">⚠️</div><p>${e.message}</p></div>`;
}
}
// ── STORIES ──
function openStoryComposer() {
document.getElementById('story-modal').classList.add('on');
updateNavAvatar();
}
function closeStoryComposer() {
document.getElementById('story-modal').classList.remove('on');
resetStoryComposer();
}
function resetStoryComposer() {
storyPendingFile = null;
storyEffect = 'none';
storyType = 'text';
document.getElementById('story-text').value = '';
document.getElementById('story-preview-area').innerHTML = '';
document.querySelectorAll('#story-ptypes .ptype').forEach(b => b.classList.toggle('on', b.dataset.s === 'text'));
document.querySelectorAll('#story-effects .story-eff').forEach(b => b.classList.toggle('on', b.dataset.eff === 'none'));
}
document.getElementById('story-ptypes')?.addEventListener('click', e => {
const btn = e.target.closest('.ptype');
if (!btn) return;
document.querySelectorAll('#story-ptypes .ptype').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
storyType = btn.dataset.s;
});
document.getElementById('story-effects')?.addEventListener('click', e => {
const btn = e.target.closest('.story-eff');
if (!btn) return;
document.querySelectorAll('#story-effects .story-eff').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
storyEffect = btn.dataset.eff || 'none';
const preview = document.querySelector('#story-preview-area .file-preview');
if (preview) preview.className = 'file-preview ' + (storyEffect === 'none' ? 'story-filter-none' : 'story-filter-' + storyEffect);
});
function previewStoryFile(inp, kind) {
const file = inp.files[0];
if (!file) return;
storyPendingFile = { file, kind };
storyType = kind === 'image' ? 'photo' : 'video';
document.querySelectorAll('#story-ptypes .ptype').forEach(b => b.classList.toggle('on', b.dataset.s === storyType));
const area = document.getElementById('story-preview-area');
area.innerHTML = '';
const wrap = document.createElement('div');
wrap.className = 'file-preview story-filter-none';
if (kind === 'image') {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
wrap.appendChild(img);
} else {
const vid = document.createElement('video');
vid.src = URL.createObjectURL(file);
vid.controls = true;
vid.playsInline = true;
wrap.appendChild(vid);
}
const removeBtn = document.createElement('div');
removeBtn.className = 'fp-remove';
removeBtn.textContent = '✕';
removeBtn.onclick = () => { area.innerHTML = ''; storyPendingFile = null; inp.value = ''; };
wrap.appendChild(removeBtn);
area.appendChild(wrap);
if (storyEffect !== 'none') wrap.classList.add('story-filter-' + storyEffect);
}
function insertStoryEmoji() {
const ta = document.getElementById('story-text');
const emojis = ['😊','😂','❤️','🔥','✨','👏','💯','🎉','🙏','😍'];
ta.value += emojis[Math.floor(Math.random() * emojis.length)];
ta.focus();
}
async function publishStory() {
const txt = document.getElementById('story-text').value.trim();
if (!txt && !storyPendingFile) { showToast('⚠️ أضف نصاً أو ملفاً'); return; }
const btn = document.getElementById('story-pub-btn');
btn.disabled = true;
btn.innerHTML = '<div class="spin-sm"></div> جاري النشر...';
try {
let fileUrl = '';
if (storyPendingFile) {
showToast('⏳ جاري رفع الملف...');
fileUrl = await uploadFile(storyPendingFile.file, storyPendingFile.kind);
}
const colorMap = {
none: 'linear-gradient(135deg,#8b5cf6,#ec4899)',
sepia: 'linear-gradient(135deg,#f59e0b,#ef4444)',
grayscale: 'linear-gradient(135deg,#475569,#0f172a)',
contrast: 'linear-gradient(135deg,#06d6a0,#0ea5e9)',
blur: 'linear-gradient(135deg,#8b5cf6,#06d6a0)'
};
const bg = colorMap[storyEffect] || colorMap.none;
await api('/api/stories','POST',{user_id:ME.id, text:txt, bg_color:bg, file_url:fileUrl});
closeStoryComposer();
await loadStories();
showToast('✅ تم نشر القصة!');
} catch(e) { showToast('❌ '+e.message); }
btn.disabled = false;
btn.innerHTML = 'نشر القصة ✨';
}
function loadStories() {
const bar = document.getElementById('home-stories');
bar.innerHTML = `<div class="sitem sadd" onclick="openStoryComposer()"><div class="sring"><div class="sav">+</div></div><div class="sname">قصتي</div></div>`;
return api(`/api/stories?user_id=${ME.id}`).then(list => {
svList = list || [];
svList.forEach((s, i) => {
const d = document.createElement('div');
d.className = 'sitem';
const avHtml = s.avatar ? `<img src="${API_URL}${s.avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">` : (s.name||'?')[0];
d.innerHTML = `<div class="sring"><div class="sav">${avHtml}</div></div><div class="sname">${(s.name||'').split(' ')[0]}</div>`;
d.onclick = () => openSV(i);
bar.appendChild(d);
});
}).catch(()=>{});
}
function openSV(idx) {
svIdx = idx;
const s = svList[idx];
if (!s) return;
document.getElementById('sv-uname').textContent = s.name || '—';
if (s.avatar) document.getElementById('sv-av').innerHTML = `<img src="${API_URL}${s.avatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else document.getElementById('sv-av').textContent = (s.name||'?')[0];
const bodyBg = document.getElementById('sv-bg');
const emoji = document.getElementById('sv-emoji');
if (s.file_url) {
const src = API_URL + s.file_url;
const isVideo = /\.(mp4|webm|mov|m4v)(\?|$)/i.test(s.file_url) || s.file_url.includes('/videos/');
bodyBg.innerHTML = isVideo
? `<video src="${src}" autoplay muted playsinline loop style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0"></video>`
: `<img src="${src}" style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0">`;
emoji.style.display = 'none';
} else {
bodyBg.innerHTML = '';
bodyBg.style.background = s.bg_color || 'linear-gradient(135deg,#8b5cf6,#ec4899)';
emoji.style.display = 'block';
emoji.textContent = s.text ? '' : '🌟';
}
document.getElementById('sv-txt').textContent = s.text || '';
const bars = document.getElementById('sv-bars');
bars.innerHTML = svList.map((_,i) => `<div class="sv-bar"><div class="sv-fill ${i<idx?'done':i===idx?'active':''}"></div></div>`).join('');
document.getElementById('sv').classList.add('on');
clearTimeout(svTimer);
svTimer = setTimeout(() => { if (svIdx < svList.length-1) openSV(svIdx+1); else closeSV(); }, 5200);
}
function sendStoryReply() {
const v = document.getElementById('sv-rep-inp').value.trim();
if (v) { showToast('↗️ تم الرد'); document.getElementById('sv-rep-inp').value = ''; }
}
// ── PROFILE POSTS AS FULL VIEWER ──
function loadProfilePosts(uid) {
const grid = document.getElementById('prof-grid');
grid.innerHTML = '';
return api(`/api/user/${uid}/posts?current=${ME.id}`).then(({posts}) => {
if (!posts || !posts.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:span 3"><div class="eico">📷</div><p>لا توجد منشورات</p></div>';
return;
}
window.__postCache = window.__postCache || {};
posts.forEach(p => {
window.__postCache[p.id] = p;
const d = document.createElement('div');
d.className = 'pgitem';
d.style.background = `hsl(${Math.random()*360},20%,10%)`;
if (p.file_url && p.type==='photo') {
const img = document.createElement('img');
img.src = API_URL + p.file_url;
img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
d.appendChild(img);
} else if (p.file_url && (p.type==='video'||p.type==='reel')) {
const v = document.createElement('video');
v.src = API_URL + p.file_url; v.muted = true; v.preload = 'none'; v.loop = true;
v.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
d.appendChild(v);
d.addEventListener('mouseenter',()=>v.play().catch(()=>{}));
d.addEventListener('mouseleave',()=>v.pause());
} else {
const ic = {text:'📝',audio:'🎵',reel:'▶️',video:'🎬',photo:'📸',story:'⭕',product:'🛍️'}[p.type]||'✦';
d.innerHTML += `<span style="font-size:28px;opacity:.5">${ic}</span>`;
}
d.innerHTML += `<div class="pgitem-ov">❤️ ${fmt(p.likes_count)} 💬 ${fmt(p.comments_count)}</div>`;
d.onclick = () => openPostViewerFromData(p);
grid.appendChild(d);
});
}).catch(()=>{});
}
function loadSavedPosts() {
const grid = document.getElementById('prof-grid');
grid.innerHTML = '';
return api(`/api/user/${ME.id}/saved`).then(posts => {
if (!posts.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:span 3"><div class="eico">🔖</div><p>لا توجد منشورات محفوظة</p></div>';
return;
}
window.__postCache = window.__postCache || {};
posts.forEach(p => {
window.__postCache[p.id] = p;
const d = document.createElement('div');
d.className = 'pgitem';
d.style.background = `hsl(${Math.random()*360},20%,10%)`;
const ic = {text:'📝',audio:'🎵',reel:'▶️',video:'🎬',photo:'📸',story:'⭕',product:'🛍️'}[p.type]||'✦';
d.innerHTML = `<span style="font-size:28px">${ic}</span><div class="pgitem-ov">❤️ ${fmt(p.likes_count)}</div>`;
d.onclick = () => openPostViewerFromData(p);
grid.appendChild(d);
});
}).catch(()=>{});
}
function openPostViewerFromData(p) {
currentViewerPost = p;
const body = document.getElementById('pv-body');
if (!body) return;
document.getElementById('pv-title').textContent = p.user?.name || 'المنشور';
let media = '';
if (p.file_url) {
const url = API_URL + p.file_url;
if (p.type === 'video' || p.type === 'reel') media = `<div class="pv-media"><video src="${url}" controls playsinline></video></div>`;
else if (p.type === 'audio') media = `<div class="pv-media" style="padding:24px;color:var(--text2);background:var(--bg3);">🎵 ملف صوتي</div>`;
else media = `<div class="pv-media"><img src="${url}"></div>`;
}
const caption = p.caption ? `<div class="pv-caption">${p.caption}</div>` : '';
const ownerActions = (p.user_id === ME.id)
? `<div class="pv-actions">
<button class="btn-fol" onclick="openEditPost('${p.id}', ${JSON.stringify(p.caption||'')})">تعديل</button>
<button class="btn-dm" onclick="deletePost('${p.id}')">حذف</button>
</div>`
: `<div class="pv-actions"><button class="btn-fol" onclick="openComments('${p.id}')">التعليقات</button><button class="btn-dm" onclick="postMenu('${p.id}','${p.user_id}','${(p.caption||'').replace(/'/g,"\\'")}')">⋯</button></div>`;
body.innerHTML = `
<div class="pv-head">
<div class="pv-author">
<div class="mav" style="width:42px;height:42px">${avatarEl(p.user,42)}</div>
<div>
<div style="font-weight:800">${p.user?.name || 'مستخدم'}</div>
<div class="pv-meta">@${p.user?.username || ''} • ${p.created_at ? timeAgo(p.created_at) : ''}</div>
</div>
</div>
</div>
${media}
${caption}
${ownerActions}
`;
document.getElementById('post-viewer').classList.add('on');
}
function closePostViewer() {
document.getElementById('post-viewer').classList.remove('on');
currentViewerPost = null;
}
function openPostViewer(postId) {
const cached = window.__postCache?.[postId];
if (cached) return openPostViewerFromData(cached);
api(`/api/posts/${postId}?user_id=${ME.id}`).then(openPostViewerFromData).catch(e => showToast('❌ '+e.message));
}
// ── PROFILE EDIT ──
function editProfile() {
document.getElementById('pe-name').value = ME.name || '';
document.getElementById('pe-bio').value = ME.bio || '';
document.getElementById('pe-cover').value = document.querySelector('.prof-cover')?.style.background || 'linear-gradient(135deg,#8b5cf6,#ec4899)';
document.getElementById('profile-edit-modal').classList.add('on');
}
function closeProfileEditModal() {
document.getElementById('profile-edit-modal').classList.remove('on');
}
async function saveProfileEdit() {
const name = document.getElementById('pe-name').value.trim();
const bio = document.getElementById('pe-bio').value.trim();
const cover = document.getElementById('pe-cover').value.trim();
try {
const u = await api('/api/user/update','PUT',{user_id:ME.id, name: name || ME.name, bio});
ME = {...ME, ...u};
localStorage.setItem('zchat_user', JSON.stringify(ME));
updateNavAvatar();
document.getElementById('prof-name').textContent = ME.name || '';
document.getElementById('prof-handle').textContent = '@' + (ME.username || '');
if (cover) document.querySelector('.prof-cover').style.background = cover;
closeProfileEditModal();
showToast('✅ تم التحديث!');
} catch(e) { showToast('❌ '+e.message); }
}
function changeAvatar() {
const inp = document.createElement('input');
inp.type = 'file'; inp.accept = 'image/*';
inp.onchange = async () => {
if (!inp.files[0]) return;
showToast('⏳ جاري رفع الصورة...');
try {
const url = await uploadFile(inp.files[0], 'avatar');
await api('/api/user/update','PUT',{user_id:ME.id, name:ME.name});
ME.avatar = url;
localStorage.setItem('zchat_user', JSON.stringify(ME));
document.getElementById('prof-pic').innerHTML = `<img src="${API_URL}${url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
updateNavAvatar();
showToast('✅ تم تحديث الصورة!');
} catch(e) { showToast('❌ '+e.message); }
};
inp.click();
}
</script>
</body>
</html>