PageSourceSearch

https://wanzani.com/assets/js/wahadici-embed.js?v=1

js wanzani.com collected 2026-09-24 10:24:23 UTC 37,631 bytes, 548 lines download raw bytes

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.