Skip to content

Back MessageList with NSTextView (re-target of #2 to main)#3

Merged
buggerman merged 2 commits intomainfrom
nstextview-buffer-v2
Apr 24, 2026
Merged

Back MessageList with NSTextView (re-target of #2 to main)#3
buggerman merged 2 commits intomainfrom
nstextview-buffer-v2

Conversation

@buggerman
Copy link
Copy Markdown
Owner

Summary

Re-lands #2 against main. #2 was stacked on selectable-chat-text as its base; merging it landed on the stack's base branch, not on main, so main never got the NSTextView buffer and cross-row drag-selection is still broken there today.

This PR is exactly the two commits from #2 cherry-picked onto current main:

  • 64b352c Back MessageList with NSTextView for cross-row selection
  • a3aee7d Render inline link previews as NSTextAttachment cells

Already reviewed in #2 — full rationale, test plan, and screenshots live on that PR. Nothing new here except the target.

Test plan

  • swift build — passes
  • swift test — 88 tests pass (unchanged)
  • swiftformat --lint . — clean
  • Scripts/build-app.sh — builds build/Brygga.app fine
  • Manual checks identical to Back MessageList with NSTextView for cross-row selection #2's; the key one is: drag across multiple rows → ⌘C → paste → confirm multi-line plain text.

Risk / rollback

Same as #2. Revert the two commits to return to main's current LazyVStack-backed buffer.

SwiftUI's .textSelection(.enabled) only permits selection within a
single Text view — drag-selecting across chat messages was impossible.
Replace the LazyVStack { MessageRow } rendering in MessageList and
ServerMessageList with a new MessageBufferView: NSViewRepresentable
wrapping NSScrollView + a non-editable NSTextView.

What you now get for free from AppKit:

- True drag-selection across any contiguous range of the buffer, across
  rows and message kinds.
- Cmd+A to select the whole buffer.
- Native Cmd+C copy of the selection as plain text.
- Native incremental Find via Cmd+F (in addition to the existing
  SwiftUI FindBar, which continues to work as a channel filter).
- Right-click context menu with Look Up / Search / Share plus our
  Copy Message / Copy Text / Copy Nickname items.

Rendering in the coordinator mirrors the prior SwiftUI output:
timestamp + sender + IRC-styled content, highlight lines get an accent
background, and a centered "── new ──" paragraph marks the first
unread message. Nick colors round-trip to NSColor via NSColor(Color).

MessageBufferView diff-appends on pure tail extensions and does a full
rebuild when options change (nick-colors toggle, timestamp format,
last-read marker, or message reordering from find-filter). Scroll
pinning follows the classic mIRC rule: if the user was at the bottom
before append, stay at the bottom; otherwise leave their position
alone so they can read history.

The dead SwiftUI views MessageRow, LineMarker, LinkPreviewView, and
the firstPreviewableURL/_linkDetector helpers are removed. Link
previews are not rendered in this build; the existing "Fetch link
previews" preference toggle has no effect until a follow-up restores
inline rendering via NSTextAttachment.

Implementation notes:
- Coordinator is @mainactor; textView and scrollView are weak refs.
- `@objc private func copyAction(_:)` is the one ObjC-adjacent line
  required to satisfy NSMenuItem's target-action contract. It is a
  private selector callback, not a new public API surface.
- `.bryggaMessageID` attributed-string key tags each message's
  paragraph range so the menu handler can recover the clicked Message.
Add a LinkPreviewAttachmentCell subclass of NSTextAttachmentCell that
draws the familiar rounded card — thumbnail on the left, site name /
title / summary on the right — directly inside the NSTextView buffer.
For direct-image URLs (Content-Type image/*) the cell fills the card
with the image itself, bounded at 420×240. The card is indented under
the message body (firstLineHeadIndent 68) so it lines up beneath the
content rather than under the timestamp column.

Observation flow:
- MessageBufferView takes a LinkPreviewStore? and a linkPreviewsEnabled
  flag. MessageList and ServerMessageList thread these through from
  AppState and the existing @AppStorage toggle respectively — the
  preference toggle works again.
- The Coordinator observes `store.cache` via withObservationTracking.
  When any preview status changes the onChange fires on the main actor,
  the coordinator reapplies from its cached last inputs, and re-subscribes.
- Image bytes are decoded once per Coordinator lifetime into an in-memory
  NSImage cache keyed by URL. Fetch uses a per-request URLSession.ephemeral
  with a 10 s timeout and 2 MB cap, matching LinkPreviewStore's own bounds.
  When an image lands the coordinator reapplies and the cell is redrawn.

Clicking anywhere on the preview card follows the URL — the attachment
character carries a .link attribute pointing at the preview's source URL.

LinkPreviewAttachmentCell is deliberately not @mainactor because NSCell's
overridable methods are declared nonisolated in AppKit. The class holds
only immutable value-type state (LinkPreview + NSImage?) so nonisolated
access is safe. Its layout constants are nonisolated static lets so they
can be referenced from the draw path without a main-actor hop.
@buggerman buggerman merged commit 82f4e4e into main Apr 24, 2026
1 check passed
@buggerman buggerman deleted the nstextview-buffer-v2 branch April 24, 2026 21:47
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.

1 participant