Spaces:
Sleeping
Sleeping
| <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 } | |
| .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,'<').replace(/>/g,'>'); | |
| const safeUser = (u.username||'user').replace(/</g,'<'); | |
| const safeCaption = (p.caption||'').replace(/</g,'<').replace(/>/g,'>') | |
| .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)}¤t=${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> | |