From c56189933aa17d33f0ed4c20a741283ee2edf122 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Sun, 2 Nov 2025 17:23:13 +0530 Subject: [PATCH] semantics: Fix content navigation order in message content when links present Fixes #1963. --- lib/widgets/content.dart | 111 +++++++++++++++++----------------- lib/widgets/message_list.dart | 95 +++++++++++++++-------------- 2 files changed, 105 insertions(+), 101 deletions(-) diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 85a573cdcb..749e2bf4bc 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -295,13 +295,14 @@ class MessageContent extends StatelessWidget { @override Widget build(BuildContext context) { final content = this.content; - return InheritedMessage(message: message, - child: DefaultTextStyle( - style: ContentTheme.of(context).textStylePlainParagraph, - child: switch (content) { - ZulipContent() => BlockContentList(nodes: content.nodes), - PollContent() => PollWidget(messageId: message.id, poll: content.poll), - })); + return MergeSemantics( + child: InheritedMessage(message: message, + child: DefaultTextStyle( + style: ContentTheme.of(context).textStylePlainParagraph, + child: switch (content) { + ZulipContent() => BlockContentList(nodes: content.nodes), + PollContent() => PollWidget(messageId: message.id, poll: content.poll), + }))); } } @@ -333,53 +334,55 @@ class BlockContentList extends StatelessWidget { @override Widget build(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ...nodes.map((node) { - return switch (node) { - LineBreakNode() => - // This goes in a Column. So to get the effect of a newline, - // just use an empty Text. - const Text(''), - ThematicBreakNode() => const ThematicBreak(), - ParagraphNode() => Paragraph(node: node), - HeadingNode() => Heading(node: node), - QuotationNode() => Quotation(node: node), - ListNode() => ListNodeWidget(node: node), - SpoilerNode() => Spoiler(node: node), - CodeBlockNode() => CodeBlock(node: node), - MathBlockNode() => MathBlock(node: node), - ImagePreviewNodeList() => MessageImagePreviewList(node: node), - ImagePreviewNode() => (){ - assert(false, - "[ImagePreviewNode] not allowed in [BlockContentList]. " - "It should be wrapped in [ImagePreviewNodeList]." - ); - return MessageImagePreview(node: node); - }(), - InlineVideoNode() => MessageInlineVideo(node: node), - EmbedVideoNode() => MessageEmbedVideo(node: node), - TableNode() => MessageTable(node: node), - TableRowNode() => () { - assert(false, - "[TableRowNode] not allowed in [BlockContentList]. " - "It should be wrapped in [TableNode]." - ); - return const SizedBox.shrink(); - }(), - TableCellNode() => () { - assert(false, - "[TableCellNode] not allowed in [BlockContentList]. " - "It should be wrapped in [TableRowNode]." - ); - return const SizedBox.shrink(); - }(), - WebsitePreviewNode() => WebsitePreview(node: node), - UnimplementedBlockContentNode() => - Text.rich(_errorUnimplemented(node, context: context)), - }; - - }), - ]); + return Semantics( + explicitChildNodes: true, + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + ...nodes.map((node) { + return switch (node) { + LineBreakNode() => + // This goes in a Column. So to get the effect of a newline, + // just use an empty Text. + const Text(''), + ThematicBreakNode() => const ThematicBreak(), + ParagraphNode() => Paragraph(node: node), + HeadingNode() => Heading(node: node), + QuotationNode() => Quotation(node: node), + ListNode() => ListNodeWidget(node: node), + SpoilerNode() => Spoiler(node: node), + CodeBlockNode() => CodeBlock(node: node), + MathBlockNode() => MathBlock(node: node), + ImagePreviewNodeList() => MessageImagePreviewList(node: node), + ImagePreviewNode() => (){ + assert(false, + "[ImagePreviewNode] not allowed in [BlockContentList]. " + "It should be wrapped in [ImagePreviewNodeList]." + ); + return MessageImagePreview(node: node); + }(), + InlineVideoNode() => MessageInlineVideo(node: node), + EmbedVideoNode() => MessageEmbedVideo(node: node), + TableNode() => MessageTable(node: node), + TableRowNode() => () { + assert(false, + "[TableRowNode] not allowed in [BlockContentList]. " + "It should be wrapped in [TableNode]." + ); + return const SizedBox.shrink(); + }(), + TableCellNode() => () { + assert(false, + "[TableCellNode] not allowed in [BlockContentList]. " + "It should be wrapped in [TableRowNode]." + ); + return const SizedBox.shrink(); + }(), + WebsitePreviewNode() => WebsitePreview(node: node), + UnimplementedBlockContentNode() => + Text.rich(_errorUnimplemented(node, context: context)), + }; + + }), + ])); } } diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 0d3038b1da..ec3ead5513 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -2205,53 +2205,54 @@ class MessageWithPossibleSender extends StatelessWidget { : () => showMessageActionSheet(context: context, message: message), child: Padding( padding: const EdgeInsets.only(top: 4), - child: Column(children: [ - if (item.showSender) - SenderRow(message: message, - timestampStyle: MessageTimestampStyle.timeOnly), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: localizedTextBaseline(context), - children: [ - const SizedBox(width: 16), - Expanded(child: showAsMuted - ? Align( - alignment: AlignmentDirectional.topStart, - child: ZulipWebUiKitButton( - label: zulipLocalizations.revealButtonLabel, - icon: ZulipIcons.eye, - size: ZulipWebUiKitButtonSize.small, - intent: ZulipWebUiKitButtonIntent.neutral, - attention: ZulipWebUiKitButtonAttention.minimal, - onPressed: () { - MessageListPage.ancestorOf(context).revealMutedMessage(message.id); - })) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - content, - if ((message.reactions?.total ?? 0) > 0) - ReactionChipsList(messageId: message.id, reactions: message.reactions!), - if (editMessageErrorStatus != null) - _EditMessageStatusRow(messageId: message.id, status: editMessageErrorStatus) - else if (editStateText != null) - Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text(editStateText, - textAlign: TextAlign.end, - style: TextStyle( - color: designVariables.labelEdited, - fontSize: 12, - height: (12 / 12), - letterSpacing: proportionalLetterSpacing(context, - 0.05, baseFontSize: 12)))) - else - Padding(padding: const EdgeInsets.only(bottom: 4)) - ])), - SizedBox(width: 16, - child: star), - ]), - ]))); + child: MergeSemantics( + child: Column(children: [ + if (item.showSender) + SenderRow(message: message, + timestampStyle: MessageTimestampStyle.timeOnly), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: localizedTextBaseline(context), + children: [ + const SizedBox(width: 16), + Expanded(child: showAsMuted + ? Align( + alignment: AlignmentDirectional.topStart, + child: ZulipWebUiKitButton( + label: zulipLocalizations.revealButtonLabel, + icon: ZulipIcons.eye, + size: ZulipWebUiKitButtonSize.small, + intent: ZulipWebUiKitButtonIntent.neutral, + attention: ZulipWebUiKitButtonAttention.minimal, + onPressed: () { + MessageListPage.ancestorOf(context).revealMutedMessage(message.id); + })) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + content, + if ((message.reactions?.total ?? 0) > 0) + ReactionChipsList(messageId: message.id, reactions: message.reactions!), + if (editMessageErrorStatus != null) + _EditMessageStatusRow(messageId: message.id, status: editMessageErrorStatus) + else if (editStateText != null) + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text(editStateText, + textAlign: TextAlign.end, + style: TextStyle( + color: designVariables.labelEdited, + fontSize: 12, + height: (12 / 12), + letterSpacing: proportionalLetterSpacing(context, + 0.05, baseFontSize: 12)))) + else + Padding(padding: const EdgeInsets.only(bottom: 4)) + ])), + SizedBox(width: 16, + child: star), + ]), + ])))); } }