@@ -48,6 +48,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
4848 colorPollVoteCountBackground: const HSLColor .fromAHSL (1 , 0 , 0 , 1 ).toColor (),
4949 colorPollVoteCountBorder: const HSLColor .fromAHSL (1 , 156 , 0.28 , 0.7 ).toColor (),
5050 colorPollVoteCountText: const HSLColor .fromAHSL (1 , 156 , 0.41 , 0.4 ).toColor (),
51+ colorTableCellBorder: const HSLColor .fromAHSL (1 , 0 , 0 , 0.80 ).toColor (),
52+ colorTableHeaderBackground: const HSLColor .fromAHSL (1 , 0 , 0 , 0.93 ).toColor (),
5153 colorThematicBreak: const HSLColor .fromAHSL (1 , 0 , 0 , .87 ).toColor (),
5254 textStylePlainParagraph: _plainParagraphCommon (context).copyWith (
5355 color: const HSLColor .fromAHSL (1 , 0 , 0 , 0.15 ).toColor (),
@@ -77,6 +79,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
7779 colorPollVoteCountBackground: const HSLColor .fromAHSL (0.2 , 0 , 0 , 0 ).toColor (),
7880 colorPollVoteCountBorder: const HSLColor .fromAHSL (1 , 185 , 0.35 , 0.35 ).toColor (),
7981 colorPollVoteCountText: const HSLColor .fromAHSL (1 , 185 , 0.35 , 0.65 ).toColor (),
82+ colorTableCellBorder: const HSLColor .fromAHSL (1 , 0 , 0 , 0.33 ).toColor (),
83+ colorTableHeaderBackground: const HSLColor .fromAHSL (0.5 , 0 , 0 , 0 ).toColor (),
8084 colorThematicBreak: const HSLColor .fromAHSL (1 , 0 , 0 , .87 ).toColor ().withValues (alpha: 0.2 ),
8185 textStylePlainParagraph: _plainParagraphCommon (context).copyWith (
8286 color: const HSLColor .fromAHSL (1 , 0 , 0 , 0.85 ).toColor (),
@@ -105,6 +109,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
105109 required this .colorPollVoteCountBackground,
106110 required this .colorPollVoteCountBorder,
107111 required this .colorPollVoteCountText,
112+ required this .colorTableCellBorder,
113+ required this .colorTableHeaderBackground,
108114 required this .colorThematicBreak,
109115 required this .textStylePlainParagraph,
110116 required this .codeBlockTextStyles,
@@ -134,6 +140,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
134140 final Color colorPollVoteCountBackground;
135141 final Color colorPollVoteCountBorder;
136142 final Color colorPollVoteCountText;
143+ final Color colorTableCellBorder;
144+ final Color colorTableHeaderBackground;
137145 final Color colorThematicBreak;
138146
139147 /// The complete [TextStyle] we use for plain, unstyled paragraphs.
@@ -189,6 +197,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
189197 Color ? colorPollVoteCountBackground,
190198 Color ? colorPollVoteCountBorder,
191199 Color ? colorPollVoteCountText,
200+ Color ? colorTableCellBorder,
201+ Color ? colorTableHeaderBackground,
192202 Color ? colorThematicBreak,
193203 TextStyle ? textStylePlainParagraph,
194204 CodeBlockTextStyles ? codeBlockTextStyles,
@@ -208,6 +218,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
208218 colorPollVoteCountBackground: colorPollVoteCountBackground ?? this .colorPollVoteCountBackground,
209219 colorPollVoteCountBorder: colorPollVoteCountBorder ?? this .colorPollVoteCountBorder,
210220 colorPollVoteCountText: colorPollVoteCountText ?? this .colorPollVoteCountText,
221+ colorTableCellBorder: colorTableCellBorder ?? this .colorTableCellBorder,
222+ colorTableHeaderBackground: colorTableHeaderBackground ?? this .colorTableHeaderBackground,
211223 colorThematicBreak: colorThematicBreak ?? this .colorThematicBreak,
212224 textStylePlainParagraph: textStylePlainParagraph ?? this .textStylePlainParagraph,
213225 codeBlockTextStyles: codeBlockTextStyles ?? this .codeBlockTextStyles,
@@ -234,6 +246,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
234246 colorPollVoteCountBackground: Color .lerp (colorPollVoteCountBackground, other.colorPollVoteCountBackground, t)! ,
235247 colorPollVoteCountBorder: Color .lerp (colorPollVoteCountBorder, other.colorPollVoteCountBorder, t)! ,
236248 colorPollVoteCountText: Color .lerp (colorPollVoteCountText, other.colorPollVoteCountText, t)! ,
249+ colorTableCellBorder: Color .lerp (colorTableCellBorder, other.colorTableCellBorder, t)! ,
250+ colorTableHeaderBackground: Color .lerp (colorTableHeaderBackground, other.colorTableHeaderBackground, t)! ,
237251 colorThematicBreak: Color .lerp (colorThematicBreak, other.colorThematicBreak, t)! ,
238252 textStylePlainParagraph: TextStyle .lerp (textStylePlainParagraph, other.textStylePlainParagraph, t)! ,
239253 codeBlockTextStyles: CodeBlockTextStyles .lerp (codeBlockTextStyles, other.codeBlockTextStyles, t),
@@ -324,6 +338,21 @@ class BlockContentList extends StatelessWidget {
324338 }(),
325339 InlineVideoNode () => MessageInlineVideo (node: node),
326340 EmbedVideoNode () => MessageEmbedVideo (node: node),
341+ TableNode () => MessageTable (node: node),
342+ TableRowNode () => () {
343+ assert (false ,
344+ "[TableRowNode] not allowed in [BlockContentList]. "
345+ "It should be wrapped in [TableNode]."
346+ );
347+ return const SizedBox .shrink ();
348+ }(),
349+ TableCellNode () => () {
350+ assert (false ,
351+ "[TableCellNode] not allowed in [BlockContentList]. "
352+ "It should be wrapped in [TableRowNode]."
353+ );
354+ return const SizedBox .shrink ();
355+ }(),
327356 UnimplementedBlockContentNode () =>
328357 Text .rich (_errorUnimplemented (node, context: context)),
329358 };
@@ -1196,6 +1225,62 @@ class GlobalTime extends StatelessWidget {
11961225 }
11971226}
11981227
1228+ class MessageTable extends StatelessWidget {
1229+ const MessageTable ({super .key, required this .node});
1230+
1231+ final TableNode node;
1232+
1233+ @override
1234+ Widget build (BuildContext context) {
1235+ final contentTheme = ContentTheme .of (context);
1236+ return SingleChildScrollViewWithScrollbar (
1237+ scrollDirection: Axis .horizontal,
1238+ child: Padding (
1239+ padding: const EdgeInsets .symmetric (horizontal: 5 ),
1240+ child: Table (
1241+ border: TableBorder .all (
1242+ width: 1 ,
1243+ style: BorderStyle .solid,
1244+ color: contentTheme.colorTableCellBorder),
1245+ defaultColumnWidth: const IntrinsicColumnWidth (),
1246+ children: List .unmodifiable (node.rows.map ((row) => TableRow (
1247+ decoration: row.isHeader
1248+ ? BoxDecoration (color: contentTheme.colorTableHeaderBackground)
1249+ : null ,
1250+ children: List .unmodifiable (row.cells.map ((cell) =>
1251+ MessageTableCell (node: cell, isHeader: row.isHeader)))))))));
1252+ }
1253+ }
1254+
1255+ class MessageTableCell extends StatelessWidget {
1256+ const MessageTableCell ({super .key, required this .node, required this .isHeader});
1257+
1258+ final TableCellNode node;
1259+ final bool isHeader;
1260+
1261+ @override
1262+ Widget build (BuildContext context) {
1263+ return TableCell (
1264+ verticalAlignment: TableCellVerticalAlignment .middle,
1265+ child: Padding (
1266+ // Web has 4px padding and 1px border on all sides.
1267+ // In web, the 1px border grows each cell by 0.5px in all directions.
1268+ // Our border doesn't affect the layout, it's just painted on,
1269+ // so we add 0.5px on all sides to match web.
1270+ // Ref: https://github.com/flutter/flutter/issues/78691
1271+ padding: const EdgeInsets .all (4 + 0.5 ),
1272+ child: node.nodes.isEmpty
1273+ ? const SizedBox .shrink ()
1274+ : _buildBlockInlineContainer (
1275+ node: node,
1276+ style: ! isHeader
1277+ ? DefaultTextStyle .of (context).style
1278+ : DefaultTextStyle .of (context).style
1279+ .merge (weightVariableTextStyle (context, wght: 700 ))),
1280+ ));
1281+ }
1282+ }
1283+
11991284void _launchUrl (BuildContext context, String urlString) async {
12001285 DialogStatus showError (BuildContext context, String ? message) {
12011286 return showErrorDialog (context: context,
0 commit comments