mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
fix(mobile): bound long preview lines and label Buzz links
A single huge line no longer reaches preview preprocessing in full, and message, repo, pull request and issue links preview with the same short label as the thread view's chips, via a shared buzzLinkLabel helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -37,6 +37,7 @@ import 'message_content/link_normalizer.dart';
|
||||
import 'message_content/preview_text.dart';
|
||||
import 'message_media.dart';
|
||||
import 'message_content/bubble_segments.dart';
|
||||
import 'message_content/buzz_link_label.dart';
|
||||
import 'message_mention_pill.dart';
|
||||
import 'message_gallery.dart';
|
||||
import 'message_gallery_frame.dart';
|
||||
@@ -612,22 +613,21 @@ class MessageContent extends HookConsumerWidget {
|
||||
buzzLink is MessageDeepLink ||
|
||||
buzzLink is EntityDeepLink;
|
||||
final isCanonicalBuzzLabel = isBuzzLink && text == url;
|
||||
String? channelName(String id) =>
|
||||
_channelNameForId(resolvedChannelNames, id);
|
||||
final buzzLabel = buzzLinkLabel(buzzLink, channelName);
|
||||
final buzzPresentation = switch (buzzLink) {
|
||||
ChannelDeepLink(:final channelId) => (
|
||||
ChannelDeepLink() => (
|
||||
icon: BuzzIcons.hash,
|
||||
label:
|
||||
_channelNameForId(resolvedChannelNames, channelId) ??
|
||||
channelId.substring(0, math.min(8, channelId.length)),
|
||||
semanticLabel:
|
||||
'Open channel ${_channelNameForId(resolvedChannelNames, channelId) ?? channelId.substring(0, math.min(8, channelId.length))}',
|
||||
label: buzzLabel!,
|
||||
semanticLabel: 'Open channel $buzzLabel',
|
||||
interactive: true,
|
||||
),
|
||||
MessageDeepLink(:final channelId, :final messageId) => (
|
||||
icon: BuzzIcons.messageSquare,
|
||||
label:
|
||||
'${_channelNameForId(resolvedChannelNames, channelId) ?? channelId.substring(0, math.min(8, channelId.length))} · ${messageId.substring(0, math.min(8, messageId.length))}',
|
||||
label: buzzLabel!,
|
||||
semanticLabel:
|
||||
'Open message ${messageId.substring(0, math.min(8, messageId.length))} in channel ${_channelNameForId(resolvedChannelNames, channelId) ?? channelId.substring(0, math.min(8, channelId.length))}',
|
||||
'Open message ${shortBuzzId(messageId)} in channel ${buzzChannelLabel(channelId, channelName)}',
|
||||
interactive: true,
|
||||
),
|
||||
EntityDeepLink(:final type, :final repository, :final eventId) => (
|
||||
@@ -636,14 +636,12 @@ class MessageContent extends HookConsumerWidget {
|
||||
'pr' => BuzzIcons.gitPullRequest,
|
||||
_ => BuzzIcons.circleDot,
|
||||
},
|
||||
label: type == 'repo'
|
||||
? repository
|
||||
: '$repository · ${eventId!.substring(0, 8)}',
|
||||
label: buzzLabel!,
|
||||
semanticLabel: switch (type) {
|
||||
'repo' => 'Repository $repository',
|
||||
'pr' =>
|
||||
'Pull request ${eventId!.substring(0, 8)} in repository $repository',
|
||||
_ => 'Issue ${eventId!.substring(0, 8)} in repository $repository',
|
||||
'Pull request ${shortBuzzId(eventId ?? '')} in repository $repository',
|
||||
_ => 'Issue ${shortBuzzId(eventId ?? '')} in repository $repository',
|
||||
},
|
||||
interactive: false,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../shared/deeplink/deep_link.dart';
|
||||
|
||||
/// The short label a Buzz channel, message, repo, pull request or issue link
|
||||
/// shows in place of its URL, or null for any other link. [channelName] gives
|
||||
/// a channel's display name, or null when it is unknown.
|
||||
String? buzzLinkLabel(
|
||||
BuzzDeepLink? link,
|
||||
String? Function(String channelId) channelName,
|
||||
) => switch (link) {
|
||||
ChannelDeepLink(:final channelId) => buzzChannelLabel(channelId, channelName),
|
||||
MessageDeepLink(:final channelId, :final messageId) =>
|
||||
'${buzzChannelLabel(channelId, channelName)} · ${shortBuzzId(messageId)}',
|
||||
EntityDeepLink(type: 'repo', :final repository) => repository,
|
||||
EntityDeepLink(:final repository, :final eventId) =>
|
||||
'$repository · ${shortBuzzId(eventId ?? '')}',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// [channelId]'s display name, or its first 8 characters when unknown.
|
||||
String buzzChannelLabel(
|
||||
String channelId,
|
||||
String? Function(String channelId) channelName,
|
||||
) => channelName(channelId) ?? shortBuzzId(channelId);
|
||||
|
||||
/// The first 8 characters of an id, as Buzz links show it.
|
||||
String shortBuzzId(String id) => id.substring(0, math.min(8, id.length));
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
||||
import '../../../shared/deeplink/deep_link.dart';
|
||||
import '../../../shared/mentions/mention_bindings.dart';
|
||||
import '../message_media.dart';
|
||||
import 'buzz_link_label.dart';
|
||||
import 'link_normalizer.dart';
|
||||
|
||||
/// One piece of a message preview: plain text or an inline token that renders
|
||||
@@ -159,13 +160,19 @@ Iterable<({String text, bool code, int end})> _keptLines(
|
||||
while (start <= content.length &&
|
||||
start <= _maxScanLength &&
|
||||
kept <= maxLines) {
|
||||
var end = content.indexOf('\n', start);
|
||||
if (end < 0) end = content.length;
|
||||
// A long line wraps past the visible lines well before this length.
|
||||
var line = content
|
||||
.substring(start, math.min(end, start + 2 * _maxLineLength))
|
||||
.trim();
|
||||
start = end + 1;
|
||||
// A long line wraps past the visible lines well before this length, so
|
||||
// reading stops there rather than at the line's real end.
|
||||
var cap = math.min(content.length, start + 2 * _maxLineLength);
|
||||
if (cap < content.length && _isHighSurrogate(content.codeUnitAt(cap - 1))) {
|
||||
cap--;
|
||||
}
|
||||
var end = start;
|
||||
while (end < cap && content.codeUnitAt(end) != _newline) {
|
||||
end++;
|
||||
}
|
||||
final cut = end == cap && cap < content.length;
|
||||
var line = content.substring(start, end).trim();
|
||||
start = cut ? content.length + 1 : end + 1;
|
||||
// A fence opens at any line starting with backticks and closes only at a
|
||||
// line holding nothing else, as in gpt_markdown.
|
||||
if (!fenceOpen && line.startsWith('```') || fenceOpen && line == '```') {
|
||||
@@ -189,8 +196,11 @@ Iterable<({String text, bool code, int end})> _keptLines(
|
||||
const _maxLineLength = 2000;
|
||||
const _maxScanLength = 8 * _maxLineLength;
|
||||
const _backslash = 0x5C;
|
||||
const _newline = 0x0A;
|
||||
const _backtick = 0x60;
|
||||
|
||||
bool _isHighSurrogate(int unit) => unit >= 0xD800 && unit <= 0xDBFF;
|
||||
|
||||
final _rule = RegExp(r'^([-*_])(?:\s*\1){2,}$');
|
||||
final _tableDivider = RegExp(r'^\|?(?:\s*:?-+:?\s*\|)+\s*:?-*:?\s*\|?$');
|
||||
final _quote = RegExp(r'^(?:>\s?)+');
|
||||
@@ -383,25 +393,30 @@ class _PreviewBuilder {
|
||||
continue;
|
||||
}
|
||||
final uri = Uri.tryParse(url);
|
||||
final deepLink = uri?.scheme == 'buzz' ? parseBuzzDeepLink(uri!) : null;
|
||||
final deepLink = uri?.scheme == 'buzz'
|
||||
? parseBuzzDeepLink(uri!) ?? parseEntityDeepLink(uri)
|
||||
: null;
|
||||
if (deepLink is ChannelDeepLink) {
|
||||
add(PreviewChannel(_channelName(deepLink.channelId)));
|
||||
add(PreviewChannel(buzzChannelLabel(deepLink.channelId, _channelName)));
|
||||
continue;
|
||||
}
|
||||
_tokens(
|
||||
_unwrapEmphasis(
|
||||
_shield(label.isEmpty ? url : decodeMarkdownLabelSyntax(label)),
|
||||
),
|
||||
);
|
||||
final shown = label.isEmpty ? url : decodeMarkdownLabelSyntax(label);
|
||||
// As in the thread view, a link showing its own URL shows a short label.
|
||||
final short = shown == url ? buzzLinkLabel(deepLink, _channelName) : null;
|
||||
if (short != null) {
|
||||
this.text(short);
|
||||
continue;
|
||||
}
|
||||
_tokens(_unwrapEmphasis(_shield(shown)));
|
||||
}
|
||||
_tokens(_unwrapEmphasis(source.substring(start)));
|
||||
}
|
||||
|
||||
String _channelName(String channelId) {
|
||||
String? _channelName(String channelId) {
|
||||
for (final entry in channelNames.entries) {
|
||||
if (entry.value == channelId) return entry.key;
|
||||
}
|
||||
return channelId.substring(0, channelId.length < 8 ? channelId.length : 8);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_shield]ed plain text with mention, custom emoji and channel tokens.
|
||||
|
||||
@@ -109,8 +109,12 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('caps a very long line', () {
|
||||
test('caps a very long line, and reads only its start', () {
|
||||
expect(_text('x' * 5000).length, 2000);
|
||||
expect(
|
||||
messagePreviewSource('x' * 128000, maxLines: 2).length,
|
||||
lessThanOrEqualTo(4000),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,6 +225,18 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('message and repo links that show their URL get a short label', () {
|
||||
final message = 'buzz://message?channel=$_channelId&id=${'ab' * 32}';
|
||||
const repo = 'buzz://repo?owner=$_alice&d=buzz';
|
||||
expect(
|
||||
_pieces(
|
||||
'[$message]($message) [$repo]($repo) [named]($message)',
|
||||
channelNames: {'general': _channelId},
|
||||
),
|
||||
[const PreviewText('general · abababab buzz named')],
|
||||
);
|
||||
});
|
||||
|
||||
test('media links become their kind', () {
|
||||
expect(_pieces('look  [clip](https://x.y/b.mp4)'), [
|
||||
const PreviewText('look '),
|
||||
|
||||
Reference in New Issue
Block a user