From 1b45b477d624bd382d11ea119d923987d583b073 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:39:04 -0400 Subject: [PATCH] One Chat entry: Signal is listed inside Chat, so the separate Signal tab is gone for people who have both Co-Authored-By: Claude Sonnet 5.5 --- public/app.js | 16 +++++++++------- public/chat-view.js | 3 ++- public/signal-view.js | 2 +- public/sw.js | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/public/app.js b/public/app.js index 15d0b3a..9b502db 100644 --- a/public/app.js +++ b/public/app.js @@ -174,7 +174,9 @@ const TABS = [['home', 'Home'], ['family', 'Family'], ['files', 'Files'], ['musi const TAB_ICON = { home: 'home', mail: 'mail', signal: 'chat', files: 'files', office: 'doc', audio: 'audio', verse: 'book', podcast: 'mic', radio: 'radio', bible: 'bible', recipes: 'chef', contacts: 'people', chat: 'chat', family: 'people', notes: 'doc', chores: 'check', store: 'store', listen: 'audio', reading: 'book', read: 'book', music: 'music', video: 'film', settings: 'cog', calendar: 'cal', search: 'search', tasks: 'check', links: 'link', planner: 'cal' }; /** The menu entries this person sees: Calendar and Tasks share one "Planner" entry, and the listening and reading tabs share "Listen" and "Read" (each part still needs its own permission). */ const leafOf = id => Object.keys(MERGED).find(k => MERGED[k][1].includes(id)); -const navTabs = () => { const out = [], done = new Set(); for (const t of TABS) { if (t[0] === 'verse' || !allowed(t[0])) continue; let id = t[0], label = t[1]; +const sgInChat = () => allowed('chat') && allowed('signal'); // with both, Signal conversations are in the Chat list and there is one entry, not two +const chatUnread = () => (ch.unread || 0) + (sgInChat() ? (sg.unread || 0) : 0); +const navTabs = () => { const out = [], done = new Set(); for (const t of TABS) { if (t[0] === 'verse' || !allowed(t[0]) || (t[0] === 'signal' && sgInChat())) continue; let id = t[0], label = t[1]; if (id === 'tasks' || id === 'calendar') { id = 'planner'; label = 'Planner'; } else if (leafOf(id)) { id = leafOf(id); label = MERGED[id][0]; } if (done.has(id)) continue; done.add(id); out.push([id, label]); } return out; }; /** Every page one by one (what search and the command palette list), with the merged entries split back out. */ @@ -202,13 +204,13 @@ function renderNav(active = activeTab) { if (typeof sgDockBadge === 'function') sgDockBadge(); if (typeof sgBadgeEverywhere === 'function') sgBadgeEverywhere(); const badge = n => n ? `${n > 99 ? '99+' : n}` : ''; - const tab = ([id, label]) => `${icon(TAB_ICON[id])}${label}${id === 'signal' ? badge(sg.unread) : id === 'chat' ? badge(ch.unread) : id === 'mail' ? badge(mailUnread) : ''}`; + const tab = ([id, label]) => `${icon(TAB_ICON[id])}${label}${id === 'signal' ? badge(sg.unread) : id === 'chat' ? badge(chatUnread()) : id === 'mail' ? badge(mailUnread) : ''}`; const brand = `
NE Hub
`; const mine = navTabs(); if (phone.matches) { const pins = navPins(), rest = mine.filter(t => !pins.includes(t[0])), more = !pins.includes(active); $('#nav').innerHTML = pins.map(id => mine.find(t => t[0] === id)).filter(Boolean).map(tab).join('') + - (rest.length ? `${icon('more')}More${rest.some(t => t[0] === 'signal') ? badge(sg.unread) : rest.some(t => t[0] === 'chat') ? badge(ch.unread) : ''}${rest.some(t => t[0] === 'mail') ? badge(mailUnread) : ''}` : ''); + (rest.length ? `${icon('more')}More${rest.some(t => t[0] === 'signal') ? badge(sg.unread) : rest.some(t => t[0] === 'chat') ? badge(chatUnread()) : ''}${rest.some(t => t[0] === 'mail') ? badge(mailUnread) : ''}` : ''); const mo = $('#more'); if (mo) mo.onclick = e => { e.preventDefault(); openLauncher(); }; } else { $('#nav').innerHTML = brand + navGroups(mine).map((g, i) => (i ? `` : '') + g.tabs.map(tab).join('')).join('') + `
`; @@ -219,7 +221,7 @@ phone.addEventListener('change', () => renderNav()); /** The Apps sheet behind "More": every app as a big icon, a search box, the ones you used lately on top, and a way to choose the bottom bar. */ function openLauncher() { - const tabs = navTabs(), badge = id => { const n = id === 'signal' ? sg.unread : id === 'mail' ? mailUnread : id === 'planner' ? (typeof taskDue === 'number' ? taskDue : 0) : 0; return n ? `${n > 99 ? '99+' : n}` : ''; }; + const tabs = navTabs(), badge = id => { const n = id === 'signal' ? sg.unread : id === 'chat' ? chatUnread() : id === 'mail' ? mailUnread : id === 'planner' ? (typeof taskDue === 'number' ? taskDue : 0) : 0; return n ? `${n > 99 ? '99+' : n}` : ''; }; const tile = ([id, label]) => `${icon(TAB_ICON[id] || 'menu')}${badge(id)}${esc(label)}`; const el = openSheet('Apps', '', `
`); @@ -255,7 +257,7 @@ function drawSubnav(name) { try { localStorage.setItem(nk('nav_last_' + g), JSON.stringify(name)); } catch {} el.innerHTML = items.map(id => `${esc(tabLabel(id))}`).join(''); el.hidden = false; } -const navIdOf = name => name === 'edit' ? 'files' : (name === 'tasks' || name === 'calendar') ? 'planner' : leafOf(name) || name; +const navIdOf = name => name === 'signal' && sgInChat() ? 'chat' : name === 'edit' ? 'files' : (name === 'tasks' || name === 'calendar') ? 'planner' : leafOf(name) || name; let curRoute = ''; const routesOf = () => ({ video: viewVideo, planner: viewPlanner, home: ME && ME.simple && typeof viewSimple === 'function' ? viewSimple : viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: args => args[0] === 'classic' ? viewAudio() : args[0] === 'free' ? (allowed('store') ? location.replace('#/store/audiobooks') : viewFreeBooks()) : viewBooks(args), verse: () => location.replace('#/links'), podcast: args => args[0] === 'vbv' ? viewVbv(args.slice(1)) : viewPodcast(args), radio: () => viewRadio(), recipes: args => viewRecipes(args), store: args => viewStore(args), chat: args => viewChat(args), notes: args => viewNotes(args), family: () => viewFamily(), chores: () => viewChores(), contacts: args => viewContacts(args), bible: args => viewBible(args), listen: () => location.replace('#/' + lastSub('listen')), reading: () => location.replace('#/' + lastSub('reading')), read: args => viewReader(args), music: viewMusic, tasks: viewTasks, search: viewSearch, calendar: viewCalendar, signal: viewSignal, links: viewLinks, settings: viewSettings, edit: viewEdit }); @@ -328,7 +330,7 @@ function homeApps() { podcast: () => tile('#/podcast', '#c2417a', 'mic', 'Podcast', 'Podcasts'), chores: () => tile('#/chores', '#2f9e6e', 'check', 'Chores', 'Jobs & points'), notes: () => tile('#/notes', '#e5b800', 'doc', 'Notes', 'Notes & shared lists'), - chat: () => tile('#/chat', '#2aa6a0', 'chat', 'Chat', ch.unread ? ch.unread + ' unread' : 'Family messages'), + chat: () => tile('#/chat', '#2aa6a0', 'chat', 'Chat', chatUnread() ? chatUnread() + ' unread' : sgInChat() ? 'Family and Signal' : 'Family messages'), store: () => tile('#/store', '#d9822b', 'store', 'Store', 'Books, audiobooks, radio & more'), recipes: () => tile('#/recipes', '#e0742b', 'chef', 'Recipes', 'Mealie & shopping list'), contacts: () => tile('#/contacts', '#5b8def', 'people', 'Contacts', 'People & birthdays'), @@ -340,7 +342,7 @@ function homeApps() { }; T.bible = () => tile('#/bible', '#8b5a2b', 'bible', 'Bible', 'Berean Standard Bible'); const HOME_GROUPS = [['Work', ['planner', 'notes', 'chores', 'files', 'office', 'links', 'recipes']], ['Communicate', ['mail', 'contacts', 'chat', 'signal']], ['Listen', ['music', 'radio', 'podcast', 'audio']], ['Read and watch', ['bible', 'read', 'video', 'store']]]; - return HOME_GROUPS.map(([label, ids]) => [label, ids.filter(id => T[id] && allowed(id))]).filter(g => g[1].length).map(([label, ids]) => `

${esc(label)}

${ids.map(id => T[id]()).join('')}
`).join(''); + return HOME_GROUPS.map(([label, ids]) => [label, ids.filter(id => T[id] && allowed(id) && !(id === 'signal' && sgInChat()))]).filter(g => g[1].length).map(([label, ids]) => `

${esc(label)}

${ids.map(id => T[id]()).join('')}
`).join(''); } function viewHome() { diff --git a/public/chat-view.js b/public/chat-view.js index 90ea23d..1b8fa35 100644 --- a/public/chat-view.js +++ b/public/chat-view.js @@ -351,10 +351,11 @@ async function chSettings() { let pr, an = null; try { [pr, an] = await Promise.all([chApi('/prefs'), chApi('/announcement')]); } catch (e) { return toast(e.message); } const q = pr.quiet, admin = ME && ME.role === 'admin'; const el = openSheet('Chat settings', '', `
+ ${typeof allowed === 'function' && allowed('signal') ? `SignalLink or unlink your Signal account, and start a new Signal conversation. Your Signal chats are in this list.Open ›` : ''}
${admin ? `

Family announcement

Shown at the top of everyone's Home. Leave it empty and save to take it down.

` : ''}
`); - el.querySelector('#chsx').onclick = closeSheet; + el.querySelector('#chsx').onclick = closeSheet; const sgl = el.querySelector('#chsig'); if (sgl) sgl.onclick = () => closeSheet(); el.querySelector('#chsf').onsubmit = async e => { e.preventDefault(); try { await chPost('/prefs', { quiet: { on: el.querySelector('#chqo').checked, from: el.querySelector('#chqf').value, to: el.querySelector('#chqt').value } }); if (admin) await chPost('/announcement', { text: el.querySelector('#chan').value }); closeSheet(); toast('Saved'); } catch (x) { el.querySelector('#chse').textContent = x.message; } }; } /** Home: the family announcement, if there is one (each person can dismiss it). */ diff --git a/public/signal-view.js b/public/signal-view.js index c87bbc7..d84bab7 100644 --- a/public/signal-view.js +++ b/public/signal-view.js @@ -57,7 +57,7 @@ function sgEvent(ev) { function viewSignal(args = []) { sg.cur = args[0] || null; sgDockHide(); return sgMount($('#view'), false); } // ... and the same screens in a panel that floats over every other screen. async function sgMount(root, dock) { - sg.root = root; sg.dock = dock; const cls = dock ? 'sgdockin' : 'wrap sgwrap', title = dock ? '' : '

Signal

'; + sg.root = root; sg.dock = dock; const cls = dock ? 'sgdockin' : 'wrap sgwrap', title = dock ? '' : `

Signal

${typeof sgInChat === 'function' && sgInChat() ? `

‹ Back to Chat · your Signal chats are also listed there

` : ''}`; root.innerHTML = `
${title}

Loading…

`; let st; try { st = await sgApi('/status'); } catch (e) { root.innerHTML = `
${title}

${esc(e.message)}

`; return; } if (sg.root !== root) return; sg.st = st; diff --git a/public/sw.js b/public/sw.js index 7b14196..c4e8a51 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v132'; +const CACHE = 'hub-v133'; const KEEP = /^hub-(offline|share|who|data-)/; // caches that outlive a new version const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];