Skip to content

Commit

Permalink
content: Handle @-topic mentions
Browse files Browse the repository at this point in the history
Fixes: #892
  • Loading branch information
rajveermalviya committed Dec 12, 2024
1 parent abb2bcb commit 48f31fb
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 2 deletions.
19 changes: 19 additions & 0 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,10 @@ class UserMentionNode extends MentionNode {
// final bool isSilent; // TODO(#647)
}

class TopicMentionNode extends MentionNode {
const TopicMentionNode({super.debugHtmlNode, required super.nodes});
}

sealed class EmojiNode extends InlineContentNode {
const EmojiNode({super.debugHtmlNode});
}
Expand Down Expand Up @@ -939,6 +943,13 @@ class _ZulipContentParser {
static final _userMentionClassNameRegexp = RegExp(
r"(^| )" r"user(?:-group)?-mention" r"( |$)");

static final _topicMentionClassNameRegexp = () {
// This matches a class `topic-mention`, plus an optional class `silent`,
// appearing in either order.
const mentionClass = "topic-mention";
return RegExp("^(?:$mentionClass(?: silent)?|silent $mentionClass)\$");
}();

static final _emojiClassNameRegexp = () {
const specificEmoji = r"emoji(?:-[0-9a-f]+)+";
return RegExp("^(?:emoji $specificEmoji|$specificEmoji emoji)\$");
Expand Down Expand Up @@ -995,6 +1006,14 @@ class _ZulipContentParser {
return parseUserMention(element) ?? unimplemented();
}

if (localName == 'span'
&& _topicMentionClassNameRegexp.hasMatch(className)) {
// TODO assert TopicMentionNode can't contain LinkNode;
// either a debug-mode check, or perhaps we can make expectations much
// tighter on a TopicMentionNode's contents overall.
return TopicMentionNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
}

if (localName == 'span'
&& _emojiClassNameRegexp.hasMatch(className)) {
final emojiCode = _emojiCodeFromClassNameRegexp.firstMatch(className)!
Expand Down
12 changes: 11 additions & 1 deletion lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
return ContentTheme._(
colorCodeBlockBackground: const HSLColor.fromAHSL(0.04, 0, 0, 0).toColor(),
colorDirectMentionBackground: const HSLColor.fromAHSL(0.2, 240, 0.7, 0.7).toColor(),
colorTopicMentionBackground: const HSLColor.fromAHSL(0.18, 183, 0.6, 0.45).toColor(),
colorGlobalTimeBackground: const HSLColor.fromAHSL(1, 0, 0, 0.93).toColor(),
colorGlobalTimeBorder: const HSLColor.fromAHSL(1, 0, 0, 0.8).toColor(),
colorMathBlockBorder: const HSLColor.fromAHSL(0.15, 240, 0.8, 0.5).toColor(),
Expand Down Expand Up @@ -73,6 +74,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
return ContentTheme._(
colorCodeBlockBackground: const HSLColor.fromAHSL(0.04, 0, 0, 1).toColor(),
colorDirectMentionBackground: const HSLColor.fromAHSL(0.25, 240, 0.52, 0.6).toColor(),
colorTopicMentionBackground: const HSLColor.fromAHSL(0.18, 183, 0.52, 0.40).toColor(),
colorGlobalTimeBackground: const HSLColor.fromAHSL(0.2, 0, 0, 0).toColor(),
colorGlobalTimeBorder: const HSLColor.fromAHSL(0.4, 0, 0, 0).toColor(),
colorMathBlockBorder: const HSLColor.fromAHSL(1, 240, 0.4, 0.4).toColor(),
Expand Down Expand Up @@ -105,6 +107,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
ContentTheme._({
required this.colorCodeBlockBackground,
required this.colorDirectMentionBackground,
required this.colorTopicMentionBackground,
required this.colorGlobalTimeBackground,
required this.colorGlobalTimeBorder,
required this.colorMathBlockBorder,
Expand Down Expand Up @@ -137,6 +140,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {

final Color colorCodeBlockBackground;
final Color colorDirectMentionBackground;
final Color colorTopicMentionBackground;
final Color colorGlobalTimeBackground;
final Color colorGlobalTimeBorder;
final Color colorMathBlockBorder; // TODO(#46) this won't be needed
Expand Down Expand Up @@ -197,6 +201,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
ContentTheme copyWith({
Color? colorCodeBlockBackground,
Color? colorDirectMentionBackground,
Color? colorTopicMentionBackground,
Color? colorGlobalTimeBackground,
Color? colorGlobalTimeBorder,
Color? colorMathBlockBorder,
Expand All @@ -219,6 +224,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
return ContentTheme._(
colorCodeBlockBackground: colorCodeBlockBackground ?? this.colorCodeBlockBackground,
colorDirectMentionBackground: colorDirectMentionBackground ?? this.colorDirectMentionBackground,
colorTopicMentionBackground: colorTopicMentionBackground ?? this.colorTopicMentionBackground,
colorGlobalTimeBackground: colorGlobalTimeBackground ?? this.colorGlobalTimeBackground,
colorGlobalTimeBorder: colorGlobalTimeBorder ?? this.colorGlobalTimeBorder,
colorMathBlockBorder: colorMathBlockBorder ?? this.colorMathBlockBorder,
Expand Down Expand Up @@ -248,6 +254,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
return ContentTheme._(
colorCodeBlockBackground: Color.lerp(colorCodeBlockBackground, other.colorCodeBlockBackground, t)!,
colorDirectMentionBackground: Color.lerp(colorDirectMentionBackground, other.colorDirectMentionBackground, t)!,
colorTopicMentionBackground: Color.lerp(colorTopicMentionBackground, other.colorTopicMentionBackground, t)!,
colorGlobalTimeBackground: Color.lerp(colorGlobalTimeBackground, other.colorGlobalTimeBackground, t)!,
colorGlobalTimeBorder: Color.lerp(colorGlobalTimeBorder, other.colorGlobalTimeBorder, t)!,
colorMathBlockBorder: Color.lerp(colorMathBlockBorder, other.colorMathBlockBorder, t)!,
Expand Down Expand Up @@ -1133,7 +1140,10 @@ class Mention extends StatelessWidget {
return Container(
decoration: BoxDecoration(
// TODO(#646) different for wildcard mentions
color: contentTheme.colorDirectMentionBackground,
color: switch (node) {
UserMentionNode() => contentTheme.colorDirectMentionBackground,
TopicMentionNode() => contentTheme.colorTopicMentionBackground,
},
borderRadius: const BorderRadius.all(Radius.circular(3))),
padding: const EdgeInsets.symmetric(horizontal: 0.2 * kBaseFontSize),
child: InlineContent(
Expand Down
26 changes: 26 additions & 0 deletions test/model/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ class ContentExample {
expectedText: 'all',
'<p><span class="silent user-mention" data-user-id="*">all</span></p>',
const UserMentionNode(nodes: [TextNode('all')]));
static final topicMentionPlain = ContentExample.inline(
'plain @-topic',
"@**topic**",
expectedText: '@topic',
'<p><span class="topic-mention">@topic</span></p>',
const TopicMentionNode(nodes: [TextNode('@topic')]));

static final topicMentionSilent = ContentExample.inline(
'silent @-topic',
"@_**topic**",
expectedText: 'topic',
'<p><span class="topic-mention silent">topic</span></p>',
const TopicMentionNode(nodes: [TextNode('topic')]));

static final topicMentionSilentClassOrderReversed = ContentExample.inline(
'silent @-topic, class order reversed',
"@_**topic**", // (hypothetical server variation)
expectedText: 'topic',
'<p><span class="silent topic-mention">topic</span></p>',
const TopicMentionNode(nodes: [TextNode('topic')]));

static final emojiUnicode = ContentExample.inline(
'Unicode emoji, encoded in span element',
Expand Down Expand Up @@ -1262,6 +1282,12 @@ void main() {
testParseExample(ContentExample.legacyChannelWildcardMentionPlain);
testParseExample(ContentExample.legacyChannelWildcardMentionSilent);
testParseExample(ContentExample.legacyChannelWildcardMentionSilentClassOrderReversed);

testParseExample(ContentExample.topicMentionPlain);
testParseExample(ContentExample.topicMentionSilent);
testParseExample(ContentExample.topicMentionSilentClassOrderReversed);

// TODO test wildcard mentions
});

testParseExample(ContentExample.emojiUnicode);
Expand Down
43 changes: 42 additions & 1 deletion test/widgets/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ void main() {
findAncestor: find.byWidget(widget), mentionText)!;
}

testWidgets('maintains font-size ratio with surrounding text', (tester) async {
testWidgets('user-mention maintains font-size ratio with surrounding text', (tester) async {
await checkFontSizeRatio(tester,
targetHtml: '<span class="user-mention" data-user-id="13313">@Chris Bobbe</span>',
targetFontSizeFinder: (rootSpan) {
Expand Down Expand Up @@ -724,6 +724,47 @@ void main() {
// TODO(#647):
// testFontWeight('non-silent self-user mention in bold context',
// expectedWght: 800, // [etc.]

testContentSmoke(ContentExample.topicMentionPlain);
testContentSmoke(ContentExample.topicMentionSilent);

testWidgets('topic-mention maintains font-size ratio with surrounding text', (tester) async {
await checkFontSizeRatio(tester,
targetHtml: '<span class="topic-mention">@topic</span>',
targetFontSizeFinder: (rootSpan) {
final widget = findMentionWidgetInSpan(rootSpan);
final style = textStyleFromWidget(tester, widget!, '@topic');
return style.fontSize!;
});
});

testFontWeight('silent topic mention in plain paragraph',
expectedWght: 400,
// @_**topic**
content: plainContent(
'<p><span class="topic-mention silent">topic</span></p>'),
styleFinder: (tester) {
return textStyleFromWidget(tester,
tester.widget(find.byType(Mention)), 'topic');
});

// TODO(#647):
// testFontWeight('non-silent topic mention in plain paragraph',
// expectedWght: 600, // [etc.]

testFontWeight('silent topic mention in bold context',
expectedWght: 600,
// # @_**topic**
content: plainContent(
'<h1><span class="topic-mention silent">topic</span></h1>'),
styleFinder: (tester) {
return textStyleFromWidget(tester,
tester.widget(find.byType(Mention)), 'topic');
});

// TODO(#647):
// testFontWeight('non-silent topic mention in bold context',
// expectedWght: 800, // [etc.]
});

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

0 comments on commit 48f31fb

Please sign in to comment.