9.2 KiB
Notifications & Focus redesign — two tabs, nothing unbounded
Approved mock: home-mocks/notifications.html (scratchpad, :8642). This spec is the
implementation contract; where mock and spec disagree, the spec wins.
Goals
- Bound the app list. Recent senders + customized apps render up top; everything else sits behind a collapsed, searchable "All apps" expander. Rows expand in place to their controls.
- Real per-app rules: sound on/off, banners-vs-history, urgency override, forget.
- A real focus-mode editor: create/rename/delete/reorder (order IS priority), trigger-kind editing for all five kinds, schedules with day pills, chip-based interrupt lists.
- Two tabs: Notifications | Focus, as category tabs like Shell.
Non-goals: lock-screen privacy (hyprlock cannot render notifications; contract-banned), per-app counters (no data), time-based history retention, merging duplicate app identities (show the raw id honestly instead).
Rule shape (pinned — service and UI program against this)
normalizedAppRule in Notifs.qml grows from { enabled } to:
| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true | Off = rejected before tracking/history/unread/toast (unchanged) |
sound |
bool | true | false = playBell skips this app |
display |
string | "banners" |
"history" = file in history + unread, no popup, no bell |
urgency |
string | "auto" |
"low" / "critical" override what the app claims |
lastSeenMs |
int | 0 | stamped in rememberApplication on every notification |
name |
string | "" | display name cached at remember time (resolution stays live-first) |
icon |
string | "" | icon cached at remember time (DesktopEntries lookup, else appIcon) |
Unknown/stale fields (incl. the old lock-screen pair) keep being dropped on read. Old
{enabled}-only blobs stay valid — every new field is optional with the defaults above.
New API: forgetApp(appId) deletes the rule key outright.
effectiveUrgency(notification) — the app's urgency override applied over
notification.urgency; consumed by playBell (low = silent), the popup timeout choice
(critical duration), the DND breakthrough gate, and NotificationCard's critical edge.
Critical breakthrough: new schema key criticalBreaksThrough (bool, def false, group
notifications, label "Critical alerts break through"). Popup gate becomes: show when
!doNotDisturb || FocusModes.allows(appId) || (Settings.criticalBreaksThrough && effectiveUrgency(n) === critical).
FocusModes API additions (pinned)
createMode(name)→ new mode{ id: unique slug, name, enabled: true, triggers: [{kind: "manual"}], silence: true, keepAwake: false, allow: [] }, appended (lowest priority).removeMode(id),renameMode(id, name)(non-empty, trimmed).moveMode(id, delta)— reorder; order is priority and the UI says so.setTriggerKind(id, kind, fields)— replaces the mode'striggerswith one trigger of the new kind. Kind-specific seeds: schedule →{ start: "22:00", end: "07:00", days: [0..6] }; workspace →{ id: 1 }; fullscreen/game/manual → no fields. (Shipped modes each carry one trigger; a hand-edited multi-trigger mode collapses to one on first kind change — the editor editstriggers[0]and that is documented in a comment.)- Manual-mode semantics: investigate how a manual-trigger mode activates today and PRESERVE
it exactly; if manual modes currently have no activation path besides
enabled, the header toggle keeps meaningenabledand the editor's "Turns on: Manually" detail explains that a manual mode quiets things whenever it is switched on. Do not invent new activation machinery. - Existing
setEnabled/update/reschedule-style schedule + day editing semantics stay;withinWindow, single-DND-ownership, and the gaming report-don't-silence rule are contract-pinned and untouched.
Routing (pinned)
SettingsRoutes category notifications gains tabs:
[{ page: "notifications", label: "Notifications" }, { page: "focus", label: "Focus" }].
New leaf focus → new FocusPage.qml; SettingsShell case + Component. groupPages
"focus" moves "notifications" → "focus" (the focusModes entry renders there now).
GamingPage's "Open Focus" action retargets openSettings("focus").
NotificationCard's "Notification settings" jump stays "notifications".
Notifications tab (NotificationsPage.qml rebuilt)
Lede unchanged. Cards:
- Quiet — Do Not Disturb toggle; "Critical alerts break through" ToggleRow
(
criticalBreaksThrough); "Quiet hours" ActionRow whose detail states the Sleep mode's live schedule (or "not scheduled" when Sleep lacks/disabled) and whose button opens the Focus tab (openSettings("focus")). - Banners & history — the four existing schema sliders (names contract-pinned; critical zero renders "Never"); history count + Clear folded into the history row.
- Applications — subtitle per mock. Sections:
- Recent: apps with
lastSeenMswithin 7 days, newest first. - Customized: any app with a non-default field (and not already in Recent).
- All apps (N): collapsed expander with an inline search field; alphabetical. Rows: cached icon (fallback letter tile), name, subtitle (relative last-seen when known · state summary like "sound off"/"History only", else the raw appId), enabled toggle, chevron. Expanded body: Play sound toggle · "Show as" dropdown (Banners & history / History only) · "Urgency" dropdown (App decides / Treat as low / Treat as critical) · "Forget this app" ActionRow (detail: "Remove its rule; it returns on its next notification").
- Recent: apps with
- Active-mode banner at top (ok-tinted) when a focus mode is active: " is quieting notifications · because · ", button "Open Focus".
Focus tab (FocusPage.qml, new)
Lede: "Modes quiet this machine on their own terms — first matching mode wins, and the order below is the priority."
- Focus modes card — accordion (one open at a time): drag grip (reorder = priority; also
keyboard up/down on the grip), mode glyph, name, summary line ("Turns on ·
silences everything except N apps" / "On now — "), enabled toggle, chevron.
Expanded: "Turns on" dropdown (five kinds) + kind fields (schedule start/end
TimeOfDayRow- style or validated HH:MM inputs + seven day pills; workspace id picker); "Silence notifications" toggle; "May interrupt" chip row (chips with ×, "+ Add app" opens a searchable picker over known apps — reuse the rules list's app universe); "Keep the screen awake" toggle; Rename + Delete mode buttons. "+ New focus mode" dashed row at the bottom. - Focus sessions card — default duration segmented chips (25/45/60/90 →
focusDurationMinutes), Caffeine toggle, session status row + Start focus/Show controls (existing behaviors move over unchanged).
Reuse existing row widgets and the Displays/Sound phase components (OptionPickerRow, etc.) before inventing new ones. No continuously repainting animations. All new components get qmldir lines in the same wave as first reference.
Search & docs
Hand-written entries (page per target): Do Not Disturb, Quiet hours, Critical alerts break
through, Application notification rules, Forget an app's notifications, Per-app notification
sound → notifications; Focus modes detail already schema-indexed (now routes to focus),
plus Focus session duration → focus if not covered by the workspaces group move. Docs and
launcher commands regenerate after the schema lands (orchestrator's audit pass).
Contracts (write, do NOT run — cite in the backlog for the next sweep)
notification-app-rules-contract: extend the pinned rule shape to the table above (defaults, optional back-compat, stale-field dropping incl. lock-screen pair), pinforgetApp, the display="history" no-popup-no-bell path, sound=false no-bell,effectiveUrgencyconsumers, and the breakthrough gate literal.focus-modes-contract: pin the new CRUD/reorder/trigger APIs, keep every existing pin (conditions-not-alarms, DND ownership, gaming reports, exception list consulted + editable — the editable needle moves to FocusPage).settings-pages-contract(page id list + component list),settings-jump-contract(GamingPage → focus),search-routing-contractexpectations,settings-window-contractif it enumerates tabs.- Backlog spec gains a Phase 7 section listing all of it.
Agent ownership (parallel)
- A — services:
services/Notifs.qml,services/FocusModes.qml,config/PreferenceSchema.qml(one new key; comments above braces). - B — UI:
modules/settings/NotificationsPage.qml, newmodules/settings/FocusPage.qml+ new components +modules/settings/qmldir,services/SettingsRoutes.qml,modules/settings/SettingsShell.qml,modules/notifications/NotificationCard.qml(effectiveUrgency),modules/settings/GamingPage.qml(Open Focus target). - C — periphery:
services/SettingsSearch.qml, the contracts above + harness fixtures, test-backlog spec, README count line only if the contract count changes.
B programs against the pinned shapes; A must not change them without updating this spec.