DEV Community

Daniel Pertu
Daniel Pertu

Posted on

One banner per search, not one per listing

Notifio watches rental search pages and tells you when a new listing appears. For most of its life it had exactly one way of telling you: an email, sent from our server, arriving in whatever inbox you gave it.

The feature list said "desktop alerts". The code had never sent one. Not a bug exactly, more a promise nobody had cashed.

Cashing it turned out to be about fifty lines of Electron and about five decisions, and the decisions are the interesting part, because every one of them is the difference between a notification people keep and a notification people switch off.

Why the local channel is the one that matters

The value of this app is measured in minutes. A room posted at 14:02 that you reply to at 14:40 is a room where you are the fortieth message in that inbox.

An email has to leave our server, cross a mail provider, get accepted by the recipient's mail server, and wait for their client to poll. Each of those hops is somebody else's scheduled work. A desktop notification, fired from a process already running on your machine, costs zero network round trips.

So in the poll cycle, the local channel goes first:

// The local channel goes first. It costs no network round trip, so the banner
// is on screen before the email has left the building, and on a rental site
// the first few minutes are the whole difference.
const fresh = finds.add(newListings.map(toFind));

if (fresh.length > 0 && config.notifications?.desktop !== false) {
  notifyFinds(site.name, fresh, log);
}
Enter fullscreen mode Exit fullscreen mode

Note what notifyFinds receives: fresh, not newListings. The finds store dedupes, so if the same listing shows up in two searches you watch, it is one banner. The store is the thing that knows what you have already been shown, so the store decides what counts as new to a human rather than new to a scraper.

One banner per search, not one per listing

This is the decision that most affects whether the feature survives contact with a user.

A quiet afternoon produces one new room. A site reindexing its results produces eleven at once. If the rule is one notification per listing, that second case stacks eleven banners on the screen, pushes everything else out of Notification Centre, and earns the app a permanent place in the macOS "allow notifications" deny list.

So a cycle produces at most one notification per search, and the body carries the first three:

const first = finds[0];
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');
Enter fullscreen mode Exit fullscreen mode

f.title || f.url is there because some sites do not give a usable title on the results page. A banner that says "New listing on Kamernet" and nothing else is a banner you have to go and investigate, which defeats the point. A URL is ugly and still tells you the neighbourhood.

Where the click goes

notification.on('click', () => {
  bits.shell.openExternal(first.url).catch(() => {});
});
Enter fullscreen mode Exit fullscreen mode

shell.openExternal, not a window inside the app.

The app has its own browser instance for scraping, and it would be very easy to open the listing in there. It would also be the wrong thing: the user is signed in to that rental site in their real browser, with their saved details, their password manager, and their session. Opening the listing anywhere else means they land on a page asking them to log in, at the exact moment where seconds matter.

Do not make people re-enter your app's world to act on your app's alert. Hand them off to where they were already going to act.

Two Electron properties that are not defaults

const notification = new bits.Notification({
  title,
  body,
  urgency: 'critical',
  timeoutType: 'never',
});
Enter fullscreen mode Exit fullscreen mode

timeoutType: 'never' means the banner stays until it is dismissed. Most notifications should not do this. This one should, because the alert is worthless if it appears and fades while you are making coffee, and the whole scenario this app exists for is "you were not looking at the screen".

urgency: 'critical' is a Linux concept and ignored elsewhere, but it costs nothing to set correctly on the platform where it means something.

Never let the nice-to-have kill the job

Electron is required lazily, inside a try:

function electron(): ElectronBits | null {
  try {
    return require('electron') as ElectronBits;
  } catch {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Two reasons. The monitor can be run headless outside Electron during development, and a top level import would break that. And more importantly, every path in this module returns quietly rather than throwing:

const bits = electron();
if (!bits?.Notification) return;
try {
  if (!bits.Notification.isSupported()) return;
} catch {
  return;
}
Enter fullscreen mode Exit fullscreen mode

A background poll loop is exactly the wrong place to discover that a platform API is missing. The alert email has already been queued by this point. A notification API that is unavailable, unsupported, or throwing must degrade to silence, never to a failed cycle.

The general rule I would apply anywhere: a secondary channel is allowed to fail silently, a primary one is not. Work out which one you are writing before you decide how loudly it should complain.

The setting exists, and defaults to on

config.notifications?.desktop !== false
Enter fullscreen mode Exit fullscreen mode

Written that way on purpose. Absent means on, because a user who has never touched the setting wants the feature that the download page promised them. Only an explicit false turns it off, so the config file can gain the key later without silently disabling anything for everybody who upgraded.

See it working

The app is on notifio.app/download for Mac and Windows. Paste in a rental search URL, leave it in the tray, and the banner is the thing that should reach you before the email does.

If you want the argument about why the local channel wins in more detail, the alerts pages go through it site by site, including what each portal's own notification pipeline has to do before its email reaches you. The short version is on the help page.

Top comments (0)