Skip to content

feat: add hide/close DM support (Slack-style DM management)#157

Merged
wesbillman merged 3 commits intomainfrom
feat/dm-hide
Mar 22, 2026
Merged

feat: add hide/close DM support (Slack-style DM management)#157
wesbillman merged 3 commits intomainfrom
feat/dm-hide

Conversation

@wesbillman
Copy link
Collaborator

Summary

Add the ability to hide/close DMs from the sidebar, matching Slack's DM management UX. Users can close DMs they don't need visible, and re-open them by starting a new DM with the same person.

Changes

Schema

  • Add hidden_at TIMESTAMPTZ column to channel_members table

Backend (Rust)

  • sprout-db: Add hide_dm() and unhide_dm() functions; open_dm() auto-clears hidden_at
  • sprout-db: Filter hidden DMs in get_accessible_channels() and list_dms_for_user()
  • sprout-relay: Add POST /api/dms/{channel_id}/hide endpoint with auth, membership, and DM-type verification
  • sprout-mcp: Add hide_dm tool for agent access

Frontend (TypeScript/React)

  • Tauri: Add hide_dm native command
  • API: Add hideDm() TypeScript function
  • Hooks: Add useHideDmMutation with optimistic cache update and rollback
  • UI: Add hover-visible "X" close button on DM sidebar items
  • Navigation: Auto-navigate to Home when hiding the active DM

How it works

  1. Hide: Hover over a DM in the sidebar → click "X" → DM disappears instantly (optimistic update)
  2. Restore: Use "New DM" dialog → search the person → DM reappears with full history

Design decisions

  • Server-side hidden_at: Persists across devices/sessions, one column, clean SQL filtering
  • Idempotent: Hiding an already-hidden DM is a no-op (just updates timestamp)
  • Auto-unhide on re-open: open_dm() clears hidden_at automatically
  • Nostr-compatible: No protocol changes — purely a per-user UI preference
  • Aligned with VISION.md: "Zero is the default. You opt in to noise, not out."

Testing

  • cargo check
  • cargo clippy ✅ (zero warnings)
  • cargo fmt
  • tsc --noEmit ✅ (zero errors)
  • biome check ✅ (no new warnings)
  • check-file-sizes
  • All pre-push hooks passed ✅

Follow-up (not blocking)

  • E2E test for hide/unhide DM flow
  • Consider auto-unhide on new incoming message (design decision)

Files changed (16)

schema/schema.sql
crates/sprout-db/src/dm.rs
crates/sprout-db/src/channel.rs
crates/sprout-db/src/lib.rs
crates/sprout-relay/src/api/dms.rs
crates/sprout-relay/src/api/mod.rs
crates/sprout-relay/src/router.rs
crates/sprout-mcp/src/server.rs
desktop/src-tauri/src/commands/dms.rs
desktop/src-tauri/src/lib.rs
desktop/src/shared/api/tauri.ts
desktop/src/features/channels/hooks.ts
desktop/src/features/sidebar/ui/SidebarSection.tsx
desktop/src/features/sidebar/ui/AppSidebar.tsx
desktop/src/app/AppShell.tsx
desktop/scripts/check-file-sizes.mjs

wesbillman and others added 3 commits March 21, 2026 15:20
- Add hidden_at column to channel_members for per-user DM hiding
- Add POST /api/dms/{channel_id}/hide REST endpoint
- Filter hidden DMs from GET /api/channels response
- Clear hidden_at when re-opening a DM via POST /api/dms
- Add hide_dm MCP tool for agent access
- Add Tauri hide_dm command and React UI with close button on DM sidebar items
- Optimistic cache update for instant UI feedback
- Register hide_dm in MCP ALL_TOOLS (dms toolset) to fix toolset gating leak
- Add validate_uuid guard to hide_dm MCP tool for consistent error handling
- Replace nested button with SidebarMenuAction sibling (valid HTML)
- Await mutation in handleHideDm before navigating (complete rollback)
- Add hide_dm handler to e2e test bridge
- Hide unread dot on hover when close button appears
- Update MCP tool count assertions (42 → 43) in toolsets + e2e tests
* origin/main:
  feat: sprout-cli — agent-first CLI with full MCP parity (48 commands) (#158)
  fix(desktop): resolve composer cursor drift on multi-line input (#153)

# Conflicts:
#	crates/sprout-relay/src/router.rs
@tlongwell-block
Copy link
Collaborator

Pushed a follow-up commit addressing review findings:

MCP toolset gatinghide_dm was missing from ALL_TOOLS in toolsets.rs, which meant it bypassed toolset access control entirely. Now registered in the dms toolset with the correct tool count (43) updated across toolsets, e2e inventory tests, and doc comments.

MCP input validation — Added the validate_uuid guard that every other channel-ID tool uses. Without it, malformed IDs produced a confusing server-side 400 instead of a clean client-side error.

Nested button — The hide X button was rendered inside SidebarMenuButton (itself a <button>), producing invalid HTML. Replaced with SidebarMenuAction as a sibling — the component designed for exactly this pattern. Unread dot now hides on hover so the two indicators don't stack.

Optimistic rollbackhandleHideDm fired the mutation and navigated away immediately. If the POST failed, the channel reappeared in the sidebar but the user was already on the home screen. Now awaits the mutation and only navigates on success.

Test coverage — Added hide_dm handler to the e2e test bridge and the expected tools list in the MCP inventory test.

All gates green: fmt, clippy (0 warnings), cargo test, tsc, biome check, file-size check, tauri check, 64/64 smoke e2e tests passing. Merged origin/main cleanly (one conflict in router.rs — our hide route alongside the new message edit + vote routes).


This comment was written by goose.

@wesbillman wesbillman merged commit dfe32bf into main Mar 22, 2026
8 checks passed
@wesbillman wesbillman deleted the feat/dm-hide branch March 22, 2026 16:00
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.

2 participants