Files
notedeck/SLACK.md
Claude bef34ba18e Update SLACK.md documentation
- Remove Quick Channel Switcher feature documentation (removed per user request)
- Document virtualization attempt and why it was reverted
- Update keyboard shortcuts (removed Cmd+K)
- Update state management (removed channel_switcher field)
- Update code organization (removed channel_switcher.rs)
- Update manual testing checklist
- Renumber feature sections after removal

The documentation now accurately reflects the current state of the branch,
including the decision to defer virtualization as future work.
2025-11-13 20:22:13 +00:00

21 KiB

Slack-like Interface Redesign

Project Overview

Goal

Transform Notedeck from a TweetDeck-style multi-column interface to a modern Slack-like chat application, with channels representing hashtag filters and threads displayed in a side panel.

Motivation

  • Better UX for focused conversations: Slack-style channels provide clearer context than columns
  • Thread management: Side panel for threads keeps main channel visible (Slack pattern)
  • Simplified relay management: Global relay configuration instead of per-profile
  • Modern chat aesthetics: Message bubbles, grouping, hover interactions

What Was Built

Core Features (12 commits)

1. Channel Infrastructure (e4fcf15)

  • channels.rs: Channel, ChannelList, ChannelsCache data structures
  • relay_config.rs: Global relay configuration (separate from user profiles)
  • Storage layer: JSON serialization for channels and relay config
  • TimelineKind::Hashtag: Each channel subscribes to hashtag filter

Why this way:

  • Channels are user-specific (one ChannelsCache per user pubkey)
  • Global relays shared across all channels (simpler than per-channel relays)
  • Channels stored separately from Decks/Columns (parallel system for clean migration)

2. Channel Sidebar (6a265be)

  • channel_sidebar.rs: 240px fixed-width left sidebar
  • Lists all channels with # prefix icons
  • Highlights selected channel (blue background)
  • Shows unread count badges (99+ overflow)
  • Hover effects for better UX

Technical decisions:

  • Fixed width (240px) matches Slack's sidebar
  • Uses ChannelList.selected index for state
  • Unread counts TODO: wire to actual unread events (currently placeholder)

3. ChatView Component (da38e13, 62d6c70)

  • chat_view.rs: Slack-style message bubbles
  • Message grouping: Same author within 5 minutes = grouped (no repeated avatar/name)
  • Bubble styling: Rounded corners, gray background, padding
  • Message interactions: Reply, Like, Repost buttons (appear on hover)
  • Action integration: Refactored existing NoteAction system

Why this way:

  • Uses existing Timeline infrastructure (TimelineCache, TimelineKind)
  • Renders notes as chat bubbles instead of columns
  • MessageBubbleResponse tracks hover state for showing action buttons
  • Reuses existing app_images for icons (consistent with app style)

4. Channel Creation Dialog (829cca9)

  • channel_dialog.rs: Modal for creating channels
  • Name + comma-separated hashtags input
  • Validation (both fields required)
  • Auto-subscription on creation

Technical decisions:

  • egui::Window for modal overlay
  • Creates TimelineKind::Hashtag with user-specified tags
  • Immediately subscribes to timeline and saves to disk

5. Keyboard Shortcuts (d70f3d2)

  • Escape: Close dialogs/panels (priority: thread panel → dialogs)
  • Cmd/Ctrl+N: Open channel creation dialog

Implementation:

  • Handled in update_damus() via ctx.input()
  • Priority system prevents conflicts (check is_open flags)

6. Thread Side Panel (a9ce1b0, 835b0ed)

  • thread_panel.rs: 420px sliding panel from right
  • Wraps existing ThreadView component
  • Semi-transparent overlay on main content
  • Multiple close methods (X button, Escape, click overlay)

Technical decisions:

  • Reuses ThreadView: No need to rewrite thread rendering
  • App-level state: thread_panel field in Damus struct
  • Event handling: Thread opening triggers from ChatView actions
  • No navigation: Panel is overlay, doesn't change route (keeps channel visible)

