Notifio watches rental listing sites and tells you the moment something new appears. On a housing site, the gap between "you were told" and "somebody else replied first" is the entire product, which is the subject of its own guide on how fast listings go. Email is the channel people actually read, but it is also the slow one, so there is a native notification too: no network round trip, straight onto the screen.
A notification that appears for four seconds while you are in another room has not told you anything. So the code asks for one that stays:
const notification = new bits.Notification({
title,
body,
urgency: 'critical',
timeoutType: 'never',
});
notification.on('click', () => {
bits.shell.openExternal(first.url).catch(() => {});
});
notification.show();
Two options, and I had assumed they were a pair doing one job. They are not.
What each option actually is
From Electron's Notification docs, with the platform tags they carry:
-
timeoutTypeLinux Windows:'default'or'never'. "IftimeoutTypeis set to'never', the notification never expires. It stays open until closed by the calling API or the user." -
urgencyLinux Windows:'normal','critical'or'low'. The urgency level.
And then the sentence that corrects my mental model, sitting right there in the same page:
On Windows, urgency type 'critical' sorts the notification higher in Action Center (above default priority notifications), but does not prevent auto-dismissal. To prevent auto-dismissal, you should also set
timeoutTypeto'never'.
So urgency is ranking, not persistence. On Windows it affects where the notification sits in Action Center relative to other apps' notifications, and nothing else. The option that keeps it on screen is timeoutType, and only that one. Our code sets both, which happens to be correct, but for a reason I had not understood until I went and read it: they do two different things and we want both.
There is a further wrinkle worth noticing. The option urgency is tagged Linux and Windows, but the instance property notification.urgency further down the same page is tagged Linux only. If you set urgency at construction on Windows it is applied; if you expect to read it back off the object cross-platform, you are relying on something narrower than what you set.
The platform we ship to where neither option exists
Notifio builds for Windows and macOS. Those are the two targets in electron-builder, which is why the download page offers an .exe and two .dmg files and nothing else.
Both of the options above are tagged Linux and Windows. Neither is tagged macOS.
So on half of our installed base, urgency: 'critical' and timeoutType: 'never' are inert. They are not errors, they do not warn, they are simply dropped. Which raises the question of what does control it on macOS, and the answer is: not us.
Apple's Notifications settings documentation lists the per-app Alert Style options, and the style half of it is exactly two choices:
Temporary: Notifications disappear after a while.
Persistent: Notifications remain until you dismiss them.
That is a setting in System Settings, per app, chosen by the person using the computer. There is no API shaped like timeoutType on macOS because the decision is not the app's to make. Apple does expose a notion of critical alerts, and the same page lists "Allow critical alerts" as a per-app toggle, but its example is the Home app with smoke sensors and door locks: a protected class an ordinary Electron app is not going to find itself in, and should not.
Counting it up, across the two platforms we actually ship:
urgency: 'critical' |
timeoutType: 'never' |
|
|---|---|---|
| Windows | ranks higher in Action Center | keeps it on screen |
| macOS | ignored | ignored |
Two options, two platforms, and exactly one cell where persistence is something our code achieved.
Which means the feature cannot depend on it
This is the part that matters more than the option names. If half your users get a notification that vanishes after a few seconds no matter what you pass, then "the notification stays up" is not a mechanism you are allowed to build on. It is a bonus on one platform.
Two things in the code are what make the feature work without it.
The click handler opens the listing in the user's real browser, not in an app window:
notification.on('click', () => {
bits.shell.openExternal(first.url).catch(() => {});
});
shell.openExternal is the whole point. The user is already signed in to the listing site in their own browser with their own session and their own password manager. An app-owned window would land them on a login page, which on a listing that is forty seconds old is the same as not telling them.
And the notification is not the record. There is a recent-finds history in the app and an email, both of which survive a banner you did not see. A missed notification costs you the seconds, not the listing.
The grouping rule, which is not a platform problem
One more decision in the same function, for a different reason:
/**
* One notification per search rather than per listing: five separate banners
* for five rooms in the same search is a notification the user turns off.
*/
const title = finds.length === 1
? `New listing on ${siteName}`
: `${finds.length} new listings on ${siteName}`;
const body = finds.length === 1
? first.title || first.url
: finds.slice(0, 3).map((f) => f.title || f.url).join('\n');
A count in the title, up to three titles in the body, and the click opens the newest. Five banners at once is how you teach somebody to disable your notifications, and a disabled notification is permanently worse than a short one. The || f.url fallbacks are there because a title is the one field that is allowed to be missing, and a banner that says nothing is worse than a banner that says a URL. Notifio watches Dutch sites like Roommatch among others, and a single search going quiet for an hour then returning six rooms at once is an ordinary evening, not an edge case.
And none of it may fail a poll
Everything above is wrapped so that it cannot take down the thing it is reporting on. Electron is required lazily, because the monitor can be run outside Electron during development:
function electron(): ElectronBits | null {
try {
return require('electron') as ElectronBits;
} catch {
return null;
}
}
And isSupported() gets its own try, separate from the constructor's:
const bits = electron();
if (!bits?.Notification) return;
try {
if (!bits.Notification.isSupported()) return;
} catch {
return;
}
That looks paranoid until you notice they are different failures. isSupported() throwing is an environment problem, discovered before any notification exists. new Notification(...) throwing is a problem with this notification, and that one is logged rather than silently swallowed, because it is the only one that indicates a bug in our own arguments.
Takeaways
-
urgencyandtimeoutTypeare not the same feature. On Windows, critical urgency sorts the notification higher and still lets it auto-dismiss. - Read the platform tags on every option, including the difference between the constructor option and the instance property of the same name.
- On macOS, persistent versus temporary is a user setting in System Settings, not an API. Design as if your notification disappears immediately, because for a lot of people it does.
- Open external links with
shell.openExternal. The user's own browser has their session; yours does not. - One notification per event, not per item. The failure mode of over-notifying is permanent.
If you want to see what gets notified about, the alert pages list the sites. And since the slow channel is the one that actually needs help, the help page is mostly a walkthrough of getting Gmail to put the alert email in Primary instead of Promotions, which is the single highest-leverage thing a user can do to this feature.
Top comments (0)