Skip to content

Commit 612ae1f

Browse files
content: Handle @-topic mentions
Fixes: #892
1 parent abb2bcb commit 612ae1f

File tree

4 files changed

+98
-2
lines changed

4 files changed

+98
-2
lines changed

lib/model/content.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,10 @@ class UserMentionNode extends MentionNode {
721721
// final bool isSilent; // TODO(#647)
722722
}
723723

724+
class TopicMentionNode extends MentionNode {
725+
const TopicMentionNode({super.debugHtmlNode, required super.nodes});
726+
}
727+
724728
sealed class EmojiNode extends InlineContentNode {
725729
const EmojiNode({super.debugHtmlNode});
726730
}
@@ -939,6 +943,13 @@ class _ZulipContentParser {
939943
static final _userMentionClassNameRegexp = RegExp(
940944
r"(^| )" r"user(?:-group)?-mention" r"( |$)");
941945

946+
static final _topicMentionClassNameRegexp = () {
947+
// This matches a class `topic-mention`, plus an optional class `silent`,
948+
// appearing in either order.
949+
const mentionClass = "topic-mention";
950+
return RegExp("^(?:$mentionClass(?: silent)?|silent $mentionClass)\$");
951+
}();
952+
942953
static final _emojiClassNameRegexp = () {
943954
const specificEmoji = r"emoji(?:-[0-9a-f]+)+";
944955
return RegExp("^(?:emoji $specificEmoji|$specificEmoji emoji)\$");
@@ -995,6 +1006,14 @@ class _ZulipContentParser {
9951006
return parseUserMention(element) ?? unimplemented();
9961007
}
9971008

1009+
if (localName == 'span'
1010+
&& _topicMentionClassNameRegexp.hasMatch(className)) {
1011+
// TODO assert TopicMentionNode can't contain LinkNode;
1012+
// either a debug-mode check, or perhaps we can make expectations much
1013+
// tighter on a TopicMentionNode's contents overall.
1014+
return TopicMentionNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
1015+
}
1016+
9981017
if (localName == 'span'
9991018
&& _emojiClassNameRegexp.hasMatch(className)) {
10001019
final emojiCode = _emojiCodeFromClassNameRegexp.firstMatch(className)!

lib/widgets/content.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
4040
return ContentTheme._(
4141
colorCodeBlockBackground: const HSLColor.fromAHSL(0.04, 0, 0, 0).toColor(),
4242
colorDirectMentionBackground: const HSLColor.fromAHSL(0.2, 240, 0.7, 0.7).toColor(),
43+
colorTopicMentionBackground: const HSLColor.fromAHSL(0.18, 183, 0.6, 0.45).toColor(),
4344
colorGlobalTimeBackground: const HSLColor.fromAHSL(1, 0, 0, 0.93).toColor(),
4445
colorGlobalTimeBorder: const HSLColor.fromAHSL(1, 0, 0, 0.8).toColor(),
4546
colorMathBlockBorder: const HSLColor.fromAHSL(0.15, 240, 0.8, 0.5).toColor(),
@@ -73,6 +74,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
7374
return ContentTheme._(
7475
colorCodeBlockBackground: const HSLColor.fromAHSL(0.04, 0, 0, 1).toColor(),
7576
colorDirectMentionBackground: const HSLColor.fromAHSL(0.25, 240, 0.52, 0.6).toColor(),
77+
colorTopicMentionBackground: const HSLColor.fromAHSL(0.18, 183, 0.52, 0.40).toColor(),
7678
colorGlobalTimeBackground: const HSLColor.fromAHSL(0.2, 0, 0, 0).toColor(),
7779
colorGlobalTimeBorder: const HSLColor.fromAHSL(0.4, 0, 0, 0).toColor(),
7880
colorMathBlockBorder: const HSLColor.fromAHSL(1, 240, 0.4, 0.4).toColor(),
@@ -105,6 +107,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
105107
ContentTheme._({
106108
required this.colorCodeBlockBackground,
107109
required this.colorDirectMentionBackground,
110+
required this.colorTopicMentionBackground,
108111
required this.colorGlobalTimeBackground,
109112
required this.colorGlobalTimeBorder,
110113
required this.colorMathBlockBorder,
@@ -137,6 +140,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
137140

138141
final Color colorCodeBlockBackground;
139142
final Color colorDirectMentionBackground;
143+
final Color colorTopicMentionBackground;
140144
final Color colorGlobalTimeBackground;
141145
final Color colorGlobalTimeBorder;
142146
final Color colorMathBlockBorder; // TODO(#46) this won't be needed
@@ -197,6 +201,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
197201
ContentTheme copyWith({
198202
Color? colorCodeBlockBackground,
199203
Color? colorDirectMentionBackground,
204+
Color? colorTopicMentionBackground,
200205
Color? colorGlobalTimeBackground,
201206
Color? colorGlobalTimeBorder,
202207
Color? colorMathBlockBorder,
@@ -219,6 +224,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
219224
return ContentTheme._(
220225
colorCodeBlockBackground: colorCodeBlockBackground ?? this.colorCodeBlockBackground,
221226
colorDirectMentionBackground: colorDirectMentionBackground ?? this.colorDirectMentionBackground,
227+
colorTopicMentionBackground: colorTopicMentionBackground ?? this.colorTopicMentionBackground,
222228
colorGlobalTimeBackground: colorGlobalTimeBackground ?? this.colorGlobalTimeBackground,
223229
colorGlobalTimeBorder: colorGlobalTimeBorder ?? this.colorGlobalTimeBorder,
224230
colorMathBlockBorder: colorMathBlockBorder ?? this.colorMathBlockBorder,
@@ -248,6 +254,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
248254
return ContentTheme._(
249255
colorCodeBlockBackground: Color.lerp(colorCodeBlockBackground, other.colorCodeBlockBackground, t)!,
250256
colorDirectMentionBackground: Color.lerp(colorDirectMentionBackground, other.colorDirectMentionBackground, t)!,
257+
colorTopicMentionBackground: Color.lerp(colorTopicMentionBackground, other.colorTopicMentionBackground, t)!,
251258
colorGlobalTimeBackground: Color.lerp(colorGlobalTimeBackground, other.colorGlobalTimeBackground, t)!,
252259
colorGlobalTimeBorder: Color.lerp(colorGlobalTimeBorder, other.colorGlobalTimeBorder, t)!,
253260
colorMathBlockBorder: Color.lerp(colorMathBlockBorder, other.colorMathBlockBorder, t)!,
@@ -1133,7 +1140,10 @@ class Mention extends StatelessWidget {
11331140
return Container(
11341141
decoration: BoxDecoration(
11351142
// TODO(#646) different for wildcard mentions
1136-
color: contentTheme.colorDirectMentionBackground,
1143+
color: switch (node) {
1144+
UserMentionNode() => contentTheme.colorDirectMentionBackground,
1145+
TopicMentionNode() => contentTheme.colorTopicMentionBackground,
1146+
},
11371147
borderRadius: const BorderRadius.all(Radius.circular(3))),
11381148
padding: const EdgeInsets.symmetric(horizontal: 0.2 * kBaseFontSize),
11391149
child: InlineContent(

test/model/content_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,27 @@ class ContentExample {
181181
'<p><span class="silent user-mention" data-user-id="*">all</span></p>',
182182
const UserMentionNode(nodes: [TextNode('all')]));
183183

184+
static final topicMentionPlain = ContentExample.inline(
185+
'plain @-topic',
186+
"@**topic**",
187+
expectedText: '@topic',
188+
'<p><span class="topic-mention">@topic</span></p>',
189+
const TopicMentionNode(nodes: [TextNode('@topic')]));
190+
191+
static final topicMentionSilent = ContentExample.inline(
192+
'silent @-topic',
193+
"@_**topic**",
194+
expectedText: 'topic',
195+
'<p><span class="topic-mention silent">topic</span></p>',
196+
const TopicMentionNode(nodes: [TextNode('topic')]));
197+
198+
static final topicMentionSilentClassOrderReversed = ContentExample.inline(
199+
'silent @-topic, class order reversed',
200+
"@_**topic**", // (hypothetical server variation)
201+
expectedText: 'topic',
202+
'<p><span class="silent topic-mention">topic</span></p>',
203+
const TopicMentionNode(nodes: [TextNode('topic')]));
204+
184205
static final emojiUnicode = ContentExample.inline(
185206
'Unicode emoji, encoded in span element',
186207
":thumbs_up:",
@@ -1262,6 +1283,12 @@ void main() {
12621283
testParseExample(ContentExample.legacyChannelWildcardMentionPlain);
12631284
testParseExample(ContentExample.legacyChannelWildcardMentionSilent);
12641285
testParseExample(ContentExample.legacyChannelWildcardMentionSilentClassOrderReversed);
1286+
1287+
testParseExample(ContentExample.topicMentionPlain);
1288+
testParseExample(ContentExample.topicMentionSilent);
1289+
testParseExample(ContentExample.topicMentionSilentClassOrderReversed);
1290+
1291+
// TODO test wildcard mentions
12651292
});
12661293

12671294
testParseExample(ContentExample.emojiUnicode);

test/widgets/content_test.dart

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,8 @@ void main() {
669669
testContentSmoke(ContentExample.legacyChannelWildcardMentionPlain);
670670
testContentSmoke(ContentExample.legacyChannelWildcardMentionSilent);
671671
testContentSmoke(ContentExample.legacyChannelWildcardMentionSilentClassOrderReversed);
672+
testContentSmoke(ContentExample.topicMentionPlain);
673+
testContentSmoke(ContentExample.topicMentionSilent);
672674

673675
Mention? findMentionWidgetInSpan(InlineSpan rootSpan) {
674676
Mention? result;
@@ -687,7 +689,7 @@ void main() {
687689
findAncestor: find.byWidget(widget), mentionText)!;
688690
}
689691

690-
testWidgets('maintains font-size ratio with surrounding text', (tester) async {
692+
testWidgets('user-mention maintains font-size ratio with surrounding text', (tester) async {
691693
await checkFontSizeRatio(tester,
692694
targetHtml: '<span class="user-mention" data-user-id="13313">@Chris Bobbe</span>',
693695
targetFontSizeFinder: (rootSpan) {
@@ -724,6 +726,44 @@ void main() {
724726
// TODO(#647):
725727
// testFontWeight('non-silent self-user mention in bold context',
726728
// expectedWght: 800, // [etc.]
729+
730+
testWidgets('topic-mention maintains font-size ratio with surrounding text', (tester) async {
731+
await checkFontSizeRatio(tester,
732+
targetHtml: '<span class="topic-mention">@topic</span>',
733+
targetFontSizeFinder: (rootSpan) {
734+
final widget = findMentionWidgetInSpan(rootSpan);
735+
final style = textStyleFromWidget(tester, widget!, '@topic');
736+
return style.fontSize!;
737+
});
738+
});
739+
740+
testFontWeight('silent topic mention in plain paragraph',
741+
expectedWght: 400,
742+
// @_**topic**
743+
content: plainContent(
744+
'<p><span class="topic-mention silent">topic</span></p>'),
745+
styleFinder: (tester) {
746+
return textStyleFromWidget(tester,
747+
tester.widget(find.byType(Mention)), 'topic');
748+
});
749+
750+
// TODO(#647):
751+
// testFontWeight('non-silent topic mention in plain paragraph',
752+
// expectedWght: 600, // [etc.]
753+
754+
testFontWeight('silent topic mention in bold context',
755+
expectedWght: 600,
756+
// # @_**topic**
757+
content: plainContent(
758+
'<h1><span class="topic-mention silent">topic</span></h1>'),
759+
styleFinder: (tester) {
760+
return textStyleFromWidget(tester,
761+
tester.widget(find.byType(Mention)), 'topic');
762+
});
763+
764+
// TODO(#647):
765+
// testFontWeight('non-silent topic mention in bold context',
766+
// expectedWght: 800, // [etc.]
727767
});
728768

729769
Future<void> tapText(WidgetTester tester, Finder textFinder) async {

0 commit comments

Comments
 (0)