Skip to content

Commit 2f9a8f6

Browse files
committed
msglist: In keyword search narrow, highlight keywords in message content
Fixes #1692.
1 parent 9cc34cd commit 2f9a8f6

File tree

5 files changed

+73
-1
lines changed

5 files changed

+73
-1
lines changed

lib/model/content.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,10 @@ class GlobalTimeNode extends InlineContentNode {
913913
}
914914
}
915915

916+
class HighlightNode extends InlineContainerNode {
917+
const HighlightNode({super.debugHtmlNode, required super.nodes});
918+
}
919+
916920
////////////////////////////////////////////////////////////////
917921
918922
/// Parser for the inline-content subtrees within Zulip content HTML.
@@ -1087,6 +1091,10 @@ class _ZulipInlineContentParser {
10871091
return GlobalTimeNode(datetime: datetime, debugHtmlNode: debugHtmlNode);
10881092
}
10891093

1094+
if (localName == 'span' && className == 'highlight') {
1095+
return HighlightNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
1096+
}
1097+
10901098
if (localName == 'audio' && className.isEmpty) {
10911099
final srcAttr = element.attributes['src'];
10921100
if (srcAttr == null) return unimplemented();

lib/model/message_list.dart

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ mixin _MessageSequence {
118118
@visibleForTesting
119119
bool get oneMessagePerBlock;
120120

121+
/// Whether the narrow includes a keyword search.
122+
///
123+
/// If false, messages won't be expected to have
124+
/// [Message.matchContent] or [Message.matchTopic].
125+
bool get hasKeywordSearchFilter;
126+
121127
/// A sequence number for invalidating stale fetches.
122128
int generation = 0;
123129

@@ -257,10 +263,25 @@ mixin _MessageSequence {
257263
}
258264
}
259265

266+
final Map<int, String> _matchContentByMessageId = {};
267+
268+
void _captureMatchContentAndTopic(List<Message> messages) {
269+
if (!hasKeywordSearchFilter) return;
270+
271+
for (final message in messages) {
272+
final Message(:matchContent, :matchTopic) = message;
273+
if (matchContent != null) {
274+
_matchContentByMessageId[message.id] = matchContent;
275+
}
276+
// TODO matchTopic
277+
}
278+
}
279+
260280
ZulipMessageContent _parseMessageContent(Message message) {
261281
final poll = message.poll;
262282
if (poll != null) return PollContent(poll);
263-
return parseContent(message.content);
283+
final content = _matchContentByMessageId[message.id] ?? message.content;
284+
return parseContent(content);
264285
}
265286

266287
/// Update data derived from the content of the index-th message.
@@ -419,6 +440,7 @@ mixin _MessageSequence {
419440
contents.clear();
420441
items.clear();
421442
middleItem = 0;
443+
_matchContentByMessageId.clear();
422444
}
423445

424446
/// Redo all computations from scratch, based on [messages].
@@ -659,6 +681,16 @@ class MessageListView with ChangeNotifier, _MessageSequence {
659681
|| KeywordSearchNarrow() => true,
660682
};
661683

684+
@override bool get hasKeywordSearchFilter => switch (narrow) {
685+
CombinedFeedNarrow()
686+
|| ChannelNarrow()
687+
|| TopicNarrow()
688+
|| DmNarrow()
689+
|| MentionsNarrow()
690+
|| StarredMessagesNarrow() => false,
691+
KeywordSearchNarrow() => true,
692+
};
693+
662694
/// Whether [message] should actually appear in this message list,
663695
/// given that it does belong to the narrow.
664696
///
@@ -793,6 +825,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
793825

794826
_adjustNarrowForTopicPermalink(result.messages.firstOrNull);
795827

828+
_captureMatchContentAndTopic(result.messages);
829+
796830
store.reconcileMessages(result.messages);
797831
store.recentSenders.handleMessages(result.messages); // TODO(#824)
798832

@@ -877,6 +911,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
877911
result.messages.removeLast();
878912
}
879913

914+
_captureMatchContentAndTopic(result.messages);
915+
880916
store.reconcileMessages(result.messages);
881917
store.recentSenders.handleMessages(result.messages); // TODO(#824)
882918

@@ -913,6 +949,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
913949
result.messages.removeAt(0);
914950
}
915951

952+
_captureMatchContentAndTopic(result.messages);
953+
916954
store.reconcileMessages(result.messages);
917955
store.recentSenders.handleMessages(result.messages); // TODO(#824)
918956

@@ -1149,6 +1187,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
11491187
void messageContentChanged(int messageId) {
11501188
final index = _findMessageWithId(messageId);
11511189
if (index != -1) {
1190+
_matchContentByMessageId.remove(messageId);
11521191
_reparseContent(index);
11531192
}
11541193
}

