mirror of
https://github.com/block/buzz.git
synced 2026-11-13 12:51:34 +00:00
## Summary Preview buzz-app's Tabler icon style in the mobile app, replacing app-drawn Lucide glyphs across channels, messages, activity, search, and settings. A shared `BuzzIcons` mapping uses the bundled Tabler 3.46.0 outline font, matching buzz-app's version, with its MIT license and source documented. Native UIKit controls retain SF Symbols. The former 300/500 navigation glyphs use Tabler's standard outline; existing selection colors and labels remain. ### Related issue No matching Tabler mobile migration issue or PR found. Related native iOS controls: #8055. ### Testing - Pre-push mobile analysis, formatting, file-size checks, and the full mobile test suite passed. - 194 focused widget tests passed, including channels, home, search, settings, and message content. - Light and dark community-settings renders inspected. - Signed iPhone Release build succeeded; installed and launched as Buzz Tabler on a physical iPhone. - Full `just ci` attempted; stopped during Rust clippy compilation because the Mac ran low on disk space. Full repository validation is not claimed. Signed-off-by: kenny lopez <klopez4212@gmail.com>
139 lines
3.9 KiB
Dart
139 lines
3.9 KiB
Dart
part of '../message_content.dart';
|
|
|
|
String? _channelNameForId(Map<String, String> channels, String channelId) {
|
|
for (final entry in channels.entries) {
|
|
if (entry.value == channelId) return entry.key;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class _ChannelLinkMd extends InlineMd {
|
|
final Map<String, String> channelNames;
|
|
final void Function(String channelId)? onChannelTap;
|
|
late final RegExp _exp = _buildPrefixPattern(
|
|
prefix: '#',
|
|
knownNames: channelNames.keys,
|
|
genericTokenPattern: r'[A-Za-z0-9_][A-Za-z0-9_-]*',
|
|
);
|
|
|
|
_ChannelLinkMd({required this.channelNames, this.onChannelTap});
|
|
|
|
@override
|
|
RegExp get exp => _exp;
|
|
|
|
@override
|
|
InlineSpan span(
|
|
BuildContext context,
|
|
String text,
|
|
final GptMarkdownConfig config,
|
|
) {
|
|
final raw = exp.firstMatch(text.trim())?.group(0);
|
|
if (raw == null) {
|
|
return TextSpan(text: text, style: config.style);
|
|
}
|
|
|
|
final channelId = channelNames[raw.substring(1).toLowerCase()];
|
|
final channelName = raw.substring(1);
|
|
final opensChannel = channelId != null && onChannelTap != null;
|
|
final child = _TokenPill(
|
|
icon: BuzzIcons.hash,
|
|
interactive: opensChannel,
|
|
semanticLabel: opensChannel
|
|
? 'Open channel $channelName'
|
|
: 'Channel $channelName',
|
|
text: channelName,
|
|
textStyle: config.style?.copyWith(fontWeight: FontWeight.w500),
|
|
);
|
|
|
|
return WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: opensChannel
|
|
? GestureDetector(onTap: () => onChannelTap!(channelId), child: child)
|
|
: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TokenPill extends StatelessWidget {
|
|
final IconData? icon;
|
|
final bool interactive;
|
|
final String? semanticLabel;
|
|
final String text;
|
|
final TextStyle? textStyle;
|
|
|
|
const _TokenPill({
|
|
super.key,
|
|
this.icon,
|
|
this.interactive = false,
|
|
this.semanticLabel,
|
|
required this.text,
|
|
this.textStyle,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final style =
|
|
textStyle?.copyWith(color: context.colors.primary) ??
|
|
context.textTheme.bodyMedium?.copyWith(color: context.colors.primary);
|
|
final fontSize = style?.fontSize ?? 16;
|
|
final pill = Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
|
decoration: BoxDecoration(
|
|
color: context.colors.primary.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(Radii.sm),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, size: fontSize * 0.95, color: context.colors.primary),
|
|
const SizedBox(width: Grid.quarter + 1),
|
|
],
|
|
Text(text, style: style),
|
|
],
|
|
),
|
|
);
|
|
if (semanticLabel == null) return pill;
|
|
return Semantics(
|
|
label: semanticLabel,
|
|
button: interactive,
|
|
child: ExcludeSemantics(child: pill),
|
|
);
|
|
}
|
|
}
|
|
|
|
RegExp _buildPrefixPattern({
|
|
required String prefix,
|
|
required Iterable<String> knownNames,
|
|
required String genericTokenPattern,
|
|
}) {
|
|
final names =
|
|
knownNames
|
|
.map((name) => name.trim())
|
|
.where((name) => name.isNotEmpty)
|
|
.toSet()
|
|
.toList()
|
|
..sort((a, b) => b.length.compareTo(a.length));
|
|
|
|
final escapedPrefix = RegExp.escape(prefix);
|
|
const leadingBoundary = r'(?<![\w./:-])';
|
|
const trailingBoundary = r'(?=$|[\s,;.!?:)\]}])';
|
|
|
|
if (names.isEmpty) {
|
|
return RegExp(
|
|
'$leadingBoundary$escapedPrefix(?:$genericTokenPattern)$trailingBoundary',
|
|
caseSensitive: false,
|
|
multiLine: true,
|
|
);
|
|
}
|
|
|
|
final knownAlternatives = names.map(RegExp.escape).join('|');
|
|
return RegExp(
|
|
'$leadingBoundary$escapedPrefix(?:(?:$knownAlternatives)$trailingBoundary|(?:$genericTokenPattern)$trailingBoundary)',
|
|
caseSensitive: false,
|
|
multiLine: true,
|
|
);
|
|
}
|