Skip to content

mobile: long-press a message link for open, copy and share - #9135

Merged
brow merged 5 commits into
mainfrom
mobile-link-long-press
Nov 7, 2026
Merged

brow merged 5 commits into
mainfrom
mobile-link-long-press

Conversation

@brow

@brow brow commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Holding a link in a message used to open the whole-message actions sheet, so there was no way to copy or share just the link. Now a hold on a link opens a small Link sheet showing the URL with Open link, Copy link and Share link. Web links share as URLs so the share sheet can preview them; other links (such as buzz://) share as text. Open link appears only for destinations the app can open (web and buzz:// links), so a mailto: link offers Copy and Share. Repo, PR and issue chips, which have no mobile destination, also offer Copy and Share on hold. While a finger is down, the link gets a light tint behind it, the same kind of press highlight the message row paints. A hold anywhere else in the message still opens the message actions, and a tap on a link still opens it directly.

The link gets its own long-press recognizer with the same hold deadline as the message row. Pointer events reach the deeper recognizer first, so at equal deadlines the link wins the arena before the message row, the same mechanism reaction pills already use to claim their own hold. Rendering is otherwise unchanged: the link widget tree only gains a RawGestureDetector, a Semantics wrapper and a DecoratedBox for the tint, press state lives in the link widget so only the link rebuilds, and the sheet is built only when opened. Open reuses the existing tap path (external browser, authenticated relay-media download, or Buzz deep link).

Validation

Added tests: a journey in message_content_test.dart, run with both iOS (200 ms) and Android (default) hold timing, that holds surrounding text (message menu opens, link sheet doesn't), holds the link (link sheet, not the message menu), copies it (clipboard gets the URL), opens it from the sheet and by a plain tap, and checks the press tint appears on touch and clears when the finger moves away. Removing the link recognizer or the tint makes it fail. Another test holds a mailto: link and checks the sheet offers Copy and Share but not Open. Swipe-to-reply tests in swipe_reply_tests.dart drag a message starting on its link and beside it, with both hold timings, and both open the reply.

Full just mobile-check mobile-test passes (after rebasing onto main, two hosted_backup_service_test.dart tests failed once under load and pass on rerun; this PR doesn't touch that code).

Automated app run on an iPhone 17 Pro simulator (iOS 27) signed in to a Builderlab community, in a private QA channel: held message text (message actions sheet, dismissed and composer focus restored as before), held a link (Link sheet), Copy link (toast, simulator pasteboard holds the URL), held again and Share link (system share sheet with the page preview). The simulator's share sheet then stopped responding to dismissal, a known simulator share-extension quirk unrelated to this change. After adding the tint, a second simulator run held the link and captured the pressed state (below). Not covered: Android device run, VoiceOver.

link-long-press.mp4
Held Link sheet Copied
Link held, tinted Link sheet Link copied

🤖 Generated with Claude Code

@brow

brow commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T23:43:10.166955Z a137b4d Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 91978ec33c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P2 · Lower link actions become unreachable with large text

In a short landscape viewport with enlarged system text, a URL occupying three lines plus the three action rows can exceed the sheet’s available height. BuzzTitledSheetLayout constrains this body with Flexible, but this Column does not scroll. isScrollControlled changes the route’s height constraint; it does not add scrolling.

User/system effect: Lower actions, including Share link, can overflow outside the reachable sheet with no way to scroll to them.

Test: Open a long HTTP link’s menu in landscape with large system text and check that every action remains reachable. This finding is based on source inspection, without an independent Flutter reproduction.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Agreed. Addressed in e350e70 by making the sheet body scrollable, like the image actions sheet. The link journey test now opens the sheet in a 640x260 viewport and checks Share link is reachable; without the fix it fails with a RenderFlex overflow.

@brow
brow force-pushed the mobile-link-long-press branch from e350e70 to 36722c7 Compare November 5, 2026 21:55
leading: const Icon(BuzzIcons.copy),
title: const Text('Copy link'),
onTap: () {
Navigator.of(sheetContext).pop();

@johnmatthewtennant johnmatthewtennant Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Withdrawn · Navigator cancels the second pointer

The proposed two-pointer failure is not established. Flutter's Navigator.pop calls _afterNavigation, which cancels active pointers. cancelPointer queues cancellation before the next pointer event. This contradicts my original callback-interleaving claim and supports the author's reported two-order widget reproduction. I missed this framework behavior; no selection guard is requested for this finding. Independently verified from Flutter source, without running the author's test.

@brow brow Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 I couldn't reproduce this. Flutter's Navigator cancels every other active pointer after any navigation (NavigatorState._cancelActivePointers, called from _afterNavigation), so once the first row pops the sheet, the second finger's tap is cancelled before it is released. I checked this with a widget test that pushes the conversation onto a route, opens the Link sheet, puts pointers on Copy link and Open link, and releases them in both orders, with no selection guard. Only one tap callback ran each time, Open never ran, and the conversation stayed open. So I left the sheet as is.

@brow

brow commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36722c7707

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

final canOpen = scheme == 'buzz' || scheme == 'http' || scheme == 'https';
return _LinkLongPress(
url: url,
onOpen: canOpen ? () => unawaited(openLink()) : null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P2 · Entity links still have no Copy or Share action

Valid buzz://repo, buzz://pr, and buzz://issue links return through the EntityDeepLink branch before this wrapper. Holding one therefore still opens the message menu rather than the Link sheet. This leaves the requested Buzz-link sharing unavailable for these rendered chips, which also hide their original URL behind a label. Keeping their taps inert is compatible with offering Copy and Share on hold.

User/system effect: a repository, PR or issue destination cannot be copied or shared through the new link menu.

Test: extend the existing entity-link table with a hold on each chip and verify Copy/Share use its exact URL while Open remains absent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in a137b4d: repo, PR and issue chips now open the Link sheet on hold with Copy and Share for the exact URL; taps stay inert and Open stays hidden. The entity-chip table test now holds each chip and checks this.

square-tomb and others added 5 commits November 5, 2026 14:47
Holding a link in a message now opens a Link sheet (open, copy, share)
instead of the whole-message actions menu. The link's recognizer shares
the message row's hold deadline, so it wins the gesture arena the same
way reaction pills do; taps and holds elsewhere in the message are
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
A long URL with large text in landscape could push Share link out of
the sheet. Make the body scrollable like the image actions sheet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
A finger on a link now tints it like the message row's press highlight,
and Share link appears for every link, including buzz:// ones (non-web
links share as text). Adds swipe-to-reply coverage for drags starting
on and beside a link.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
openLink only handles buzz and http(s) destinations, so a mailto or
relative link's Open action just closed the sheet. The sheet now offers
only Copy and Share for those.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
Entity chips (buzz://repo, pr, issue) were wrapped in IgnorePointer, so a
hold fell through to the message menu. They now open the Link sheet with
Copy and Share; taps stay inert and Open stays hidden since mobile has no
destination for them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
@brow
brow force-pushed the mobile-link-long-press branch from 5b8ef2e to a137b4d Compare November 5, 2026 22:52
@brow

brow commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: a137b4d433

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@johnmatthewtennant johnmatthewtennant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed exact head a137b4d4332543b818a41d0fb4e416dff9615239 with the full review collection. No important correctness blockers found. Source review only; no independent app or device run.

@brow
brow marked this pull request as ready for review November 7, 2026 23:40
@brow
brow requested a review from a team as a code owner November 7, 2026 23:40
@brow
brow merged commit a7a534d into main Nov 7, 2026
53 checks passed
@brow
brow deleted the mobile-link-long-press branch November 7, 2026 23:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a137b4d433

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// openLink handles only these schemes; the sheet hides Open for others.
final scheme = Uri.tryParse(url)?.scheme;
final canOpen = scheme == 'buzz' || scheme == 'http' || scheme == 'https';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate Open on the parsed Buzz destination

The earlier mailto: case is fixed, but fresh evidence remains: this scheme-only predicate marks every buzz: URI as openable. For example, an authored [agent](buzz://agent/new?card=av_123) parses as an AgentCreationDeepLink, while openLink dispatches only channel, message, and invite links before returning; agent-creation, add-community, Nostr-bind, malformed, and unknown Buzz links therefore show an enabled Open link action that merely closes the sheet. Derive onOpen from the parsed types actually handled, or route every supported parsed deep link through the pending-link dispatcher.

Useful? React with 👍 / 👎.

morgmart added a commit that referenced this pull request Nov 9, 2026
…ived-solo-read

* origin/main:
  fix(relay): say "channel owner or admin" in the call_webhook save refusal (#9169)
  feat(mobile): consolidate Solo chat settings in one sheet (#9255)
  fix(cli): preserve attachments across successive edits (#9226)
  fix(mobile): swipe left to reply so swipe-back keeps rightward drags (#9127)
  mobile: long-press a message link for open, copy and share (#9135)
  feat(mobile): long-press a Solo chat to open its details sheet (#9163)
  fix(mobile): shimmer loading photos in the iOS carousel (#9167)
saminsohag added a commit to Infinitix-LLC/buzz that referenced this pull request Nov 9, 2026
Links render as widgets again so a hold can open the link sheet and tint
the link; gpt_markdown text spans support neither. Each ordinary link is
keyed message-link:<url>, and link tests find links by that key instead
of as LinkTextSpans, restoring upstream assertions where they existed.

A Buzz URL of an unknown shape stays plain text instead of becoming a
link whose tap and Open action do nothing, as it was before the
migration.

Signed-off-by: Samin Yeasar Sohag <[email protected]>
bostonaholic added a commit that referenced this pull request Nov 9, 2026
…ction-p

* origin/main: (111 commits)
  feat(mobile): tag the reacted-to message author on reactions (#9141)
  fix(mobile): steady New DM picker rows and animate picks (#9249)
  perf(mobile): keep remote images on disk and show small ones as thumbnails (#9158)
  fix(relay): deliver slow cross-node events late instead of closing their recipients (#9235)
  fix(mobile): read only shown events from filtered Activity rows (#9215)
  fix(mobile): show the channel name as the notification subtitle (#9173)
  fix(mobile): keep Home's header clean under sheets (#9152)
  fix(mobile): show already-loaded custom emoji in the iOS reactions sheet (#9150)
  mobile: add follow and remind actions to inbox row menu (#9142)
  fix(mobile): start no threads in Solo chats (#9123)
  mobile: persist user profiles so names and avatars render on launch (#9116)
  fix(mobile): keep a sent thread reply above the composer (#9026)
  [buzz] Recover database pool capacity after stalled connections (#9267)
  feat: send a User-Agent on relay WebSocket upgrades (#9246)
  Record why relay connections close and quiet close warnings (#9245)
  fix(relay): say "channel owner or admin" in the call_webhook save refusal (#9169)
  feat(mobile): consolidate Solo chat settings in one sheet (#9255)
  fix(cli): preserve attachments across successive edits (#9226)
  fix(mobile): swipe left to reply so swipe-back keeps rightward drags (#9127)
  mobile: long-press a message link for open, copy and share (#9135)
  ...

Signed-off-by: Matthew Boston <[email protected]>
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.

3 participants