lib/widgets/content.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
4848
colorDirectMentionBackground: const HSLColor.fromAHSL(0.2, 240, 0.7, 0.7).toColor(),
4949
colorGlobalTimeBackground: const HSLColor.fromAHSL(1, 0, 0, 0.93).toColor(),
5050
colorGlobalTimeBorder: const HSLColor.fromAHSL(1, 0, 0, 0.8).toColor(),
51+
colorHighlightBackground: const Color(0xfffcef9f),
5152
colorLink: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor(),
5253
colorMathBlockBorder: const HSLColor.fromAHSL(0.15, 240, 0.8, 0.5).toColor(),
5354
colorMessageMediaContainerBackground: const Color.fromRGBO(0, 0, 0, 0.03),
@@ -82,6 +83,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
8283
colorDirectMentionBackground: const HSLColor.fromAHSL(0.25, 240, 0.52, 0.6).toColor(),
8384
colorGlobalTimeBackground: const HSLColor.fromAHSL(0.2, 0, 0, 0).toColor(),
8485
colorGlobalTimeBorder: const HSLColor.fromAHSL(0.4, 0, 0, 0).toColor(),
86+
colorHighlightBackground: const Color(0xffffe757).withValues(alpha: 0.35),
8587
colorLink: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor(), // the same as light in Web
8688
colorMathBlockBorder: const HSLColor.fromAHSL(1, 240, 0.4, 0.4).toColor(),
8789
colorMessageMediaContainerBackground: const HSLColor.fromAHSL(0.03, 0, 0, 1).toColor(),
@@ -115,6 +117,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
115117
required this.colorDirectMentionBackground,
116118
required this.colorGlobalTimeBackground,
117119
required this.colorGlobalTimeBorder,
120+
required this.colorHighlightBackground,
118121
required this.colorLink,
119122
required this.colorMathBlockBorder,
120123
required this.colorMessageMediaContainerBackground,
@@ -148,6 +151,10 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
148151
final Color colorDirectMentionBackground;
149152
final Color colorGlobalTimeBackground;
150153
final Color colorGlobalTimeBorder;
154+
155+
// From Figma: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=10904-102278&m=dev
156+
final Color colorHighlightBackground;
157+
151158
final Color colorLink;
152159
final Color colorMathBlockBorder; // TODO(#46) this won't be needed
153160
final Color colorMessageMediaContainerBackground;
@@ -209,6 +216,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
209216
Color? colorDirectMentionBackground,
210217
Color? colorGlobalTimeBackground,
211218
Color? colorGlobalTimeBorder,
219+
Color? colorHighlightBackground,
212220
Color? colorLink,
213221
Color? colorMathBlockBorder,
214222
Color? colorMessageMediaContainerBackground,
@@ -232,6 +240,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
232240
colorDirectMentionBackground: colorDirectMentionBackground ?? this.colorDirectMentionBackground,
233241
colorGlobalTimeBackground: colorGlobalTimeBackground ?? this.colorGlobalTimeBackground,
234242
colorGlobalTimeBorder: colorGlobalTimeBorder ?? this.colorGlobalTimeBorder,
243+
colorHighlightBackground: colorHighlightBackground ?? this.colorHighlightBackground,
235244
colorLink: colorLink ?? this.colorLink,
236245
colorMathBlockBorder: colorMathBlockBorder ?? this.colorMathBlockBorder,
237246
colorMessageMediaContainerBackground: colorMessageMediaContainerBackground ?? this.colorMessageMediaContainerBackground,
@@ -262,6 +271,7 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
262271
colorDirectMentionBackground: Color.lerp(colorDirectMentionBackground, other.colorDirectMentionBackground, t)!,
263272
colorGlobalTimeBackground: Color.lerp(colorGlobalTimeBackground, other.colorGlobalTimeBackground, t)!,
264273
colorGlobalTimeBorder: Color.lerp(colorGlobalTimeBorder, other.colorGlobalTimeBorder, t)!,
274+
colorHighlightBackground: Color.lerp(colorHighlightBackground, other.colorHighlightBackground, t)!,
265275
colorLink: Color.lerp(colorLink, other.colorLink, t)!,
266276
colorMathBlockBorder: Color.lerp(colorMathBlockBorder, other.colorMathBlockBorder, t)!,
267277
colorMessageMediaContainerBackground: Color.lerp(colorMessageMediaContainerBackground, other.colorMessageMediaContainerBackground, t)!,
@@ -1278,6 +1288,10 @@ class _InlineContentBuilder {
12781288
return WidgetSpan(alignment: PlaceholderAlignment.middle,
12791289
child: GlobalTime(node: node, ambientTextStyle: widget.style));
12801290

1291+
case HighlightNode():
1292+
return _buildNodes(node.nodes,
1293+
style: TextStyle(backgroundColor: ContentTheme.of(_context!).colorHighlightBackground));
1294+
12811295
case UnimplementedInlineContentNode():
12821296
return _errorUnimplemented(node, context: _context!);
12831297
}

test/model/content_test.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,13 @@ class ContentExample {
261261
GlobalTimeNode(
262262
datetime: DateTime.parse("2024-03-07T23:00:00Z")));
263263

264+
static final highlight = ContentExample.inline(
265+
'highlight (for search)',
266+
null, // keyword highlighting is done by the server; no Markdown representation
267+
expectedText: 'keyword',
268+
'<p><span class="highlight">keyword</span></p>',
269+
const HighlightNode(nodes: [TextNode('keyword')]));
270+
264271
static final messageLink = ContentExample.inline(
265272
'message link',
266273
'#**api design>notation for near links@1972281**',
@@ -1823,6 +1830,8 @@ void main() async {
18231830
);
18241831
});
18251832

1833+
testParseExample(ContentExample.highlight);
1834+
18261835
//
18271836
// Block content.
18281837
//

test/widgets/content_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,8 @@ void main() {
11671167
});
11681168
});
11691169

1170+
testContentSmoke(ContentExample.highlight);
1171+
11701172
group('InlineAudio', () {
11711173
Future<void> prepare(WidgetTester tester, String html) async {
11721174
await prepareContent(tester, plainContent(html),

0 commit comments

Comments
 (0)