Skip to content

Commit c1861da

Browse files
rajveermalviyagnprice
authored andcommitted
content: Handle html parsing of InlineVideoNode
1 parent 4ebdaa0 commit c1861da

File tree

3 files changed

+112
-3
lines changed

3 files changed

+112
-3
lines changed

lib/model/content.dart

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,39 @@ class ImageNode extends BlockContentNode {
374374
}
375375
}
376376

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+
377410
class EmbedVideoNode extends BlockContentNode {
378411
const EmbedVideoNode({
379412
super.debugHtmlNode,
@@ -1013,10 +1046,43 @@ class _ZulipContentParser {
10131046
}
10141047

10151048
static final _videoClassNameRegexp = () {
1016-
const sourceType = r"(youtube-video|embed-video)";
1049+
const sourceType = r"(message_inline_video|youtube-video|embed-video)";
10171050
return RegExp("^message_inline_image $sourceType|$sourceType message_inline_image\$");
10181051
}();
10191052

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+
10201086
BlockContentNode parseEmbedVideoNode(dom.Element divElement) {
10211087
assert(_debugParserContext == _ParserContext.block);
10221088
assert(divElement.localName == 'div'
@@ -1140,8 +1206,11 @@ class _ZulipContentParser {
11401206
final match = _videoClassNameRegexp.firstMatch(className);
11411207
if (match != null) {
11421208
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);
11451214
}
11461215
}
11471216
}

lib/widgets/content.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ class BlockContentList extends StatelessWidget {
9898
"It should be wrapped in [ImageNodeList]."
9999
);
100100
return MessageImage(node: node);
101+
} else if (node is InlineVideoNode) {
102+
return Text.rich(
103+
TextSpan(children: [
104+
const TextSpan(text: "(unimplemented:", style: errorStyle),
105+
TextSpan(text: node.debugHtmlText, style: errorCodeStyle),
106+
const TextSpan(text: ")", style: errorStyle),
107+
]),
108+
style: errorStyle);
101109
} else if (node is EmbedVideoNode) {
102110
return Text.rich(
103111
TextSpan(children: [

test/model/content_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,36 @@ class ContentExample {
700700
previewImageSrcUrl: 'https://uploads.zulipusercontent.net/75aed2df4a1e8657176fcd6159fc40876ace4070/68747470733a2f2f692e76696d656f63646e2e636f6d2f766964656f2f32303936333634392d663032383137343536666334386537633331376566346330376261323539636434623430613336343962643865623530613434313862353965633366356166352d645f363430'
701701
),
702702
]);
703+
704+
static const videoInline = ContentExample(
705+
'video preview for user uploaded video',
706+
'[Big-Buck-Bunny.webm](/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm)',
707+
'<p>'
708+
'<a href="/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm">Big-Buck-Bunny.webm</a>'
709+
'</p>\n'
710+
'<div class="message_inline_image message_inline_video">'
711+
'<a href="/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm" title="Big-Buck-Bunny.webm">'
712+
'<video preload="metadata" src="/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm"></video></a></div>', [
713+
ParagraphNode(links: null, nodes: [
714+
LinkNode(url: '/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm', nodes: [TextNode('Big-Buck-Bunny.webm')]),
715+
]),
716+
InlineVideoNode(srcUrl: '/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm'),
717+
]);
718+
719+
static const videoInlineClassesFlipped = ContentExample(
720+
'video preview for user uploaded video, (hypothetical) class name reorder',
721+
'[Big-Buck-Bunny.webm](/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm)',
722+
'<p>'
723+
'<a href="/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm">Big-Buck-Bunny.webm</a>'
724+
'</p>\n'
725+
'<div class="message_inline_video message_inline_image">'
726+
'<a href="/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm" title="Big-Buck-Bunny.webm">'
727+
'<video preload="metadata" src="/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm"></video></a></div>', [
728+
ParagraphNode(links: null, nodes: [
729+
LinkNode(url: '/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm', nodes: [TextNode('Big-Buck-Bunny.webm')]),
730+
]),
731+
InlineVideoNode(srcUrl: '/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm'),
732+
]);
703733
}
704734

705735
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -1017,6 +1047,8 @@ void main() {
10171047
testParseExample(ContentExample.videoEmbedVimeoPreviewDisabled);
10181048
testParseExample(ContentExample.videoEmbedVimeo);
10191049
testParseExample(ContentExample.videoEmbedVimeoClassesFlipped);
1050+
testParseExample(ContentExample.videoInline);
1051+
testParseExample(ContentExample.videoInlineClassesFlipped);
10201052

10211053
testParse('parse nested lists, quotes, headings, code blocks',
10221054
// "1. > ###### two\n > * three\n\n four"

0 commit comments

Comments
 (0)