Skip to content

Commit 58f5c7c

Browse files
rajveermalviyagnprice
authored andcommitted
content: Initial support for inline <audio>, showing source links
Fixes #1665. Currently instead of showing an audio player interface it just displays the audio `title` attribute text in a link pointing to the `src` URL.
1 parent 898d907 commit 58f5c7c

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

lib/model/content.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,22 @@ class _ZulipInlineContentParser {
10871087
return GlobalTimeNode(datetime: datetime, debugHtmlNode: debugHtmlNode);
10881088
}
10891089

1090+
if (localName == 'audio' && className.isEmpty) {
1091+
final srcAttr = element.attributes['src'];
1092+
if (srcAttr == null) return unimplemented();
1093+
1094+
final String title = switch (element.attributes) {
1095+
{'title': final titleAttr} => titleAttr,
1096+
_ => Uri.tryParse(srcAttr)?.pathSegments.lastOrNull ?? srcAttr,
1097+
};
1098+
1099+
final link = LinkNode(
1100+
url: srcAttr,
1101+
nodes: [TextNode(title)]);
1102+
(_linkNodes ??= []).add(link);
1103+
return link;
1104+
}
1105+
10901106
if (localName == 'span' && className == 'katex') {
10911107
return parseInlineMath(element) ?? unimplemented();
10921108
}

test/model/content_test.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,24 @@ class ContentExample {
13271327
InlineVideoNode(srcUrl: '/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm'),
13281328
]);
13291329

1330+
static const audioInline = ContentExample(
1331+
'audio inline',
1332+
'![crab-rave.mp3](/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3)',
1333+
'<p><audio controls preload="metadata" src="/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3" title="crab-rave.mp3"></audio></p>', [
1334+
ParagraphNode(links: null, nodes: [
1335+
LinkNode(url: '/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3', nodes: [TextNode('crab-rave.mp3')]),
1336+
]),
1337+
]);
1338+
1339+
static const audioInlineNoTitle = ContentExample(
1340+
'audio inline no title',
1341+
'![](/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3)',
1342+
'<p><audio controls preload="metadata" src="/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3"></audio></p>', [
1343+
ParagraphNode(links: null, nodes: [
1344+
LinkNode(url: '/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3', nodes: [TextNode('crab-rave.mp3')]),
1345+
]),
1346+
]);
1347+
13301348
static const websitePreviewSmoke = ContentExample(
13311349
'website preview smoke',
13321350
'https://pub-14f7b5e1308d42b69c4a46608442a50c.r2.dev/image+title+description.html',
@@ -1990,6 +2008,9 @@ void main() async {
19902008
testParseExample(ContentExample.videoInline);
19912009
testParseExample(ContentExample.videoInlineClassesFlipped);
19922010

2011+
testParseExample(ContentExample.audioInline);
2012+
testParseExample(ContentExample.audioInlineNoTitle);
2013+
19932014
testParseExample(ContentExample.websitePreviewSmoke);
19942015
testParseExample(ContentExample.websitePreviewWithoutTitle);
19952016
testParseExample(ContentExample.websitePreviewWithoutDescription);

test/widgets/content_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,26 @@ void main() {
11671167
});
11681168
});
11691169

1170+
group('InlineAudio', () {
1171+
Future<void> prepare(WidgetTester tester, String html) async {
1172+
await prepareContent(tester, plainContent(html),
1173+
// We try to resolve relative links on the self-account's realm.
1174+
wrapWithPerAccountStoreWidget: true);
1175+
}
1176+
1177+
testWidgets('tapping on audio link opens it in browser', (tester) async {
1178+
final url = eg.realmUrl.resolve('/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3');
1179+
await prepare(tester, ContentExample.audioInline.html);
1180+
1181+
await tapText(tester, find.text('crab-rave.mp3'));
1182+
1183+
final expectedLaunchMode = defaultTargetPlatform == TargetPlatform.iOS ?
1184+
LaunchMode.externalApplication : LaunchMode.inAppBrowserView;
1185+
check(testBinding.takeLaunchUrlCalls())
1186+
.single.equals((url: url, mode: expectedLaunchMode));
1187+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
1188+
});
1189+
11701190
group('MessageImageEmoji', () {
11711191
Future<void> prepare(WidgetTester tester, String html) async {
11721192
await prepareContent(tester, plainContent(html),

0 commit comments

Comments
 (0)