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.label ? `${esc(g.label)} ` : ''}
` : '') + g.tabs.map(tab).join('')).join('') + `
${icon('back')}Collapse `;
@@ -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', '', `
${icon('menu')}Edit bottom bar
`);
@@ -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', '', ``);
- 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 = ``;
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'];