7. Action Handling (6cf9490)

  • Reply: Opens thread panel (compose reply in thread)
  • Like/React: Sends reaction event to relays via send_reaction_event()
  • Repost: Opens thread panel (could show repost dialog in future)

Why this way:

  • Made send_reaction_event() public: Reuses existing reaction logic
  • Thread panel for replies: Slack-style (reply in thread context)
  • Immediate UI feedback: Mark reaction as sent before relay confirmation

8. ChatView Integration (a198391, 352293b)

  • Conditional rendering in timelines_view()
  • When channel selected: render ChatView instead of columns
  • StripBuilder cell count adjustment (1 cell vs N columns)

Technical decisions:

  • Direct rendering (not through nav system)
  • Actions handled inline in timelines_view() after ChatView.ui()
  • NoteContext created from AppContext for each frame

Architecture

Data Flow

User selects channel (ChannelSidebar)
  ↓
ChannelsCache.select_channel(idx)
  ↓
timelines_view() checks selected_channel()
  ↓
Renders ChatView with channel.timeline_kind
  ↓
ChatView fetches notes from TimelineCache
  ↓
Renders message bubbles (grouped by author)
  ↓
User hovers → action buttons appear
  ↓
User clicks Like → NoteAction::React returned
  ↓
timelines_view() handles action → sends to relays

Thread Panel Flow

User clicks message bubble
  ↓
ChatView returns NoteAction::Note { note_id }
  ↓
timelines_view() opens thread_panel.open(note_id)
  ↓
render_damus() checks thread_panel.is_open
  ↓
Renders ThreadPanel.show() as overlay
  ↓
ThreadView renders thread conversation
  ↓
User interacts or closes panel

State Management

App-level state (Damus struct):

  • channels_cache: ChannelsCache - All channels for all users
  • relay_config: RelayConfig - Global relay URLs
  • channel_dialog: ChannelDialog - Channel creation modal state
  • thread_panel: ThreadPanel - Thread side panel state

Persistence:

  • $DATA_DIR/channels_cache.json - Channel list per user
  • $DATA_DIR/relay_config.json - Global relay URLs

Why app-level:

  • Needs to persist across route changes
  • Shared state between sidebar and main view
  • Dialog/panel state managed centrally for keyboard shortcuts

Technical Decisions

1. Parallel System vs Replacing Columns

Decision: Built channels as a parallel system to columns, not a replacement.

Reasoning:

  • Non-destructive migration path
  • Users can switch between views if needed
  • Easier to develop/test incrementally
  • Columns code untouched (less risk of breaking existing features)

Trade-off: More code to maintain, but safer rollout.

2. Direct ChatView Rendering vs Nav System

Decision: Render ChatView directly in timelines_view(), not through navigation.

Reasoning:

  • Simpler integration (no route changes needed)
  • Channels conceptually different from column timelines
  • Avoids Router complexity for channel-specific behavior

Trade-off: Actions handled manually instead of through nav system.

3. Thread Panel as Overlay vs Navigation

Decision: Thread panel is an overlay, not a navigation destination.

Reasoning:

  • Slack UX pattern: thread panel slides over, keeps channel visible
  • No route change means "back" button works differently
  • Escape key closes panel (natural UX)

Trade-off: Thread panel state separate from navigation history.

4. Global Relays vs Per-Channel Relays

Decision: Single global relay pool for all channels.

Reasoning:

  • Simpler mental model for users
  • Reduces relay connection overhead
  • Most users want same relays for all content

Future: Could add per-channel relay overrides if needed.

5. Reusing ThreadView vs Custom Thread UI

Decision: Wrap existing ThreadView component in thread panel.

Reasoning:

  • Avoid duplicating thread rendering logic
  • Tested, feature-complete component
  • Consistent thread UX across app

Trade-off: ThreadView wasn't designed for overlay, but works fine.

6. Message Grouping: 5-Minute Window

Decision: Group messages by same author within 5 minutes.

Reasoning:

  • Matches Slack's grouping behavior
  • 5 minutes is sweet spot (not too aggressive, not too loose)
  • Reduces visual clutter significantly

