Chats on Android: open them inside Sioul, or filter their apps' notifications¶
The question: on Android, how could Sioul open chat websites (Discord, Slack, Telegram, Element and Matrix, WhatsApp Web, Messenger, Mattermost…) inside itself, with Qt WebView, and pass their notifications through its own rules (the areas and hours, the senders' lists and the matrix of who may reach the person when: areas.md, porch.md, sites.md)? The aim: chats without their native apps, which carry trackers and ads, and Sioul deciding when their messages reach the person. The other route: keep the native apps and filter their notifications with Android's NotificationListenerService.
Researched on 6 October 2026, from primary sources first: Android's and Qt's documentation, the Android SDK's API stubs (android-36), Qt WebView's and Chromium's sources, Mozilla's GeckoView documentation and sources, the projects' own repositories and issues. Developer reports and third-party posts come after, named as such. Strength of each finding: primary (documentation, source code, a licence or terms text, read at the link given), corroborated (two or more independent developer reports agree), report (one developer report or forum post), inferred (read in code or deduced, not documented). "(unverified)" marks what could not be confirmed at a primary source; "needs a phone" marks what only a test on a real device can settle. Sioul's own state: android.md (sites open in the system browser on Android, since Qt WebEngine has no Android version).
Answers so far¶
(Updated 6 October 2026. Numbers point to the sections below.)
- Qt WebView is a thin wrapper, not a base for chats. It shows a page, reads its title, runs a script after the page has loaded, sets the user agent and cookies. Its Android code grants no camera or microphone (no calls), opens no file chooser, ignores downloads and pop-up windows, offers no bridge to the page (no WebChannel), lets a renderer crash end the app, and lives only in Qt's activity, which ends its process when it closes. Everything a chat needs is Java Sioul would write anyway: better written around Android's WebView directly, with AndroidX WebKit, in a process of its own (1).
- Android's WebView has no notifications, by design. It starts Chromium with "Web Notification API and the Push API are not supported": a page sees no
Notification, noPushManager, noshowNotification; service workers run but can neither notify nor be pushed; the badge API exists and goes nowhere; the Permissions API is missing. A chat in a WebView delivers news only while its page runs, and only to a script Sioul puts there (2). - Catching them is possible, chat by chat. AndroidX WebKit runs Sioul's script before the page's own in every matching frame, with a bridge that names the sending site, and gives each chat its own profile. A stand-in for
Notification, as Ferdium, Franz and Station use on the desktop, should catch Telegram, Element, WhatsApp Web and Mattermost (from their code; untested); Discord copiesNotificationat load and gates it behind its own setting (Ferdium patched Discord's internals in 2026); the title and the badge give counts everywhere; reading a page's DOM is the fragile last resort. Wrappers' recipes break a few times a year per chat, and no wrapper holds notifications to release them later (3). - Background life is the hard part. A chat's WebView must live outside Qt's process, with a foreground service. Chromium on Android freezes a page it thinks hidden after about a minute (timers, network events, WebSocket messages; read in its source, to confirm on a phone), so the page must never have been attached to a window, or be reloaded after each look; Android wrappers that tried report pages going silent. With the screen off the processor sleeps and the chat reconnects at the next wake: news comes late, not lost, which suits gathered notifications. Nobody has measured the memory and battery; calls cannot ring that way (4).
- GeckoView solves notifications, not the rest. It hands every web notification to the app and runs service workers; with UnifiedPush (as Fennec, IronFox and Peel do), sites using Web Push notify with no page running. It costs a 90 MB AAR (about 90 to 190 MB more APK), Gecko's main process inside Sioul's, an update every two weeks since September 2026, and microG's FIDO library in place of Google's; MPL-2.0 joins GPL-3.0-or-later as a "Larger Work" (5).
- Chats on a phone: Telegram and Mattermost are made for phones; Element sends phones to the app stores unless told not to; Discord ("Running Discord in a mobile browser is not supported"), WhatsApp Web (redirected to
/mobile/by its server), Messenger (now facebook.com/messages) and Slack need a desktop user agent. WhatsApp Web cannot replace the app: it needs a primary phone, used at least every 14 days. Open protocols: Matrix (the cleanest: a Rust SDK, UnifiedPush), Telegram (TDLib, anapi_idof one's own, UnifiedPush), Mattermost and Slack (per server or workspace), XMPP; none for Discord (self-bots forbidden), WhatsApp, Messenger, Signal. Slack, Mattermost and some Matrix servers mail what was missed (6). - The listener filters every app the person keeps. It reads every notification (conversations with senders' names, sometimes a URI), snoozes one until a given time (Android reposts it with its own exact alarm, keeps updates meanwhile, drops it if the app cancels it), and cancels any that is not ongoing. It acts only after the first sound, so held apps' channels must be silent (or a global hint, or a do-not-disturb mode); ongoing notifications and ringing calls cannot be held. Access is a "restricted setting" for an APK installed by hand; Sioul would see every notification unless the person narrows it per app (7).
- The three apps: Chromium's browsers name the site in each notification (tag, channel), Firefox's by host only; on /e/OS the default browser has no Web Push at all. Discord: conversations with display names and "
# ", no address: hold by kind and conversation, not by person. Proton Mail: title = sender's name, text = subject, sub-text = receiving address, the sender's address not shown: hold by time and account, judge senders only on the computer (Bridge). A snoozed notification keeps its own tap; a copy can carry the app's tap, except Chrome's after a cancel (8). - Terms and ethics: the official page, unchanged, with a script that only listens, is the least risky way into Discord and WhatsApp Web (a grey zone in their terms); changing a page's behaviour is forbidden by their terms, and clients acting through Discord's or WhatsApp's servers get accounts banned; Telegram, Matrix and Mattermost welcome other clients. The listener involves no service's terms but asks the person's trust. Holding must never change what others see (read receipts, typing), and "always through" must go by conversation, since chat senders are names anyone can take (9).
Recommended: the listener first, with snoozing (E); mail that announces chats (G); Matrix inside Sioul next (F); for a chat without its app, a Custom Tab of the person's browser (H), else a chat host of Sioul's own on demand (B), kept alive only per chat after measuring (C); not Qt WebView (A); GeckoView only once a chat that matters takes Web Push through UnifiedPush (D). See "Options for Sioul".
1. Qt WebView on Android (Qt 6.11)¶
Qt WebView's Android back end is two files: a Java controller around android.webkit.WebView and a C++ plugin that wraps it in a Qt window. Read in the 6.11 branch (https://github.com/qt/qtwebview/tree/6.11, last change 5 October 2026), with Qt's bug tracker.
- A native view in a child window, nothing drawn over it. The plugin makes the WebView a foreign window (
QWindow::fromWinId,qandroidwebview.cpp) placed in the scene byQQuickWindowContainer. Qt's documentation: "Due to platform limitations, overlapping the WebView with other QML components is not supported" (https://doc.qt.io/qt-6.11/qtwebview-index.html, "Limitations"); a Qt developer: "The WebView is a native view that lives outside the QML Scene, the z value … has no and cannot have any effect" (QTBUG-100875, open since 2022, https://qt-project.atlassian.net/browse/QTBUG-100875). Primary. Takeaway: Sioul's menus, dialogs and bars would have to sit beside a chat, never over it. - What the QML type offers (https://doc.qt.io/qt-6.11/qml-qtwebview-webview.html):
url,title,loading,loadProgress,canGoBack/canGoForward,httpUserAgent(read and write),settings(local storage, JavaScript, file access);runJavaScript(script, callback),loadHtml,setCookie/deleteCookie/deleteAllCookies; signalsloadingChanged,urlChanged,titleChanged,loadProgressChanged,cookieAdded/cookieRemoved. On Android,runJavaScriptisWebView.evaluateJavascripton the page as it is now, not before its scripts; the user agent isWebSettings.setUserAgentString; cookies go through Android's app-wideCookieManager, and DOM storage is switched on by Qt ("In QtWebEngine and on iOS local storage is enabled by default, so we follow that"); both persist in the app's WebView data folder. The favicon is received and dropped (the code that would pass it on is commented out). Primary (the controller, https://github.com/qt/qtwebview/blob/6.11/src/jar/src/org/qtproject/qt/android/view/QtAndroidWebViewController.java, and the plugin, https://github.com/qt/qtwebview/blob/6.11/src/plugins/android/qandroidwebview.cpp). Takeaway: enough to show a page and read its title; nothing to catch a notification before the page's own code runs. - Logins kept, mostly. WebView "does not (yet) save Chromium data during shutdown", so it writes DOM storage at once ("aggressively persist DOM Storage to minimize data loss",
kEnableAggressiveDOMStorageFlushing,aw_main_delegate.cc); cookies are written now and then, andCookieManager.flush()"Ensures all cookies currently accessible through the getCookie API are written to persistent storage". Third-party cookies are refused by default ("Apps targeting LOLLIPOP or later default to disallowing third party cookies",CookieManager.setAcceptThirdPartyCookies, AOSP), and Qt does not change it: a login or a captcha in a frame of another site may fail. Primary. Takeaway: flush cookies when the window goes away; allow third-party cookies per chat only where its login needs them. - What its Java controller leaves out. It sets a
WebViewClient(page started and finished, errors) and aWebChromeClientwith four methods only: progress, icon, title, geolocation (granted only if the app holds the location permission). So:- camera and microphone:
onPermissionRequestis not overridden, and Android's default "If this method isn't overridden, the permission is denied" (WebChromeClient.java, https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/webkit/WebChromeClient.java): no calls; - file upload: no
onShowFileChooser, so<input type="file">does nothing ("WebView wont allow fileupload", QTBUG-85426, open since 2020); - downloads: no
DownloadListener: "clicking a download link fails silently" (QTBUG-102471; QTBUG-108658); - pop-up windows (sign-in windows,
window.open): noonCreateWindow(QTBUG-102471, "Add support for popup dialogs", open); - a bridge to the page: no
addJavascriptInterface, no WebChannel ("The WebView of QML is not provide WebChannel support", QTBUG-114209, open since 2023; Qt's WebSocket transport would need a TLS WebSocket server inside the app, since an https page may not openws://); the native WebView is not exposed (QTBUG-42334, open since 2014); - a renderer that dies: no
onRenderProcessGone, and Android's default is that the "application will crash if render process crashed, or be killed if render process was killed by the system" (WebViewClient.java, AOSP): one chat's renderer killed in the background takes Sioul with it; - other schemes: any non-web link is handed to
startActivity(ACTION_VIEW)unchecked (QTBUG-150791, "Dispatching unsanitized url requests to system handler", in assessment, September 2026); - deleting a WebView can crash the renderer (QTBUG-85322, open since 2020).
Primary (source and tracker; links at https://qt-project.atlassian.net/browse/QTBUG-NNNNN).
Takeaway: for chats Sioul would have to fork the controller or write its own Java around
android.webkit.WebViewanyway.
- camera and microphone:
- It needs Qt's activity. The controller is built with
QtAndroidPrivate::activity()and makesnew WebView(m_activity)on the activity's thread; when the application is anything but active, the plugin callsWebView.onPause()(onApplicationStateChanged). Primary. Qt's activity also ends its process when destroyed:QtActivityBase.onDestroycallsQtNative.terminateQtNativeApplication()thenSystem.exit(0), unless the activity is only being recreated (read in Qt 6.11.2'sQt6Android.jarwithjavap; the same finding shaped Sioul's background service). Takeaway: a Qt WebView cannot live in a service, and dies with the window; it cannot keep a chat alive. - Several views: one
android.webkit.WebViewper QMLWebView, all in one renderer process ("Only one renderer per app is currently used in multi-process mode", https://chromium.googlesource.com/chromium/src/+/refs/heads/main/android_webview/docs/web-platform-compatibility.md) and one cookie jar and storage (Qt exposes no profiles). Primary. Memory per chat: see 4. Takeaway: chats share one renderer: a heavy one slows the others, and one renderer lost loses them all. - Not in Sioul's build yet: the Qt modules Sioul's Android build needs (android.md, "Building it") do not include Qt WebView, an add-on module to install with Qt for Android. Licence: LGPL-3.0, GPL-2.0 or GPL-3.0 (the sources' SPDX lines), fine beside GPL-3.0-or-later. Primary.
- Other apps' "internal browsers" are often Custom Tabs (which apps use which was not checked): the person's own browser drawn inside the app, with a "Shared cookie jar and permissions model so users don't have to sign-in to sites they are already connected to"; the app cannot run its own script in it, and Google points to WebView "if you need to inject javascript directly from your app" (https://developer.chrome.com/docs/android/custom-tabs). Primary. Takeaway: a Custom Tab would open a chat inside Sioul with the browser's logins and the browser's push (where it has any, 8.1), and the listener could hold that site's notifications (option H below); a WebView is needed only to catch notifications in the page itself.
What follows for Sioul: Qt WebView gives a page in the window and little else; every piece a chat needs (calls, files, pop-ups, a bridge, surviving a renderer crash, living without the window) is Java Sioul would write itself. Writing that Java around android.webkit.WebView directly, with AndroidX WebKit (section 3), costs no more and keeps the view out of Qt's activity.
2. Notifications in Android's WebView¶
Read in Chromium's source (main, October 2026) and Android's.
- Switched off at start, on purpose. WebView's startup: "// Web Notification API and the Push API are not supported (crbug.com/434712)" then
cl->AppendSwitch(switches::kDisableNotifications);(AwMainDelegate::BasicStartupComplete, https://chromium.googlesource.com/chromium/src/+/refs/heads/main/android_webview/lib/aw_main_delegate.cc). That switch turns off two Blink features:{wrf::EnableNotifications, switches::kDisableNotifications, false}and, "Chrome's Push Messaging implementation relies on Web Notifications",{wrf::EnablePushMessaging, switches::kDisableNotifications, false}(https://chromium.googlesource.com/chromium/src/+/refs/heads/main/content/child/runtime_features.cc). Primary. Takeaway: no chat can show a notification from a WebView by itself; Sioul must catch it before it is lost. - What a page sees.
Notificationis declaredRuntimeEnabled=Notifications, so with the feature off the interface does not exist:'Notification' in windowis false,typeof Notificationis"undefined", and readingNotification.permissionthrows a ReferenceError.ServiceWorkerRegistration.showNotificationandgetNotificationscarry the same flag;PushManagerandregistration.pushManagercarryRuntimeEnabled=PushMessaging(the IDL files under https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/modules/notifications/ and…/push_messaging/). Primary for the IDL; what each chat does when it finds noNotification(hides its setting, nags, falls back to the title) needs a phone. Takeaway: a chat sees a browser without notifications; Sioul's script must give it one before the chat's code looks. - The compatibility tables agree: MDN's data lists Android WebView without
Notification, withoutServiceWorkerRegistration.showNotificationand withoutPushManager(webview_android: version_added false, https://github.com/mdn/browser-compat-data/blob/main/api/Notification.json). Primary. - Nothing on the browser side either.
AwBrowserContext::GetPlatformNotificationService()returnsnullptr;GetPushMessagingService(): "// TODO(johnme): Support push messaging in WebView." andNULL; Background Fetch and Background Sync have no controller, and WebView turns offkBackgroundFetchandkPeriodicBackgroundSync(aw_browser_context.cc,aw_field_trials.cc, same tree).WebChromeClienthas no callback for notifications at all (AOSPWebChromeClient.java). Primary. Takeaway: no push can wake a chat in a WebView; a chat delivers news only while its page runs. - Service workers run, without notifications or push.
ServiceWorkerController(API 24) "Manages Service Workers used by WebView": the app may intercept their requests (ServiceWorkerClient.shouldInterceptRequest) and set their cache and network settings, nothing more (https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/webkit/ServiceWorkerController.java). A worker'sshowNotificationdoes not exist, and no push event ever comes. Primary. Takeaway: everything that can make a notification in a WebView runs in the page itself, so a script in the page catches all of it. - Shared workers are off (
kDisableSharedWorkers, "Not yet supported in single-process mode",aw_main_delegate.cc). Primary. Whether a given chat needsSharedWorker(or falls back) needs a check per chat. - The Badging API is there and goes nowhere.
navigator.setAppBadgeandclearAppBadgehave no runtime flag (onlySecureContext), resolve, and on Android the call to the browser is compiled out:#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) …aroundbadge_service()->SetBadge(…)(https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/modules/badging/navigator_badge.cc). Primary. Takeaway: a page that sets its badge hands Sioul an unread count for free, if Sioul's script wrapssetAppBadge. - The Permissions API is missing: "The Web Permissions API is not implemented in WebView, and in general any code in Chromium that relies on being able to silently check the status of permissions will not work - only actual permission requests are supported" (
android_webview/docs/web-platform-compatibility.md). Primary. Whatnavigator.permissions.query({name: "notifications"})returns then in a WebView needs a phone. Takeaway: the script must answer that question too, or a chat that asks it first may never try to notify.
3. Catching a page's notifications anyway¶
3.1 What Android gives for it (AndroidX WebKit)¶
Read in the AndroidX sources (androidx-main, https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/webkit/webkit/src/main/java/androidx/webkit/): WebViewCompat.java, WebViewFeature.java, ProfileStore.java, WebSettingsCompat.java. A small library (Apache-2.0) added with Gradle; each feature depends on the WebView installed on the phone and is checked at run time (WebViewFeature.isFeatureSupported).
- A script before the page's own:
addDocumentStartJavaScript(webview, script, allowedOriginRules)runs "in any frame whose origin matches allowedOriginRules when the document begins to load … the script will run before any of the page's JavaScript code … It will block the loading of the page until it's finished, so should be kept as short as possible" (featureDOCUMENT_START_SCRIPT). Primary. Takeaway: Sioul's stand-in forNotificationis in place before the chat's code looks for it; Qt WebView'srunJavaScriptcomes too late for that. - A bridge bound to origins:
addWebMessageListener(webView, jsObjectName, allowedOriginRules, listener)injects an object that is "available immediately when the page begins to load" in matching frames only; the page callspostMessage(string or ArrayBuffer), and the app's listener receives the message with itssourceOriginandisMainFrame, and may answer through aJavaScriptReplyProxy(featureWEB_MESSAGE_LISTENER). The documentation warns that a wildcard rule exposes the object to any page. Primary. Takeaway: the bridge Qt WebView lacks; each message tells Sioul which site sent it, which the olderaddJavascriptInterfacedoes not. - Scripts in a world of their own:
addJavaScriptOnEvent(…, world)andgetExecutionWorld(featureJS_INJECTION_IN_FRAME_AND_WORLD) run a script beside the page, out of its reach. The stand-in forNotificationmust live in the page's own world to replace the page'swindow.Notification; reading the page (3.3) can run apart. Primary. - One profile per chat:
ProfileStore.getOrCreateProfile(name)andWebViewCompat.setProfile(webView, name)give a WebView its own cookies, storage and service workers (featureMULTI_PROFILE). Primary. Takeaway: each chat logged in apart, two accounts of one service possible, and forgetting a site is deleting its profile. - Its sounds off:
setAudioMuted(featureMUTE_AUDIO). Primary. - Security keys and passkeys: WebAuthn is off in a WebView by default (
WEB_AUTHENTICATION_SUPPORT_NONE, "This is the default behavior");setWebAuthenticationSupportturns it on for the app's own sites (tied by Digital Asset Links) or, following Android's "Privileged apps" guide, for any site (WEB_AUTHENTICATION_SUPPORT_FOR_BROWSER). Primary; what works without Google Play Services (on /e/OS) needs a phone. Takeaway: a chat that asks for a security key at login may refuse inside Sioul on a phone; a login made in the browser does not carry over (another cookie jar). - A user agent per chat:
WebSettings.setUserAgentString; "Android WebView has supported User-Agent Client Hints since version 116, but only for applications that send the default User-Agent string" (Android Developers Blog, December 2024, https://android-developers.googleblog.com/2024/12/user-agent-reduction-on-android-webview.html), so a desktop user agent is not contradicted by client hints; touch events and the screen's size still tell a phone (inferred). Primary. Takeaway: a desktop user agent built from the WebView's real Chrome version, nothing added to it (3.3), set only for the chats that need it (6).
3.2 The stand-in: what it must answer¶
Inferred from section 2 (what the page lacks) and from how pages test for notifications; each point needs a test per chat.
Notification: a constructor taking a title and options (body,tag,icon,silent,requireInteraction,data),Notification.permissionanswering"granted",requestPermission()resolving"granted"(with the old callback form too),close(), and theonclick,onclose,onshow,onerrorhandlers. Each new one is posted to Sioul with its site, its tag (a later one with the same tag replaces the earlier), and an id that Sioul sends back when the person taps it, so that the page's ownonclickopens the right conversation.- The service-worker form:
ServiceWorkerRegistration.prototype.showNotificationandgetNotifications, called from the page (some chats show their notifications through their worker even while the page is open). Calls from inside the worker cannot be reached (no script injection into workers), and they matter little: push never comes (section 2). - The questions pages ask first:
navigator.permissions.query({name: "notifications"})answering"granted"(WebView has no Permissions API, section 2); the badge (navigator.setAppBadgeandclearAppBadge) passed on as an unread count. - Whether the person is looking: chats commonly notify only when their page is hidden or has no focus. A page kept running in a never-attached WebView counts as visible (section 4), so the stand-in may also have to say that the page is hidden (
document.hidden,visibilityState,hasFocus(), avisibilitychangeevent) while the person is not in it, and the truth when they are. - What the page already filters: chats stay quiet about what the person is looking at: Telegram A on a phone notifies for every chat but the open one, Telegram K never for the open chat, Element not for the room on screen when the person was active in the last two minutes, Mattermost not when the window has focus and the channel is open (each one's code). And a page closes its own notifications (
close(), or a new one with the same tag) when the person reads elsewhere: the signal for Sioul to drop a held one. Primary. - What it breaks: a site that checks that its
Notificationis the browser's own (rare), or that notifies from an iframe of another origin (the script must be allowed there too). A Content-Security-Policy should not stop a script the WebView injects itself, as it stops a<script>the page adds (inferred, untested). Answering "granted" everywhere also takes away the site's own permission prompt, so Sioul's site settings must carry the person's yes or no per site. - Takeaway: one generic stand-in, written once, catches every chat that notifies from its page; the per-chat work is checking that each one does, and when.
3.3 How wrappers do it¶
Read in their sources, pinned at the commits linked (October 2026).
On the desktop (Electron, where the page has a real Notification but the wrapper needs to see it):
- Ferdium (active; recipes for each service): a stand-in class replaces
window.Notification(static permission = 'granted',requestPermissionin callback and promise form,onclickfired back), and since 2025ServiceWorkerRegistration.prototype.showNotificationtoo ("some sites use service workers for notifications, but electron doesn't support this"); injected after the page starts, not before its scripts (src/webview/notifications.ts,recipe.ts,Service.ts, https://github.com/ferdium/ferdium-app/blob/764f82f80e73497324d9e46042333eb5a0885fc5/src/webview/notifications.ts). A caught notification inside its do-not-disturb hours is dropped, not kept (ServicesStore.ts). Unread counts come from each recipe's code, run every 2 seconds. Primary.- Discord: the stand-in alone catches nothing; the recipe reaches into Discord's bundle to force its internal notification setting, because "Discord defaults to NEVER without DiscordNative and returns before Ferdium's Notification shim"; the pull request calls it "a bit of a hack. We're messing with Discord's internals" (ferdium-recipes PR #708, August 2026; issue #982 open since 2022). Primary.
- Telegram Web A sends its notifications through its service worker whenever push is supported; Ferdium's 2023 workaround stopped applying when Telegram's desktop app moved from Electron to Tauri, and an issue "Notifications don't arrive in version A of Telegram Web" is open (ferdium-app #2521, August 2026). Where push is missing, as in a WebView, Telegram A falls back to
new Notification()(telegram-tt,src/util/notifications.tsx). Primary (the cause of #2521: inferred). - WhatsApp Web: counts read from WhatsApp's own IndexedDB (unread counts, muted and archived chats), and the app's name stripped from the user agent. Messenger: five fallback ways to count, a forced local setting for its notifications, and two moves of address in a year (messenger.com retired in April 2026 for facebook.com/messages).
- Upkeep: over the year to October 2026, few recipe changes (Discord 1, WhatsApp 1, Messenger 2) against many open issues (WhatsApp 19, Messenger 19): thin maintenance, not stable sites (GitHub API counts). Primary (counts), inferred (reading).
- Franz 5 (open source, superseded by a closed Franz 6, which advertises "Native Signal — a real desktop client, not a wrapped web page"): the ancestor of Ferdium's stand-in, with a per-recipe hook to clean what a site passes (https://github.com/meetfranz/franz/blob/94b1ef298aa72a41eac448f4e5e21a74d857f2fa/src/webview/notifications.js). Primary.
- Rambox Community Edition (archived 2022): wraps the browser's own
Notificationand stubs its event methods ("It seems that gmail is checking if such event handler func are available"); silences a service by denying it the notification permission; for unreliable sites, makes its own "You have N new messages" when the count rises. Primary. - Station (community-maintained): the most complete stand-in, every standard property, events, and a real click event sent back (
webview-preload.ts, https://github.com/getstation/desktop-app/blob/c64d541d2a3eb1ac5e8792888daedff51a215c2f/packages/app/src/notification-center/webview-preload.ts); replacesNotificationin same-origin frames, not in cross-origin ones; keeps every caught notification and skips the pop-up while snoozed, without releasing them later; and mutes the page's own sounds while snoozed, since chats play their "ding" apart from notifications (whatsappInjectedScript.js). Primary. - Wavebox (proprietary): only its "integrated" apps (Slack, Gmail, Outlook…) "update in real-time, even when sleeping", through the services themselves; the others get nothing while asleep unless "auto-wake" wakes them "every ~30 minutes" (https://hub.wavebox.io/auto-wake/). Primary.
- WhatsApp Web refuses user agents with extra words (an app's name,
Electron/…) with "WhatsApp works with Google Chrome": reported from 2019 to September 2026 by several wrappers, each fixing it by sending a plain Chrome user agent for web.whatsapp.com only (https://github.com/stablyai/orca/issues/19740, https://github.com/samuellee-zy/Hangar/issues/13, https://github.com/wavebox/waveboxapp/issues/1026). Corroborated.
On Android (WebView or GeckoView):
- Native Alpha (WebView, the best-known open wrapper): no notifications at all, "There is no notification support. The app in its current state can not and does not send notifications" (a contributor, May 2026, https://github.com/cylonid/NativeAlphaForAndroid/issues/174); it calls
pauseTimers()when put away. Primary. - Peel (a fork of Native Alpha moved to GeckoView, active): GeckoView hands the page's notifications to the app (
WebNotificationDelegate), one Android channel per site, taps sent back to the page; and Web Push through UnifiedPush, experimental since August 2026 (https://github.com/mazzz1y/peel). A user reports WhatsApp Web never offers push there, only notifications while its page is open (issue #40). Primary (code), report (WhatsApp). - WebApps (archived), SlimSocial: no notifications; WebApps' users saw chats go silent and the app closed "after some time of inactivity" (issue #80). Hermit (proprietary): "we don't believe you can use [push notifications] in Hermit any time soon"; it offers feeds and "Web Monitors" instead: plain HTML fetched in the background, no JavaScript, five minutes at the least (https://hermit.chimbori.com/features/web-monitors). Frost for Facebook (archived): polled Facebook's mobile pages every 15 minutes or more and parsed them. Primary.
- WhatsappWebToGo (2018, F-Droid): "using the web notifications api is not possible as the webview doesn't implement it"; a fork read WhatsApp's page every five seconds by class names long gone; the maintainer: "Notifications alone would not be reliable and the battery impact of keeping the app in background all the time would be to high" (https://github.com/92lleo/WhatsappWebToGo/issues/4). Primary.
- Small WebView wrappers that do it (2026):
web-to-appdefinesNotification, answersnavigator.permissions.query, and adds aPushManagerthat fails cleanly so that sites fall back to local notifications (WebViewManager.kt, https://github.com/shiaho777/web-to-app); a Zalo wrapper adds a fakewindow.electronso the page takes its desktop path, and a foreground service. Both inject after the page starts, not at document start. Primary. - android-webcord (2026): its author writes that a backgrounded WebView "stops receiving Discord gateway events", then takes the account's token from the page and polls Discord's API from a foreground service (
DiscordApi.kt, https://github.com/zachvlat/android-webcord): what Discord calls a self-bot (section 9). Primary (code), report (the suspension, which agrees with section 4). - Discord's own mobile site is second-class: Vencord's Android app calls it "not very usable" (https://github.com/Vencord/Vendroid); others need a desktop user agent and find no microphone for voice; QR and captcha loops at login are reported, account bans for a plain web wrapper are not. Corroborated.
- Discord mods (Aliucord, pinned to the last pre-React-Native app of 2022 and "against Discord's TOS" in its own README; Vendetta discontinued in 2024, Bunny archived in 2025; Revenge and Kettu active): a moving target outside the terms (https://github.com/Aliucord/Aliucord). Primary.
Takeaway: nobody has shipped "hold, then release" for web chats (a Ferdium request, "Focus mode: Hold notifcations until certain time", https://github.com/ferdium/ferdium-app/issues/1982 of 2024, is open and unanswered); the desktop wrappers prove the stand-in, the Android ones prove that without background life and push there is nothing to hold.
3.4 The three ways to know a chat has news¶
| Way | How robust | Work per chat | What it misses or breaks | Who uses it |
|---|---|---|---|---|
A stand-in for Notification, at document start |
good where the page calls it; fails where the page checks its own setting first (Discord) or sends through its service worker (Telegram A when push exists) | one generic script, then a check per chat | notifications shown from inside a worker; pages probing the object's shape; the page's own sounds | Franz, Ferdium, Station, Rambox and Nativefier (wrapping), small Android wrappers |
| The title and the badge | stable for years: "(3) Discord", "Element [3]", "(•)"; setAppBadge where a page uses it |
a pattern per site, often one for all ([([{] digits [}\])]) |
who wrote and what; a dot says "something" only | Rambox, Nativefier, Wavebox, Station (watching the title), Ferdium as fallback |
| Reading the page (its DOM, its own store or IndexedDB) | fragile: generated class names change (WhatsApp's often, Messenger's atomic CSS and staged rollouts); internal stores are exact until they vanish | a recipe per chat, fixes a few times a year | breaks silently; reading a site's internals is what its terms forbid (section 9) | Ferdium and Franz recipes, Station (Slack's store), WhatsappWebToGo |
Takeaway: the stand-in and the title by default; a rising count makes "3 new in Discord" when nothing else fires; reading the page only as a last resort, per chat, saying when it may be stale.
4. Keeping a chat alive in the background¶
A chat in a WebView delivers news only while its page runs (section 2: no push). Four things decide whether it runs once Sioul is put away: Android (is the process alive and not frozen?), Doze (network and CPU), Chromium (does it throttle or freeze a page it thinks hidden?), and the chat's own server (how long does it keep a silent connection?).
- A WebView outside an activity works, with limits. Android: "WebView should always be instantiated with an Activity Context. If instantiated with an Application Context, WebView will be unable to provide several features, such as JavaScript dialogs and autofill" (
WebView(Context), https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/webkit/WebView.java). A library does it all the same: flutter_inappwebview's headless WebView is made with the activity when there is one (added to it invisible) and with the application's context otherwise (HeadlessInAppWebViewManager.java,HeadlessInAppWebView.java, https://github.com/pichillilorenzo/flutter_inappwebview/tree/master/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview). Primary (Android) and report (a library's practice). Takeaway: a chat's WebView can live in a service; a page shown to the person needs an activity, so its dialogs work. - One process holds a chat's login. "Each directory can be used by only one process in the application … different processes in the same application cannot directly share WebView-related data … login cookies may have to be copied from one process's cookie jar to the other" (
WebView.setDataDirectorySuffix, API 28). Primary. Takeaway: the WebView that listens in the background and the one the person reads must be in the same process, and that process cannot be Qt's window process (which exits with its activity, section 1); so the chats need a process of their own with a plain Android activity to show them. - Which process. Sioul's coming foreground service runs in a process of its own, without Qt's window (
:steps, for the do-not-disturb shared between devices). Chats would add WebView's browser side (Chromium inside the app's process) to whichever process holds them::stepsitself (one process, one notification, heavier), or a:chatsprocess bound by it, since "if process A has bound to a Service with the Context.BIND_AUTO_CREATE flag … then process B's classification is always at least as important as process A's" (https://developer.android.com/guide/components/activities/process-lifecycle). Primary (the rule), inferred (the layout). Takeaway: keep the chats out of:stepsso its alarms stay light, and bind a:chatsprocess from it only while a chat is kept alive. - Chromium freezes a page it thinks hidden, on Android after one minute. Blink's
kStopInBackground("Freeze scheduler task queues in background after allowed grace time") is enabled by default on Android (not on desktop Android or Cast), and a page is frozen once it has been hidden and silent forkDefaultDelayForBackgroundTabFreezing = base::Minutes(1)(third_party/blink/common/features.cc;PageSchedulerImpl::UpdateFrozenStateandIsBackgrounded() = !IsPageVisible() && !IsAudioPlaying(), https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/platform/scheduler/main_thread/page_scheduler_impl.cc). WebSocket messages, timers and network events all sit in freezable queues (TaskType::kWebSocket→DeferrableTaskQueueTraits,SetCanBeFrozen(true),frame_scheduler_impl.cc); Chrome describes freezing as suspending "Event handlers (for example, input, network, and sensor), Timers, Promise resolvers" (https://developer.chrome.com/blog/freezing-on-energy-saver, January 2025). Nothing in WebView's code exempts it (no override inaw_field_trials.ccoraw_main_delegate.cc). Before freezing, hidden pages' timers are throttled: to once a second after 10 seconds, and chained timers to once a minute after 5 minutes hidden (https://developer.chrome.com/blog/timer-throttling-in-chrome-88). Primary (source); that a WebView's page freezes after a minute is inferred from that source and needs a phone. Chrome's field trials may set another delay (the delay is a feature parameter); a 2019 report gave five minutes for Chrome's tabs (https://www.xda-developers.com/google-chrome-android-stop-background-tabs/, report). Takeaway: a chat whose page is hidden stops receiving within about a minute, whatever Sioul's process does; it must not count as hidden. - When a WebView's page counts as hidden. Hidden when the WebView is paused (
onPause(), which Qt calls whenever the app is not active, section 1), or once it has been attached to a window and is now detached or its window is not visible. Never attached and not paused, it counts as visible: "When WebView is not paused, we declare it visible even before it is attached to window to allow for background operations. If it ever gets attached though, the WebView is visible as long as it is attached to a window and the window is visible" (BrowserViewRenderer::IsClientVisible, https://chromium.googlesource.com/chromium/src/+/refs/heads/main/android_webview/browser/gfx/browser_view_renderer.cc;AwContents.updateWebContentsVisibility,android_webview/java/src/org/chromium/android_webview/AwContents.java). Primary. Takeaway: a chat kept in a never-shown WebView runs as if on screen; one the person has looked at and closed is frozen a minute later, unless it is made again or kept attached. - Three ways around it, each with its failure.
- Make the page again when the person leaves it: the shown WebView is closed (the page let go properly first, see sites.md, "Closing", for Discord's token) and a never-attached one loads the chat in the background. Costs a full reload (seconds, data) each time; a chat that allows one open window (WhatsApp Web) must hand over from one to the other. Inferred.
- Keep it attached to an invisible window and say the window is visible: Cordova's background-mode plugin calls
view.dispatchWindowVisibilityChanged(View.VISIBLE)a second after going to the background, "to disable most optimizations done by Android" (disableWebViewOptimizations, https://github.com/katzer/cordova-plugin-background-mode/blob/master/src/android/BackgroundModeExt.java, unmaintained since 2024). Undone by the next real visibility change, lost when the activity is destroyed (a swipe from the recent apps), relies on undocumented behaviour. Report. - Play sound: an audible page is never frozen (
IsAudioPlaying()above). A hack that costs battery; refused here.
- Android must keep the process, and Doze must let it work. A foreground service keeps the process out of the cached state, and so out of Android's freezer. In Doze, Android "Suspends network access" and "Ignores wake locks" (https://developer.android.com/training/monitoring-device-state/doze-standby, updated August 2026), but AOSP exempts processes at or above a foreground service from both: network rules allow
procState <= FOREGROUND_THRESHOLD_STATE(PROCESS_STATE_BOUND_FOREGROUND_SERVICE,NetworkPolicyManager.isProcStateAllowedWhileIdleOrPowerSaveMode), and partial wake locks are disabled in idle only for processes below that state (PowerManagerService.setWakeLockDisabledStateLocked), both at https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/ . The WebView's renderer follows the app: "The default policy is to set the priority to RENDERER_PRIORITY_IMPORTANT regardless of visibility" (WebView.setRendererPriorityPolicy). Primary (code; the exemption is not in the Doze page itself). Takeaway: with a foreground service the chats keep their network in Doze; but with the screen off the processor sleeps unless something holds it awake, and then no timer of the page runs. - The processor sleeps; the chat's server does not wait. With the screen off and no wake lock, the page's timers do not fire, so its keep-alive messages stop; incoming data wakes the phone briefly. Discord's gateway, for one: "If your app fails to send a heartbeat event in time, your connection will be closed and you will be forced to Resume", every
heartbeat_interval(45,000 ms in the example), and "After Resuming, your app will receive the missed events in the same way it would have had the connection had stayed active" (https://docs.discord.com/developers/events/gateway; documented for bots, the web client uses the same gateway: inferred). Primary. Takeaway: in a sleeping phone a chat drops its connection and catches up at the next wake: news comes late, not lost (for chats that resume). - How apps without push keep a connection, documented in their code. Signal, without Google's push or in its "force websocket" mode, runs a foreground service and replaces its sleep timer with alarms (
AlarmSleepTimer;IncomingMessageObserver.kt, https://github.com/signalapp/Signal-Android/blob/main/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt). Element's F-Droid build: "the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync", noting that such alarms "will not be triggered more than every minutes under normal system operation, and when in low power mode about every 15 mn" (https://github.com/element-hq/element-android/blob/develop/docs/notifications.md). Android's own words on those alarms: "it will not dispatch these alarms more than about every minute …; when in low-power idle modes this duration may be significantly longer, such as 15 minutes" (AlarmManager.setExactAndAllowWhileIdle, AOSP), and the Doze page says "more than once per nine minutes, per app". Primary. Users of Signal without Google's services report needing the battery optimisation exemption and a higher drain (GrapheneOS forum, https://discuss.grapheneos.org/d/4442-signal-notification-issue-googleplay-services-without-logged-in, report). Takeaway: Sioul's coming foreground service already wakes every 2 to 15 minutes with alarms (the do-not-disturb's exchange); the chats can ride on those wakes: a few seconds awake for the pages to reconnect and deliver. - What it costs. No measured figure was found for a chat web app in an Android WebView. What is known: all chats share one renderer process (section 1); a page that counts as visible keeps its timers at full rate, and likely its animation frames (below). Sioul itself holds about 140 MB in the background (android.md). Needs a phone:
adb shell dumpsys meminfofor Sioul's chat process and the WebView's sandboxed renderer, anddumpsys batterystatsover a night, with one and with three chats. Takeaway: measure before promising; offer chats kept alive one by one, off by default, with their cost said. - Calls cannot ring from there. An incoming call in a chat's page needs the page awake at that moment, the microphone (an activity, section 1), and a ringing screen; a frozen or sleeping page misses it, and catching up later tells only of a missed call. Inferred. Takeaway: calls through a chat's web page are for when its page is open; Sioul should not promise to ring for them.
- What wrappers found. No open-source Android wrapper keeps a chat page connected for hours in the background (3.3): Native Alpha pauses its pages' timers when put away; android-webcord's author writes that a backgrounded WebView "stops receiving Discord gateway events" (agreeing with the freezing above); WhatsappWebToGo's maintainer judged the battery cost "to high". On the desktop, where pages are kept loaded and unthrottled (Station sets
backgroundThrottling: false), pages still die after a computer's sleep (ferdium-app #1092, open since 2023), and wrappers that unload idle pages lose their notifications until they wake them (Ferdium's hibernation; Wavebox's "auto-wake" every ~30 minutes). Phone makers that kill background apps regardless (https://dontkillmyapp.com/) add their own failures. Primary (code, documentation) and report. Takeaway: show, per chat, when it was last heard from, and reload a chat after a long sleep, saying it may have missed messages; never let silence pass for "nothing new". - Frames in the background. A never-attached WebView counts as visible, so it keeps WebView's 60 Hz begin-frame source as parent (
RootBeginFrameSourceWebView,begin_frame_source_webview.cc); a page that animates (requestAnimationFrame, animated emoji) may then run its animation code in the background with nothing on screen. Inferred; to measure.
5. GeckoView instead of WebView¶
Mozilla's engine as an Android library, the base of Firefox for Android, Fennec F-Droid and IronFox. Read in its sources (https://github.com/mozilla-firefox/firefox, mobile/android/geckoview/), its javadoc (https://mozilla.github.io/geckoview/javadoc/mozilla-central/) and changelog, Mozilla's Maven and Bugzilla.
5.1 Notifications, push, service workers¶
- Every web notification reaches the app. On Android, Gecko's alerts service (
AndroidAlerts.cpp) turns a page'snew Notification()and a worker'sshowNotificationalike into aWebNotificationhanded to the app'sWebNotificationDelegate.onShowNotification; the app answersshow()ordismiss(), and passes the person's tap back withclick(). AWebNotificationcarries the title, text, tag, icon address,requireInteraction,silent, vibration, actions, and the page'ssourceandorigin; it can be stored (Parcelable since version 100) and still clicked after the process died. The site's permission is answered by the app (PermissionDelegate,PERMISSION_DESKTOP_NOTIFICATION) and kept by Gecko. Primary. Takeaway: no stand-in needed: pages see a real browser, and Sioul holds what it is handed, with the site named by Gecko itself. - Web Push: Gecko does the page's side, the app brings the push service.
WebPushDelegate.onSubscribe(scope, appServerKey)must return aWebPushSubscription(an endpoint, P-256 keys and an auth secret "generated by the embedder"); a message arrives when the app callsWebPushController.onPushEvent(scope, decrypted data), which wakes the site's service worker, whose notification then comes back through the delegate (GeckoView's own test,WebPushTest.kt). Without a delegate every subscription fails. Primary. - Push without Google exists, outside Mozilla. Firefox for Android pushes through Mozilla's autopush bridged to Google's FCM; Mozilla closed UnifiedPush support as WONTFIX in June 2025 ("it doesn't sound like unified push is a direction we want to go in at the moment", https://bugzilla.mozilla.org/show_bug.cgi?id=1802846). Fennec F-Droid and IronFox carry it anyway through UnifiedPush-AC (MPL-2.0, https://gitlab.com/ironfox-oss/unifiedpush-ac); Peel does the same for single sites (3.3); a distributor app (ntfy, NextPush, Sunup, /e/OS's own) holds one connection for every app. Primary. Takeaway: for sites that use Web Push, GeckoView plus UnifiedPush delivers in the background with no page running and no foreground service, the cheapest route of all.
- Which chats use Web Push: Telegram's web apps do (both subscribe with
pushManager); WhatsApp Web has it in its code behind a gate, and a Peel user saw it never offered; Messenger's pages carry a push service worker; Discord's web app and Element's have none (section 6). Whether a site accepts an endpoint that is neither Google's nor Mozilla's must be tried per site. Primary (code) and report. - Service workers run (on by default), may open a window for a notification's click through
GeckoRuntime.ServiceWorkerDelegate.onOpenWindow, and a push needs only the runtime, not a visible view (inferred from code). OneGeckoRuntimeper process, enforced ("Only one GeckoRuntime instance is allowed"). Primary.
5.2 What it costs¶
- Size: the arm64 AAR of GeckoView 157 is 90.4 MB (Maven,
content-length90,433,476 bytes), 191.8 MB uncompressed, of whichlibxul.so152.4 MB; so about 90 MB more APK with compressed libraries (then extracted at install, about 265 MB on the phone) or about 190 MB uncompressed. Fennec F-Droid's whole arm64 browser is a 122 MiB APK. Primary (measured) and inferred (the arithmetic). - Android versions: minimum Android 8 (minSdk 26, under Sioul's Android 9); the current AAR requires compiling against API 37.1 (
minCompileSdk=37in its metadata), above Sioul'sandroid-36. Primary. - Processes and memory: Gecko's main process is the app's own (Sioul's, beside Qt and Rust, with
libxul.so); the AAR declares 89 services for child processes (content per site with Fission, GPU, network socket, media…). No published measure of memory per open site was found. Primary (manifest) and inferred. Takeaway: a Gecko crash or out-of-memory kill takes Sioul's window with it, unless Gecko lives in a process of its own (it must then own the chats' activity too, as in section 4); andlibxul.soloads beside Qt's and Sioul's libraries, so a library both bundle (SQLite, for one) must be checked for clashing symbols. - Licence: MPL-2.0 files may join a GPL-3.0-or-later program as a "Larger Work" (MPL 2.0 §3.3, GPL being a "Secondary License"), provided no file carries the "Incompatible With Secondary Licenses" notice; a search of Gecko's sources found it only in copies of the licence text itself (searchfox, 6 October 2026). Mozilla's FAQ (Q8, Q14): say where GeckoView's source is, for the exact version shipped. Primary. FSF's list agrees (unverified here: gnu.org did not answer).
- A proprietary dependency: Mozilla's GeckoView declares
com.google.android.gms:play-services-fido(Google's, for security keys) and imports it; Fennec and IronFox replace it with microG's free client (gecko-liberate.patch). GeckoView also queries Google Safe Browsing by default. Primary. Takeaway: exclude Google's library in Gradle and add microG's, decide Safe Browsing, and list Gecko's own connections in Sioul's privacy notes. - F-Droid: its inclusion policy accepts prebuilt libraries only from listed Maven repositories, and Mozilla's is not among them (
fdroidserver'sscanner.py); Fennec F-Droid builds Gecko from source in a recipe allowed ten hours, compiling LLVM too. A hand-installed APK, as Sioul is today, is unaffected. Primary. - Upkeep: Firefox moved to a release every two weeks from Firefox 155 (1 September 2026, https://blog.mozilla.org/sumo/2026/08/19/firefox-new-release-cadence-and-what-to-expect/), GeckoView on Maven with it, with no extended-support GeckoView on Maven; an incompatible change keeps the old code three releases (about six weeks now); Firefox 157 alone fixed 64 vulnerabilities (MFSA 2026-97). Between versions 139 and 157 none of the breaking changes touched notifications or push; the build tooling (compileSdk, Gradle plugin) is what moves. Primary. Takeaway: an engine showing other people's content must follow Mozilla's train: a rebuilt, re-signed APK every two to six weeks, with an update channel.
5.3 Inside a Qt for Android app¶
- The same door as Qt WebView:
GeckoViewis an AndroidView(aFrameLayout), so it can be wrapped in a Qt window withQWindow::fromWinId, as Qt WebView wraps Android's WebView (section 1); Qt removes such a native view when the application is hidden and puts it back when shown (qandroidplatformforeignwindow.cpp), and GeckoView releases its display on detach while its sessions keep running. Nothing Qt draws can cover it; GeckoView's default surface may need its texture backend (BACKEND_TEXTURE_VIEW) to compose with Qt's. That it works at all with Qt is inferred; no Qt host of GeckoView was found (Flutter and React Native have plugins). Primary (code) and inferred. - Gradle: Sioul's Android package folder can carry a
build.gradleadding Mozilla's Maven repository and the AAR; Qt 6.11's template already uses Java 17, which GeckoView needs; the manifest merge adds Gecko's services by itself. All GeckoView calls go through Android's UI thread. Primary. - Several chats: one session per site, shown in one view at a time; a hidden session can be made inactive "to significantly reduce its memory footprint"; a crashed or killed session is reopened from its saved state. Primary.
- Per-site settings:
GeckoSessionSettingssets a desktop user agent (USER_AGENT_MODE_DESKTOPoruserAgentOverride), a desktop viewport ("a width of 980 CSS px") and acontextIdthat "partitions the cookie jars … between sessions", so two accounts of one site can coexist (GeckoSessionSettings.java). Primary. - Chats in Gecko: calls fail in Telegram Web under Firefox (Bugzilla 1903659, assigned, September 2026) and call buttons are missing in Messenger's encrypted chats (Bugzilla 1896361), each with a Mozilla fix-up on Nightly only; Mattermost supports Chrome only on phones; the rest is as in section 6 (desktop user agents for Discord, Slack, WhatsApp Web and Messenger). Primary. Takeaway: GeckoView trades Chromium's quirks for Gecko's; calls in chat pages are unreliable in both.
What follows for Sioul: GeckoView solves the notification half of the problem cleanly (no stand-in, Web Push through UnifiedPush) and leaves the rest as hard as WebView (background life for sites without push, desktop user agents) while adding 90 to 190 MB, a two-week update duty and a FOSS dependency swap. It becomes worth it if the chats that matter to the person use Web Push.
6. Each chat's web version on a phone¶
Read in the services' own help pages, terms and shipped code (Telegram's and Element's web apps are open source), and by asking their servers with a phone's user agent and a desktop's (curl, 6 October 2026, logged out). Terms are quoted in section 9.
Telegram (web.telegram.org, versions K and A)¶
- Made for phones: Telegram lists both as "Mac, Windows, Linux, Mobile" (https://telegram.org/apps); version A has code paths for touch screens. No desktop user agent needed. Primary.
- Notifications from the page, with push when it can: version K shows
new Notification()(or the worker's) from the page and subscribes to Web Push; its worker drops a push while a tab is alive. Version A sends its notifications through its service worker when push is supported, elsenew Notification(); on touch screens it notifies for every chat but the one on screen (telegram-tt,src/util/notifications.tsx;tweb,src/lib/serviceWorker/). Primary. Takeaway: in a WebView (no push) both notify from the page, and the stand-in catches them; Telegram A notifies even while its page is in front. - An open API: third-party clients are welcome ("We welcome all developers to use our API and source code to create Telegram-like messaging applications on our platform free of charge", https://core.telegram.org/api/terms), with Sioul's own
api_id; TDLib (Boost licence) registers for push with a UnifiedPush endpoint ("Simple push … can also be used by an open source push notification spec like UnifiedPush", https://core.telegram.org/api/push-updates). New accounts using unofficial clients are "automatically put under observation" (https://core.telegram.org/api/obtaining_api_id). Primary.
Discord (discord.com/app)¶
- Not supported on phones: "Running Discord in a mobile browser is not supported" (system requirements, edited July 2026, https://support.discord.com/hc/en-us/articles/213491697). Its servers answer a phone's user agent like a desktop's (no redirect); what its scripts do with a phone's needs a phone; wrappers use a desktop user agent and get the desktop's three columns (3.3). Primary.
- Notifications from the page only: its bundle copies the browser's
Notificationonce at load and has no Web Push and no service worker for it; Ferdium needed to force Discord's own notification setting to see anything (3.3). Primary (shipped code). Takeaway: the stand-in must exist before Discord's first script; Discord in a WebView is the hardest case of all. - No open API for a person's messages: bots only; a person's account automated outside the bot API is a "self-bot", "forbidden, and can result in an account termination" (https://support.discord.com/hc/en-us/articles/115002192352). Primary.
Element and Matrix (app.element.io)¶
- Sends phones to the app stores unless told not to (
window.location.href = "mobile_guide/"for Android and iOS user agents, unlesssessionStorage.skip_mobile_redirectis set or the address carries a deep link,apps/web/src/vector/index.ts, https://github.com/element-hq/element-web). No desktop user agent needed. Primary. - Notifications from the page only:
new window.Notification(title, …)whenwindow.Notificationexists and is granted; its service worker serves media only, no push; it skips a room the person is viewing and active in; which events notify follows the account's push rules on the server (BasePlatform.ts,Notifier.ts). Primary. - An open protocol, the cleanest case: any client may log in; push through a pusher and a push gateway, which UnifiedPush's ntfy implements (Matrix specification, https://spec.matrix.org/latest/client-server-api/; Element's documentation on ntfy). A Matrix client inside Sioul (matrix-rust-sdk is Rust) needs no web page at all. Primary.
WhatsApp Web (web.whatsapp.com)¶
- Refuses phones at the server: a navigation with an Android user agent, WebView or not, is redirected (
302) to/mobile/; a desktop Chrome user agent gets the app (checked twice, 6 October 2026); WhatsApp: "WhatsApp Web is supported on Windows, Linux and MacOS" (https://faq.whatsapp.com/803865374003522). It also refuses a user agent with extra words (3.3). Primary. - Cannot replace the app: "You'll still need your primary phone to register your WhatsApp account and link new devices"; linked devices "will log out if your phone is unused for over 14 days" (https://faq.whatsapp.com/378279804439436; the page renders by script, its rule also reported by the press, https://www.slashgear.com/1283905/how-to-use-whatsapp-multiple-android-devices). Primary and corroborated. Takeaway: WhatsApp without its app on some phone is impossible; on the person's only phone, the listener (section 7) is the route.
- Notifications from the page, and push behind a gate: its code has page notifications (checking focus and visibility) and a Web Push subscription gated by an "offline notifications" switch (shipped code); a Peel user saw no push offered (3.3). Primary (code), report.
Messenger (facebook.com/messages)¶
- messenger.com retired for messaging in April 2026, Messenger on the web moving to facebook.com/messages, which needs a Facebook account (press reports quoting Meta, https://techcrunch.com/2026/02/19/meta-is-shutting-down-messengers-standalone-website/; on 6 October 2026 messenger.com still showed a login page and Meta's help still pointed there). Corroborated, with a conflict.
- Phones sent to the mobile site: facebook.com/messages with an Android user agent redirects to m.facebook.com, whose messages page is deprecated (
refsrc=deprecated); a desktop user agent stays. Facebook serves a push service worker (/sw?s=push). Primary (observed, logged out). - No API for a person's inbox; Meta's interoperability under the Digital Markets Act lets other messaging services reach WhatsApp and Messenger users in Europe, not a client reach the person's account (https://developers.facebook.com/m/messaging-interoperability/). Primary.
Slack (app.slack.com)¶
- Not for phone browsers: its requirements list desktop browsers only, and its sign-in redirects an Android user agent to the app download page (
302toslack.com/download) where a desktop one gets the page. Primary (help page and observed). - Notifications from open workspaces only: "You'll only get notifications for workspaces that you have open in your browser" (https://slack.com/help/articles/360001559367). Primary.
- An API, per workspace: a person may install their own app unless the workspace's owners require approval (https://slack.com/help/articles/222386767); Socket Mode gives events without a public server, with tokens pasted by the person; custom apps keep normal rate limits (Slack's changelog, 29 May 2025). The workspace's owner, often an employer, has the last word. Primary.
Mattermost (a server of one's own or one's employer's)¶
- Supported on phones: its requirements list "Mobile Web" on Android with Chrome (https://docs.mattermost.com/deployment-guide/software-hardware-requirements.html). Primary.
- Notifications from the page, unless the person is looking at that channel; levels per channel ("mention" by default). Primary (code).
- An open API: REST and WebSocket with the person's own login; personal access tokens only if an administrator allows them; push only through the server's own push proxy, so a third-party client keeps its own connection (https://docs.mattermost.com/deployment-guide/mobile/host-your-own-push-proxy-service). Primary.
Google Chat, Microsoft Teams, Instagram, Signal, XMPP¶
- Google Chat: "Chat in Gmail isn't supported in mobile browsers" (https://support.google.com/chat/answer/11488354); its API needs a Google Workspace account and the administrator's consent. Primary.
- Microsoft Teams: "Running Teams in a browser is supported on PCs and Macs …"; "On mobile devices we recommend that you use the Teams app" (https://learn.microsoft.com/en-us/microsoftteams/limits-specifications-teams); Microsoft Graph reads a person's chats for work accounts (
Chat.Read, which users may consent to themselves unless their organisation forbids it), and "Delegated (personal Microsoft account): Not supported" (https://learn.microsoft.com/en-us/graph/api/chat-list-messages). Primary. - Instagram: direct messages exist on the desktop web; on a phone's WebView, untested; its API serves professional accounts only; its terms read like Meta's (https://help.instagram.com/581066165581870). Primary and (unverified) for phones.
- Signal has no web version ("Signal is currently focused exclusively on the Android, iOS, and Desktop applications", https://support.signal.org/hc/en-us/articles/360007320051); third-party clients over its servers are tolerated, not sanctioned, and break when Signal's server changes (signal-cli: "The official Signal clients expire after three months"). Without Google's services Signal still works, with "delayed notifications or increased battery use" (https://support.signal.org/hc/en-us/articles/11229579171354). Primary. Takeaway: Signal stays a native app; the listener is the route.
- XMPP: an open standard with push (XEP-0357); like Matrix, a client inside Sioul would face no terms. Primary.
At a glance¶
| Chat | In a phone's WebView | Desktop user agent | Notifies from | Other clients | Open API |
|---|---|---|---|---|---|
| Telegram (K, A) | made for it | no | the page; Web Push when available | welcomed (own api_id) |
TDLib, with UnifiedPush |
| Element (Matrix) | after its "mobile guide" | no (a flag or a deep link) | the page only | the normal case | Matrix, with UnifiedPush |
| Mattermost | supported | no | the page only | yes (the server's rules) | REST and WebSocket |
| Discord | "not supported" | yes | the page only, behind its own setting | self-bots forbidden | none for a person |
| WhatsApp Web | refused by the server | yes, and nothing added to it | the page; push behind a gate | banned | none (DMA interop is for services) |
| Messenger (facebook.com) | sent to a deprecated mobile page | yes | the page (Ferdium's recipe catches it) and Web Push | forbidden | none |
| Slack | sign-in sends phones to the app | yes | the page, per open workspace | the workspace owner's rules | per workspace, an app of one's own |
| Google Chat, Teams | unsupported | yes | untested | the organisation's rules | Workspace or work accounts only |
| Signal | no web version | — | — | tolerated, unsanctioned | none official |
Mail that announces a chat¶
Sioul already shows "Open
- Slack: "These notifications are bundled and sent once every 15 minutes or once an hour, depending on your preferences", for mentions, direct messages and followed threads, when the person is not active (https://slack.com/help/articles/360025446073). Primary.
- Mattermost: mentions and direct messages "as soon as you're away from Mattermost for 5 minutes", batched every 15 minutes or hourly if the administrator enabled batching (https://docs.mattermost.com/end-user-guide/preferences/manage-your-notifications.html). Primary.
- Matrix servers running Synapse: notification mails if the server enables them (
enable_notifs, off by default), after "The time to wait before emailing about a notification" (notif_delay_before_mail, 10 minutes by default, https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html). Primary. - Discord has "communication emails"; users report them unreliable for missed messages (Discord's community forum). WhatsApp, Telegram and Signal send none. Report. Takeaway: for Slack, Mattermost and some Matrix servers, Sioul learns "it has news" from mail it fetches anyway, through the Porch's rules, at no cost in the background; late by 5 to 60 minutes, which suits gathered notifications.
7. The other route: Android's NotificationListenerService¶
Keep the chats' own apps and let Sioul read, hold and release their notifications. Read in AOSP (main, October 2026): NotificationListenerService.java, NotificationManagerService.java (NMS below), SnoozeHelper.java, NotificationAttentionHelper.java, under https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/ (core/java/android/service/notification/, services/core/java/com/android/server/notification/).
What it sees¶
- Every notification of every app, as the app posted it: package, id, tag, key, group, time, the
Notificationwith its extras, and a ranking (its channel, importance, whether it is a conversation and its conversation shortcut, whether do-not-disturb holds it:Ranking.isConversation(),getConversationShortcutInfo(),getChannel(),matchesInterruptionFilter()). Primary. - Conversations (
Notification.MessagingStyle): the messages (android.messages: text, time, sender,sender_person), the user (android.messagingUser),android.conversationTitle,android.isGroupConversation,android.people.list. APersonhas a name, a key ("If no key is provided, the name serves as the key for the purpose of identification") and a URI, which should be a contact's lookup URI, amailto:or atel:("The system will also attempt to resolve mailto: and tel: schema URIs",Person.java). All of it is what the posting app chose to put there: one app gives an address, another only a display name. Primary. Takeaway: Sioul can apply its senders' lists only where an app puts an address or a number in the notification; otherwise it has a name, and a name is a guess (section 8). - The person can narrow it: since Android 12 (API 31) a listener's access can be limited to some apps and some kinds (conversations, alerting, silent, ongoing) in Android's settings (
FLAG_FILTER_TYPE_*,META_DATA_DEFAULT_FILTER_TYPES,migrateNotificationFilter(…, disallowedPkgs)). Primary. Takeaway: Sioul can ask for the chat apps only, and say how to restrict it. - One-time codes may come redacted. From Android 15, a notification marked sensitive (a one-time code, as the phone's notification assistant judges) reaches an "untrusted" listener redacted: trusted means holding
RECEIVE_SENSITIVE_NOTIFICATIONS, being signed with the platform key, an app op, or a companion-device association (isAppTrustedNotificationListenerService,redactStatusBarNotification, flagredact_sensitive_notifications_from_untrusted_listeners). Primary; which notifications /e/OS's assistant marks needs a phone. Takeaway: Sioul would not see codes in other apps' notifications; it does not need them (it reads its own mail's codes, porch.md).
What it can do¶
- Cancel, unless the notification is ongoing:
cancelNotificationFromListenerLockedrefusesFLAG_ONGOING_EVENT(a call in progress, a foreground service, music). Primary. - Snooze for a time (
snoozeNotification(key, durationMs), API 26; levels from the SDK'sapi-versions.xml): the notification goes now and the system posts it again at the end, with its own exact alarm, which rings in Doze (SnoozeHelper.scheduleRepostAtTime:setExactAndAllowWhileIdle(RTC_WAKEUP, …)), not silently (repost(key, userId, false)→enqueueNotificationInternal(…)): it alerts again as its channel says. An update the app posts meanwhile stays snoozed, with the new content ("Ignored enqueue for snoozed notification",mSnoozeHelper.update); a cancel by the app (a message read on another device) removes it from the snoozed (mSnoozeHelper.cancel). A group's summary goes with its last child. At most 500 snoozed at once (CONCURRENT_SNOOZE_LIMIT). Only keys and times are written to disk, so after a restart the content is gone until the app posts again (inferred fromwriteXml).getSnoozedNotifications()lists them. Primary. Takeaway: snoozing until the next time a chat may reach the person keeps the app's own notification whole (its tap, its reply, its conversation), and Android brings it back on time even while Sioul sleeps. - Not before it sounds. NMS plays the sound and vibration (
mAttentionHelper.buzzBeepBlinkLocked) before it tells the listeners (notifyListenersPostedAndLogLocked), in the samePostNotificationRunnable; only a notification assistant may change a notification "before they are posted", and that service is a system API ("@SystemApi",NotificationAssistantService.java). Primary. Takeaway: a listener can only take a notification away after its first sound, vibration and banner; to hold an app quietly, its sound must be off beforehand. - Three ways to have it off beforehand.
- The app's channels set silent by the person in Android's settings (a listener cannot change another app's channels without a companion-device association,
updateNotificationChannel); Sioul can open the app's notification settings. Then held notifications never sounded, and released ones come back silent: Sioul's own gathered notification says that they wait, with sound if wanted. Primary (API), inferred (the flow). - A global hint: any enabled listener may ask that "the primary device UI should disable notification sound, but not phone calls" (
requestListenerHints(HINT_HOST_DISABLE_NOTIFICATION_EFFECTS)); NMS honours it for every app (disableNotificationEffects→ "listenerNoti"). For all apps at once, and whether banners still show needs a phone. Primary. - Sioul's do-not-disturb mode (android.md, "Pauses"): holds by category and starred people (
ZenPolicyhas no way to name apps), so it holds every app, not only the chats. Primary.
- The app's channels set silent by the person in Android's settings (a listener cannot change another app's channels without a companion-device association,
- What cannot be held: ongoing notifications (above); an incoming call, which the app rings itself or through a ringtone channel and a full-screen screen, with the notification often ongoing: hiding it does not stop the ringing (inferred); and nothing at all while Sioul lacks the access.
- Showing it again as Sioul's: a PendingIntent taken from another app's notification can go into Sioul's own. When any app posts a notification, NMS gives every PendingIntent in it the right to start an activity from the background when sent from that notification (
setPendingIntentAllowBgActivityStarts(…, ALLOWLIST_TOKEN, FLAG_ACTIVITY_SENDER | …), inenqueueNotificationInternal), whoever made the intent; Android's documentation lists the case among the allowed background starts: "The activity is started from a PendingIntent that was sent by the system (for example, from a notification tap)" (https://developer.android.com/guide/components/activities/background-starts). A PendingIntent outlives the notification it came in; it fails if its app cancels or replaces it (FLAG_ONE_SHOT,FLAG_CANCEL_CURRENT, the app stopped or removed). Primary (code); that a tap on Sioul's copy opens Discord's conversation on Android 14 and 15, whose background-start rules changed, needs a phone. Sioul's copy loses the app's conversation shortcut (the "Conversations" section, bubbles) and should not copy its reply actions. Takeaway: prefer snoozing the original; when Sioul shows its own summary, a tap that opens the app at the right place is likely but must be tried.
Its costs¶
- The access: "Notification access" (Settings ▸ Apps ▸ Special app access ▸ Device & app notifications). For an APK installed by hand, Android 13 and later grey it out until the person opens Sioul's App info ▸ ⋮ ▸ "Allow restricted settings": Android 15's enhanced confirmation lists
OPSTR_ACCESS_NOTIFICATIONSamong its protected settings and treats installs from a local or downloaded file as guarded (EnhancedConfirmationService.java, https://android.googlesource.com/platform/packages/modules/Permission/+/refs/heads/main/service/java/com/android/ecm/EnhancedConfirmationService.java); Android 13's rule was the same for installers not using sessions (Esper, https://www.esper.io/blog/android-13-sideloading-restriction-harder-malware-abuse-accessibility-apis, report). An app installed from a store preinstalled on the phone (/e/OS's App Lounge, F-Droid as privileged extension) is not guarded. Primary. Takeaway: the setup must walk the person through two screens, said plainly, with buttons to each. - Privacy: Sioul would receive every notification of every app the person did not exclude, messages' text included. Kept on the phone, never shared between devices, never logged; the person narrows it to the chat apps (above). Android notes listeners that clear or snooze a notification within five seconds of its posting (
OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,NOTIFICATION_RAPID_CLEAR_THRESHOLD_MS = 5000), a defence against malware hiding codes; what reads that record is not in AOSP. Primary. - Running all the time: the system binds an enabled listener and keeps its process alive ("too many 3P apps could be kept in memory as notification listeners and cause extreme memory pressure", the comment above
BIND_NOT_PERCEPTIBLEin NMS'sgetBindFlags). Primary. Each notification costs a call into Sioul; with the listener in a small process of its own, without Qt (as the doses' alarms load Sioul's library alone), the cost is low (inferred; to measure). - The apps stay: their trackers, ads and own push transport (Google's, through microG on /e/OS) stay too; what the listener gains is the timing, not the privacy.
- On /e/OS: the listener API is AOSP's, unchanged; what differs is upstream of it. The native apps' push goes through microG's cloud messaging, so an app whose push microG does not deliver posts nothing to hold (reports for Proton Mail, 8.3); apps installed from App Lounge, which /e/OS preinstalls, are not "guarded" by Android's restricted settings while Sioul's hand-installed APK is (above); which notifications /e/OS's notification assistant marks sensitive (redacted to Sioul from Android 15) is unknown. Primary (AOSP) and needs a phone.
8. Three apps through the listener: the browser, Discord, Proton Mail¶
The owner's addition: "If Sioul can filter notifications from other apps, the web browser, Discord and Proton Mail are natural candidates." What a listener can tell from each, read in their code where it is open (Chromium, Firefox, Proton Mail) and in documentation and reports where it is not (Discord). Section 7 says what a listener can do with any notification.
8.1 The browser's notifications for sites¶
- Chromium's browsers name the site in three places. Chrome posts every site notification with the id
-1and a tag made of the origin:p#<origin>#…for a service worker's (push) notification,n#<origin>#…for a page's (notification_id_generator.cc, https://chromium.googlesource.com/chromium/src/+/refs/heads/main/content/browser/notifications/notification_id_generator.cc; Chrome itself reads the origin back from the tag,NotificationPlatformBridge.getOriginFromNotificationTag). Each site has its own channel,web:<origin>;<creation time>, in the group of channelssites(SiteChannelsManager.java); the notification's sub-text is the host and its groupWeb:<host>. No MessagingStyle, no Person: the title and text are whatever the site wrote. Primary. Takeaway: per site, reliably: hold Discord's site and let the bank's through, by origin. - Firefox's browsers name it once. Firefox, Fennec F-Droid and IronFox post every site notification on one channel (
mozac.feature.webnotifications.generic.channel, "Site notifications"), with a tag that is a hash, and the site only in the sub-text, as a host (NativeNotificationBridge.kt,AndroidAlerts.cpp, in https://github.com/mozilla-firefox/firefox). Primary. Takeaway: per site by host; two sites on one host cannot be told apart. - A site installed as an app (a WebAPK, a Trusted Web Activity) notifies under its own package, not the browser's (
NotificationPlatformBridge.java); /e/OS's browser makes them underfoundation.e.webapk. Primary. - Background push on /e/OS: almost none. /e/OS's browser (
foundation.e.browser, a fork of Cromite on Chromium 153, its README andversions.txt, https://gitlab.e.foundation/e/os/browser) and Cromite remove Google's push (GCM) and return no push service at all (Cromite'sRemove-binary-blob-integrations.patch; its FAQ: push notifications "will not work in Cromite because cloud integrations are disabled"); Cromite also turns page notifications off by default, allowed per site (its maintainer, cromite#2096). Chrome over microG: Web Push reported broken since 2018, microG not passing the message's "subtype" (GmsCore#357, #668; report and inferred from microG's code). Mozilla's Firefox pushes only through Google's FCM; Fennec F-Droid and IronFox through UnifiedPush, off by default, with breakages reported on /e/OS in 2026 (its built-infoundation.e.ntfydistributor). Primary (code, maintainers) and report. Takeaway: on /e/OS a site notifies through the browser mostly while its page is open; for sites in the background, a Firefox fork with UnifiedPush. - Hold by snoozing, not by cancelling, in Chromium's browsers. A cancel from a listener sends the notification's delete intent; Chrome then deletes the notification from its database, and a later tap finds nothing and closes it (
persistent_notification_handler.cc,notification_event_dispatcher_impl.cc). Firefox's notifications have no delete intent and their tap is handled from the origin it carries (AndroidAlerts.cpp,WebNotificationIntentProcessor.kt). Inferred from code; needs a phone. Takeaway: a snoozed Chrome notification keeps its tap; a cancelled one copied by Sioul would open nothing. - Verdict. Reliable (code): the site's origin in Chromium's tag and channel, its host in Firefox's sub-text; holding one site and not another. A guess: that /e/OS's browser and Cromite post as Chrome does (no patch of theirs touches the notifications, by the patches' names). Needs a phone: whether /e/OS's browser shows page notifications at all by default; Chrome or Firefox push over microG; Fennec or IronFox with /e/OS's distributor; a copied Firefox tap launched from Sioul's notification.
8.2 Discord (com.discord)¶
- Closed source (React Native since 2022): what a listener sees comes from documentation and reports.
- Its categories, documented by name: server messages ("Messages"), direct messages, friend requests, reaction notifications, friend activity, forum notifications, voice connected; "Allow full-screen notifications" and "Integrate calls with Phone app" for calls; "When someone sends you several messages in a row, Discord always groups them into one tidy notification" ("[Mobile] Notifications Settings 101", edited July 2026, https://support.discord.com/hc/en-us/articles/218892547; Discord's help pages refuse scripted reads, so the text was read through its public help-centre API). The channel ids behind the names are not documented. Primary.
- One public dump of a server message (2022, through Home Assistant's notification sensor): channel
messages, groupGROUP_MESSAGE_CREATE, categorymsg, MessagingStyle, conversation title "# ", a group conversation, each message's sender a display name, the user's own Person keyed me(https://github.com/home-assistant/core/issues/79867). Report. - Conversations: users report since 2020 that direct messages, and since late 2021 every server channel, appear in Android's Conversations section, which needs a MessagingStyle notification with a conversation shortcut (Discord's community forum, 2020 to 2024; Android: https://developer.android.com/develop/ui/views/notifications/conversations). Corroborated.
- Push: Google's FCM; on /e/OS through microG's cloud messaging, with the device registered (GmsCore#2080). No other transport documented. Report.
- Verdict. Reliable: the package, MessagingStyle, the categories by name; holding a notification by its key. A guess: the channel ids (
messagesseen once), the conversation title's form, the shortcut id per channel or conversation, senders known by display name only (no address, no Discord id seen). Needs a phone:adb shell dumpsys notification --noredactwith Discord to list channel ids, tags, groups, shortcut ids,Person.keyanduri; how a mention differs from a plain message; how a call looks; whether a snoozed conversation comes back whole after Discord re-posts it for the next message. Takeaway: Sioul can hold Discord by kind (server, direct message, call) and by conversation, not by person in the sense of its senders' lists; a list of Discord names would be a list of guesses.
8.3 Proton Mail (ch.protonmail.android)¶
- Open source: the app of 2026 is built from https://github.com/ProtonMail/android-mail (GPL-3.0, read at the 7.11.5 release, August 2026), on a Rust core shipped as a binary library. Primary.
- The push is sealed; the app opens it. Google's FCM carries a session id and an encrypted message; the app decrypts it on the phone into the sender (name, address), the subject and the message id (
PMFirebaseMessagingService.kt,DecryptPushNotification.kt). Primary. - What the notification shows: channel
v7_email_channel_id("Emails", high importance; login alerts onv7_login_channel_id); one notification per message (no tag), grouped per account with a summary; title = the sender's display name, the address only when the sender gave no name (sender.senderName.ifEmpty { sender.senderAddress }); text = the subject; sub-text = which of the person's addresses received it; actions Archive, Trash, Mark as read; a tap opens the message (ProcessNewMessagePushNotification.kt,NotificationProvider.kt). No MessagingStyle, no Person, nomailto:anywhere. Primary. Takeaway: a listener usually gets a name, a subject and the receiving address, not the sender's address. - Read elsewhere: Proton removes a notification when the mail is read on another device, but only if it finds it among its active ones; a notification Sioul has snoozed is not, so it may come back at the end of the snooze for a mail already read (
DismissEmailNotificationsForUser.kt). Inferred from code. - Push only through Google: FCM, no UnifiedPush, no polling of new mail (Proton: "We currently use Google Play Services to implement push notifications", https://proton.me/support/not-receiving-push-notifications); with microG, reports conflict: an issue open since March 2026 says no notifications arrive, another user says they do (GmsCore#3301). Primary and corroborated, contradicted once.
- Sioul's senders' lists and times on it. Holding and releasing: yes (one notification per mail, a summary per account). The receiving address (sub-text) is reliable, so a work address's mail can wait for work time. Senders: an address or pattern matches only when the title is an address; otherwise a display name, which anyone can choose ("PayPal" from any domain), matched against contact names at best. So an unmatched or name-only Proton notification should count as neutral, never as safe. The address-exact path is Proton Mail Bridge (IMAP on the computer, paid plans, no Android version, https://proton.me/mail/bridge): Sioul on the computer then applies its lists to Proton mail as to any mail. Primary (code, Proton's pages) and a guess (names). Takeaway: on the phone, Proton Mail is "a mail app whose sender is a name": route by receiving address, hold by time, judge senders only on the computer.
- Verdict. Reliable (code): the package, the channels, one notification per mail grouped per account, title = display name, text = subject, sub-text = receiving address. A guess: matching a display name to the senders' lists or a contact's category; nothing at all to match when the person turned Proton's "extended notifications" off (the title is then "New message"). Needs a phone: whether Proton's push arrives at all on /e/OS with microG; the extras a listener really sees; a snoozed notification for a mail read meanwhile on the computer.
8.4 Showing them again with their own tap¶
- Snoozing keeps everything: the app's own notification comes back as it was (its tap, its actions, its conversation), reposted by Android at the time given (section 7). Primary.
- Cancelling and posting Sioul's own copy with the app's
contentIntent: Android allows it ("The activity is started from a PendingIntent that was sent by the system (for example, from a notification tap)", https://developer.android.com/guide/components/activities/background-starts, updated May 2026), and NMS gives every PendingIntent in a posted notification the right to start an activity when sent from it, whoever made it (section 7). It fails when the app has cancelled or replaced the intent, and in Chromium's browsers, whose cancelled notifications are forgotten (8.1); Proton's tap is an immutable deep link to the message, Firefox's carries the site's origin, so both should survive (inferred). Needs a phone on Android 14 and 15. Takeaway: snooze by default; copy the tap only into Sioul's gathered notification ("Discord (3) · Proton Mail (1)"), where opening the app is enough.
9. Terms, account risks, ethics¶
Not legal advice: what each service's own words say about the ways Sioul could use it, read on 6 October 2026.
9.1 What the terms say¶
- Discord (terms effective 29 September 2025, https://discord.com/terms): "You may not copy, modify, create derivative works based upon, distribute, sell, lease, or sublicense any of our software or services. You also may not reverse engineer or decompile our software or services"; among the restrictions, "scraping our services without our written consent, including by using any robot, spider, crawler, scraper, or other automatic device, process, or software" and "using any unauthorized software designed to modify the services". Self-bots: "Automating normal user accounts … is forbidden, and can result in an account termination if found" (https://support.discord.com/hc/en-us/articles/115002192352). Nothing about user agents; Discord itself once told a user to "use your mobile browser's desktop mode" (@discord on X, 2018). Primary.
- WhatsApp (EEA terms effective 17 August 2026, https://www.whatsapp.com/legal/terms-of-service-eea): "you must not directly or through automated means: (a) reverse engineer, alter, modify, create derivative works from, decompile, or extract code from our Services"; "Using these apps or linking your WhatsApp account to unofficial versions of WhatsApp violates our Terms of Service", with temporary or permanent bans and "restrictions on your account, including the ability to link devices" (https://faq.whatsapp.com/1217634902127718). Primary.
- Telegram welcomes third-party clients under its API terms (https://core.telegram.org/api/terms): an
api_idof one's own; no "Telegram" in the app's name unless after "Unofficial"; "It is forbidden to interfere with the basic functionality of Telegram", read receipts, online and typing statuses included; and a client that shows channels "must include support for official sponsored messages". Primary. - Meta (Messenger) (https://www.facebook.com/legal/terms): "You may not access or collect data from our Products using automated means (without our prior permission)", and modifying or reverse engineering needs written permission. Primary.
- Slack: the workspace's owner sets the rules (user terms, https://slack.com/terms-of-service/user); the API terms forbid using the APIs "to replicate or compete with the Services" (https://slack.com/terms-of-service/api). Matrix and Mattermost: each server's operator decides; third-party clients are the normal case in Matrix (matrix.org's terms say nothing against them). Primary.
- Telegram also welcomes changes to its own clients: "Developers are welcome to add new features or improve and extend existing Telegram features provided that these modifications do not violate these Terms" (API terms 1.2). Instagram restricts accounts for "automating access to, or collecting information in an automated way without our permission" (https://help.instagram.com/740480200552298). Primary.
- EU law on studying a program: "The person having a right to use a copy of a computer program shall be entitled, without the authorisation of the rightholder, to observe, study or test the functioning of the program … while performing any of the acts of loading, displaying, running, transmitting or storing the program which he is entitled to do", and contracts cannot take that away (Directive 2009/24/EC, articles 5(3) and 8, https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32009L0024). Primary. It protects learning how a page calls the notification API; it does not stop a service from closing an account under its own terms (inferred, not legal advice).
9.2 What it means for each way¶
| Way | Discord | Telegram | Messenger | Slack | Matrix, Mattermost | |
|---|---|---|---|---|---|---|
| The official web page in Sioul's view, a desktop user agent | unsupported, not forbidden | the official web app; the user agent is not named | allowed | not named | unsupported on phones; the owner's rules apply | allowed |
| A stand-in that only listens to the page's notifications | grey: arguably not "modify the services" | grey ("alter, modify") | allowed | grey | allowed | allowed |
| Changing the page's behaviour or reading its internals (Ferdium's Discord hack, reading stores) | "unauthorized software designed to modify the services" | "alter, modify" | allowed if it leaves statuses alone | forbidden without permission | the owner's call | allowed |
| A client of Sioul's own over an API | self-bot: forbidden, bans reported for unusual calls | forbidden, actively banned | allowed, own api_id, observed at first |
no API | own app per workspace | allowed |
| The listener over the native app | outside the terms: the phone's notifications are the person's | same | same | same | same | same |
The cells read the clauses of 9.1 (inferred, not legal advice); none of the services was asked.
- Bans in practice: none reported for a plain web wrapper of Discord (wrappers' trackers, 3.3); bans reported for unofficial Discord clients making unusual API calls (cordless's author; Arch Wiki: third-party clients "may get your account banned"); WhatsApp bans unofficial apps by name (GB WhatsApp, FM WhatsApp…); Telegram puts new unofficial-client accounts "under observation" and gives an address to recover a wrongly banned one. Primary and corroborated. Takeaway: the official page unchanged, with a stand-in that only listens, carries the least risk; anything that acts for the person (an API client for Discord or WhatsApp, a page hack) puts their account at stake, and Sioul should refuse it.
9.3 Ethics, as Sioul holds them¶
- Holding a message is the person's choice of when to read, which no service's terms touch; what Sioul must not do is change what others see: read receipts, typing, online status (Telegram's terms say it outright, and it is good manners everywhere).
- A held message can be urgent. Sioul's rules already let safe senders and calls through (porch.md, areas.md); for chats, where the sender is a display name anyone can take (Discord, Proton Mail on a phone, 8), "always let through" should be given per conversation (a Discord direct message, a group), which the listener can identify, rather than per name, which it cannot.
- Seeing every notification is a trust Sioul must not stretch: ask for the access only for the apps it filters, say plainly what it sees, keep it on the phone, never log content.
- No dark patterns of its own: what waits is counted honestly ("Discord (3)"), never hidden for good, and a chat that went silent because its page or push died says so.
9.4 Accessibility¶
- A WebView is as readable as Chrome: TalkBack reads web pages through Chromium's accessibility layer, the same in Chrome, WebView and custom tabs ("Occasionally there will be an edge case or small bug that only happens in WebView", https://chromium.googlesource.com/chromium/src/+/HEAD/docs/accessibility/browser/android.md). How well a chat reads then depends on its own markup. Primary.
- Chats design their web screen-reader support for desktops: Slack asks screen-reader users to use "Slack's desktop application rather than a web browser if possible" (https://slack.com/help/articles/360000411963); Discord names "the desktop and browser clients" (https://support.discord.com/hc/en-us/articles/7180791233559). A desktop layout on a phone also misses Android's touch targets ("at least 48dpx48dp", https://developer.android.com/guide/topics/ui/accessibility/apps). Primary; the effect on TalkBack users is inferred.
- Native apps are not flawless either: Telegram's tracker lists TalkBack problems ("[TalkBack] Not possible to answer or decline a call", 2020; more in 2025, https://bugs.telegram.org/?tag_ids=29); Qt Quick has had TalkBack-breaking bugs on Android (QTBUG-102825, "even freezing TalkBack for the whole device", fixed since). Primary. Takeaway: for a person using TalkBack, the listener over native apps (E) and chats drawn by Sioul itself (Matrix, F) keep accessibility where it is best; desktop-mode pages are the worst case. Sioul's window with a native chat view must be tried with TalkBack (focus moving between Qt's items and the page).
Options for Sioul¶
| Option | Gives | Costs | Risks | |
|---|---|---|---|---|
| A | Qt WebView in Sioul's window | a chat inside the window | the add-on module, little code | no calls, files, downloads, sign-in pop-ups, notifications; dies with the window; a renderer crash ends Sioul (1) |
| B | A chat host of Sioul's own: Android's WebView with AndroidX WebKit, in a process of its own, a plain Android activity, one profile per chat, opened when the person opens a chat | chats without their apps while open; calls (permissions), files, downloads, sign-in pop-ups; notifications caught while open (3) | a Java part to write and keep: activity, permissions, file chooser, downloads, the stand-in, user agents per chat | Discord, WhatsApp Web, Messenger and Slack unsupported on phones (desktop layout); a few breakages per chat a year (3.3) |
| C | B, kept alive in the background | news from chats while Sioul is away, through its rules | a foreground service (Sioul's coming one), never-attached WebViews, a reload after each look; memory and battery unmeasured | battery; late news when the phone sleeps; WhatsApp Web needs its app on a phone anyway; calls cannot ring (4) |
| D | GeckoView for B or C | real notifications to Sioul (no stand-in); service workers; Web Push through UnifiedPush where a site uses it, with no page running (5) | 90 to 190 MB more APK; Gecko inside Sioul's process; an update every two weeks; microG's FIDO library instead of Google's | the chats that matter most (Discord, Element, WhatsApp Web) have no usable Web Push; calls in Telegram and Messenger broken in Gecko |
| E | A NotificationListenerService over the apps the person keeps (Discord, WhatsApp, Signal, Proton Mail, the browser…) | every app held until its time (snooze), released by Android on time even while Sioul sleeps; the apps' own push and calls (7, 8) | "Notification access" (a restricted setting for an APK installed by hand); the held apps' channels set silent by the person; a small listener process | Sioul sees every notification (narrowable per app); apps change their notifications; the apps' trackers stay; senders known by name only |
| F | Open protocols inside Sioul: Matrix first (matrix-rust-sdk), then Mattermost's API, maybe Telegram's TDLib | real accounts as senders, so the lists and the matrix apply; push without Google (UnifiedPush); no web page (6) | a client per protocol: reading, replying, Matrix's encryption keys | scope: Sioul grows a chat client; Telegram's terms (own api_id, sponsored messages in channels) |
| G | Mail that announces a chat (Sioul's announced_by) |
"Slack has news" through the Porch's rules, at no cost (6) | mapping senders to sites | late (5 to 60 minutes); only Slack, Mattermost and Matrix servers that send them |
| H | A Custom Tab of the person's browser, opened from Sioul, with the listener holding that browser's site notifications | the chat inside Sioul's task with the browser's logins, files and calls; per-site holding (8.1) | almost no code beyond E | no script of Sioul's in the page; push only if the browser has it (on /e/OS: Fennec or IronFox with UnifiedPush, not the default browser) |
Recommendation¶
- E first. It reaches the aim that matters most, Sioul deciding when messages reach the person, for every chat at once, with the apps' own push and calls, and without a web engine. A listener in a small process of its own (Rust decides, as for the doses' alarms); snooze as the one way to hold, so each app's notification comes back whole and on time; the person narrows the access to the apps Sioul filters and sets their channels silent, both guided with buttons to Android's pages; "always through" given per conversation, not per name; calls never held. It cannot remove the apps' trackers; WhatsApp cannot lose its app anyway (6).
- G at once: nearly free, through the Porch.
- F for Matrix next: the one chat where leaving the app costs nothing in terms, push or reliability, and where senders are real accounts the lists can name.
- H before B for a chat the person wants without its app: a Custom Tab costs almost nothing once E exists, keeps the browser's logins, and lets the listener hold that site's notifications, when the person's browser can push (on /e/OS: Fennec or IronFox with UnifiedPush). B, never A, where Sioul must catch notifications in the page itself (Telegram, Element until F, Discord in desktop mode, Messenger on facebook.com), opened on demand. C only per chat, off by default, after measuring memory and battery on a phone, and with "last heard at" shown per chat.
- D not now: test it the day a chat that matters accepts a UnifiedPush endpoint for Web Push (Telegram first), since then GeckoView would deliver with no page running.
Open questions¶
- Freezing in a real WebView: does a page whose WebView was attached and then detached freeze after a minute on /e/OS's WebView (Chromium 148 to 153, Cromite-based), as Blink's source says, and does a never-attached page keep running in Doze with a foreground service? How much battery per chat per night, and per animated page?
- AndroidX WebKit on /e/OS: which features its WebView supports (
DOCUMENT_START_SCRIPT,WEB_MESSAGE_LISTENER,MULTI_PROFILE,JS_INJECTION_IN_FRAME_AND_WORLD), checked at run time. - Per chat, with the stand-in: does Discord notify at all without its desktop shell (Ferdium needed a hack), does Telegram A fall back to the page, do WhatsApp Web and Mattermost notify when the page says it is hidden, what does each do with
navigator.permissions? - Discord and WhatsApp Web on a phone in desktop mode: sign-in (captcha, QR), usability on a small screen, account flags; WhatsApp's linked-device rules with the WebView as a linked device.
- The listener, on a phone: Discord's channel ids, tags, shortcut ids and
Personkeys; how a mention and a call look; whether a snoozed Discord conversation or Proton mail comes back stale; whether held apps with silent channels stay silent, and whether the global hint leaves banners; whether a copied tap opens the right place on Android 14 and 15. - /e/OS specifics: microG delivery for Discord and Proton Mail today; which notifications /e/OS's assistant marks sensitive; the restricted-settings path for Sioul's APK; the browser's default for page notifications; whether a chat opened in a Custom Tab of Fennec or IronFox (option H) keeps its Web Push through UnifiedPush.
- GeckoView: memory with five chats; whether Telegram accepts a UnifiedPush endpoint; startup time of the runtime in the background; whether
QWindow::fromWinIdcomposes GeckoView's surface with Qt's. - Terms: whether the owner accepts the grey zone of a listen-only stand-in on Discord and WhatsApp Web (9.2); none of the services was asked.
- Accessibility: TalkBack in each chat's web page inside a WebView against the native apps; not tested.
Links checked¶
All 105 links of this note were requested on 6 October 2026: 85 answered at once. Android's developer pages redirect scripted requests and were read through a browser fetch; Discord's and Signal's help pages refuse scripts (403) and were read through their public help-centre APIs (/api/v2/help_center/…); Meta's pages (facebook.com, whatsapp.com, instagram.com, developers.facebook.com) refuse scripted requests (400) or render by script: their wording comes from the research agents' reads and is corroborated where marked; one GitHub page was rate-limited and read as a raw file. gnu.org did not answer (FSF's licence list, marked unverified in 5.2).