implemented in-chat search with text highlighting, added search navigation UI, and integrated scrollable list for message jumping

This commit is contained in:
2026-05-09 22:21:36 +02:00
parent b36d1e02f5
commit 4c190de479
9 changed files with 698 additions and 36 deletions
+58 -10
View File
@@ -18,6 +18,9 @@ import 'bubble.dart';
import 'chat_bubble_poll.dart';
import 'chat_bubble_reactions.dart';
import 'chat_message_options_dialog.dart';
import 'highlighted_linkify.dart';
enum SearchHighlight { none, secondary, active }
class ChatBubble extends StatefulWidget {
final BuildContext context;
@@ -33,6 +36,9 @@ class ChatBubble extends StatefulWidget {
final void Function({bool renew}) refetch;
final String? highlightQuery;
final SearchHighlight matchHighlight;
const ChatBubble({
required this.context,
required this.isSender,
@@ -41,6 +47,8 @@ class ChatBubble extends StatefulWidget {
required this.refetch,
this.isRead = false,
this.selfId,
this.highlightQuery,
this.matchHighlight = SearchHighlight.none,
super.key,
});
@@ -142,13 +150,29 @@ class _ChatBubbleState extends State<ChatBubble>
BubbleStyle _getStyle() {
final styles = ChatBubbleStyles(context);
final BubbleStyle base;
if (widget.bubbleData.messageType !=
GetRoomResponseObjectMessageType.comment) {
return styles.getSystemStyle();
base = styles.getSystemStyle();
} else {
base = widget.isSender
? styles.getSelfStyle(false)
: styles.getRemoteStyle(false);
}
switch (widget.matchHighlight) {
case SearchHighlight.none:
return base;
case SearchHighlight.secondary:
return base.copyWith(
borderWidth: 1.5,
borderColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.45),
);
case SearchHighlight.active:
return base.copyWith(
borderWidth: 3,
borderColor: Theme.of(context).colorScheme.primary,
);
}
return widget.isSender
? styles.getSelfStyle(false)
: styles.getRemoteStyle(false);
}
void _showOptionsDialog() => showChatMessageOptionsDialog(
@@ -196,15 +220,29 @@ class _ChatBubbleState extends State<ChatBubble>
GetRoomResponseObjectMessageType.deletedComment;
final parent = widget.bubbleData.parent;
final actorBaseStyle = TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
);
final actorText = Text(
widget.bubbleData.actorDisplayName,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
style: actorBaseStyle,
);
final actorWidget = (widget.highlightQuery?.trim().isNotEmpty ?? false)
? Text.rich(
TextSpan(
children: buildHighlightedSpans(
text: widget.bubbleData.actorDisplayName,
query: widget.highlightQuery,
baseStyle: actorBaseStyle,
),
),
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
)
: actorText;
final timeText = Text(
DateTime.fromMillisecondsSinceEpoch(
@@ -252,8 +290,16 @@ class _ChatBubbleState extends State<ChatBubble>
style: _getStyle(),
child: _BubbleContent(
actorText: actorText,
actorWidget: actorWidget,
timeText: timeText,
messageWidget: message.getWidget(),
messageWidget: message.getWidget(
highlightQuery: widget.highlightQuery,
style:
widget.bubbleData.messageType ==
GetRoomResponseObjectMessageType.system
? Theme.of(context).textTheme.bodySmall
: null,
),
parent: parent,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
@@ -282,6 +328,7 @@ class _ChatBubbleState extends State<ChatBubble>
class _BubbleContent extends StatelessWidget {
final Text actorText;
final Widget actorWidget;
final Text timeText;
final Widget messageWidget;
final GetChatResponseObject? parent;
@@ -298,6 +345,7 @@ class _BubbleContent extends StatelessWidget {
const _BubbleContent({
required this.actorText,
required this.actorWidget,
required this.timeText,
required this.messageWidget,
required this.parent,
@@ -323,7 +371,7 @@ class _BubbleContent extends StatelessWidget {
),
child: Stack(
children: [
if (showActorDisplayName) Positioned(top: 0, left: 0, child: actorText),
if (showActorDisplayName) Positioned(top: 0, left: 0, child: actorWidget),
Padding(
padding: EdgeInsets.only(
bottom: showBubbleTime ? 18 : 0,