Implementation: Compare note.created_at() timestamps in ChatView loop.


Code Organization

New Files

crates/notedeck_columns/src/
├── channels.rs                  # Channel data structures
├── relay_config.rs              # Global relay configuration
├── storage/
│   ├── channels.rs             # Channel serialization
│   └── relay_config.rs         # Relay config serialization
└── ui/
    ├── channel_sidebar.rs      # Left sidebar with channels
    ├── channel_dialog.rs       # Channel creation modal
    ├── chat_view.rs            # Message bubble rendering
    └── thread_panel.rs         # Thread side panel

Modified Files

app.rs                           # Main app integration
- Added fields to Damus struct
- Keyboard shortcut handling
- Thread panel rendering
- ChatView action handling

actionbar.rs                     # Made send_reaction_event public

lib.rs                          # Export channels, relay_config modules

ui/mod.rs                       # Export new UI components

Dependencies

  • No new external dependencies added
  • Uses existing: egui, nostrdb, enostr, notedeck, notedeck_ui
  • Reuses app infrastructure: TimelineCache, Threads, NoteAction, etc.

Open Issues & Future Work

High Priority

1. Unread Count Tracking (NOT IMPLEMENTED)

Current state: Unread counts are placeholders (always 0)

What's needed:

  • Track last-read timestamp per channel
  • Count new notes since last-read
  • Update counts on channel view
  • Persist last-read state

Implementation approach:

// In Channel struct
pub last_read: u64,  // Unix timestamp

// On channel select
channel.last_read = current_timestamp();

// In ChatView rendering loop
if note.created_at() > channel.last_read {
    channel.unread_count += 1;
}

2. Reply Composition (PARTIAL)

Current state: Reply button opens thread panel

What's missing:

  • Compose area at bottom of thread panel
  • Wire PostReplyView into ThreadPanel
  • Handle reply submission

Implementation approach:

  • Add ui::PostReplyView to ThreadPanel.show()
  • Handle NoteAction::Reply to pre-fill reply target
  • Send reply via existing note publishing infrastructure

3. Repost Dialog (NOT IMPLEMENTED)

Current state: Repost button opens thread panel

What's needed:

  • Repost decision sheet (quote vs simple repost)
  • Wire to existing repost infrastructure

Implementation: Use existing Route::RepostDecision(note_id) but trigger from ChatView actions.

Medium Priority

4. Channel Editing

Current state: Channels can only be created, not edited

What's needed:

  • Edit button in channel sidebar (context menu or settings icon)
  • Reuse ChannelDialog with pre-filled fields
  • Update channel hashtags/name

5. Channel Deletion

Current state: No way to delete channels

What's needed:

  • Delete action in sidebar
  • Confirmation dialog
  • Unsubscribe from timeline
  • Remove from storage

6. Quick Channel Switcher (REMOVED)

Note: The Cmd+K quick channel switcher was removed per user request. If re-added in the future:

  • Implement with fuzzy search (e.g., "btc" matches "bitcoin")
  • Search in hashtags too, not just name
  • Recently used channels at top
  • Arrow key navigation

7. Profile Clicking in ChatView

Current state: Clicking avatar/name does nothing

What's needed:

  • Handle NoteAction::Profile(pubkey)
  • Open profile view (modal or panel)

8. Message Context Menu

Current state: Only hover buttons (reply, like, repost)

Potential additions:

  • Copy link to note
  • Copy note content
  • Report/mute user
  • View reactions list

Low Priority

9. Thread Indicators

