unread-badge.mjs 2.2 KB

1234567891011121314151617181920212223242526272829303132333435
  1. /** Fetch the authority in the existing browser session; renderer count input is never trusted. */
  2. export function installUnreadBadge({ ipcMain, app, owner, origins, url, quitting, fetch, interval = 3000 }) {
  3. let account=null,generation=0,poll,pending=false
  4. const badge=count=>{if(app.dock)app.dock.setBadge(count ? String(count) : '')}
  5. const clear=()=>{account=null;generation++;pending=false;clearInterval(poll);poll=undefined;badge(0)}
  6. const trusted=event=>{
  7. try{return !quitting()&&event.sender===owner()?.webContents&&event.senderFrame===owner().webContents.mainFrame&&origins().includes(new URL(event.senderFrame.url).origin)}catch{return false}
  8. }
  9. async function refresh(){
  10. if(!account||pending||quitting())return
  11. const current=generation,identity=account,origin=new URL(url()).origin;pending=true
  12. try{
  13. const options={credentials:'include',redirect:'error',cache:'no-store'}
  14. const auth=await fetch(`${origin}/api/app/bootstrap?csrfOnly=1`,options)
  15. if(!auth.ok)throw new Error('authentication required')
  16. const user=await auth.json()
  17. // auth-disabled local identity is represented as null by csrfOnly.
  18. if((user.userId??'local')!==identity){if(current===generation)clear();return}
  19. const response=await fetch(`${origin}/api/app/unread`,options)
  20. if([401,403].includes(response.status)){if(current===generation)clear();return}
  21. if(!response.ok)return
  22. const value=await response.json()
  23. if(current===generation&&account===identity&&origins().includes(origin)&&Number.isSafeInteger(value.total)&&value.total>=0)badge(value.total)
  24. }catch{ /* Offline preserves the last authenticated count. Navigation/logout clears immediately. */ }
  25. finally{if(current===generation)pending=false}
  26. }
  27. ipcMain.handle('desktop:unread-state',async(event,input)=>{
  28. if(!trusted(event))throw new Error('不允许此页面更新未读状态')
  29. if(input!==null&&(typeof input!=='string'||!input.trim()||input.length>256))throw new Error('无效账号范围')
  30. if(input===null){clear();return}
  31. if(account!==input){clear();account=input;poll=setInterval(()=>void refresh(),interval);poll.unref?.()}
  32. await refresh()
  33. })
  34. return { clear,refresh,stop(){clear();ipcMain.removeHandler('desktop:unread-state')} }
  35. }