@@ -9,6 +9,8 @@ import 'package:flutter_math_fork/flutter_math.dart';
99import 'package:markdown/markdown.dart' as md;
1010import 'package:photo_view/photo_view.dart' ;
1111import '../l10n/app_localizations.dart' ;
12+ import '../models/user_profile.dart' ;
13+ import 'account/profile_picture.dart' ;
1214
1315/// Markdown Renderer
1416class MarkdownRenderer extends StatelessWidget {
@@ -118,6 +120,8 @@ class MarkdownRenderer extends StatelessWidget {
118120 'code' : CustomCodeBuilder (),
119121 // LaTeX 支持
120122 'latex' : LatexBuilder (),
123+ // @mention 支持
124+ 'mention' : MentionChipBuilder (),
121125 },
122126 // 自定义图片
123127 imageBuilder: (uri, title, alt) {
@@ -140,6 +144,7 @@ class MarkdownRenderer extends StatelessWidget {
140144 extensionSet: md.ExtensionSet (
141145 md.ExtensionSet .gitHubFlavored.blockSyntaxes,
142146 [
147+ MentionInlineSyntax (),
143148 md.EmojiSyntax (),
144149 ...md.ExtensionSet .gitHubFlavored.inlineSyntaxes,
145150 LatexSyntax (),
@@ -274,6 +279,77 @@ class _CodeBlockWidget extends StatelessWidget {
274279 }
275280}
276281
282+ // @mention
283+ class MentionInlineSyntax extends md.InlineSyntax {
284+ MentionInlineSyntax () : super (r'@([A-Za-z0-9_]+)(?=\s|$)' );
285+
286+ @override
287+ bool onMatch (md.InlineParser parser, Match match) {
288+ final username = match[1 ]! ;
289+ if (UserProfileDemoData .findByUsername (username) == null ) {
290+ parser.addNode (md.Text ('@$username ' ));
291+ return true ;
292+ }
293+ final element = md.Element .text ('mention' , '@$username ' );
294+ element.attributes['username' ] = username;
295+ parser.addNode (element);
296+ return true ;
297+ }
298+ }
299+
300+ class MentionChipBuilder extends MarkdownElementBuilder {
301+ @override
302+ Widget ? visitElementAfter (md.Element element, TextStyle ? preferredStyle) {
303+ final username = element.attributes['username' ] ??
304+ element.textContent.replaceFirst ('@' , '' );
305+ final profile = UserProfileDemoData .findByUsername (username);
306+ return _MentionChipWidget (username: username, profile: profile);
307+ }
308+ }
309+
310+ class _MentionChipWidget extends StatelessWidget {
311+ final String username;
312+ final UserProfile ? profile;
313+
314+ const _MentionChipWidget ({required this .username, this .profile});
315+
316+ @override
317+ Widget build (BuildContext context) {
318+ final colorScheme = Theme .of (context).colorScheme;
319+ final avatarUrl = profile? .avatar;
320+ return Container (
321+ margin: const EdgeInsets .symmetric (horizontal: 1 ),
322+ padding: const EdgeInsets .fromLTRB (4 , 2 , 6 , 2 ),
323+ decoration: BoxDecoration (
324+ color: colorScheme.primaryContainer,
325+ borderRadius: BorderRadius .circular (12 ),
326+ ),
327+ child: Row (
328+ mainAxisSize: MainAxisSize .min,
329+ children: [
330+ _buildAvatar (avatarUrl, colorScheme),
331+ const SizedBox (width: 4 ),
332+ Text (
333+ '@$username ' ,
334+ style: TextStyle (
335+ fontSize: 13 ,
336+ fontWeight: FontWeight .w500,
337+ color: colorScheme.onPrimaryContainer,
338+ ),
339+ ),
340+ ],
341+ ),
342+ );
343+ }
344+
345+ Widget _buildAvatar (String ? avatarUrl, ColorScheme colorScheme) {
346+ return ProfilePictureWidget (
347+ avatarUrl: avatarUrl,
348+ radius: 8 ,
349+ );
350+ }
351+ }
352+
277353/// LaTeX 语法解析器
278354class LatexSyntax extends md.InlineSyntax {
279355 LatexSyntax () : super (r'\$\$(.+?)\$\$|\$(.+?)\$' );
0 commit comments