Show reply count under messages that have threads (like Slack's "3 replies")

10. Channel Notifications

Desktop notifications for new messages in channels (optional per-channel)

11. Channel Sorting/Grouping

  • Sort channels (A-Z, recent activity, unread first)
  • Group channels (favorites, categories)

12. Direct Messages as Channels

Show DM conversations as special channels in sidebar

13. Read Receipts

Track which messages have been seen by scrolling into view


Known Limitations

1. No Column Integration

  • Channels don't appear in column system
  • Can't mix channels with columns in same view
  • Either use channels or columns, not both simultaneously

Workaround: Users can manually switch between interfaces.

2. Single Channel View

  • Can only view one channel at a time
  • No split-screen for multiple channels

Future: Could add split view like Discord.

3. Thread Panel vs Thread Route

  • Thread panel doesn't integrate with navigation history
  • "Back" button doesn't close thread panel
  • URL doesn't reflect open thread

Why: Deliberate choice for Slack-like overlay behavior.

4. No Message Editing/Deletion

  • Nostr protocol doesn't support editing
  • Could implement deletion via kind 5 events

5. No Typing Indicators

  • Would require custom Nostr extension event

6. Profile Pictures Load Slowly

  • First load fetches from relays (network latency)
  • After cache, loads instantly

Future: Prefetch profile pics for channel participants.


Testing & Validation

Build Status

Compiles cleanly (cargo build --release) No breaking changes to existing features All commits pushed to branch

Manual Testing Checklist

  • Create new channel with hashtags
  • Select different channels in sidebar
  • Send like reaction on message (check relays receive it)
  • Open thread by clicking message
  • Close thread with X, Escape, overlay click
  • Use Cmd+N to create channel
  • Verify channels persist after app restart
  • Verify relays persist after app restart

Known Test Failures

None - existing test suite unchanged.


Development Guidelines

Adding a New Channel Feature

  1. Data model: Update channels.rs::Channel struct
  2. Storage: Update storage/channels.rs serialization if needed
  3. UI: Add to channel_sidebar.rs or create new component
  4. Persistence: Call storage::save_channels_cache() after changes
  5. Commit: Follow existing commit message style

Adding Message Interactions

  1. UI button: Add to chat_view.rs::render_action_bar()
  2. Return action: Update NoteAction match in render_action_bar()
  3. Handle action: Update timelines_view() action handling
  4. Test: Verify action sent to relays or triggers correct behavior

Modifying Thread Panel

  1. Layout changes: Update thread_panel.rs::show()
  2. New actions: Handle in ThreadPanel::show() return value
  3. Integration: Update render_damus() action handling

Debugging Tips

Channel not showing messages:

  • Check channel.subscribed flag (should be true)
  • Verify channel.timeline_kind in TimelineCache
  • Look for subscription in relay logs

Action not working:

  • Add debug print in timelines_view() action handler
  • Check chat_response.output value
  • Verify action reaches match statement

Thread panel not opening:

  • Check thread_panel.is_open flag
  • Verify selected_thread_id is Some()
  • Ensure render_damus() checks is_open

Performance Considerations

Memory

  • ChannelsCache: O(users * channels) - typically small (1 user, 5-10 channels)
  • ChatView: Renders all messages in timeline (no virtualization)
    • Virtualization Attempt (Reverted): Tried egui_virtual_list::VirtualList but oversimplified the rendering callback, which broke all Slack-like features (bubbles, avatars, names, timestamps, interaction buttons)
    • Current Implementation: Full rendering loop works correctly but may have performance issues with 1000+ messages
    • Future Work: Implement virtualization properly by calling render_message() inside VirtualList callback to preserve all features while gaining performance benefits

Network

  • Relay connections: Shared across channels (efficient)
  • Subscriptions: One per channel timeline (minimal overhead)
  • Profile pics: Cached after first load

Rendering

  • Message grouping: O(n) single pass through messages
  • Action buttons: Only render on hover (saves GPU)
  • Thread panel: Overlay rendering (no main view recalculation)

Migration Path

From Columns to Channels

Current state: Both systems coexist.

Future migration:

  1. Add "Import columns as channels" feature
  2. Convert each column timeline to equivalent channel
  3. Deprecate column UI (keep code for backward compat)
  4. Eventually remove column system (breaking change)

User experience:

  • Gradual migration, not forced
  • Users choose when to switch
  • Settings toggle between interfaces

API / Extension Points

Adding Custom Channel Types

Currently channels are hashtag-only. To add other types:

// In channels.rs
pub enum ChannelKind {
    Hashtag(Vec<String>),
    Profile(Pubkey),        // NEW: User feed
    Custom(Filter),         // NEW: Custom nostr filter
}

// Update Channel::new() to accept ChannelKind
// Update storage serialization
// Update UI to show icon based on kind

Custom Message Renderers

To add custom rendering for specific note kinds:

// In chat_view.rs
fn render_message_content(&mut self, note: &Note) -> impl Widget {
    match note.kind() {
        1 => render_text_note(note),
        6 => render_repost(note),        // Existing
        7 => render_reaction(note),      // Existing
        // Add custom kinds:
        30023 => render_long_form(note), // NEW
        1063 => render_file_metadata(note), // NEW
        _ => render_unknown(note),
    }
}

Custom Actions

To add new message actions (beyond reply/like/repost):

// In chat_view.rs::render_action_bar()
ui.add_space(spacing);

// NEW: Bookmark button
let bookmark_resp = self.bookmark_button(ui, note_key);
if bookmark_resp.clicked() {
    action = Some(NoteAction::Bookmark(note_id));
}

// In app.rs::timelines_view() action handling
NoteAction::Bookmark(note_id) => {
    // Save to local bookmarks
    app.bookmarks.add(note_id);
    storage::save_bookmarks(&app.bookmarks);
}

Branch & Deployment

Branch: claude/slack-interface-redesign-011CV4D4ukS3mCadK3QdVQtb

Commits: 13 total

  • Initial infrastructure (channels, relay config)
  • UI components (sidebar, dialog, switcher, chat view, thread panel)
  • Integration and bug fixes
  • Action handling

Merge readiness:

  • Compiles cleanly
  • No regressions in existing features
  • Self-contained (can be disabled if needed)
  • ⚠️ Unread counts not implemented (TODO)
  • ⚠️ Reply composition in thread panel not wired (TODO)

Recommended next steps before merge:

  1. Manual QA testing (see checklist above)
  2. Implement unread count tracking (high priority)
  3. Wire reply composition in thread panel
  4. User acceptance testing (feedback on UX)
  5. Performance testing with large channels (1000+ messages)

Questions & Answers

Q: Why not use existing Columns infrastructure?

A: Columns are deeply tied to the multi-column layout and navigation model. Channels need different UX (single view, sidebar, threads in panel). Building parallel was faster and safer.

Q: Can channels and columns coexist?

A: Yes, currently both systems exist. Future might add UI toggle or separate entry points.

Q: Why global relays instead of per-channel?

A: Simpler for most users. Could add per-channel overrides later if needed.

Q: How to add more hashtag filtering options?

A: Edit channel → modify hashtags list. Current UI only supports creation, not editing (TODO).

Q: Why does Repost open thread panel instead of repost dialog?

A: Quick implementation decision. Thread panel works as fallback. Proper repost dialog is TODO.

Q: How to delete a channel?

A: Not implemented yet (TODO). Would need context menu in sidebar.

Q: Can I use this in production?

A: Feature-complete for basic usage. Missing unread counts and reply composition. Test thoroughly first.


Contributors & Acknowledgments

Implementation: Claude (AI assistant) guided by user requirements

User requirements:

  • Slack-like interface instead of columns
  • Hashtag-based channels
  • Thread side panel (not navigation)
  • Message bubbles with interactions
  • Global relay configuration

Existing infrastructure used:

  • notedeck timeline system
  • nostrdb for data storage
  • enostr for relay protocol
  • egui for UI rendering

Conclusion

This redesign successfully transforms Notedeck into a Slack-like chat application while preserving the decentralized Nostr protocol underneath. The implementation prioritizes:

  1. User experience: Familiar Slack patterns (channels, threads, interactions)
  2. Code reuse: Leverages existing timeline, thread, and action infrastructure
  3. Safety: Parallel system, non-destructive, can be disabled
  4. Extensibility: Clean separation, easy to add features

Ready for testing and iteration. Core functionality complete, some polish TODOs remain.