Skip to content

Commit 55f6453

Browse files
content: Handle @-topic mentions
Fixes: #892
1 parent 17a377c commit 55f6453

File tree

4 files changed

+97
-2
lines changed

4 files changed

+97
-2
lines changed

lib/model/content.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,10 @@ class UserMentionNode extends MentionNode {
646646
// final bool isSilent;
647647
}
648648

649+
class TopicMentionNode extends MentionNode {
650+
const TopicMentionNode({super.debugHtmlNode, required super.nodes});
651+
}
652+
649653
sealed class EmojiNode extends InlineContentNode {
650654
const EmojiNode({super.debugHtmlNode});
651655
}
@@ -818,6 +822,13 @@ class _ZulipContentParser {
818822
return RegExp("^(?:$mentionClass(?: silent)?|silent $mentionClass)\$");
819823
}();
820824

825+
static final _topicMentionClassNameRegexp = () {
826+
// This matches a class `topic-mention`, plus an optional class `silent`,
827+
// appearing in either order.
828+
const mentionClass = "topic-mention";
829+
return RegExp("^(?:$mentionClass(?: silent)?|silent $mentionClass)\$");
830+
}();
831+
821832
static final _emojiClassNameRegexp = () {
822833
const specificEmoji = r"emoji(?:-[0-9a-f]+)+";
823834
return RegExp("^(?:emoji $specificEmoji|$specificEmoji emoji)\$");
@@ -877,6 +888,14 @@ class _ZulipContentParser {
877888
return UserMentionNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
878889
}
879890

891+
if (localName == 'span'
892+
&& _topicMentionClassNameRegexp.hasMatch(className)) {
893+
// TODO assert TopicMentionNode can't contain LinkNode;
894+
// either a debug-mode check, or perhaps we can make expectations much
895+
// tighter on a TopicMentionNode's contents overall.
896+
return TopicMentionNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
897+
}
898+
880899
if (localName == 'span'
881900
&& _emojiClassNameRegexp.hasMatch(className)) {
882901
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(),
@@ -69,6 +70,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
6970
return ContentTheme._(
7071
colorCodeBlockBackground: const HSLColor.fromAHSL(0.04, 0, 0, 1).toColor(),
7172
colorDirectMentionBackground: const HSLColor.fromAHSL(0.25, 240, 0.52, 0.6).toColor(),
73+
colorTopicMentionBackground: const HSLColor.fromAHSL(0.18, 183, 0.52, 0.40).toColor(),
7274
colorGlobalTimeBackground: const HSLColor.fromAHSL(0.2, 0, 0, 0).toColor(),
7375
colorGlobalTimeBorder: const HSLColor.fromAHSL(0.4, 0, 0, 0).toColor(),
7476
colorMathBlockBorder: const HSLColor.fromAHSL(1, 240, 0.4, 0.4).toColor(),
@@ -97,6 +99,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
9799
ContentTheme._({
98100
required this.colorCodeBlockBackground,
99101
required this.colorDirectMentionBackground,
102+
required this.colorTopicMentionBackground,
100103
required this.colorGlobalTimeBackground,
101104
required this.colorGlobalTimeBorder,
102105
required this.colorMathBlockBorder,
@@ -126,6 +129,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
126129

127130
final Color colorCodeBlockBackground;
128131
final Color colorDirectMentionBackground;
132+
final Color colorTopicMentionBackground;
129133
final Color colorGlobalTimeBackground;
130134
final Color colorGlobalTimeBorder;
131135
final Color colorMathBlockBorder; // TODO(#46) this won't be needed
@@ -181,6 +185,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
181185
ContentTheme copyWith({
182186
Color? colorCodeBlockBackground,
183187
Color? colorDirectMentionBackground,
188+
Color? colorTopicMentionBackground,
184189
Color? colorGlobalTimeBackground,
185190
Color? colorGlobalTimeBorder,
186191
Color? colorMathBlockBorder,
@@ -200,6 +205,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
200205
return ContentTheme._(
201206
colorCodeBlockBackground: colorCodeBlockBackground ?? this.colorCodeBlockBackground,
202207
colorDirectMentionBackground: colorDirectMentionBackground ?? this.colorDirectMentionBackground,
208+
colorTopicMentionBackground: colorTopicMentionBackground ?? this.colorTopicMentionBackground,
203209
colorGlobalTimeBackground: colorGlobalTimeBackground ?? this.colorGlobalTimeBackground,
204210
colorGlobalTimeBorder: colorGlobalTimeBorder ?? this.colorGlobalTimeBorder,
205211
colorMathBlockBorder: colorMathBlockBorder ?? this.colorMathBlockBorder,
@@ -226,6 +232,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
226232
return ContentTheme._(
227233
colorCodeBlockBackground: Color.lerp(colorCodeBlockBackground, other.colorCodeBlockBackground, t)!,
228234
colorDirectMentionBackground: Color.lerp(colorDirectMentionBackground, other.colorDirectMentionBackground, t)!,
235+
colorTopicMentionBackground: Color.lerp(colorTopicMentionBackground, other.colorTopicMentionBackground, t)!,
229236
colorGlobalTimeBackground: Color.lerp(colorGlobalTimeBackground, other.colorGlobalTimeBackground, t)!,
230237
colorGlobalTimeBorder: Color.lerp(colorGlobalTimeBorder, other.colorGlobalTimeBorder, t)!,
231238
colorMathBlockBorder: Color.lerp(colorMathBlockBorder, other.colorMathBlockBorder, t)!,
@@ -1081,7 +1088,10 @@ class Mention extends StatelessWidget {
10811088
return Container(
10821089
decoration: BoxDecoration(
10831090
// TODO(#646) different for wildcard mentions
1084-
color: contentTheme.colorDirectMentionBackground,
1091+
color: switch (node) {
1092+
UserMentionNode() => contentTheme.colorDirectMentionBackground,
1093+
TopicMentionNode() => contentTheme.colorTopicMentionBackground,
1094+
},
10851095
borderRadius: const BorderRadius.all(Radius.circular(3))),
10861096
padding: const EdgeInsets.symmetric(horizontal: 0.2 * kBaseFontSize),
10871097
child: InlineContent(

test/model/content_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,27 @@ class ContentExample {
139139
'<p><span class="silent user-group-mention" data-user-group-id="186">test-empty</span></p>',
140140
const UserMentionNode(nodes: [TextNode('test-empty')]));
141141

142+
static final topicMentionPlain = ContentExample.inline(
143+
'plain @-topic',
144+
"@**topic**",
145+
expectedText: '@topic',
146+
'<p><span class="topic-mention">@topic</span></p>',
147+
const TopicMentionNode(nodes: [TextNode('@topic')]));
148+
149+
static final topicMentionSilent = ContentExample.inline(
150+
'silent @-topic',
151+
"@_**topic**",
152+
expectedText: 'topic',
153+
'<p><span class="topic-mention silent">topic</span></p>',
154+
const TopicMentionNode(nodes: [TextNode('topic')]));
155+
156+
static final topicMentionSilentClassOrderReversed = ContentExample.inline(
157+
'silent @-topic, class order reversed',
158+
"@_**topic**", // (hypothetical server variation)
159+
expectedText: 'topic',
160+
'<p><span class="silent topic-mention">topic</span></p>',
161+
const TopicMentionNode(nodes: [TextNode('topic')]));
162+
142163
static final emojiUnicode = ContentExample.inline(
143164
'Unicode emoji, encoded in span element',
144165
":thumbs_up:",
@@ -1013,6 +1034,10 @@ void main() {
10131034
testParseExample(ContentExample.groupMentionSilent);
10141035
testParseExample(ContentExample.groupMentionSilentClassOrderReversed);
10151036

1037+
testParseExample(ContentExample.topicMentionPlain);
1038+
testParseExample(ContentExample.topicMentionSilent);
1039+
testParseExample(ContentExample.topicMentionSilentClassOrderReversed);
1040+
10161041
// TODO test wildcard mentions
10171042
});
10181043

test/widgets/content_test.dart

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ void main() {
647647
return mergedStyleOfSubstring(fullNameSpan, mentionText)!;
648648
}
649649

650-
testWidgets('maintains font-size ratio with surrounding text', (tester) async {
650+
testWidgets('user-mention maintains font-size ratio with surrounding text', (tester) async {
651651
await checkFontSizeRatio(tester,
652652
targetHtml: '<span class="user-mention" data-user-id="13313">@Chris Bobbe</span>',
653653
targetFontSizeFinder: (rootSpan) {
@@ -684,6 +684,47 @@ void main() {
684684
// TODO(#647):
685685
// testFontWeight('non-silent self-user mention in bold context',
686686
// expectedWght: 800, // [etc.]
687+
688+
testContentSmoke(ContentExample.topicMentionPlain);
689+
testContentSmoke(ContentExample.topicMentionSilent);
690+
691+
testWidgets('topic-mention maintains font-size ratio with surrounding text', (tester) async {
692+
await checkFontSizeRatio(tester,
693+
targetHtml: '<span class="topic-mention">@topic</span>',
694+
targetFontSizeFinder: (rootSpan) {
695+
final widget = findMentionWidgetInSpan(rootSpan);
696+
final style = textStyleFromWidget(tester, widget!, '@topic');
697+
return style.fontSize!;
698+
});
699+
});
700+
701+
testFontWeight('silent topic mention in plain paragraph',
702+
expectedWght: 400,
703+
// @_**topic**
704+
content: plainContent(
705+
'<p><span class="topic-mention silent">topic</span></p>'),
706+
styleFinder: (tester) {
707+
return textStyleFromWidget(tester,
708+
tester.widget(find.byType(Mention)), 'topic');
709+
});
710+
711+
// TODO(#647):
712+
// testFontWeight('non-silent topic mention in plain paragraph',
713+
// expectedWght: 600, // [etc.]
714+
715+
testFontWeight('silent topic mention in bold context',
716+
expectedWght: 600,
717+
// # @_**topic**
718+
content: plainContent(
719+
'<h1><span class="topic-mention silent">topic</span></h1>'),
720+
styleFinder: (tester) {
721+
return textStyleFromWidget(tester,
722+
tester.widget(find.byType(Mention)), 'topic');
723+
});
724+
725+
// TODO(#647):
726+
// testFontWeight('non-silent topic mention in bold context',
727+
// expectedWght: 800, // [etc.]
687728
});
688729

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

0 commit comments

Comments
 (0)