Skip to content

Bulk actions, undo removal, keybinding swap#6

Merged
jasonlong merged 6 commits intomainfrom
bulk-actions
Apr 17, 2026
Merged

Bulk actions, undo removal, keybinding swap#6
jasonlong merged 6 commits intomainfrom
bulk-actions

Conversation

@jasonlong
Copy link
Copy Markdown
Owner

@jasonlong jasonlong commented Apr 17, 2026

Summary

  • Bulk selection: press x (or click the row's icon column) to toggle a notification into a checked set; d, u, o then apply to every checked row in display order. Header gains a blue · N selected suffix when the set is non-empty.
  • Undo removed: deleted the UndoEntry/UndoEffect plumbing, AppState.undo(), startRestoreSubscriptionUndo, the 2.5s-window restore flow, and the Cmd+Z binding. Also trimmed the now-dead ThreadActionStore.pushesUndo parameter, InboxStore.restoreDismissedSecurityAlert, and GitHubAPIClient.restoreSubscription.
  • Keybinding swap:
    • space → page down (was toggle-check)
    • x → toggle check (was unsubscribe)
    • u → unsubscribe (was undo)
    • Cmd+Z → unbound
  • Perf on the rebuild hot path (runs every keystroke in search and after every action):
    • Reuse projectedUnread for the menubar-icon count instead of re-projecting.
    • Skip serverNotificationsForCurrentMode + sortedByRecency when groupByRepo is off.
    • Memoize filteredNotifications as a stored property (was re-trimming + re-lowercasing + re-filtering on every read).
    • Swap search filter to localizedCaseInsensitiveContains (no per-row string allocations).
    • Clear checkedThreadIDs before the bulk loop so per-iteration rebuilds don't prune a shrinking set.
  • Hover detector: SwiftUI's .onHover wouldn't fire for the icon region under the row's outer tap gesture, so an NSTrackingArea-backed NSViewRepresentable handles it. Deduped against lastDeliveredHover and split sync (mouseEntered/Exited) from async (tracking-area/window-change) paths to avoid redundant @State writes and the AppKit display-cycle crash.
  • UI polish: x sel added to the footer, inter-hint spacing tightened, label contrast bumped from tertiary to secondary; settings row renamed to "Select for bulk actions".

Closes #5

jasonlong and others added 5 commits April 17, 2026 10:51
Spacebar now toggles the current row into a checked set instead of
paging down. Clicking the leading icon column (or hovering to preview
the unchecked square) also toggles the row. When the checked set is
non-empty, `d`, `x`, and `o` fan out across every checked item in
display order; single-row behavior is unchanged when nothing is
checked. Refresh clears the set and rows dropping out of the filtered
list are pruned automatically.

Hover detection for the icon column uses an NSTrackingArea-backed
NSViewRepresentable — SwiftUI's `.onHover` on nested views wasn't
firing under the row's outer tap gesture. Stuck-hover states on
scroll are cleared by re-checking the cursor position against the
view's visible rect, deferred to the next runloop to avoid mutating
SwiftUI state inside AppKit's display cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Undo never held up in practice — between the 2.5s dispatch delay, the
stale-entry warnings, and the subscription-restore / security-alert
restore branches it was more complexity than it was worth. Ripping it
out frees `u` for unsubscribe, which lets `x` take over the
bulk-toggle role and returns `Space` to its original page-down
behavior.

Removes: `UndoEntry`, `UndoEffect`, `undoStack`, `applyUndo`,
`pushUndo`, `pushSecurityAlertDismissUndo`, `removeUndoEntry`,
`pushesUndo` parameter, the `restoreSubscription` ActionKind,
`AppState.undo`, `startRestoreSubscriptionUndo`, the
"last action can no longer be undone" warning, the `.undo`
KeyboardCommand, the `.commandZ` KeyInput, and `InboxStore`'s now-dead
`restoreDismissedSecurityAlert`. Committed-actions persistence is
orthogonal and stays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After undo was removed, a few artifacts survived: GitHubAPIClient's
restoreSubscription method (plus its test) had no production caller,
CLAUDE.md still listed Cmd+Z and "undo stack", and the icon hover
detector was firing redundant SwiftUI @State writes on every scroll
tick.

Performance work on the rebuild path, since this runs on every
keystroke in search and after every action:

- Drop a duplicate threadActions.projectedNotifications(from: serverNotifications)
  call that was computing the menubar-icon count over already-projected
  data.
- Skip serverNotificationsForCurrentMode + sortedByRecency when
  groupByRepo is off (orderedNotifications ignores repoOrderSource in
  that branch).
- Memoize filteredNotifications as a stored property so its
  trim+lowercase+filter no longer runs on every read (~30 call sites).
- Swap the filter from lowercased().contains() to
  localizedCaseInsensitiveContains() — no per-row string allocations.
- Clear checkedThreadIDs before the bulk loop so each per-item
  rebuildDerivedState doesn't waste cycles pruning a shrinking set.

Hover detector tightened:
- Dedupe deliverHoverChange with a lastDeliveredHover field so no-op
  updates don't invalidate SwiftUI state.
- Route the synchronous mouseEntered/mouseExited path without
  DispatchQueue.main.async; keep async only on the tracking-area/
  window-change callbacks where it's needed to stay off AppKit's
  display cycle.

Also removed stale "// Content" / "// Right side" narration comments
in NotificationRowView.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When one or more rows are checked, append "· N selected" to the
"X unread · Y in inbox" header. The selected suffix uses the accent
color so it stands out against the secondary-colored base.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add "x sel" to the footer shortcut hints; tighten between-hint
  spacing to 8pt and drop the key/label gap to 0.
- Bump the hint label color from tertiary to secondary for slightly
  higher contrast.
- Rename the settings shortcut row from "Toggle selection for bulk
  actions" to "Select for bulk actions" to match the footer wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jasonlong jasonlong marked this pull request as ready for review April 17, 2026 16:03
When the user marks an item done, unsubscribes, or opens it, a small
toast slides up from the footer showing what just happened — specific
for single items (e.g. "Marked shadcn-ui/ui#10408 done") and counted
for bulk ("Marked 4 items done"). Each toast lives 2 seconds; new
ones stack over any still visible. Toasts clear when the panel
closes so a stale one doesn't linger until the next open.

The toast is a rounded card with inverted contrast — black
background with white text in light mode, white with black in dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jasonlong jasonlong merged commit 66f58c5 into main Apr 17, 2026
1 check passed
@jasonlong jasonlong deleted the bulk-actions branch April 17, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open all

1 participant