diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 90 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/i18n.js | 1 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 3 |
3 files changed, 71 insertions, 23 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 7b1b5fc..3197c31 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -503,15 +503,22 @@ function RegisterPage() { // ── Home Page ──────────────────────────────────────────────────────────────── -function NotificationFeed({ notifications, onMarkRead }) { +function NotificationFeed({ notifications, onMarkRead, onPurge }) { if (!notifications.length) return null; return html` <div class="notif-feed"> - <h3>${t('notif.title')}</h3> + <h3> + ${t('notif.title')} + <button class="btn-secondary notif-purge" onClick=${onPurge}> + ${t('notif.purge')} + </button> + </h3> ${notifications.map(n => html` <div key=${n.id} class="notif-item ${n.read ? '' : 'notif-unread'}" onClick=${() => { - if (!n.read) onMarkRead(n.id); + // Reading it is the point of clicking it: it goes, here and in the + // count, rather than sitting there greyed out. + onMarkRead(n.id); if (n.link) navigate(n.link); }}> <span class="notif-kind">${n.kind}</span> @@ -523,12 +530,13 @@ function NotificationFeed({ notifications, onMarkRead }) { `; } -function HomePage({ groups, notifications, onMarkRead }) { +function HomePage({ groups, notifications, onMarkRead, onPurge }) { if (groups.length === 0) { return html` <div> <h2>${t('home.welcome')}</h2> - <${NotificationFeed} notifications=${notifications} onMarkRead=${onMarkRead} /> + <${NotificationFeed} notifications=${notifications} + onMarkRead=${onMarkRead} onPurge=${onPurge} /> <p class="page-message"> ${t('home.no_groups')} ${' '}${t('home.browse_prefix')}<a href="#/explore">${t('home.browse_link')}</a>${t('home.browse_suffix')} @@ -540,7 +548,8 @@ function HomePage({ groups, notifications, onMarkRead }) { return html` <div> <h2>${t('home.my_groups')}</h2> - <${NotificationFeed} notifications=${notifications} onMarkRead=${onMarkRead} /> + <${NotificationFeed} notifications=${notifications} + onMarkRead=${onMarkRead} onPurge=${onPurge} /> <div class="group-grid"> ${groups.map(g => html` <a key=${g.id} class="group-card" href="#/group/${g.id}"> @@ -809,7 +818,8 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk return results; } -function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { +function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, + onJoined }) { const [status, setStatus] = useState('idle'); const [entries, setEntries] = useState([]); const [cached, setCached] = useState(false); @@ -924,6 +934,9 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { cacheGroupIndex(groupId, group ? group.name : groupId, synced); }; + // We are in: an invitation to this group has served its purpose. + if (onJoined) onJoined(groupId); + const indexMsg = await transport.fetchIndex(); if (cancelled) return; const freshEntries = indexMsg.entries || []; @@ -2132,10 +2145,9 @@ const THEME_OPTIONS = ['light', 'dark', 'system']; function SettingsPage({ user, theme, onThemeChange, groups, onLogout }) { const [locale, setLoc] = useState(getLocale); - const [muted, setMuted] = useState(() => { - try { return JSON.parse(localStorage.getItem('mb_muted') || '{}'); } - catch { return {}; } - }); + // Comes from the hub with the group list, so it is the same on every device. + const [muted, setMuted] = useState( + () => Object.fromEntries((groups || []).map(g => [g.id, !!g.muted]))); const [nodeKey, setNodeKey] = useState(''); const [currentNodeKey, setCurrentNodeKey] = useState(null); const [nodeKeyStatus, setNodeKeyStatus] = useState(''); @@ -2192,13 +2204,20 @@ function SettingsPage({ user, theme, onThemeChange, groups, onLogout }) { onThemeChange(e.target.value); }, [onThemeChange]); - const toggleMute = useCallback((gid) => { - setMuted(prev => { - const next = { ...prev, [gid]: !prev[gid] }; - localStorage.setItem('mb_muted', JSON.stringify(next)); - return next; - }); - }, []); + const toggleMute = useCallback(async (gid) => { + // Server-side: this used to write to localStorage, which nothing read, so + // muting a group had no effect on anything. The hub now declines to create + // the notification at all. + const next = !muted[gid]; + setMuted(prev => ({ ...prev, [gid]: next })); + try { + await hubFetch(`/v1/groups/${gid}/mute`, { + method: 'POST', token: user.token, body: { muted: next }, + }); + } catch (err) { + setMuted(prev => ({ ...prev, [gid]: !next })); + } + }, [muted, user.token]); const submitNodeKey = useCallback(async () => { const key = nodeKey.trim(); @@ -2782,11 +2801,34 @@ function App() { const markRead = useCallback((id) => { if (!user) return; + // Drop it here and now. Waiting for the round trip leaves it on screen while + // the page navigates, which reads as "the click did nothing". + setNotifications(prev => prev.filter(n => n.id !== id)); + setUnreadCount(c => Math.max(0, c - 1)); hubFetch(`/v1/notifications/${id}/read`, { method: 'POST', token: user.token }) - .then(() => fetchNotifications()) - .catch(() => {}); + .catch(() => fetchNotifications()); + }, [user, fetchNotifications]); + + const purgeNotifications = useCallback(() => { + if (!user) return; + setNotifications([]); + setUnreadCount(0); + hubFetch('/v1/notifications', { method: 'DELETE', token: user.token }) + .catch(() => fetchNotifications()); }, [user, fetchNotifications]); + /** Clear the invitation for a group once its code has actually been redeemed. */ + const dismissGroupNotifications = useCallback((groupId) => { + if (!user) return; + setNotifications(prev => { + const gone = prev.filter(n => n.group_id === groupId && n.kind === 'group_invite'); + gone.forEach(n => hubFetch(`/v1/notifications/${n.id}/read`, + { method: 'POST', token: user.token }).catch(() => {})); + if (gone.length) setUnreadCount(c => Math.max(0, c - gone.length)); + return prev.filter(n => !gone.includes(n)); + }); + }, [user]); + useEffect(() => { setMenuOpen(false); }, [route]); const changeTheme = useCallback((val) => { @@ -2865,16 +2907,18 @@ function App() { page = html`<${GroupPage} groupId=${groupId} group=${group} token=${user.token} username=${user.username} userId=${user.userId} - onRefreshAuth=${refreshAuth} />`; + onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications} />`; } else if (route === '/admin') { page = (user.role === 'moderator' || user.role === 'admin') ? html`<${AdminPage} token=${user.token} />` - : html`<${HomePage} groups=${groups} notifications=${notifications} onMarkRead=${markRead} />`; + : html`<${HomePage} groups=${groups} notifications=${notifications} + onMarkRead=${markRead} onPurge=${purgeNotifications} />`; } else if (route === '/settings') { page = html`<${SettingsPage} user=${user} theme=${theme} onThemeChange=${setTheme} groups=${groups} onLogout=${authCtx.logout} />`; } else { - page = html`<${HomePage} groups=${groups} notifications=${notifications} onMarkRead=${markRead} />`; + page = html`<${HomePage} groups=${groups} notifications=${notifications} + onMarkRead=${markRead} onPurge=${purgeNotifications} />`; } return html` diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index 1b03375..6f27c32 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -280,6 +280,7 @@ const en = { + 'it here. After that this browser is recognised and you will not be asked again.', 'group.join_code_btn': 'Join', + 'notif.purge': 'Clear all', 'notif.title': 'Notifications', 'notif.empty': 'No notifications', 'notif.mark_all_read': 'Mark all read', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index d948dc2..da29027 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -1433,3 +1433,6 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } } .btn-danger:hover { filter: brightness(1.1); } .btn-danger:disabled { opacity: 0.5; cursor: not-allowed; } + +.notif-feed h3 { display: flex; align-items: center; justify-content: space-between; } +.notif-purge { font-size: 0.8em; padding: 4px 10px; } |