diff --git a/CLIENTS.md b/CLIENTS.md
index db389c6..f98e99e 100644
--- a/CLIENTS.md
+++ b/CLIENTS.md
@@ -60,8 +60,11 @@ absolute API base if offline-launch or store policy requires it.
tokens are pruned on 404/410/UNREGISTERED. (db `device_tokens`, repo `deviceTokens`,
`push.js`.) *Mobile app still needs the Capacitor push plugin wired + FCM/APNs creds
to deliver end-to-end.*
-- [ ] **Wire the Capacitor push plugin** in the mobile app → register the token via
- `POST /api/v1/devices` on launch; handle taps (deep link to the chat/session).
+- [x] **Capacitor push plugin wired** in the web UI (`setupNativePush` in home.html):
+ inside the app it requests permission, registers, and `POST`s the FCM/APNs token to
+ `/api/v1/devices`; notification taps deep-link via `selectChat`; logout unregisters the
+ token. Web Push is skipped when running natively. Inert in a normal browser. *Activates
+ once the Capacitor Android/iOS app is built and FCM/APNs creds are set.*
- [ ] **Mobile screen capture** for "Share Screen" from a phone (ReplayKit / MediaProjection plugin).
- [ ] **Deep links / universal links** so a session/meeting link opens the app.
diff --git a/server/public/home.html b/server/public/home.html
index 8137bfa..75ff167 100644
--- a/server/public/home.html
+++ b/server/public/home.html
@@ -721,7 +721,7 @@
-
+
Loading…
@@ -1766,7 +1766,7 @@ async function sendMessage(){
// Request permission from a user gesture (e.g. opening a chat) AND subscribe on grant — the
// subscribe-on-grant is essential on iOS, where permission is granted in-session and push
// won't work until a subscription exists.
-function ensureNotifyPermission(){ try{ if('Notification' in window && Notification.permission==='default') Notification.requestPermission().then(r=>{ if(r==='granted'){ try{ subscribePush(); }catch(_){} } }).catch(()=>{}); }catch(_){} }
+function ensureNotifyPermission(){ try{ const p=nativePlatform(); if(p==='ios'||p==='android') return; if('Notification' in window && Notification.permission==='default') Notification.requestPermission().then(r=>{ if(r==='granted'){ try{ subscribePush(); }catch(_){} } }).catch(()=>{}); }catch(_){} }
// Notification preferences (set in Settings; stored per browser). Default ON.
function notifOn(kind){ try{ return localStorage.getItem('notif_'+kind)!=='off'; }catch(_){ return true; } }
// Mobile has no top bar (#7): keep bell+profile in the chat-list header; desktop keeps them in
@@ -1782,6 +1782,7 @@ function placeHdrRight(){
// iOS (permission must be requested from a tap inside the installed PWA).
function maybeNotifPrompt(){
try{
+ const p=nativePlatform(); if(p==='ios'||p==='android') return; // native app handles its own permission flow
if(!('Notification' in window) || Notification.permission!=='default') return;
if(sessionStorage.getItem('notifPromptDismissed')==='1' || document.getElementById('notifPrompt')) return;
const b=document.createElement('div'); b.id='notifPrompt'; b.className='notif-prompt';
@@ -1796,7 +1797,33 @@ function maybeNotifPrompt(){
// pushActive => the page can leave hidden-tab popups to push (avoids double-notifying).
let pushActive=false, _swReg=null;
function urlB64ToUint8(base64){ const pad='='.repeat((4-base64.length%4)%4); const b=(base64+pad).replace(/-/g,'+').replace(/_/g,'/'); const raw=atob(b); const arr=new Uint8Array(raw.length); for(let i=0;i{ const token=t&&t.value; if(!token) return; _nativeTok=token;
+ try{ await postJSON('/api/v1/devices',{ platform:plat, token }); pushActive=true; console.log('[push] native device registered'); }
+ catch(e){ console.warn('[push] device register failed:', e); } });
+ PN.addListener('registrationError', (e)=>console.warn('[push] native registration error:', e));
+ // Tapping an OS notification opens the relevant chat (payload carries kind+id).
+ PN.addListener('pushNotificationActionPerformed', (a)=>{ try{ const d=(a&&a.notification&&a.notification.data)||{}; if(d.id) selectChat(d.kind||'dm', d.id); }catch(_){} });
+ let perm=await PN.checkPermissions();
+ if(perm.receive!=='granted') perm=await PN.requestPermissions();
+ if(perm.receive!=='granted'){ console.log('[push] native permission not granted'); return true; }
+ await PN.register();
+ console.log('[push] native push registered');
+ }catch(e){ console.warn('[push] native push setup failed:', e); }
+ return true; // handled the native path (skip Web Push regardless of outcome)
+}
async function setupPush(){
+ if(await setupNativePush()) return; // native app → FCM/APNs, not Web Push
if(!('serviceWorker' in navigator) || !('PushManager' in window)){ console.warn('[push] not supported by this browser'); return; }
try{ await navigator.serviceWorker.register('/sw.js'); }catch(e){ console.warn('[push] SW register failed:', e); return; }
try{ _swReg=await navigator.serviceWorker.ready; }catch(e){ console.warn('[push] SW never became ready:', e); return; } // ensure an ACTIVE worker before subscribe()
@@ -1819,6 +1846,8 @@ async function subscribePush(){
}
// On logout, remove this device's push subscription so notifications STOP for the logged-out user.
async function unsubscribePush(){
+ // Native app: drop this device's FCM/APNs token so push stops for the logged-out user.
+ try{ if(_nativeTok){ await postJSON('/api/v1/devices/remove',{ token:_nativeTok }); _nativeTok=null; pushActive=false; return; } }catch(_){}
try{
if(!_swReg){ try{ _swReg=await navigator.serviceWorker.getRegistration(); }catch(_){} }
if(!_swReg) return;