Skip to content

Commit 7383336

Browse files
content: Implement embed video preview
Partially implements zulip#356, provides video thumbnail previews for embedded external videos (Youtube & Vimeo).
1 parent aabfbe0 commit 7383336

File tree

2 files changed

+85
-7
lines changed

2 files changed

+85
-7
lines changed

lib/widgets/content.dart

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,7 @@ class BlockContentList extends StatelessWidget {
107107
]),
108108
style: errorStyle);
109109
} else if (node is EmbedVideoNode) {
110-
return Text.rich(
111-
TextSpan(children: [
112-
const TextSpan(text: "(unimplemented:", style: errorStyle),
113-
TextSpan(text: node.debugHtmlText, style: errorCodeStyle),
114-
const TextSpan(text: ")", style: errorStyle),
115-
]),
116-
style: errorStyle);
110+
return MessageEmbedVideo(node: node);
117111
} else if (node is UnimplementedBlockContentNode) {
118112
return Text.rich(_errorUnimplemented(node));
119113
} else {
@@ -403,6 +397,33 @@ class MessageImage extends StatelessWidget {
403397
}
404398
}
405399

400+
class MessageEmbedVideo extends StatelessWidget {
401+
const MessageEmbedVideo({super.key, required this.node});
402+
403+
final EmbedVideoNode node;
404+
405+
@override
406+
Widget build(BuildContext context) {
407+
final store = PerAccountStoreWidget.of(context);
408+
final previewImageSrcUrl = store.tryResolveUrl(node.previewImageSrcUrl);
409+
410+
return MessageMediaContainer(
411+
onTap: () => _launchUrl(context, node.hrefUrl),
412+
child: Stack(
413+
alignment: Alignment.center,
414+
children: [
415+
if (previewImageSrcUrl != null)
416+
RealmContentNetworkImage(
417+
previewImageSrcUrl,
418+
filterQuality: FilterQuality.medium),
419+
const Icon(
420+
Icons.play_arrow_rounded,
421+
color: Colors.white,
422+
size: 32),
423+
]));
424+
}
425+
}
426+
406427
class MessageMediaContainer extends StatelessWidget {
407428
const MessageMediaContainer({
408429
super.key,

test/widgets/content_test.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,63 @@ void main() {
270270
});
271271
});
272272

273+
group("MessageEmbedVideo", () {
274+
Future<void> prepareContent(WidgetTester tester, String html) async {
275+
addTearDown(testBinding.reset);
276+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
277+
prepareBoringImageHttpClient();
278+
279+
await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp(
280+
home: PerAccountStoreWidget(accountId: eg.selfAccount.id,
281+
child: MessageContent(
282+
message: eg.streamMessage(content: html),
283+
content: parseContent(html))))));
284+
await tester.pump(); // global store
285+
await tester.pump(); // per-account store
286+
debugNetworkImageHttpClientProvider = null;
287+
}
288+
289+
testWidgets('video preview for youtube embed', (tester) async {
290+
const example = ContentExample.videoEmbedYoutube;
291+
await prepareContent(tester, example.html);
292+
293+
final expectedVideo = example.expectedNodes[1] as EmbedVideoNode;
294+
final expectedResolvedPreviewUrl = eg
295+
.store()
296+
.tryResolveUrl(expectedVideo.previewImageSrcUrl)!;
297+
final image = tester.widget<RealmContentNetworkImage>(
298+
find.byType(RealmContentNetworkImage));
299+
check(image.src)
300+
.equals(expectedResolvedPreviewUrl);
301+
302+
final expectedLaunchUrl = expectedVideo.hrefUrl;
303+
await tester.tap(find.byIcon(Icons.play_arrow_rounded));
304+
check(testBinding.takeLaunchUrlCalls())
305+
.single.equals((url: Uri.parse(expectedLaunchUrl), mode: LaunchMode.platformDefault));
306+
});
307+
308+
testWidgets('video preview for vimeo embed', (tester) async {
309+
const example = ContentExample.videoEmbedVimeo;
310+
await prepareContent(tester, example.html);
311+
312+
final expectedTitle = (((example.expectedNodes[0] as ParagraphNode).nodes[0] as LinkNode).nodes[0] as TextNode).text;
313+
await tester.ensureVisible(find.text(expectedTitle));
314+
315+
final expectedVideo = example.expectedNodes[1] as EmbedVideoNode;
316+
final expectedResolvedUrl = eg.store().tryResolveUrl(expectedVideo.previewImageSrcUrl)!;
317+
final image = tester.widget<RealmContentNetworkImage>(
318+
find.byType(RealmContentNetworkImage));
319+
check(image.src)
320+
.equals(expectedResolvedUrl);
321+
322+
final expectedLaunchUrl = expectedVideo.hrefUrl;
323+
await tester.tap(find.byIcon(Icons.play_arrow_rounded));
324+
check(testBinding.takeLaunchUrlCalls())
325+
.single.equals((url: Uri.parse(expectedLaunchUrl), mode: LaunchMode.platformDefault));
326+
});
327+
});
328+
329+
273330
group("CodeBlock", () {
274331
testContentSmoke(ContentExample.codeBlockPlain);
275332
testContentSmoke(ContentExample.codeBlockHighlightedShort);

0 commit comments

Comments
 (0)