Releases: Thuong180702/notihub
Release list
v1.3.6 - Messenger Notifications Actually Say What They're About
🚀 Notihub v1.3.6
Release Title: v1.3.6 - Messenger Notifications Actually Say What They're About
🎯 What this fixes
Messenger banners said "1 new message" with no sender and no text — and two messages only ever produced one banner. If you've seen the menu bar badge stop at 1 no matter how many messages actually arrived, this is why, and it's fixed.
🔍 The bug
Messenger runs its entire messaging stack inside a SharedWorker and raises its notifications from there. Measured directly in the running app:
| Realm | window.Notification |
|---|---|
| Messenger's page | our shim — intercepted |
Messenger's SharedWorker |
function Notification() { [native code] } — never seen |
Every real notification went straight past us. What arrived instead was the fallback: reading the unread count out of the tab title. That title counts unread conversations, not messages — two messages from the same person move the count 0 → 1 exactly once, so the badge froze at 1 and the banner had nothing but a generic count to show.
A SharedWorker also turned out to be the one JS realm nothing in this app could reach. preload.js only ever ran in the page. Electron has no preload for a shared or dedicated worker at all — the closest thing, a service-worker preload, runs in an isolated ShadowRealm (measured: self isn't even defined there), which rules that path out too.
🛠️ How it works now
The DevTools protocol reaches where preloads can't. webContents.debugger auto-attaches to every worker a service creates, with waitForDebuggerOnStart so the shim installs before the site's own script can grab a reference to the native Notification. New file: src/worker-notify.js.
That still leaves the SharedWorker itself unreachable — auto-attach only follows a page's dedicated workers, and a shared worker is a browser-level target. So the preload now hides window.SharedWorker from the page before any site script runs. Messenger's own fallback path takes over, the same messaging bundle loads as a dedicated worker instead, and that one the debugger can attach to. Verified end-to-end: firing a notification from inside that worker now reaches notify() with the real sender name and message body, and two messages in one thread arrive as two separate banners.
New per-service option in config.json, on by default:
"hideSharedWorker": trueSet it to false for a service if hiding SharedWorker ever breaks a site that has no fallback for its absence.
Also fixed: notifications with no icon at all. nativeImage only decodes PNG and JPEG. Messenger's auto-detected favicon is a .ico, and the app had been saving it over the working PNG already in config.json — the icon silently failed to decode and the banner shipped with nothing. Unusable sources are now remembered and skipped in favor of the next candidate (Google's favicon service always re-encodes to PNG). Checked across all eight bundled presets; every one now resolves to a usable image.
Smaller fixes in the same pass:
- A site's notification
iconandtagnow travel through the whole pipeline — a sender's avatar shows where available, and same-thread messages group in Notification Center instead of piling up as separate items (grouped, not deduplicated: a second message no longer risks silently replacing the first). document.hasFocus()is now shimmed alongsidedocument.hidden— a site that gates its own notification logic on focus rather than visibility saw the hidden panel as focused and stayed silent, for the same underlying reason the visibility override already existed.
✅ How this was verified
npm test: 95 reported checks, including a new 15-check suite (test/test-worker-notify.js) for the worker-realm shim in isolation — install guards, title/body/tag/icon relay, ascending ids per notification, click replay, and permission reporting.- Live simulation through the actual realm Messenger uses. Connected to the running app over the DevTools protocol, located
MAWMainV4WebWorkerBundle(Messenger's messaging worker), and fired notifications from inside it — confirmed two messages in one thread arrive as two distinct banners with real sender names and message text, not the generic unread-count line. - A real banner from the packaged v1.3.6 build, captured on screen: sender name, service name, message body, and the Messenger icon all present.
- Confirmed Messenger keeps working with
SharedWorkerhidden — 43 conversations listed, composer present, page otherwise unaffected. - A dead end recorded rather than shipped: a service-worker preload was built and measured to run in a
ShadowRealmwith no access to the worker's own scope, and removed once that was confirmed rather than left in as dead code.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.6-mac.dmg |
Native macOS DMG Installer | a8512642cd71d996e41435786eff05c00978be563e78600337a2670b9b7cb4e9 |
notihub-v1.3.6-mac.zip |
Portable Zip Archive | 6d22f1f3308089a676e28e8da93274541f1e3d39a0cfe811a043771434a98077 |
These are the same values src/updater.js verifies each download against before installing.
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.6-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.6-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading
Use Settings → Download & Install. No bundle identifier or data-directory change, so existing logins stay intact.
If DevTools is open on a service tab after upgrading, that tab falls back to the old title-count notifications until DevTools is closed — webContents.debugger and DevTools can't attach to the same tab at once. --dev no longer opens DevTools automatically for this reason; use the new --devtools flag when you actually want it open.
⚠️ Gatekeeper Notice
Notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse a manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates Notihub installs itself are never quarantined, so this is only needed for a fresh manual install.
v1.3.5 - Google Sign-In Works
🚀 Notihub v1.3.5
Release Title: v1.3.5 - Google Sign-In Works
🎯 What this fixes
Signing in with Google works again. If Gmail — or any service you sign into with a Google account — answered with "Couldn't sign you in / This browser or app may not be secure", this release fixes it.
🔍 It was never the User-Agent
Every previous attempt at this was aimed at the User-Agent. v1.3.1 stripped the app's name from it, v1.3.2 corrected the tokens, and v1.3.3 established the UA was not what sent sign-ins to the browser. All of that was necessary. None of it was sufficient, and this release explains why.
The block turns out to be reproducible without an account at all: it never fires on page load, only on submitting the identifier. That made it testable, so each theory was measured instead of argued:
| Configuration | Result |
|---|---|
| Correct Chrome UA, no Electron token | 🚫 blocked |
…plus Sec-CH-UA client hints matching the UA |
🚫 blocked |
…plus navigator.userAgentData listing "Google Chrome" |
🚫 blocked |
…plus a populated window.chrome |
✅ signs in |
Electron leaves window.chrome an empty object. A real Chrome exposes chrome.app, chrome.runtime, chrome.csi and chrome.loadTimes on it, and that is what Google's sign-in reads. Nothing about the User-Agent was ever going to change that answer.
Re-run to be sure: window.chrome alone is both necessary and sufficient — the client hints contribute nothing and were removed rather than shipped as decoration.
Checked and ruled out along the way:
- The Ferdium bug. The largest app in this position merged a fix for a UA generated as
AppleWebKit/537.36.0instead of537.36. Notihub's UA was already correct. - An outdated Chrome version. Chrome stable is 151; Notihub advertises 150. One major behind is what a great many real users run.
🛠️ What changed
src/chrome-surface.js holds the object, injected into the page's main world before its own scripts run, from two places:
- the sign-in popup, via a new
popup-preload.js— deliberately not the main preload, which shimsNotificationand polls the title; doing either in the window Google is inspecting would add back a difference from a stock Chrome - the service view, because adding Gmail redirects the view itself to
accounts.google.com
✅ How this was verified
- A credential-free reproduction. Submitting any address triggers the verdict, so the whole matrix ran on
test@example.com. No account and no password were involved in finding or fixing this. - End to end, with the shipped files under the real sandbox: without the preload,
window.chromeis empty and Google refuses; with it, the keys are present and sign-in proceeds. - A regression this caught in itself. The first attempt had the preloads
require()the shared module. Sandboxed preloads only resolveelectronand a few builtins, so that threwmodule not foundand killed the entire preload — silently disabling the Notification shim, the app's whole purpose. Each preload now inlines the string, and a test asserts the copies match byte for byte. npm test: 80 reported checks.
⚠️ Honest limits
This works by filling in a gap Electron leaves, not by hiding what the app is — the User-Agent still claims nothing untrue. But the check it satisfies is Google's embedded-webview policy, which exists because an embedded browser can read a password field. Two consequences worth stating plainly:
- This can break again if Google changes what it inspects. The reproduction is credential-free and takes a minute, so diagnosing the next round will be fast.
- The durable answer for a notification hub is not to embed a Google sign-in at all: OAuth through the system browser plus the Gmail API would tell the app about new mail without a password ever meeting an embedded page. That is a larger change, and Google's own guidance offers no desktop equivalent today.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.5-mac.dmg |
Native macOS DMG Installer | 6f020b4e00018c20fe745fd509c3e3019febc98500d7b889a2e2bf70b86128d3 |
notihub-v1.3.5-mac.zip |
Portable Zip Archive | aa8acdb572e146090e5bd7a03d2ddd548acad88bc37f3e2e2af2a5f657c6283f |
These match what GitHub publishes as the digest field on each asset — the same value src/updater.js verifies the download against before installing.
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.5-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.5-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading
Use Settings → Download & Install. No bundle identifier or data-directory change, so existing logins and notification permissions stay intact.
⚠️ Gatekeeper Notice
Notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse a manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates Notihub installs itself are never quarantined, so this is only needed for a fresh manual install.
v1.3.4 - Notification Permission Told Honestly
🚀 Notihub v1.3.4
Release Title: v1.3.4 - Notification Permission Told Honestly
🎯 What this fixes
Setup said "✓ Allowed" whether or not macOS had actually allowed anything. If you ever saw the permission step confirm itself while notifications never arrived, that checkmark was the bug — not a symptom of one.
🔍 The bug
The permission step showed a test notification and then waited 2.5 seconds:
// src/main.js — the old ofa:test-notification handler
setTimeout(() => {
if (!cfg.notificationsOk) patchConfig({ notificationsOk: true });
reply({ ok: true, fallback: true });
}, 2500);Silence was treated as success. When macOS refuses, it doesn't reply — no banner appears and, often, no error event fires either. Nothing came back, the timer expired, and the app wrote down "allowed".
Three different situations produced that identical checkmark:
- Permission genuinely denied — macOS drops the banner silently.
- The system authorization alert was still on screen. 2.5 s routinely expired before you had clicked anything, so the app declared success on a dialog you hadn't answered yet.
- macOS explicitly said no.
patchConfigsat outside the handler'srepliedguard, so even after a realfailedevent the timer still flippednotificationsOktotrue2.5 s later. The UI showed "macOS refused" for a moment, and every visit after that showed "✓ Allowed".
Once set, nothing ever unset it. The one place that learned the truth — a real notification failing — only wrote to console.error:
// src/main.js — the old notify()
n.on('failed', (_e, err) => {
dbg(...); console.error(...); // and that was all
});So turning notifications off in System Settings left the app claiming permission indefinitely.
🛠️ How it works now
The rule is that only macOS gets to say yes, and a timer expiring is not macOS saying anything. The probe is a small state machine in the new src/permissions.js with four outcomes instead of one boolean:
| Status | Meaning | Written to disk |
|---|---|---|
granted |
a banner actually rendered | true |
denied |
macOS refused | false |
pending |
asked, still waiting | — |
unverified |
the wait elapsed with no verdict | nothing |
unverified is the state that used to be reported as success. It now says so plainly, in amber, next to a button that opens System Settings — because "we heard nothing" is genuinely not the same as "you're all set".
Three consequences:
- The wait is 9 s, not 2.5 s, and it no longer concludes anything. It only changes the wording, and the probe stays open behind it — answer the macOS alert at 20 seconds and you still get a correct
granted. - Real notifications keep the verdict honest. Every delivery reports back: one that shows confirms permission, one that fails revokes it. Turn notifications off next month and the app notices on the next message instead of insisting it has permission.
- The stored flag is never written from a guess. Only a real
showorfailedmoves it.
The setup window never blocks
Pressing Allow is fire-and-forget. Nothing is disabled, nothing is modal, and Continue and Skip for now stay clickable in every one of the five states — including while a probe is in flight. A probe that never resolves leaves a warning behind and nothing else.
🔒 Also fixed: web permissions could answer two different ways
Only setPermissionRequestHandler was installed. Chromium checks a permission before it requests one, and those go through separate handlers — so the unset check handler fell through to Electron's permissive default. navigator.permissions.query({name: 'geolocation'}) reported granted from that default, while the request handler stood ready to deny it. Sites got a yes, called the API, and got a no.
Both handlers now share one predicate, so they cannot disagree. Camera, microphone, screen capture, location, clipboard read, MIDI, USB, serial, HID and file-system access are all denied — this is a notification relay and none of it needs that hardware.
clipboard-sanitized-write is now explicitly allowed. Chromium checks it without ever requesting it, so it had been permitted implicitly by that same default; adding a check handler without listing it would have quietly broken every "Copy link" button in Slack, Messenger and Gmail. It permits writing sanitized content only, never reading.
Smaller fix: the global shortcut registered at startup ignored its return value, so a hotkey another app already owned failed silently. It now reports the conflict.
✅ How this was verified
npm test: 80 reported checks, including a new 26-check permission suite. It covers the exact failure shape —start → failed → timeoutmust staydenied, and a timeout must not write the stored flag whether it was previouslytrueorfalse. Two of the checks readsrc/main.jsitself, because the web-permission bug was an omission: no unit test of a module can notice a handler that was never installed.- A real notification through the packaged app, confirming
notification SHOWNreaches the new code path and that an already-correct stored value is left alone rather than rewritten on every message. - All five UI states driven through the real
setup.html, checking the button, the status line, the hint, and — in every state — that Continue and Skip are still usable.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.4-mac.dmg |
Native macOS DMG Installer | 1fea8163383193fe8bc71b260a11c42364a38291f4b96215b2ff41210efd4af9 |
notihub-v1.3.4-mac.zip |
Portable Zip Archive | db7073a231ca649060e434368014a4b017f4f6a546928a64d5fb8880d03541b0 |
These match what GitHub publishes as the digest field on each asset — the same value src/updater.js verifies the download against before installing.
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.4-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.4-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading
Use Settings → Download & Install. No bundle identifier or data-directory change, so existing logins stay intact.
After upgrading, check the permission row in Settings. If it now shows amber or red where it used to show a checkmark, nothing broke in this release — that is the real state of the permission, finally being reported. Press Try again, or use Open notification settings to turn it on.
⚠️ Gatekeeper Notice
Notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse a manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates Notihub installs itself are never quarantined, so this is only needed for a fresh manual install.
v1.3.3 - Sign-in Windows Stay In Notihub
🚀 Notihub v1.3.3
Release Title: v1.3.3 - Sign-in Windows Stay In Notihub
🎯 What this fixes
Signing in to a service no longer throws you out to your default browser. If clicking sign-in on Gmail, Outlook or anything else opened Safari or Chrome instead of staying in Notihub, this release is the fix.
🔍 Correcting what v1.3.2 claimed
v1.3.2's notes said Google sign-in broke because v1.3.1 leaked the app's name into the User-Agent. That explanation was wrong, and this release corrects it.
The User-Agent leak was real and is genuinely fixed — but it was never what sent you to the browser. Loading Google's sign-in page under both the broken v1.3.1 User-Agent and the fixed one produces the same normal sign-in form, with a password field and no "browser may not be secure" block. The UA was not the cause.
The actual cause is older than all of this:
// src/main.js — unchanged since before v1.2.0
wc.setWindowOpenHandler(({ url }) => {
shell.openExternal(url); // *every* popup, including sign-in windows
return { action: 'deny' };
});Every popup a service opened was handed to the default browser — sign-in windows included. A sign-in popup sent to your browser can never work: the cookie it sets lands in Safari or Chrome, not in that service's session inside Notihub, so you end up signed in everywhere except the app you were trying to fix.
This was never a v1.3.1 regression. It has been there the whole time, in v1.2.0 and v1.3.0 too — those versions felt fine because you were already signed in and never hit a sign-in popup. My apologies for the wrong diagnosis in v1.3.2.
🛠️ How it works now
A popup opens inside Notihub, on that service's own session, when either:
- The URL is part of an auth flow — a known provider (Google, Apple, Microsoft/Entra, Okta, Auth0, OneLogin, Duo, Yahoo, Telegram), an auth-shaped path (
/oauth,/login,/signin,/sso,/saml,/authorize,/2fa, …), or the service's own site; or - It is a real popup —
window.openwith window features, which is measurably how auth popups arrive. This catches identity providers not on any list.
Everything else still opens in your real browser: a target="_blank" link, or a featureless window.open to an unrelated site — which is exactly where a link a colleague sent you belongs. A popup to a non-web scheme (javascript:, file:) opens nothing at all.
Two smaller things that follow from it:
- The menubar panel no longer hides itself mid-login. It hides on blur, which was right until a sign-in window it opened took focus and made the panel vanish underneath you.
- Multi-hop flows stay in-app the whole way (service → provider → the provider's own consent page), instead of escaping on the second hop.
✅ How this was verified
Measured, not assumed — the same method that caught the wrong v1.3.2 diagnosis:
- The disproof: Google's sign-in page loaded under the v1.3.1 UA and the fixed UA returns byte-for-byte the same result —
blocked: false, password field present, no popup. The UA was not the cause. - The real handler, driven through five cases: a listed provider's popup opens in-app; an unlisted provider's popup opens in-app via the window-features rule; a
target="_blank"link goes to the browser; a featurelesswindow.opento an unrelated site goes to the browser; ajavascript:popup opens nothing. - The part that actually matters: the in-app popup runs on the same session object as its service view and can read a cookie set on that session — so the sign-in it completes lands where the service tab will find it.
npm test: 66 checks, including new coverage for the popup routing rule.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.3-mac.dmg |
Native macOS DMG Installer | b836752215dedd2c1e8109ff08c26f14d292ccd83f7559951506770e6257cc6c |
notihub-v1.3.3-mac.zip |
Portable Zip Archive | a93fe5b0841b7c80290a88b6100833149dfe6343e7fdb9bccdad6b2333164e75 |
These match what GitHub publishes as the digest field on each asset — the same value src/updater.js verifies the download against before installing.
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.3-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.3-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading
Use Settings → Download & Install. No bundle identifier or data-directory change, so existing logins and notification permissions stay intact.
If a sign-in still escapes to your browser, the URL your browser opened identifies the provider that needs adding — paste it into an issue and it is a one-line fix.
⚠️ Gatekeeper Notice
Notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse a manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates Notihub installs itself are never quarantined, so this is only needed for a fresh manual install.
v1.3.2 - Sign-in Fixes & Tab Reordering
🚀 Notihub v1.3.2
Release Title: v1.3.2 - Sign-in Fixes & Tab Reordering
⚠️ If you are on v1.3.1, please update
v1.3.1 broke signing in. Two separate regressions, both introduced by that release and both fixed here:
- Google refused to sign you in, and clicking sign-in bounced you out to your default browser.
- Zalo's login QR rendered as a blank white square, so it could not be scanned.
Nothing was wrong with your account, your session, or Zalo — v1.3.1 was misrepresenting itself to both sites. Details below.
🛠️ Fixes
🔑 Google sign-in — a capital letter in the User-Agent
v1.3.1 renamed the app to "Notihub". Electron builds its User-Agent out of the app's own name, and the code that strips that name back out was a regex hardcoded to the lowercase notihub. So every service started seeing:
… (KHTML, like Gecko) Notihub/43.3.0 Chrome/150.0.7871.212 Safari/537.36
^^^^^^^^^^^^^^ no longer stripped
Google refuses to complete a sign-in from a User-Agent carrying a product token it doesn't recognise, and hands the flow to a real browser instead — exactly the reported symptom.
The stripping is now an allowlist of the tokens a stock Chrome UA is made of, rather than a list of names to remove, so it cannot be broken by renaming the app again. It lives in src/config.js with unit tests covering the capitalised name, a name with spaces, and an already-clean UA. The UA the app sends is now byte-for-byte what v1.3.0 sent.
📱 Zalo's login QR — force dark erased it
v1.3.1's new Dark Service Pages used Chromium's auto-dark pass. Chromium leaves <img> and <canvas> alone, so that looked safe — but inline <svg> is page content, not an image, and its fills get inverted like any other colour. Zalo draws its login QR as inline SVG.
Measured on the real login screen: the QR is 43% dark pixels normally and 0% under force dark — a blank white square with only the three blue corner markers left.
Fixed with a per-service opt-out:
- Zalo is excluded out of the box (it ships in the preset).
- Right-click any tab → "Don't Darken This Site" for anything else that comes out wrong.
- Dark Service Pages otherwise stays on and unchanged.
Checked the other QR sign-ins while I was there: WhatsApp and Telegram draw theirs into a <canvas>, and both measured pixel-identical with force dark on — they were never affected and are not excluded.
🌟 New
↔️ Drag a tab to reorder it
Press and hold a tab for a moment, then drag: it lifts off the strip and the others slide around it. Drag against either end and the strip scrolls itself, so a tab can be moved further than the visible width. The order is saved and survives a restart, and it drives everything that reads it — the tray list, Control+Tab cycling, and the Cmd+1…9 shortcuts.
Press-and-hold is what the gesture is bound to precisely so that drag-to-pan still works: move straight away and you pan the strip as before, hold first and you reorder.
🔁 A restart prompt you can actually see
An update finishes minutes after the click that started it, by which point the "Restart Now" button was sitting on a Settings page nobody was looking at. Now, the moment an update finishes installing, a prompt comes to the front with Restart Now and Later.
Later changes nothing else: the update stays installed and the Restart button stays in Settings for whenever it suits you.
✅ How this was verified
Both regressions were reproduced and then re-measured after the fix, against the real sites:
- User-Agent: printed from a real Electron run with
app.setName('Notihub')— the sent UA no longer contains the app name orElectron, and matches v1.3.0's exactly. Unit tests cover the capitalised-name case that shipped. - Zalo QR: driving the actual
config.jsdecision the waymain.jsdoes, against the live login page — 44% dark pixels with the preset default (scannable, screenshotted) versus 0% when force dark is applied, which is now only reachable by explicitly opting that site back in. - WhatsApp / Telegram QR: measured unchanged (49%/57% dark pixels) with force dark both off and on.
- Tab reordering and the restart prompt: driven through synthetic pointer and click events — hold-then-drag reorders and reports the new order, a quick drag still pans without reordering, a plain click still selects, and Later/Restart Now each do the right thing.
npm test: 54 existing tests plus new coverage for the UA stripping, the per-service dark decision, and the reordering.
A third bug turned up during that testing rather than in review: a drag ending without the click that normally follows it (pointer cancelled, released off-window) left the click-suppression armed, so the next click on a tab would be swallowed. Fixed.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.2-mac.dmg |
Native macOS DMG Installer | eab33e69b99ebf8ea90baefcc02c5e06d9a78b19828adb622c0b1692594647cd |
notihub-v1.3.2-mac.zip |
Portable Zip Archive | 3d4bfe4738329aeb12f6787a29c287a3e2080334a07bb52e7aa88fa188630a9b |
These match what GitHub publishes as the digest field on each asset — the same value src/updater.js verifies the download against before installing.
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.2-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.2-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading from v1.3.1
Use Settings → Download & Install. No bundle identifier or data-directory change, so logins and notification permissions stay intact.
If Google is still refusing to sign you in after updating, reload that tab (right-click → Reload Service): the fixed User-Agent applies from the next page load.
⚠️ Gatekeeper Notice
Notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse a manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates Notihub installs itself are never quarantined, so this is only needed for a fresh manual install.
v1.3.1 - Menu Bar Tab Scrolling & Dark Service Pages
🚀 Notihub v1.3.1
Release Title: v1.3.1 - Menu Bar Tab Scrolling & Dark Service Pages
🌟 Highlights
↔️ Menu Bar Panel tabs are actually reachable now. In the compact 420px panel, a tab strip past the 3rd or 4th service was pushed off the edge with no way to scroll to it — the strip laid itself out at full content width instead of shrinking to fit. Drag or scroll horizontally to reach the rest.- 🌗 Dark service pages, not just dark chrome. Switching Notihub to Dark used to darken only the tab bar and Settings — a site with no dark theme of its own (a Slack sign-in screen, for example) stayed a white rectangle inside a dark window. New Dark Service Pages toggle in Settings → Appearance (on by default) darkens those too, while leaving a site's real dark theme (Gmail, the Slack app) alone.
- ✏️ The app is "Notihub" everywhere you read it — window title, tray, menu bar, notifications, docs. The CLI command, bundle file, and config folder stay lowercase
notihubon purpose (see below).
🛠️ Detailed Changelog
↔️ Tab strip scrolling in Menu Bar mode
.tabs-container used flex: 0 1 auto, so it sized itself to its content and only then discovered there wasn't room — pushing the Settings button off-panel instead of triggering overflow-x: auto. Fixed with flex: 1 1 0; min-width: 0, plus the scrolling actually needed to be usable:
- Drag-to-pan with a 5px threshold, so a plain click on a tab still switches to it.
- Mouse wheel scroll (a wheel has no horizontal axis on its own).
- Scroll position now survives a re-render —
renderTabs()fires on every badge change and once a minute from the watchdog, and used to resetscrollLeftto 0 each time. - The active tab scrolls into view on an actual tab switch (Cmd+1…9, Control+Tab, tray) — not on every badge tick, so it doesn't yank the strip while you're mid-scroll.
- A soft edge fade hints when more tabs are scrolled out of sight, since the scrollbar itself stays hidden.
🌗 Dark Service Pages
Setting Notihub to Dark only ever told pages prefers-color-scheme: dark — a site that ignores that query, or has no dark theme at all, stayed white. Each service view now gets Chromium's own auto-dark-mode override applied over CDP (Emulation.setAutoDarkModeOverride) whenever the app is dark, re-applied on every navigation since a cross-site load can swap the renderer process out from under it. Chromium skips pages that already declare color-scheme support, so a site with a real dark theme keeps it instead of being inverted twice. New toggle in Settings → Appearance, on by default.
✏️ App name: Notihub in the UI, notihub on disk
The name shown to the user — window title, tray tooltip, menu bar, notifications — is now Notihub, set via app.setName(). Two things were deliberately not renamed, because other code depends on the lowercase spelling:
notihub.app, the bundle file name —src/updater.jslooks for exactly that name inside a downloaded release zip. Renaming it would have broken self-update for every already-installed copy.~/Library/Application Support/notihub, the data directory —app.setName()would otherwise have moved Electron's userData (and every service'spersist:session partition) to aNotihubfolder, signing everyone out on upgrade.app.setPath('userData', …)pins it back to where the data already lives.
✅ How this was verified
- Tab strip: driven headlessly in a 420×200 harness at the real Menu Bar panel width — confirmed the Settings button stays in-panel, wheel/drag/click and scroll-position-across-rerender all behave, and a tab revealed by
scrollIntoViewclears the edge fade. - Dark Service Pages: a plain white test page under real Electron went from average luma 252 → 21 with the override enabled, back to 252 with it cleared.
- App name / userData: launched against a throwaway
$HOMEand confirmed noNotihub-named folder appears alongside the existingnotihubone. npm test: all 54 existing unit tests still pass unchanged.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.1-mac.dmg |
Native macOS DMG Installer | e57d512e52d8ffaf47832fd3dda582c9a0b3e4af9db7061a842f11d01dae8214 |
notihub-v1.3.1-mac.zip |
Portable Zip Archive | 58faec7398b77ebfc226db6795ef2cba9cc433b0deea31626de9b06b6be158b7 |
These match what GitHub publishes as the digest field on each asset — the same value src/updater.js verifies the download against before installing.
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.1-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.1-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading from v1.3.0
Use Settings → Download & Install — this release ships the same self-update path introduced in v1.3.0. No bundle identifier or data-directory change, so logins and notification permissions stay intact.
⚠️ Gatekeeper Notice
Notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse a manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates Notihub installs itself are never quarantined, so this is only needed for a fresh manual install.
v1.3.0 - Self-Updating
🚀 notihub v1.3.0
Release Title: v1.3.0 - Self-Updating
🌟 Highlights
- ⬇️ notihub updates itself. An available update is now a Download & Install button instead of a link to this page. It fetches the release zip, checks it against the SHA-256 GitHub publishes for that exact asset, and replaces the installed app — you restart when it suits you.
- 🔓 No more "notihub is damaged" on updates.
com.apple.quarantineis applied by browsers, so a build notihub downloads for itself is never quarantined. Thexattrcommand is now only ever needed for the first manual install. - 🍺 Works the same however you installed. The Homebrew cask declares
auto_updates, sobrew upgradesteps aside instead of fighting the app over versions. - 🐛 Fixes a broken v1.2.0 npm package — see below. If you installed the CLI from npm, please upgrade.
🛠️ Detailed Changelog
⬇️ In-place updates without Squirrel.Mac
Squirrel.Mac is macOS's usual update mechanism and it is unavailable to this project: it installs only an update signed by the same Apple Developer ID as the running copy, which ad-hoc signing (no paid Developer ID) cannot provide. Rather than leave updates manual, src/updater.js does by hand exactly what a user was doing:
- Download
notihub-v<version>-mac.zipfrom the release. - Verify it against the
digestGitHub publishes per asset. A mismatch deletes the download before anything is unpacked — nothing is installed that does not hash to the published value. - Unpack with
ditto(framework symlinks survive;cpwould flatten them). - Confirm the unpacked build passes
codesignand reports the version the release advertised. - Move the installed bundle aside, copy the new one in, and restore the old one if the copy fails.
If notihub cannot write over its own bundle — a non-admin account, or a copy running from somewhere read-only — the button falls back to opening the release page, exactly as before.
🐛 The npm package published for v1.2.0 was broken
npm rejected the bare name notihub as too similar to an existing package, so v1.2.0 was published scoped, as @thuong180702/notihub. Two places derived the app name from the package name, and so began looking for /Applications/@thuong180702/notihub.app:
bin/notihub.js— the CLI stopped finding the installed app, silently demotingnotihub,notihub notifyandnotihub doctorto an unpackaged Electron run, which macOS refuses to deliver notifications from. Anyone who installed@thuong180702/notihub@1.2.0from npm should upgrade.scripts/build-app.js— building from source failed outright.
Both now read a new productName field, which is the app's name and is independent of whatever the registry requires the package to be called.
🔁 Downloads retry
A dropped connection partway through the ~120MB transfer (seen as net::ERR_CONNECTION_RESET while testing this) used to cost the whole update. Downloads now retry before surfacing a failure.
🧷 Restarting into the new build
notihub holds a single-instance lock, so a new copy that starts before the old process has exited sees the lock and quits — which would look like the update killed the app. The restart now waits for the old process to exit, then goes through open -a so LaunchServices gives the new copy its menu bar slot and notification identity.
✅ How this was verified
The full chain was run against the live v1.2.0 release under real Electron — download, checksum reject and accept, extract, version-mismatch reject, swap, codesign, launch, and cleanup: 19 checks, alongside 16 new unit tests.
Two bugs were caught by that testing rather than by review:
- Deriving the bundle path from the executable returned
Electron.appon a dev run (that binary also sits three levels inside a.app) — an updater willing to write there would have overwrittennode_modules. It now requires its own bundle name. - The single-instance lock race described above.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.3.0-mac.dmg |
Native macOS DMG Installer | 50bef908d980334ff8c35a89ada3eebf1db8cdce6d644716341e5481e447aa87 |
notihub-v1.3.0-mac.zip |
Portable Zip Archive | 64e7a0e28734274ffd9d0267548b2ebd4f5a782c08ade838aefaf6f11c61584a |
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: npm (CLI companion)
npm install -g @thuong180702/notihubMethod 3: DMG Installer
- Download
notihub-v1.3.0-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 4: Direct Zip Download
- Download
notihub-v1.3.0-mac.zip. - Unzip and move
notihub.appinto/Applications.
⬆️ Upgrading from v1.2.0 or earlier
This is the last update you have to install by hand — v1.3.0 onwards, notihub offers to do it for you.
Replace the app as usual. No bundle identifier change, so notifications stay granted.
⚠️ Gatekeeper Notice
notihub is signed ad-hoc, not notarized by Apple, so macOS will refuse this manual install with "notihub is damaged and can't be opened". This isn't corruption — it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appUpdates notihub installs itself are never quarantined, so this is the last time you should need it.
v1.2.0 - WhatsApp Icon Fix & Dedicated Homebrew Tap
🚀 notihub v1.2.0
Release Title: v1.2.0 - WhatsApp Icon Fix & Dedicated Homebrew Tap
🌟 Highlights
- 🖼️ Sharper WhatsApp icon: the WhatsApp preset now uses the bundled hand-drawn SVG instead of Google's favicon service, which only ever returns a blurry 16×16 placeholder globe for
web.whatsapp.com. - 🍺 Dedicated Homebrew tap: notihub now installs from its own tap,
thuong180702/homebrew-notihub, instead of a raw Cask URL pointed at this repo.
🛠️ Detailed Changelog
🖼️ WhatsApp preset uses the bundled icon
- Every other preset in
src/presets.jsfetches its favicon live via Google's favicon API, which is preferred over a bundled asset for accuracy. WhatsApp is the one exception:web.whatsapp.comhas no real favicon registered (404s to a generic placeholder), and thewhatsapp.comroot only has a 23×23 icon — worse than the bundled SVG in every case. - No user action needed; the fix applies automatically on next launch.
🍺 Homebrew install simplified
- Added a proper tap repo,
homebrew-notihub, holdingCasks/notihub.rb. - Removed the now-redundant
Casks/notihub.rbfrom this repo. - Install is now:
brew tap thuong180702/notihub brew install --cask notihub
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.2.0-mac.dmg |
Native macOS DMG Installer | 4b860c2254a5f83e83988fb802257e705f0bd8a4f3b8e2512e964ad734b456dc |
notihub-v1.2.0-mac.zip |
Portable Zip Archive | fad46a9a5a16a2fc1c1c44ce192555f7ff09a18d949c6518f5d5515fe4a15eee |
📥 Installation Options
Method 1: Homebrew (Recommended)
brew tap thuong180702/notihub
brew install --cask notihubMethod 2: DMG Installer
- Download
notihub-v1.2.0-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 3: Direct Zip Download
- Download
notihub-v1.2.0-mac.zip. - Unzip and move
notihub.appinto/Applications.
Method 4: Terminal / Curl
curl -LO https://github.com/Thuong180702/notihub/releases/download/v1.2.0/notihub-v1.2.0-mac.dmg
open notihub-v1.2.0-mac.dmg⬆️ Upgrading from v1.1.1 or earlier
Replace the app as usual — no bundle identifier change this time, no notification re-grant needed.
If you previously installed via the old raw-URL Cask method, switch to the tap:
brew tap thuong180702/notihub
brew install --cask notihub⚠️ Gatekeeper Notice
notihub is signed ad-hoc, not notarized by Apple. macOS will refuse to open it with "notihub is damaged and can't be opened" on first launch — this isn't corruption, it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appv1.1.1 - Menu Bar Icon Fix
🚀 notihub v1.1.1
Release Title: v1.1.1 - Menu Bar Icon Fix
🌟 Highlights
- 🔧 The menu bar icon is back: on some Macs the notihub tray icon silently stopped appearing — the app ran fine, logged nothing, but owned no visible menu bar item. Root cause was a broken per-bundle-id menu bar slot held by macOS, not anything in notihub's code. The app now ships under a new bundle identifier, which gets a clean slot.
🛠️ Detailed Changelog
🔧 Menu bar icon no longer disappears
- App bundle identifier changed from
io.github.thuong180702.notihubtoio.github.thuong180702.notihub.app. - The Homebrew cask's
zap trashpaths were updated to match the new identifier, so uninstall still cleans up after itself.
🔍 How this was diagnosed
The symptom looked like a code regression — it appeared right after the one-for-all → notihub rename, so the rename was the obvious suspect. It wasn't. Ruled out, each by reproduction rather than by reading code:
- notihub's own code — the untouched v0.8.0 source, the version that "worked", failed identically when run today.
- Electron — Electron 42.8.1, 43.3.0 and 44.0.0-beta.1 all failed; a 15-line Electron app doing nothing but
new Tray(icon)showed nothing either. - The icon, the bundle metadata, the ad-hoc signature, the install location — all verified correct, and changing them changed nothing.
- The menu bar itself — a native Swift
NSStatusItemtest binary placed an item without trouble at the same moment notihub could not.
The decisive test was a native NSStatusItem probe packaged into several bundles that were byte-identical except for CFBundleIdentifier:
CFBundleIdentifier |
status item frame | Visible |
|---|---|---|
…one-for-all |
(1091, 949, 63, 33) |
✅ |
…notihub2 (with CFBundleName still notihub) |
(1091, 949, 63, 33) |
✅ |
…notihub.app |
(1092, 949, 63, 33) |
✅ |
…notihub |
(1449, 960, 63, 22) / (0, −17, 63, 22) |
❌ |
Under the old identifier macOS still created the item and reported isVisible = true, but placed it at an invalid position — underneath the clock, or off-screen entirely — always using the legacy 22pt menu bar height instead of the 33pt one. Keeping the display name notihub while changing only the identifier fixed it, which is what confirmed the identifier was the variable and the name was not.
The bad state could not be cleared from the app side: it was absent from the app's preferences (both AnyHost and ByHost), survived removing stale LaunchServices registrations, ignored hand-written NSStatusItem Preferred Position / VisibleCC keys, and survived restarting ControlCenter, SystemUIServer and Dock.
⚠️ Notification permission must be re-granted
macOS ties notification permission to the bundle identifier, so this build looks like a new app to the system. Approve the prompt on the first notification, or enable it manually in System Settings → Notifications → notihub.
Your data is not affected — services, history settings and preferences live in ~/Library/Application Support/notihub, which is keyed by app name, not by bundle identifier.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.1.1-mac.dmg |
Native macOS DMG Installer | 98a3ab9535e173eb6ffebd8c77a6ff2d780ae44b3d4fa274046518c360981679 |
notihub-v1.1.1-mac.zip |
Portable Zip Archive | 8e3216764309e23c564d7da08750f916da37796a16ac809c492f7513e376f30c |
📥 Installation Options
Method 1: Homebrew (Recommended)
brew install --cask https://raw.githubusercontent.com/Thuong180702/notihub/main/Casks/notihub.rbMethod 2: DMG Installer
- Download
notihub-v1.1.1-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 3: Direct Zip Download
- Download
notihub-v1.1.1-mac.zip. - Unzip and move
notihub.appinto/Applications.
Method 4: Terminal / Curl
curl -LO https://github.com/Thuong180702/notihub/releases/download/v1.1.1/notihub-v1.1.1-mac.dmg
open notihub-v1.1.1-mac.dmg⬆️ Upgrading from v1.0.1 or earlier
Replace the app as usual. Because the bundle identifier changed, also do this once:
- Quit any running copy of notihub before installing.
- Re-approve notifications when prompted (see above).
- If System Settings → General → Login Items & Extensions shows a duplicate
notihubentry left over from the old identifier, remove the stale one.
⚠️ Gatekeeper Notice
notihub is signed ad-hoc, not notarized by Apple. macOS will refuse to open it with "notihub is damaged and can't be opened" on first launch — this isn't corruption, it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.appv1.0.1 - Update Notifications
🚀 notihub v1.0.1
Release Title: v1.0.1 - Update Notifications
🌟 Highlights
- 🔔 Update Notifications: notihub now checks GitHub Releases in the background (on launch, then every 6 hours) and shows an amber dot on the Settings icon plus a banner in Settings when a newer version is out — with a one-click link to the release page.
🛠️ Detailed Changelog
🔔 Update check
- Background check against
api.github.com/repos/Thuong180702/notihub/releases/latest, compared against the running build's version (app.getVersion()). - Amber status dot on the tab-strip Settings button (reusing the same dot language as the per-service connection status added in v1.0.0), plus a persistent "notihub vX.X.X — up to date" line and manual "Check for Updates" button in Settings → Startup.
- When a newer version is found: a dedicated banner at the top of Settings with a "View Release" button that opens the GitHub release page in the default browser.
- This is a notify-only check — no download, no install. See below for why.
⚠️ Why not auto-install
Investigated wiring up full auto-download-and-install (Squirrel.Mac via Electron's native autoUpdater) before shipping this release. Built two ad-hoc-signed test builds and ran a real update cycle end-to-end against a local feed server to verify feasibility rather than guess:
ERROR: Code signature at URL .../update.zip did not pass validation:
code failed to satisfy specified code requirement(s)
Squirrel.Mac requires the downloaded update to satisfy the same code-signing requirement as the running app. Ad-hoc signing (no paid Apple Developer ID) produces a different signature identity on every build, so Squirrel.Mac correctly refuses to install it — this isn't a bug to fix in notihub's code, it's a hard requirement that needs a real Developer ID certificate + notarization first. Auto-install stays out of scope until that's in place; the notify-only check in this release is the safe, working alternative.
📦 Download Assets & Checksums
| Asset File | Format | SHA-256 Checksum |
|---|---|---|
notihub-v1.0.1-mac.dmg |
Native macOS DMG Installer | 703cf64dff8e93bdeabfeb5fa8c1c1714df51bc519ce60f68e85ce30d0fcd816 |
notihub-v1.0.1-mac.zip |
Portable Zip Archive | 405e7a831b8f563fbd0f1721e9b4fcbfc796b31c0b67e80f2e39bb315762e543 |
📥 Installation Options
Method 1: Homebrew (Recommended)
brew install --cask https://raw.githubusercontent.com/Thuong180702/notihub/main/Casks/notihub.rbMethod 2: DMG Installer
- Download
notihub-v1.0.1-mac.dmgbelow. - Double click to open the DMG, then drag
notihub.appintoApplications.
Method 3: Direct Zip Download
- Download
notihub-v1.0.1-mac.zip. - Unzip and move
notihub.appinto/Applications.
Method 4: Terminal / Curl
curl -LO https://github.com/Thuong180702/notihub/releases/download/v1.0.1/notihub-v1.0.1-mac.dmg
open notihub-v1.0.1-mac.dmg⚠️ Gatekeeper Notice
notihub is signed ad-hoc, not notarized by Apple. macOS will refuse to open it with "notihub is damaged and can't be opened" on first launch — this isn't corruption, it's Gatekeeper rejecting an unnotarized app. Clear the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/notihub.app