@@ -48,6 +48,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
48
48
colorPollVoteCountBackground: const HSLColor .fromAHSL (1 , 0 , 0 , 1 ).toColor (),
49
49
colorPollVoteCountBorder: const HSLColor .fromAHSL (1 , 156 , 0.28 , 0.7 ).toColor (),
50
50
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 (),
51
53
colorThematicBreak: const HSLColor .fromAHSL (1 , 0 , 0 , .87 ).toColor (),
52
54
textStylePlainParagraph: _plainParagraphCommon (context).copyWith (
53
55
color: const HSLColor .fromAHSL (1 , 0 , 0 , 0.15 ).toColor (),
@@ -77,6 +79,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
77
79
colorPollVoteCountBackground: const HSLColor .fromAHSL (0.2 , 0 , 0 , 0 ).toColor (),
78
80
colorPollVoteCountBorder: const HSLColor .fromAHSL (1 , 185 , 0.35 , 0.35 ).toColor (),
79
81
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 (),
80
84
colorThematicBreak: const HSLColor .fromAHSL (1 , 0 , 0 , .87 ).toColor ().withValues (alpha: 0.2 ),
81
85
textStylePlainParagraph: _plainParagraphCommon (context).copyWith (
82
86
color: const HSLColor .fromAHSL (1 , 0 , 0 , 0.85 ).toColor (),
@@ -105,6 +109,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
105
109
required this .colorPollVoteCountBackground,
106
110
required this .colorPollVoteCountBorder,
107
111
required this .colorPollVoteCountText,
112
+ required this .colorTableCellBorder,
113
+ required this .colorTableHeaderBackground,
108
114
required this .colorThematicBreak,
109
115
required this .textStylePlainParagraph,
110
116
required this .codeBlockTextStyles,
@@ -134,6 +140,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
134
140
final Color colorPollVoteCountBackground;
135
141
final Color colorPollVoteCountBorder;
136
142
final Color colorPollVoteCountText;
143
+ final Color colorTableCellBorder;
144
+ final Color colorTableHeaderBackground;
137
145
final Color colorThematicBreak;
138
146
139
147
/// The complete [TextStyle] we use for plain, unstyled paragraphs.
@@ -189,6 +197,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
189
197
Color ? colorPollVoteCountBackground,
190
198
Color ? colorPollVoteCountBorder,
191
199
Color ? colorPollVoteCountText,
200
+ Color ? colorTableCellBorder,
201
+ Color ? colorTableHeaderBackground,
192
202
Color ? colorThematicBreak,
193
203
TextStyle ? textStylePlainParagraph,
194
204
CodeBlockTextStyles ? codeBlockTextStyles,
@@ -208,6 +218,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
208
218
colorPollVoteCountBackground: colorPollVoteCountBackground ?? this .colorPollVoteCountBackground,
209
219
colorPollVoteCountBorder: colorPollVoteCountBorder ?? this .colorPollVoteCountBorder,
210
220
colorPollVoteCountText: colorPollVoteCountText ?? this .colorPollVoteCountText,
221
+ colorTableCellBorder: colorTableCellBorder ?? this .colorTableCellBorder,
222
+ colorTableHeaderBackground: colorTableHeaderBackground ?? this .colorTableHeaderBackground,
211
223
colorThematicBreak: colorThematicBreak ?? this .colorThematicBreak,
212
224
textStylePlainParagraph: textStylePlainParagraph ?? this .textStylePlainParagraph,
213
225
codeBlockTextStyles: codeBlockTextStyles ?? this .codeBlockTextStyles,
@@ -234,6 +246,8 @@ class ContentTheme extends ThemeExtension<ContentTheme> {
234
246
colorPollVoteCountBackground: Color .lerp (colorPollVoteCountBackground, other.colorPollVoteCountBackground, t)! ,
235
247
colorPollVoteCountBorder: Color .lerp (colorPollVoteCountBorder, other.colorPollVoteCountBorder, t)! ,
236
248
colorPollVoteCountText: Color .lerp (colorPollVoteCountText, other.colorPollVoteCountText, t)! ,
249
+ colorTableCellBorder: Color .lerp (colorTableCellBorder, other.colorTableCellBorder, t)! ,
250
+ colorTableHeaderBackground: Color .lerp (colorTableHeaderBackground, other.colorTableHeaderBackground, t)! ,
237
251
colorThematicBreak: Color .lerp (colorThematicBreak, other.colorThematicBreak, t)! ,
238
252
textStylePlainParagraph: TextStyle .lerp (textStylePlainParagraph, other.textStylePlainParagraph, t)! ,
239
253
codeBlockTextStyles: CodeBlockTextStyles .lerp (codeBlockTextStyles, other.codeBlockTextStyles, t),
@@ -324,6 +338,21 @@ class BlockContentList extends StatelessWidget {
324
338
}(),
325
339
InlineVideoNode () => MessageInlineVideo (node: node),
326
340
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
+ }(),
327
356
UnimplementedBlockContentNode () =>
328
357
Text .rich (_errorUnimplemented (node, context: context)),
329
358
};
@@ -1196,6 +1225,62 @@ class GlobalTime extends StatelessWidget {
1196
1225
}
1197
1226
}
1198
1227
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
+
1199
1284
void _launchUrl (BuildContext context, String urlString) async {
1200
1285
DialogStatus showError (BuildContext context, String ? message) {
1201
1286
return showErrorDialog (context: context,
0 commit comments