1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 wahadici-embed.js â Appels + notifications pour le SITE PRINCIPAL (index.html) 3 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4 ⢠à inclure une seule fois, avant </body> : 5 <script src="/assets/js/wahadici-embed.js?v=1"></script> 6 ⢠Nécessite window.Wanzani (déjà chargé sur le site). 7 ⢠Interopérable avec wahadici.html : même signalisation Supabase Realtime 8 (wa-ring-<uid> / wa-call-<callId>) + même serveur TURN. 9 ⢠Fichiers son attendus dans AUDIO_BASE (chargés à la demande) : 10 notif.mp3, audio_call.mp3, video_call.mp3, outcall_open.mp3, 11 busy_call.mp3, line_busy.mp3 12 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 13(function(){ 14 'use strict'; 15 if (window.__wahadiciEmbed) return; window.__wahadiciEmbed = true; 16 try{ console.log('[wahadici-embed] script chargé'); }catch(e){} 17 18 // Filet de sécurité : charger le moteur de messagerie même si la balise 19 // <script wahadici-chat.js> manque dans index.html (ex. cache Cloudflare). 20 try{ 21 if(!window.__wahadiciSite && !document.querySelector('script[data-wahadici-site]')){ 22 var _cs=document.createElement('script'); 23 _cs.src='/assets/js/wahadici-site.js?v=21'; 24 _cs.setAttribute('data-wahadici-site','1'); 25 _cs.onload=function(){ try{ console.log('[wahadici-embed] module site chargé'); }catch(e){} }; 26 _cs.onerror=function(){ try{ console.warn('[wahadici-embed] wahadici-site.js introuvable dans /assets/js/'); }catch(e){} }; 27 (document.head||document.documentElement).appendChild(_cs); 28 } 29 if(!window.WZFX && !document.querySelector('script[data-wahadici-fx]')){ 30 var _fx=document.createElement('script'); 31 _fx.src='/assets/js/wahadici-call-fx.js?v=2'; 32 _fx.setAttribute('data-wahadici-fx','1'); 33 (document.head||document.documentElement).appendChild(_fx); 34 } 35 }catch(e){} 36 37 var AUDIO_BASE = '/assets/audio/'; 38 var MUTE_KEY = 'wa_msg_muted'; // partagé avec wahadici.html (même origine) 39 var ICE = [ 40 { urls:'stun:stun.l.google.com:19302' }, 41 { urls:'turn:84.201.14.212:3478', username:'wanzani', credential:'e10fdacd1820c42b64ee30f5893287b1' } 42 ]; 43 44 var D = document; 45 function sb(){ return (window.Wanzani && window.Wanzani.client) || null; } 46 function log(){ try{ console.log.apply(console, ['[wahadici-embed]'].concat([].slice.call(arguments))); }catch(e){} } 47 48 var ME = { id:null, name:'' }; 49 50 /* âââ Sons (chargés à la demande, respect du réglage silencieux) âââââââââââ */ 51 function muted(){ try{ return localStorage.getItem(MUTE_KEY)==='1'; }catch(e){ return false; } } 52 var _audio = {}; 53 function snd(name, loop, vol){ 54 if(!_audio[name]){ 55 var a = new Audio(AUDIO_BASE + name + '.mp3'); 56 a.preload='none'; a.loop=!!loop; a.volume=(vol==null?1:vol); 57 try{ a.setAttribute('playsinline',''); }catch(e){} 58 _audio[name]=a; 59 } 60 return _audio[name]; 61 } 62 var curRing=null, unlocked=false, lastNotif=0; 63 function unlockAudio(){ 64 if(unlocked) return; unlocked=true; 65 ['notif','audio_call','video_call','outcall_open','busy_call','line_busy'].forEach(function(n){
66 try{ var a=snd(n); a.muted=true; var p=a.play(); if(p&&p.then){ p.then(function(){ a.pause(); try{a.currentTime=0;}catch(e){} a.muted=false; }).catch(function(){ a.muted=false; }); } }catch(e){} 67 }); 68 } 69 ['pointerdown','touchstart','click','keydown'].forEach(function(ev){ window.addEventListener(ev, unlockAudio, {once:true, passive:true, capture:true}); }); 70 function ring(name){ stopRing(); var a=snd(name,true,1); curRing=a; try{a.currentTime=0;}catch(e){} var p=a.play(); if(p&&p.catch)p.catch(function(){}); } 71 function stopRing(){ if(curRing){ try{curRing.pause(); curRing.currentTime=0;}catch(e){} curRing=null; } } 72 function oneShot(name){ stopRing(); var a=snd(name,false,1); try{a.currentTime=0;}catch(e){} var p=a.play(); if(p&&p.catch)p.catch(function(){}); } 73 function notify(){ if(muted()) return; var t=Date.now(); if(t-lastNotif<500) return; lastNotif=t; var a=snd('notif',false,.75); try{a.currentTime=0;}catch(e){} var p=a.play(); if(p&&p.catch)p.catch(function(){}); } 74 75 /* âââ Notification navigateur + titre clignotant (appel remarquable même en arrière-plan) âââ */ 76 var _title=null, _flash=null; 77 function startFlash(txt){ if(_flash) return; _title=D.title; var on=false; _flash=setInterval(function(){ D.title = (on?_title:('ð '+txt)); on=!on; }, 900); } 78 function stopFlash(){ if(_flash){ clearInterval(_flash); _flash=null; if(_title!=null) D.title=_title; } } 79 function osNotify(title, body){ 80 try{ 81 if(!('Notification' in window)) return; 82 if(Notification.permission==='granted'){ var n=new Notification(title,{body:body||'',tag:'wahadici-call'}); n.onclick=function(){ try{window.focus();}catch(e){} n.close(); }; } 83 else if(Notification.permission!=='denied'){ Notification.requestPermission(); } 84 }catch(e){} 85 } 86 87 /* âââ UI : écran d'appel + appel entrant (auto-contenu) âââââââââââââââââââ */ 88 var SVG = { 89 phone:'<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2A19.79 19.79 0 0 1 11.4 19a19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 3.12 4.18 2 2 0 0 1 5.09 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L9.91 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>', 90 hang:'<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:rotate(135deg)"><path d="M22 16.92v3a2 2 0 0 1-2.18 2A19.79 19.79 0 0 1 11.4 19a19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 3.12 4.18 2 2 0 0 1 5.09 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L9.91 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></svg>', 91 mic:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>', 92 cam:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>', 93 flip:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>', 94 spk:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>', 95 full:'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>' 96 }; 97 function injectCSS(){ 98 if(D.getElementById('wembed-css')) return; 99 var s=D.createElement('style'); s.id='wembed-css'; 100 s.textContent='' 101 +'#wembed-call{position:fixed;inset:0;z-index:2147483000;display:none;background:#07141b;color:#fff;font-family:inherit;overflow:hidden}' 102 +'#wembed-call.on{display:block}' 103 +'#wembed-bg{position:absolute;inset:0;background:radial-gradient(120% 90% at 50% 0%,#12414a 0%,#07141b 62%);z-index:0}' 104 +'#wembed-remote{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#07141b;display:none;z-index:1}' 105 +'#wembed-local{position:absolute;right:16px;bottom:132px;width:112px;height:154px;object-fit:cover;border-radius:16px;border:2px solid rgba(255,255,255,.5);background:#000;display:none;z-index:50;cursor:grab;box-shadow:0 8px 26px rgba(0,0,0,.45);touch-action:none}' 106 +'#wembed-local.drag{cursor:grabbing;transform:scale(1.03)}' 107 +'#wembed-meta{position:absolute;
107left:0;right:0;top:30px;text-align:center;z-index:20;text-shadow:0 2px 12px rgba(0,0,0,.75);pointer-events:none}' 108 +'#wembed-meta .nm{font-size:23px;font-weight:900;letter-spacing:.2px}' 109 +'#wembed-meta .st{font-size:13px;opacity:.85;margin-top:5px}' 110 +'#wembed-meta .tm{font-size:15px;margin-top:3px;font-variant-numeric:tabular-nums;opacity:.95}' 111 +'#wembed-ava{position:absolute;left:50%;top:40%;transform:translate(-50%,-50%);width:132px;height:132px;border-radius:50%;background:linear-gradient(140deg,#0d9488,#0f766e);display:flex;align-items:center;justify-content:center;font-size:50px;font-weight:900;z-index:10;box-shadow:0 0 0 10px rgba(13,148,136,.18),0 0 0 22px rgba(13,148,136,.09)}' 112 +'#wembed-ctrls{position:absolute;left:50%;bottom:26px;transform:translateX(-50%);display:flex;gap:13px;z-index:70;padding:10px 14px;background:rgba(0,0,0,.34);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:38px}' 113 +'.wembed-btn{width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,.17);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.15s;flex-shrink:0}' 114 +'.wembed-btn:hover{background:rgba(255,255,255,.3);transform:translateY(-2px)}' 115 +'.wembed-btn.off{background:rgba(255,255,255,.9);color:#111}' 116 +'.wembed-btn.off svg{stroke:#111}' 117 +'.wembed-btn.end{background:#ef4444}.wembed-btn.end:hover{background:#dc2626}' 118 +'@media(max-width:640px){#wembed-ctrls{gap:9px;bottom:18px;padding:8px 10px}.wembed-btn{width:50px;height:50px}#wembed-local{width:92px;height:126px;bottom:112px;right:12px}#wembed-meta .nm{font-size:20px}}' 119 +'#wembed-inc{position:fixed;inset:0;z-index:2147483001;display:none;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(160deg,#0d9488,#0f766e);color:#fff;text-align:center;font-family:inherit}' 120 +'#wembed-inc.on{display:flex}' 121 +'#wembed-inc .ava{width:104px;height:104px;border-radius:50%;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;font-size:44px;font-weight:900;margin-bottom:18px}' 122 +'.wembed-ic{width:66px;height:66px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer}'; 123 (D.head||D.documentElement).appendChild(s); 124 } 125 function buildUI(){ 126 injectCSS(); 127 if(!D.getElementById('wembed-call')){ 128 var c=D.createElement('div'); c.id='wembed-call'; 129 c.innerHTML='' 130 +'<div id="wembed-bg"></div>' 131 +'<video id="wembed-remote" autoplay playsinline></video>' 132 +'<video id="wembed-local" autoplay playsinline muted></video>' 133 +'<div id="wembed-ava">?</div>' 134 +'<div id="wembed-meta"><div class="nm" id="wembed-name"></div><div class="st" id="wembed-status">Connexionâ¦</div><div class="tm" id="wembed-timer"></div></div>' 135 +'<div id="wembed-ctrls">' 136 +'<div class="wembed-btn" id="wembed-mute" title="Muet">'+SVG.mic+'</div>' 137 +'<div class="wembed-btn" id="wembed-cam" title="Caméra">'+SVG.cam+'</div>' 138 +'<div class="wembed-btn" id="wembed-flip" title="Basculer caméra">'+SVG.flip+'</div>' 139 +'<div class="wembed-btn" id="wembed-spk" title="Haut-parleur">'+SVG.spk+'</div>' 140 +'<div class="wembed-btn" id="wembed-full" title="Plein écran">'+SVG.full+'</div>' 141 +'<div class="wembed-btn end" id="wembed-hang" title="Raccrocher">'+SVG.hang+'</div>' 142 +'</div>'; 143 D.body.appendChild(c); 144 D.getElementById('wembed-mute').onclick=toggleMute; 145 D.getElementById('wembed-cam').onclick=toggleCam; 146 D.getElementById('wembed-flip').onclick=flipCamera; 147 D.getElementById('wembed-hang').onclick=function(){ hangup(true); }; 148 /* WZ v7 : haut-parleur (si le navigateur le permet), plein écran, vignette déplaçable */ 149 var spk=D.getElementById('wembed-spk'); 150 if(spk){ 151 if(!(window.HTMLMediaElement && HTMLMediaElement.prototype.setSinkId)) spk.style.display='none'; 152 else spk.onclick=toggleSpeaker; 153 } 154 var fu=D.getElementById('wembed-full'); if(fu) fu.onclick=toggleFull; 155 makeDraggable(D.getElementById('wembed-local')); 156 } 157 if(!D.getElementById('wembed-inc')){ 158 var i=D.createElement('div'); i.id='wembed-inc'; 159 i.innerHTML='<div class="ava" id="wembed-inc-ava">?</div>' 160 +'<div style="font-size:23px;font-weight:800" id="wembed-inc-name"></div>' 161 +'<div style="opacity:.85;margin-top:6px;font-size:14px" id="wembed-inc-sub"></div>' 162 +'<div style="display:flex;gap:52px;margin-top:52px">' 163 +'<div id="wembed-inc-no"><div class="wembed-ic" style="background:#ef4444">'+SVG.hang+'</div><div style="margin-top:8px;font-size:12px">Refuser</div></div>' 164 +'<div id="wembed-inc-yes"><div class="wembed-ic" style="background:#22c55e">'+SVG.phone+'</div><div style="margin-top:8px;font-size:12px">Accepter</div></div>' 165 +'</div>'; 166 D.body.appendChild(i); 167 D.getElementById('wembed-inc-yes').onclick=accept; 168 D.getElementById('wembed-inc-no').onclick=declineIncoming; 169 } 170 } 171 function G(id){ return D.getElementById(id); } 172 173 /* âââ Ãtat appel + WebRTC âââââââââââââââââââââââââââââââââââââââââââââââââ */ 174 var pc=null, stream=null, ringCh=null, room=null, cur=null, incoming=null, pend=[], tmr=null, sec=0, ringTO=null, facing='user', connected=false; 175 176 /* âââ WZ v5 â JOURNAL DES APPELS (table public.calls) âââ 177 Les deux telephones decrivent le meme appel : on ecrit en upsert sur 178 call_ref, l'index unique garantit UNE seule ligne par appel. */ 179 var CALLS_OK=null, CALLS_ERR=''; 180 function callsDone(){ CALLS_OK=true; try{ window.dispatchEvent(new CustomEvent('wz-call-logged')); }catch(e){} } 181 /* WZ v6 : l'upsert peut echouer selon la forme de l'index unique en base 182 (PostgreSQL refuse ON CONFLICT sur un index partiel). On retombe alors 183 proprement sur un UPDATE puis, si la ligne n'existe pas, un INSERT. */ 184 async function logCallRaw(payload){ 185 if(CALLS_OK===false || !payload || !payload.call_ref) return; 186 var c=sb(); if(!c) return; 187 try{ 188 var r=await c.from('calls').upsert(payload,{onConflict:'call_ref'}); 189 if(r && !r.error){ callsDone(); return; } 190 CALLS_ERR=(r&&r.error&&(r.error.message||r.error.code))||'upsert refuse'; 191 log('upsert refuse ('+CALLS_ERR+') â repli update/insert'); 192 }catch(e){ CALLS_ERR=String(e&&e.message||e); } 193 try{ 194 var up=await c.from('calls').update(payload).eq('call_ref',payload.call_ref).select('id'); 195 if(up && !up.error && up.data && up.data.length){ callsDone(); return; } 196 var ins=await c.from('calls').insert(payload); 197 if(ins && !ins.error){ callsDone(); return; } 198 CALLS_ERR=(ins&&ins.error&&(ins.error.message||ins.error.code))||CALLS_ERR; 199 CALLS_OK=false; 200 log('journal des appels indisponible :', CALLS_ERR, 'â lancer wahadici-appels.sql (ou wahadici-appels-fix.sql)'); 201 }catch(e){ CALLS_OK=false; CALLS_ERR=String(e&&e.message||e); } 202 } 203 function logCur(fields){ 204 if(!cur || !ME.id) return; 205 var p={ call_ref:cur.callId, mode:cur.mode||'audio', 206 caller_id:(cur.role==='caller'?ME.id:cur.peer), 207 callee_id:(cur.role==='caller'?cur.peer:ME.id) }; 208 if(cur.convId) p.conv_id=cur.convId; 209 if(fields){ for(var k in fields){ p[k]=fields[k]; } } 210 logCallRaw(p); 211 } 212 function nowISO(){ return new Date().toISOString(); } 213 214 function showCall(name, mode, statusTxt){ 215 buildUI(); 216 G('wembed-name').textContent=name||''; 217 G('wembed-status').textContent=statusTxt||'Connexionâ¦'; 218 G('wembed-timer').textContent='';
219 G('wembed-ava').textContent=(name||'?').charAt(0).toUpperCase(); 220 var vid=(mode==='video'); 221 G('wembed-remote').style.display=vid?'block':'none'; 222 G('wembed-local').style.display=vid?'block':'none'; 223 G('wembed-ava').style.display=vid?'none':'flex'; 224 G('wembed-cam').style.display=vid?'flex':'none'; 225 G('wembed-flip').style.display=vid?'flex':'none'; 226 G('wembed-call').classList.add('on'); 227 try{ if(window.WZFX){ WZFX.bind(fxBridge()); WZFX.open(mode); } }catch(e){} /* WZ v7 */ 228 } 229 /* âââ WZ v7 : confort de l'écran d'appel ââââââââââââââââââââââââââââââââ */ 230 var spkOn=false; 231 async function toggleSpeaker(){ 232 var r=G('wembed-remote'), b=G('wembed-spk'); if(!r||!r.setSinkId) return; 233 try{ 234 var devs=await navigator.mediaDevices.enumerateDevices(); 235 var outs=devs.filter(function(d){ return d.kind==='audiooutput'; }); 236 if(!outs.length) return; 237 spkOn=!spkOn; 238 var pick=spkOn ? (outs.find(function(d){ return /speaker|haut/i.test(d.label); })||outs[outs.length-1]) : outs[0]; 239 await r.setSinkId(pick.deviceId); 240 if(b) b.classList.toggle('off', spkOn); 241 log('sortie audio :', pick.label||pick.deviceId); 242 }catch(e){ log('haut-parleur indisponible', e&&e.message); } 243 } 244 function toggleFull(){ 245 var c=G('wembed-call'); if(!c) return; 246 try{ 247 if(D.fullscreenElement||D.webkitFullscreenElement){ (D.exitFullscreen||D.webkitExitFullscreen).call(D); } 248 else { (c.requestFullscreen||c.webkitRequestFullscreen).call(c); } 249 }catch(e){} 250 } 251 function makeDraggable(elm){ 252 if(!elm || elm.__drag) return; elm.__drag=true; 253 var sx=0, sy=0, ox=0, oy=0, on=false; 254 function pt(ev){ var t=(ev.touches&&ev.touches[0])||ev; return {x:t.clientX||0,y:t.clientY||0}; } 255 function down(ev){ 256 var p=pt(ev), r=elm.getBoundingClientRect(); 257 on=true; sx=p.x; sy=p.y; ox=r.left; oy=r.top; 258 elm.classList.add('drag'); 259 elm.style.right='auto'; elm.style.bottom='auto'; elm.style.left=ox+'px'; elm.style.top=oy+'px'; 260 } 261 function move(ev){ 262 if(!on) return; 263 var p=pt(ev); 264 var nx=Math.max(6, Math.min(window.innerWidth -elm.offsetWidth -6, ox+(p.x-sx))); 265 var ny=Math.max(6, Math.min(window.innerHeight-elm.offsetHeight-6, oy+(p.y-sy))); 266 elm.style.left=nx+'px'; elm.style.top=ny+'px'; 267 if(ev.cancelable) try{ ev.preventDefault(); }catch(e){} 268 } 269 function up(){ on=false; elm.classList.remove('drag'); } 270 elm.addEventListener('mousedown',down); window.addEventListener('mousemove',move); window.addEventListener('mouseup',up); 271 elm.addEventListener('touchstart',down,{passive:true}); window.addEventListener('touchmove',move,{passive:false}); window.addEventListener('touchend',up,{passive:true}); 272 } 273 /* pont vers le module d'animations/partage (wahadici-call-fx.js) */ 274 function fxBridge(){ 275 return { 276 send:function(o){ o=o||{}; o.kind='fx'; roomSend(o); }, 277 root:G('wembed-call'), remoteEl:G('wembed-remote'), localEl:G('wembed-local'), 278 getStream:function(){ return stream; }, 279 getPC:function(){ return pc; }, 280 setLocalPreview:function(s){ var l=G('wembed-local'); if(l){ l.srcObject=s; try{ var q=l.play(); if(q&&q.catch)q.catch(function(){}); }catch(e){} } }, 281 convId:function(){ return (cur&&cur.convId)||null; }, 282 uid:function(){ return ME.id; }, 283 toast:function(m){ try{ if(typeof showToast==='function') showToast(m); }catch(e){} } 284 }; 285 } 286 function hideCall(){ try{ if(window.WZFX) WZFX.close(); }catch(e){} /* WZ v7 */ 287 var c=G('wembed-call'); if(c){ c.classList.remove('on'); } var r=G('wembed-remote'),l=G('wembed-local'); if(r)r.srcObject=null; if(l)l.srcObject=null; } 288 function startTimer(){ stopRing(); connected=true; sec=0; if(G('wembed-status'))G('wembed-status').textContent='Connecté'; 289 logCur({ status:'answered', answered_at:nowISO() }); /* WZ v5 */ 290 clearInterval(tmr); tmr=setInterval(function(){ sec++; var m=Math.floor(sec/60),s=sec%60; if(G('wembed-timer'))G('wembed-timer').textContent=String(m).padStart(2,'0')+':'+String(s).padStart(2,'0'); },1000); } 291 292 function newPC(){ 293 var p=new RTCPeerConnection({iceServers:ICE}); 294 p.onicecandidate=function(e){ if(e.candidate) roomSend({kind:'ice',candidate:e.candidate}); }; 295 p.ontrack=function(e){ var r=G('wembed-remote'); if(r){ r.srcObject=e.streams[0]; if(r.play)r.play().catch(function(){}); } }; 296 p.onconnectionstatechange=function(){ if(p.connectionState==='c
296onnected') startTimer(); if(p.connectionState==='failed'){ hangup(true); } }; 297 if(stream) stream.getTracks().forEach(function(tr){ p.addTrack(tr, stream); }); 298 return p; 299 } 300 function getMedia(mode){ if(stream) return Promise.resolve(stream); 301 return navigator.mediaDevices.getUserMedia({ audio:true, video: mode==='video'?{facingMode:'user'}:false }).then(function(s){ stream=s; facing='user'; var lv=G('wembed-local'); if(lv)lv.srcObject=s; return s; }); } 302 function roomSend(p){ if(room) room.send({type:'broadcast',event:'signal',payload:p}); } 303 function ringSend(uid,ev,p){ var c=sb().channel('wa-ring-'+uid,{config:{broadcast:{self:false}}}); c.subscribe(function(st){ if(st==='SUBSCRIBED'){ c.send({type:'broadcast',event:ev,payload:p}); setTimeout(function(){ try{sb().removeChannel(c);}catch(e){} },900); } }); } 304 function joinRoom(callId, cb){ room=sb().channel('wa-call-'+callId,{config:{broadcast:{self:false}}}); room.on('broadcast',{event:'signal'},function(m){ onRoom(m.payload); }); room.subscribe(function(st){ if(st==='SUBSCRIBED'&&cb) cb(); }); } 305 function drain(){ return Promise.resolve().then(async function(){ while(pend.length){ try{ await pc.addIceCandidate(pend.shift()); }catch(e){} } }); } 306 307 async function onRoom(p){ 308 try{ 309 if(p.kind==='ready'){ if(cur&&cur.role==='caller'&&pc&&pc.signalingState==='stable'){ var off=await pc.createOffer({offerToReceiveAudio:true,offerToReceiveVideo:true}); await pc.setLocalDescription(off); roomSend({kind:'offer',sdp:off}); } } 310 else if(p.kind==='offer'){ if(cur&&cur.role==='callee'){ if(!pc)pc=newPC(); await pc.setRemoteDescription(p.sdp); await drain(); var ans=await pc.createAnswer(); await pc.setLocalDescription(ans); roomSend({kind:'answer',sdp:ans}); } } 311 else if(p.kind==='answer'){ if(pc){ await pc.setRemoteDescription(p.sdp); await drain(); } } 312 else if(p.kind==='ice'){ if(pc&&pc.remoteDescription){ try{ await pc.addIceCandidate(p.candidate); }catch(e){} } else pend.push(p.candidate); } 313 else if(p.kind==='fx'){ try{ if(window.WZFX) WZFX.recv(p); }catch(e){} } /* WZ v7 */ 314 else if(p.kind==='bye'){ hangup(false); } 315 }catch(e){ console.error('[wahadici-embed] onRoom', e); } 316 } 317 318 /* WZ-FIX v4 : identite resolue a la demande (plus de dependance a l'instant du boot) */ 319 function setMe(uid){ 320 if(!uid || ME.id===uid) return; 321 ME.id=uid; 322 try{ sb().from('profiles').select('pseudo,prenom,nom').eq('id',uid).single().then(function(r){ var p=(r&&r.data)||{}; ME.name=((p.prenom||'')+' '+(p.nom||'')).trim()||('@'+(p.pseudo||'')); }); }catch(e){} 323 buildUI(); initRing(true); wireInbox(); 324 log('actif pour', uid); 325 try{ window.dispatchEvent(new CustomEvent('wz-calls-ready',{detail:{me:uid}})); }catch(e){} 326 } 327 async function ensureMe(){ 328 if(ME.id) return true; 329 try{ var a=await window.Wanzani.auth.getUser(); if(a && !a.error && a.user){ setMe(a.user.id); return true; } }catch(e){} 330 return false; 331 } 332 async function start(peerUid, mode, peerName, convId){ 333 if(cur){ return; } 334 if(!sb()){ if(typeof showToast==='function') showToast('Serveur indisponible'); return; } 335 if(!ME.id){ 336 var ok=await ensureMe(); 337 if(!ok){ try{ if(typeof openAuthModal==='function'){ openAuthModal('login'); return; } }catch(e){} if(typeof showToast==='function') showToast('Reconnectez-vous pour appeler'); return; } 338 } 339 if(!peerUid){ return; } 340 var callId=ME.id+'.'+Date.now()+'.'+Math.random().toString(36).slice(2,7); 341 cur={callId:callId, peer:peerUid, mode:mode, role:'caller', convId:convId||null}; connected=false; 342 try{ await getMedia(mode); }catch(e){ cur=null; alert('Micro/caméra refusés'); return; } 343 pc=newPC(); 344 showCall(peerName||'Appelâ¦', mode, 'Appel en coursâ¦'); 345 joinRoom(callId, function(){ ringSend(peerUid,'invite',{callId:callId, from:ME.id, fromName:ME.name||'', mode:mode, convId:(convId||null)}); }); 346 logCur({ status:'ringing', started_at:nowISO() }); /* WZ v5 */ 347 ring('outcall_open'); 348 ringTO=setTimeout(function(){ if(cur&&cur.role==='caller'&&(!pc||pc.connectionState!=='connected')){ oneShot('line_busy'); 349 logCur({ status:'missed', ended_at:nowISO(), duration_seconds:0 }); cur._logged=true; /* WZ v5 */ 350 hangup(true); } }, 35000); 351 // résoudre le nom si inconnu 352 if(!peerName && window.Wanzani.profile && window.Wanzani.profile.get){ 353 window.Wanzani.profile.get(peerUid).then(function(r){ var p=r&&r.profile; if(p){ var nm=((p.prenom||'')+' '+(p.nom||'')).trim()||('@'+(p.pseudo||'')); if(G('wembed-name'))G('wembed-name').textContent=nm; if(G('wembed-ava'))G('wembed-ava').textContent=(nm.charAt(0)||'?').toUpperCase(); } }).catch(function(){}); 354 } 355 } 356
357 function onInvite(p){ 358 if(cur){ ringSend(p.from,'reject',{callId:p.callId, reason:'busy'}); return; } 359 incoming=p; buildUI(); 360 try{ var _p={ call_ref:p.callId, caller_id:p.from, callee_id:ME.id, mode:p.mode||'audio', status:'ringing', started_at:nowISO() }; 361 if(p.convId) _p.conv_id=p.convId; logCallRaw(_p); }catch(e){} /* WZ v5 */ 362 var nm=p.fromName||'Appel entrant'; 363 G('wembed-inc-ava').textContent=(nm.charAt(0)||'?').toUpperCase(); 364 G('wembed-inc-name').textContent=nm; 365 G('wembed-inc-sub').textContent=(p.mode==='video'?'Appel vidéo entrantâ¦':'Appel audio entrantâ¦'); 366 G('wembed-inc').classList.add('on'); 367 ring(p.mode==='video'?'video_call':'audio_call'); 368 startFlash('Appel entrant'); 369 osNotify('ð Appel entrant', nm+(p.mode==='video'?' â vidéo':'')); 370 } 371 async function accept(){ 372 if(!incoming) return; var p=incoming; incoming=null; 373 G('wembed-inc').classList.remove('on'); stopRing(); stopFlash(); 374 if(!sb()) return; 375 cur={callId:p.callId, peer:p.from, mode:p.mode, role:'callee', convId:p.convId||null}; connected=false; 376 try{ await getMedia(p.mode); }catch(e){ cur=null; ringSend(p.from,'reject',{callId:p.callId,reason:'media'}); return; } 377 pc=newPC(); 378 showCall(p.fromName||'Appel', p.mode, 'Connexionâ¦'); 379 joinRoom(p.callId, function(){ roomSend({kind:'ready'}); }); 380 } 381 function declineIncoming(){ if(!incoming) return; ringSend(incoming.from,'reject',{callId:incoming.callId}); 382 try{ logCallRaw({ call_ref:incoming.callId, caller_id:incoming.from, callee_id:ME.id, mode:incoming.mode||'audio', status:'declined', ended_at:nowISO(), duration_seconds:0 }); }catch(e){} /* WZ v5 */ G('wembed-inc').classList.remove('on'); incoming=null; stopRing(); stopFlash(); } 383 384 function hangup(tell){ 385 stopRing(); stopFlash(); 386 /* WZ v5 : consigner la fin AVANT de remettre l'etat a zero */ 387 try{ 388 if(cur && !cur._logged){ 389 logCur({ status: connected?'answered':(cur.role==='caller'?'cancelled':'declined'), 390 ended_at:nowISO(), duration_seconds: connected?(sec||0):0 }); 391 cur._logged=true; 392 } 393 }catch(e){} 394 clearTimeout(ringTO); ringTO=null; clearInterval(tmr); tmr=null; 395 if(tell!==false && room) roomSend({kind:'bye'}); 396 if(tell!==false && cur && cur.role==='caller' && (!pc||pc.connectionState!=='connected')) ringSend(cur.peer,'cancel',{callId:cur.callId}); 397 if(pc){ try{pc.close();}catch(e){} pc=null; } 398 if(stream){ stream.getTracks().forEach(function(tr){ try{tr.stop();}catch(e){} }); stream=null; } 399 if(room){ try{ sb().removeChannel(room); }catch(e){} room=null; } 400 pend=[]; connected=false; cur=null; 401 hideCall(); 402 } 403 404 function toggleMute(){ if(!stream) return; var a=stream.getAudioTracks()[0]; if(!a) return; a.enabled=!a.enabled; var b=G('wembed-mute'); if(b)b.style.opacity=a.enabled?1:.45; } 405 function toggleCam(){ if(!stream) return; var v=stream.getVideoTracks()[0]; if(!v) return; v.enabled=!v.enabled; var b=G('wembed-cam'); if(b)b.style.opacity=v.enabled?1:.45; var lv=G('wembed-local'); if(lv)lv.style.opacity=v.enabled?1:.3; } 406 async function flipCamera(){ 407 if(!stream||!pc) return; 408 var nf=(facing==='user')?'environment':'user'; 409 var curT=stream.getVideoTracks()[0]; var curId=(curT&&curT.getSettings)?curT.getSettings().deviceId:null; 410 if(curT){ stream.removeTrack(curT); try{curT.stop();}catch(e){} } 411 function grab(cons){ return navigator.mediaDevices.getUserMedia({video:cons,audio:false}).then(function(s){ return s.getVideoTracks()[0]||null; }).catch(function(){ return null; }); } 412 var nt=await grab({facingMode:{exact:nf}}); 413 if(!nt){ try{ var cams=(await navigator.mediaDevices.enumerateDevices()).filter(function(d){return d.kind==='videoinput';}); var o=cams.filter(function(c){return c.deviceId&&c.deviceId!==curId;}); if(o.length) nt=await grab({deviceId:{exact:o[0].deviceId}}); }catch(e){} } 414 if(!nt) nt=await grab({facingMode:nf}); 415 if(nt) facing=nf; else nt=await grab(true); 416 if(!nt) return; 417 var sender=pc.getSenders&&pc.getSenders().find(function(s){return s.track&&s.track.kind==='video';}); 418 if(sender){ try{ await sender.replaceTrack(nt); }catch(e){} } 419 stream.addTrack(nt); var lv=G('wembed-local'); if(lv)lv.srcObject=stream; 420 } 421 422 /* WZ-FIX v4 : le canal d'appels entrants doit survivre a une session restauree
423 tardivement, a une mise en veille du telephone et a une coupure reseau. */ 424 var ringState='idle', ringWD=null, ringRetry=0; 425 function killRing(){ if(ringCh){ try{ sb().removeChannel(ringCh); }catch(e){} ringCh=null; } ringState='idle'; } 426 function scheduleRingRetry(){ 427 var d=Math.min(30000, 1500*Math.pow(2, Math.min(ringRetry++,4))); 428 setTimeout(function(){ if(ME.id && ringState!=='joined') initRing(true); }, d); 429 } 430 function chState(){ try{ return ringCh && ringCh.state; }catch(e){ return null; } } 431 function startRingWatchdog(){ 432 if(ringWD) return; 433 ringWD=setInterval(function(){ 434 if(!ME.id) return; 435 var st=chState(); 436 if(!ringCh || (st && st!=='joined' && ringState!=='joining')) initRing(true); 437 }, 20000); 438 } 439 function reviveRing(){ if(!ME.id) return; if(!ringCh || chState()!=='joined') initRing(true); } 440 try{ 441 D.addEventListener('visibilitychange', function(){ if(!D.hidden) reviveRing(); }); 442 window.addEventListener('online', reviveRing); 443 window.addEventListener('focus', reviveRing); 444 }catch(e){} 445 function initRing(force){ 446 if(!sb()||!ME.id) return; 447 if(ringCh){ if(!force && (ringState==='joining' || chState()==='joined')) return; killRing(); } 448 ringState='joining'; 449 ringCh=sb().channel('wa-ring-'+ME.id,{config:{broadcast:{self:false}}}); 450 ringCh.on('broadcast',{event:'invite'},function(m){ onInvite(m.payload); }); 451 ringCh.on('broadcast',{event:'reject'},function(m){ if(cur&&m.payload&&m.payload.callId===cur.callId){ oneShot(m.payload.reason==='busy'?'busy_call':'line_busy'); 452 logCur({ status:'declined', ended_at:nowISO(), duration_seconds:0 }); cur._logged=true; /* WZ v5 */ 453 hangup(false); } }); 454 ringCh.on('broadcast',{event:'cancel'},function(m){ if(incoming&&m.payload&&m.payload.callId===incoming.callId){ stopRing(); stopFlash(); G('wembed-inc').classList.remove('on'); incoming=null; } }); 455 ringCh.subscribe(function(st){ 456 if(st==='SUBSCRIBED'){ ringState='joined'; ringRetry=0; log('prêt à recevoir des appels sur wa-ring-'+ME.id); } 457 else if(st==='CHANNEL_ERROR'||st==='TIMED_OUT'||st==='CLOSED'){ ringState='down'; log('canal d\'appels perdu ('+st+') â nouvelle tentative'); scheduleRingRetry(); } 458 }); 459 startRingWatchdog(); 460 } 461 462 /* âââ Brancher les boutons d'appel existants du Messenger âââââââââââââââââ */ 463 function looksUuid(s){ return typeof s==='string' && s.length>=32 && s.indexOf('-')>0; } 464 function resolvePeer(cid){ 465 var raw=String(cid==null?'':cid); 466 var stripped = raw.charAt(0)==='c' ? raw.slice(1) : raw; 467 var cand=[]; 468 try{ if(window.uidCache){ if(uidCache[raw]) cand.push(uidCache[raw]); if(uidCache[stripped]) cand.push(uidCache[stripped]); } }catch(e){} 469 try{ if(window.MS && MS.convs){ var c=MS.convs.find(function(x){ return String(x.id)===raw || String(x.id)===stripped || ('c'+x.uid)===raw; }); if(c && c.uid) cand.push(c.uid); } }catch(e){} 470 if(looksUuid(stripped)) cand.push(stripped); 471 for(var i=0;i<cand.length;i++){ if(looksUuid(cand[i])) return cand[i]; } 472 return null; 473 } 474 function bindStartCall(){ 475 window.startCall=function(cid, mode){ 476 var peer=resolvePeer(cid); 477 log('startCall cid=', cid, 'â peer=', peer); 478 if(!peer){ if(typeof showToast==='function') showToast('Ce contact n\'est pas encore synchronisé avec Wanzani'); else alert('Contact non synchronisé'); return; } 479 start(peer, mode==='video'?'video':'audio'); 480 }; 481 // Appel direct par UUID réel (utilisé par les vraies fenêtres flottantes du site) 482 window.wahadiciCall=function(peerUid, mode, peerName, convId){ 483 log('wahadiciCall peer=', peerUid); 484 if(!peerUid || !looksUuid(peerUid)){ if(typeof showToast==='function') showToast('Contact introuvable pour l\'appel'); return; } 485 start(peerUid, mode==='video'?'video':'audio', peerName, convId); 486 }; 487 } 488 489 /* âââ Notification sonore des messages reçus ââââââââââââââââââââââââââââââ */ 490 function wireInbox(){ 491 if(wireInbox.done) return; wireInbox.done=true; /* WZ-FIX v4 */ 492 try{ 493 if(!(window.Wanzani.messages && window.Wanzani.messages.subscribeToInbox)) return; 494 window.Wanzani.messages.subscribeToInbox(function(m){ 495 try{ if(m && m.author_id && m.author_id!==ME.id) notify(); }catch(e){} 496 }); 497 }catch(e){} 498 } 499 500 /* âââ Démarrage âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 501 /* WZ-FIX v4 : AVANT, si la session Supabase n'etait pas encore restauree au 502 chargement, boot() sortait definitivement : ni window.wahadiciCall (donc AUCUN 503 bouton d'appel affiche), ni canal wa-ring (donc AUCUN appel entrant recu).
504 Desormais : boutons branches tout de suite, identite reessayee, et 505 re-initialisation automatique des que la session apparait. */ 506 function hookAuth(){ 507 if(hookAuth.done) return; 508 try{ 509 var c=sb(); if(!c || !c.auth || !c.auth.onAuthStateChange) return; 510 hookAuth.done=true; 511 c.auth.onAuthStateChange(function(ev, session){ 512 try{ 513 if(session && session.user){ setMe(session.user.id); } 514 else if(ev==='SIGNED_OUT'){ ME.id=null; killRing(); log('deconnecte â canal d\'appels ferme'); } 515 }catch(e){} 516 }); 517 }catch(e){} 518 } 519 function tryAuth(n){ 520 try{ 521 window.Wanzani.auth.getUser().then(function(a){ 522 if(a && !a.error && a.user){ setMe(a.user.id); return; } 523 if(n<40) setTimeout(function(){ tryAuth(n+1); }, n<10?1000:3000); 524 }).catch(function(){ if(n<40) setTimeout(function(){ tryAuth(n+1); }, 2000); }); 525 }catch(e){ if(n<40) setTimeout(function(){ tryAuth(n+1); }, 2000); } 526 } 527 window.wahadiciCallDiag=function(){ 528 var o={ moi:ME.id||null, nom:ME.name||'', canal:'wa-ring-'+(ME.id||'?'),
529 etatCanal:chState()||ringState, boutonAppelDispo:(typeof window.wahadiciCall==='function'), 530 enAppel:!!cur, 531 journalAppels:(CALLS_OK===true?'OK':(CALLS_OK===false?('ERREUR â '+CALLS_ERR):'pas encore sollicite')) }; 532 try{ var _c=sb(); if(_c) _c.from('calls').select('id').limit(1).then(function(r){ 533 try{ console.log('[wahadici-embed] table calls :', (r&&r.error)?('ERREUR â '+(r.error.message||r.error)):'lisible OK'); }catch(e){} 534 }); }catch(e){} 535 o.verdict = !o.moi ? 'Pas identifie : reconnectez-vous (aucun appel entrant possible).' 536 : (o.etatCanal==='joined' ? 'OK : ce navigateur peut recevoir les appels.' 537 : 'Canal d\'appels non abonne (etat '+o.etatCanal+') : les appels entrants n\'arriveront pas.'); 538 try{ console.log('[wahadici-embed] diag', o); }catch(e){} 539 return o; 540 }; 541 function boot(){ 542 var client=sb(); if(!client||!window.Wanzani.auth){ return setTimeout(boot,400); } 543 bindStartCall(); /* les boutons d'appel existent des maintenant */ 544 hookAuth(); 545 tryAuth(0); 546 } 547 if(D.readyState==='loading') D.addEventListener('DOMContentLoaded', boot); else boot(); 548})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.