aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js90
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/i18n.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css3
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; }