@@ -374,6 +374,39 @@ class ImageNode extends BlockContentNode {
374
374
}
375
375
}
376
376
377
+ class InlineVideoNode extends BlockContentNode {
378
+ const InlineVideoNode ({
379
+ super .debugHtmlNode,
380
+ required this .srcUrl,
381
+ });
382
+
383
+ /// A URL string for the video resource, on the Zulip server.
384
+ ///
385
+ /// This may be a relative URL string. It also may not work without adding
386
+ /// authentication credentials to the request.
387
+ ///
388
+ /// Unlike [EmbedVideoNode.hrefUrl] , this should always be a URL served by
389
+ /// either the Zulip server itself or a service it trusts. It's therefore
390
+ /// fine from a privacy perspective to eagerly request data from this resource
391
+ /// when the user passively scrolls the video into view.
392
+ final String srcUrl;
393
+
394
+ @override
395
+ bool operator == (Object other) {
396
+ return other is InlineVideoNode
397
+ && other.srcUrl == srcUrl;
398
+ }
399
+
400
+ @override
401
+ int get hashCode => Object .hash ('InlineVideoNode' , srcUrl);
402
+
403
+ @override
404
+ void debugFillProperties (DiagnosticPropertiesBuilder properties) {
405
+ super .debugFillProperties (properties);
406
+ properties.add (StringProperty ('srcUrl' , srcUrl));
407
+ }
408
+ }
409
+
377
410
class EmbedVideoNode extends BlockContentNode {
378
411
const EmbedVideoNode ({
379
412
super .debugHtmlNode,
@@ -1013,10 +1046,43 @@ class _ZulipContentParser {
1013
1046
}
1014
1047
1015
1048
static final _videoClassNameRegexp = () {
1016
- const sourceType = r"(youtube-video|embed-video)" ;
1049
+ const sourceType = r"(message_inline_video| youtube-video|embed-video)" ;
1017
1050
return RegExp ("^message_inline_image $sourceType |$sourceType message_inline_image\$ " );
1018
1051
}();
1019
1052
1053
+ BlockContentNode parseInlineVideoNode (dom.Element divElement) {
1054
+ assert (_debugParserContext == _ParserContext .block);
1055
+ assert (divElement.localName == 'div'
1056
+ && _videoClassNameRegexp.hasMatch (divElement.className));
1057
+
1058
+ final videoElement = () {
1059
+ if (divElement.nodes.length != 1 ) return null ;
1060
+ final child = divElement.nodes[0 ];
1061
+ if (child is ! dom.Element ) return null ;
1062
+ if (child.localName != 'a' ) return null ;
1063
+ if (child.className.isNotEmpty) return null ;
1064
+
1065
+ if (child.nodes.length != 1 ) return null ;
1066
+ final grandchild = child.nodes[0 ];
1067
+ if (grandchild is ! dom.Element ) return null ;
1068
+ if (grandchild.localName != 'video' ) return null ;
1069
+ if (grandchild.className.isNotEmpty) return null ;
1070
+ return grandchild;
1071
+ }();
1072
+
1073
+ final debugHtmlNode = kDebugMode ? divElement : null ;
1074
+ if (videoElement == null ) {
1075
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1076
+ }
1077
+
1078
+ final src = videoElement.attributes['src' ];
1079
+ if (src == null ) {
1080
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1081
+ }
1082
+
1083
+ return InlineVideoNode (srcUrl: src, debugHtmlNode: debugHtmlNode);
1084
+ }
1085
+
1020
1086
BlockContentNode parseEmbedVideoNode (dom.Element divElement) {
1021
1087
assert (_debugParserContext == _ParserContext .block);
1022
1088
assert (divElement.localName == 'div'
@@ -1140,8 +1206,11 @@ class _ZulipContentParser {
1140
1206
final match = _videoClassNameRegexp.firstMatch (className);
1141
1207
if (match != null ) {
1142
1208
final videoClass = match.group (1 ) ?? match.group (2 )! ;
1143
- if (videoClass case 'youtube-video' || 'embed-video' ) {
1144
- return parseEmbedVideoNode (element);
1209
+ switch (videoClass) {
1210
+ case 'message_inline_video' :
1211
+ return parseInlineVideoNode (element);
1212
+ case 'youtube-video' || 'embed-video' :
1213
+ return parseEmbedVideoNode (element);
1145
1214
}
1146
1215
}
1147
1216
}
0 commit comments