Leave worker notifications out of the dashboard entirely

This commit is contained in:
Nariman Jelveh
2026-08-27 15:59:25 -07:00
parent 1d3963539f
commit 56b4ef7f2b
3 changed files with 20 additions and 40 deletions
@@ -31,7 +31,6 @@ import {
planBurstToasts,
reconcileWithServer,
setReadAt,
shouldToast,
titleWithBadge,
toEntry,
unreadCount,
@@ -71,7 +70,6 @@ const checkAllIcon = `<svg ${STROKE_ICON} stroke-width="2"><path d="M2 13l4 4L14
/** The glyph in front of an entry, by who sent it. */
const GLYPHS = {
sharing: `<svg ${STROKE_ICON}><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4"/></svg>`,
worker: `<svg ${STROKE_ICON}><path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/></svg>`,
default: bellIcon,
};
@@ -515,15 +513,11 @@ export default function UIDashboardNotifications ({ $el_window, socket }) {
for ( const entry of fresh ) surfaced.add(entry.uid);
for ( const entry of result.added ) justAdded.add(entry.uid);
render();
// Quiet arrivals only update the badge and the list; the toast and
// its spoken counterpart stay silent alike.
const loud = fresh.filter((entry) => shouldToast(entry.notification));
announce(loud);
announce(fresh);
// Open, the panel is already showing them.
if ( isOpen ) return;
const { shown, folded } = planBurstToasts(loud);
const { shown, folded } = planBurstToasts(fresh);
for ( const entry of [...shown].reverse() ) toast(entry);
if ( folded > 0 ) toastSummary(folded);
};
+11 -17
View File
@@ -45,6 +45,12 @@
/** Notifications announcing a share; the two templates the backend writes. */
const SHARE_TEMPLATES = new Set(['file-shared-with-you', 'file-shared-before-you-joined']);
/**
* Senders the notification center leaves out entirely a worker's deploy
* confirmations are noise next to things that need the user.
*/
const HIDDEN_SOURCES = new Set(['worker']);
// SQLite's CURRENT_TIMESTAMP: UTC with no zone marker, which `Date.parse`
// would otherwise read as local time.
const SQL_TIMESTAMP = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
@@ -81,7 +87,8 @@ export const parseCreatedAt = (value) => {
* (`{ uid, value, created_at, acknowledged }`) or a socket item (`{ uid,
* notification, created_at? }`). A push carries no `acknowledged` and is
* unread the server only pushes what the user hasn't dismissed. `null`
* for anything without a uid there is nothing to acknowledge it by.
* for anything without a uid there is nothing to acknowledge it by and
* for senders the center doesn't show.
*
* @param {Object} raw
* @param {number} now - Epoch ms; stands in for `created_at` when absent
@@ -92,6 +99,7 @@ export const toEntry = (raw, now) => {
if ( typeof uid !== 'string' || uid === '' ) return null;
const payload = raw.notification ?? raw.value;
const notification = payload && typeof payload === 'object' ? payload : {};
if ( HIDDEN_SOURCES.has(notification.source) ) return null;
const createdAt = parseCreatedAt(raw.created_at);
return {
uid,
@@ -202,28 +210,14 @@ export const isUnread = (entry) => entry.readAt === null;
export const unreadCount = (entries) => entries.filter(isUnread).length;
/** The senders that have a glyph of their own; anything else gets the bell. */
const GLYPH_SOURCES = new Set(['sharing', 'worker']);
/**
* Senders whose notifications go straight to the panel and badge without a
* toast a worker finishing a deploy is routine, not an interruption.
*/
const QUIET_SOURCES = new Set(['worker']);
/**
* Whether an arrival should interrupt with a toast, or just wait in the panel.
*
* @param {NotificationPayload} notification
* @returns {boolean}
*/
export const shouldToast = (notification) => ! QUIET_SOURCES.has(notification?.source);
const GLYPH_SOURCES = new Set(['sharing']);
/**
* Which glyph an entry shows, by who sent it. A plain property lookup
* would let a `source` like `constructor` reach into the prototype.
*
* @param {NotificationPayload} notification
* @returns {'sharing'|'worker'|'default'}
* @returns {'sharing'|'default'}
*/
export const glyphKey = (notification) => (
GLYPH_SOURCES.has(notification?.source) ? notification.source : 'default'
@@ -30,7 +30,6 @@ import {
planBurstToasts,
reconcileWithServer,
setReadAt,
shouldToast,
sortEntries,
titleWithBadge,
toEntry,
@@ -123,6 +122,13 @@ describe('toEntry', () => {
expect(toEntry(null, NOW)).toBeNull();
expect(toEntry(undefined, NOW)).toBeNull();
});
it('leaves out worker notifications, listed or pushed', () => {
const worker = { title: 'Successfully deployed https://x.puter.work', source: 'worker', template: 'user-requesting-share' };
expect(toEntry({ uid: 'a', value: worker, created_at: '2026-08-27 12:00:00' }, NOW)).toBeNull();
expect(toEntry({ uid: 'b', notification: worker }, NOW)).toBeNull();
expect(toEntry({ uid: 'c', notification: { ...worker, source: 'sharing' } }, NOW)).not.toBeNull();
});
});
describe('sortEntries', () => {
@@ -289,7 +295,6 @@ describe('notificationTarget', () => {
describe('glyphKey', () => {
it('names the glyph for known senders', () => {
expect(glyphKey({ source: 'sharing' })).toBe('sharing');
expect(glyphKey({ source: 'worker' })).toBe('worker');
});
it('falls back to the bell for anything else', () => {
@@ -402,16 +407,3 @@ describe('planBurstToasts', () => {
expect(folded).toBe(9);
});
});
describe('shouldToast', () => {
it('keeps worker notifications out of the toasts', () => {
expect(shouldToast({ source: 'worker', title: 'Successfully deployed https://x.puter.work' })).toBe(false);
});
it('toasts everything else', () => {
expect(shouldToast({ source: 'sharing' })).toBe(true);
expect(shouldToast({ source: 'billing' })).toBe(true);
expect(shouldToast({})).toBe(true);
expect(shouldToast(null)).toBe(true);
});
});