ソースを参照

Render audio from URLs in messages

JC Brand 4 年 前
コミット
095d9b60cd

+ 11 - 0
docs/source/configuration.rst

@@ -793,6 +793,17 @@ domain_placeholder
 The placeholder text shown in the domain input on the registration form.
 
 
+embed_audio
+-----------
+
+* Default:  ``true``
+
+If set to ``false``, audio files won't be embedded in chats, instead only their links will be shown.
+
+It also accepts an array strings of whitelisted domain names to only render videos that belong to those domains.
+E.g. ``['conversejs.org']``
+
+
 embed_videos
 ------------
 

+ 2 - 0
karma.conf.js

@@ -47,9 +47,11 @@ module.exports = function(config) {
       { pattern: "src/plugins/chatview/tests/http-file-upload.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/markers.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/me-messages.js", type: 'module' },
+      { pattern: "src/plugins/chatview/tests/message-audio.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/message-images.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/message-videos.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/messages.js", type: 'module' },
+      { pattern: "src/plugins/chatview/tests/oob.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/receipts.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/spoilers.js", type: 'module' },
       { pattern: "src/plugins/chatview/tests/xss.js", type: 'module' },

+ 1 - 0
src/plugins/chatview/index.js

@@ -37,6 +37,7 @@ converse.plugins.add('converse-chatview', {
             'auto_focus': true,
             'debounced_content_rendering': true,
             'embed_videos': true,
+            'embed_audio': true,
             'filter_url_query_params': null,
             'image_urls_regex': null,
             'message_limit': 0,

+ 25 - 0
src/plugins/chatview/tests/message-audio.js

@@ -0,0 +1,25 @@
+/*global mock, converse */
+
+const { sizzle, u } = converse.env;
+
+describe("A Chat Message", function () {
+
+    it("will render audio files from their URLs",
+            mock.initConverse(['chatBoxesFetched'], {},
+            async function (done, _converse) {
+        await mock.waitForRoster(_converse, 'current');
+        const base_url = 'https://conversejs.org';
+        const message = base_url+"/logo/audio.mp3";
+
+        const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+        await mock.openChatBoxFor(_converse, contact_jid);
+        const view = _converse.chatboxviews.get(contact_jid);
+        await mock.sendMessage(view, message);
+        await u.waitUntil(() => view.querySelectorAll('.chat-content audio').length, 1000)
+        const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
+        expect(msg.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
+            `<audio controls="" src="${message}"></audio>`+
+            `<a target="_blank" rel="noopener" href="${message}">${message}</a>`);
+        done();
+    }));
+});

+ 0 - 163
src/plugins/chatview/tests/messages.js

@@ -1267,167 +1267,4 @@ describe("A Chat Message", function () {
             done();
         }));
     });
-
-
-    describe("which contains an OOB URL", function () {
-
-        it("will render audio from oob mp3 URLs",
-            mock.initConverse(
-                ['chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, contact_jid);
-            const view = _converse.chatboxviews.get(contact_jid);
-            spyOn(view.model, 'sendMessage').and.callThrough();
-
-            let stanza = u.toStanza(`
-                <message from="${contact_jid}"
-                         type="chat"
-                         to="romeo@montague.lit/orchard">
-                    <body>Have you heard this funny audio?</body>
-                    <x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
-                </message>`)
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg audio').length, 1000);
-            let msg = view.querySelector('.chat-msg .chat-msg__text');
-            expect(msg.classList.length).toEqual(1);
-            expect(u.hasClass('chat-msg__text', msg)).toBe(true);
-            expect(msg.textContent).toEqual('Have you heard this funny audio?');
-            let media = view.querySelector('.chat-msg .chat-msg__media');
-            expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
-                `<audio controls="" src="https://montague.lit/audio.mp3"></audio>    `+
-                `<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">Download audio file "audio.mp3"</a>`);
-
-            // If the <url> and <body> contents is the same, don't duplicate.
-            stanza = u.toStanza(`
-                <message from="${contact_jid}"
-                         type="chat"
-                         to="romeo@montague.lit/orchard">
-                    <body>https://montague.lit/audio.mp3</body>
-                    <x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
-                </message>`);
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            msg = view.querySelector('.chat-msg:last-child .chat-msg__text');
-            expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('Have you heard this funny audio?'); // Emtpy
-            media = view.querySelector('.chat-msg:last-child .chat-msg__media');
-            expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
-                `<audio controls="" src="https://montague.lit/audio.mp3"></audio>    `+
-                `<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">`+
-                `Download audio file "audio.mp3"</a>`);
-            done();
-        }));
-
-        it("will render video from oob mp4 URLs",
-            mock.initConverse(
-                ['chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, contact_jid)
-            const view = _converse.chatboxviews.get(contact_jid);
-            spyOn(view.model, 'sendMessage').and.callThrough();
-
-            let stanza = u.toStanza(`
-                <message from="${contact_jid}"
-                         type="chat"
-                         to="romeo@montague.lit/orchard">
-                    <body>Have you seen this funny video?</body>
-                    <x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
-                </message>`);
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg video').length, 2000)
-            let msg = view.querySelector('.chat-msg .chat-msg__text');
-            expect(msg.classList.length).toBe(1);
-            expect(msg.textContent).toEqual('Have you seen this funny video?');
-            let media = view.querySelector('.chat-msg .chat-msg__media');
-            expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
-                `<video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video>`);
-
-
-            // If the <url> and <body> contents is the same, don't duplicate.
-            stanza = u.toStanza(`
-                <message from="${contact_jid}"
-                         type="chat"
-                         to="romeo@montague.lit/orchard">
-                    <body>https://montague.lit/video.mp4</body>
-                    <x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
-                </message>`);
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            msg = view.querySelector('.chat-msg:last-child .chat-msg__text');
-            expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('Have you seen this funny video?');
-            media = view.querySelector('.chat-msg:last-child .chat-msg__media');
-            expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
-                `<video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video>`);
-            done();
-        }));
-
-        it("will render download links for files from oob URLs",
-            mock.initConverse(
-                ['chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            await mock.waitForRoster(_converse, 'current', 1);
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, contact_jid);
-            const view = _converse.chatboxviews.get(contact_jid);
-            spyOn(view.model, 'sendMessage').and.callThrough();
-            const stanza = u.toStanza(`
-                <message from="${contact_jid}"
-                         type="chat"
-                         to="romeo@montague.lit/orchard">
-                    <body>Have you downloaded this funny file?</body>
-                    <x xmlns="jabber:x:oob"><url>https://montague.lit/funny.pdf</url></x>
-                </message>`);
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg a').length, 1000);
-            const msg = view.querySelector('.chat-msg .chat-msg__text');
-            expect(u.hasClass('chat-msg__text', msg)).toBe(true);
-            expect(msg.textContent).toEqual('Have you downloaded this funny file?');
-            const media = view.querySelector('.chat-msg .chat-msg__media');
-            expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
-                `<a target="_blank" rel="noopener" href="https://montague.lit/funny.pdf">Download file "funny.pdf"</a>`);
-            done();
-        }));
-
-        it("will render images from oob URLs",
-            mock.initConverse(
-                ['chatBoxesFetched'], {},
-                async function (done, _converse) {
-
-            const base_url = 'https://conversejs.org';
-            await mock.waitForRoster(_converse, 'current', 1);
-            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
-            await mock.openChatBoxFor(_converse, contact_jid)
-            const view = _converse.chatboxviews.get(contact_jid);
-            spyOn(view.model, 'sendMessage').and.callThrough();
-            const url = base_url+"/logo/conversejs-filled.svg";
-
-            const stanza = u.toStanza(`
-                <message from="${contact_jid}"
-                         type="chat"
-                         to="romeo@montague.lit/orchard">
-                    <body>Have you seen this funny image?</body>
-                    <x xmlns="jabber:x:oob"><url>${url}</url></x>
-                </message>`);
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            _converse.connection._dataRecv(mock.createRequest(stanza));
-            await new Promise(resolve => view.model.messages.once('rendered', resolve));
-            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg a').length, 1000);
-            const msg = view.querySelector('.chat-msg .chat-msg__text');
-            expect(u.hasClass('chat-msg__text', msg)).toBe(true);
-            expect(msg.textContent).toEqual('Have you seen this funny image?');
-            const media = view.querySelector('.chat-msg .chat-msg__media');
-            expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "")).toEqual(
-                `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
-                `Download image file "conversejs-filled.svg"</a>`);
-            done();
-        }));
-    });
 });

+ 167 - 0
src/plugins/chatview/tests/oob.js

@@ -0,0 +1,167 @@
+/*global mock, converse */
+
+const { Promise, u } = converse.env;
+
+describe("A Chat Message", function () {
+    describe("which contains an OOB URL", function () {
+
+        it("will render audio from oob mp3 URLs",
+            mock.initConverse(
+                ['chatBoxesFetched'], {},
+                async function (done, _converse) {
+
+            await mock.waitForRoster(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+            await mock.openChatBoxFor(_converse, contact_jid);
+            const view = _converse.chatboxviews.get(contact_jid);
+            spyOn(view.model, 'sendMessage').and.callThrough();
+
+            const url = 'https://montague.lit/audio.mp3';
+            let stanza = u.toStanza(`
+                <message from="${contact_jid}"
+                         type="chat"
+                         to="romeo@montague.lit/orchard">
+                    <body>Have you heard this funny audio?</body>
+                    <x xmlns="jabber:x:oob"><url>${url}</url></x>
+                </message>`)
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            await new Promise(resolve => view.model.messages.once('rendered', resolve));
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg audio').length, 1000);
+            let msg = view.querySelector('.chat-msg .chat-msg__text');
+            expect(msg.classList.length).toEqual(1);
+            expect(u.hasClass('chat-msg__text', msg)).toBe(true);
+            expect(msg.textContent).toEqual('Have you heard this funny audio?');
+            let media = view.querySelector('.chat-msg .chat-msg__media');
+            expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
+                `<audio controls="" src="https://montague.lit/audio.mp3"></audio>`+
+                `<a target="_blank" rel="noopener" href="https://montague.lit/audio.mp3">${url}</a>`);
+
+            // If the <url> and <body> contents is the same, don't duplicate.
+            stanza = u.toStanza(`
+                <message from="${contact_jid}"
+                         type="chat"
+                         to="romeo@montague.lit/orchard">
+                    <body>https://montague.lit/audio.mp3</body>
+                    <x xmlns="jabber:x:oob"><url>https://montague.lit/audio.mp3</url></x>
+                </message>`);
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            await new Promise(resolve => view.model.messages.once('rendered', resolve));
+            msg = view.querySelector('.chat-msg:last-child .chat-msg__text');
+            expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('Have you heard this funny audio?'); // Emtpy
+            media = view.querySelector('.chat-msg:last-child .chat-msg__media');
+            expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "").trim()).toEqual(
+                `<audio controls="" src="https://montague.lit/audio.mp3"></audio>`+
+                `<a target="_blank" rel="noopener" href="${url}">${url}</a>`);
+            done();
+        }));
+
+        it("will render video from oob mp4 URLs",
+            mock.initConverse(
+                ['chatBoxesFetched'], {},
+                async function (done, _converse) {
+
+            await mock.waitForRoster(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+            await mock.openChatBoxFor(_converse, contact_jid)
+            const view = _converse.chatboxviews.get(contact_jid);
+            spyOn(view.model, 'sendMessage').and.callThrough();
+
+            let stanza = u.toStanza(`
+                <message from="${contact_jid}"
+                         type="chat"
+                         to="romeo@montague.lit/orchard">
+                    <body>Have you seen this funny video?</body>
+                    <x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
+                </message>`);
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg video').length, 2000)
+            let msg = view.querySelector('.chat-msg .chat-msg__text');
+            expect(msg.classList.length).toBe(1);
+            expect(msg.textContent).toEqual('Have you seen this funny video?');
+            let media = view.querySelector('.chat-msg .chat-msg__media');
+            expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
+                `<video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video>`);
+
+
+            // If the <url> and <body> contents is the same, don't duplicate.
+            stanza = u.toStanza(`
+                <message from="${contact_jid}"
+                         type="chat"
+                         to="romeo@montague.lit/orchard">
+                    <body>https://montague.lit/video.mp4</body>
+                    <x xmlns="jabber:x:oob"><url>https://montague.lit/video.mp4</url></x>
+                </message>`);
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            await new Promise(resolve => view.model.messages.once('rendered', resolve));
+            msg = view.querySelector('.chat-msg:last-child .chat-msg__text');
+            expect(msg.innerHTML.replace(/<!-.*?->/g, '')).toEqual('Have you seen this funny video?');
+            media = view.querySelector('.chat-msg:last-child .chat-msg__media');
+            expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
+                `<video controls="" preload="metadata" style="max-height: 50vh" src="https://montague.lit/video.mp4"></video>`);
+            done();
+        }));
+
+        it("will render download links for files from oob URLs",
+            mock.initConverse(
+                ['chatBoxesFetched'], {},
+                async function (done, _converse) {
+
+            await mock.waitForRoster(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+            await mock.openChatBoxFor(_converse, contact_jid);
+            const view = _converse.chatboxviews.get(contact_jid);
+            spyOn(view.model, 'sendMessage').and.callThrough();
+            const stanza = u.toStanza(`
+                <message from="${contact_jid}"
+                         type="chat"
+                         to="romeo@montague.lit/orchard">
+                    <body>Have you downloaded this funny file?</body>
+                    <x xmlns="jabber:x:oob"><url>https://montague.lit/funny.pdf</url></x>
+                </message>`);
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            await new Promise(resolve => view.model.messages.once('rendered', resolve));
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg a').length, 1000);
+            const msg = view.querySelector('.chat-msg .chat-msg__text');
+            expect(u.hasClass('chat-msg__text', msg)).toBe(true);
+            expect(msg.textContent).toEqual('Have you downloaded this funny file?');
+            const media = view.querySelector('.chat-msg .chat-msg__media');
+            expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(/<!-.*?->/g, '')).toEqual(
+                `<a target="_blank" rel="noopener" href="https://montague.lit/funny.pdf">Download file "funny.pdf"</a>`);
+            done();
+        }));
+
+        it("will render images from oob URLs",
+            mock.initConverse(
+                ['chatBoxesFetched'], {},
+                async function (done, _converse) {
+
+            const base_url = 'https://conversejs.org';
+            await mock.waitForRoster(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+            await mock.openChatBoxFor(_converse, contact_jid)
+            const view = _converse.chatboxviews.get(contact_jid);
+            spyOn(view.model, 'sendMessage').and.callThrough();
+            const url = base_url+"/logo/conversejs-filled.svg";
+
+            const stanza = u.toStanza(`
+                <message from="${contact_jid}"
+                         type="chat"
+                         to="romeo@montague.lit/orchard">
+                    <body>Have you seen this funny image?</body>
+                    <x xmlns="jabber:x:oob"><url>${url}</url></x>
+                </message>`);
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            _converse.connection._dataRecv(mock.createRequest(stanza));
+            await new Promise(resolve => view.model.messages.once('rendered', resolve));
+            await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg a').length, 1000);
+            const msg = view.querySelector('.chat-msg .chat-msg__text');
+            expect(u.hasClass('chat-msg__text', msg)).toBe(true);
+            expect(msg.textContent).toEqual('Have you seen this funny image?');
+            const media = view.querySelector('.chat-msg .chat-msg__media');
+            expect(media.innerHTML.replace(/<!-.*?->/g, '').replace(/(\r\n|\n|\r)/gm, "")).toEqual(
+                `<a target="_blank" rel="noopener" href="${base_url}/logo/conversejs-filled.svg">`+
+                `Download image file "conversejs-filled.svg"</a>`);
+            done();
+        }));
+    });
+});

+ 5 - 1
src/shared/chat/message-body.js

@@ -4,6 +4,8 @@ import renderRichText from 'shared/directives/rich-text.js';
 import { CustomElement } from 'shared/components/element.js';
 import { api } from "@converse/headless/core";
 
+import './styles/message-body.scss';
+
 
 export default class MessageBody extends CustomElement {
 
@@ -13,6 +15,7 @@ export default class MessageBody extends CustomElement {
             is_me_message: { type: Boolean },
             show_images: { type: Boolean },
             embed_videos: { type: Boolean },
+            embed_audio: { type: Boolean },
             text: { type: String },
         }
     }
@@ -31,12 +34,13 @@ export default class MessageBody extends CustomElement {
         const offset = 0;
         const mentions = this.model.get('references');
         const options = {
+            'embed_audio': this.embed_audio,
+            'embed_videos': this.embed_videos,
             'nick': this.model.collection.chatbox.get('nick'),
             'onImgClick': this.onImgClick,
             'onImgLoad': () => this.onImgLoad(),
             'render_styling': !this.model.get('is_unstyled') && api.settings.get('allow_message_styling'),
             'show_images': this.show_images,
-            'embed_videos': this.embed_videos,
             'show_me_message': true
         }
         return renderRichText(this.text, offset, mentions, options, callback);

+ 1 - 0
src/shared/chat/message.js

@@ -240,6 +240,7 @@ export default class Message extends CustomElement {
                     ?is_me_message="${this.model.isMeCommand()}"
                     ?show_images="${api.settings.get('show_images_inline')}"
                     ?embed_videos="${api.settings.get('embed_videos')}"
+                    ?embed_audio="${api.settings.get('embed_audio')}"
                     text="${text}"></converse-chat-message-body>
                 ${ (this.model.get('received') && !this.model.isMeCommand() && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
                 ${ (this.model.get('edited')) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }

+ 5 - 0
src/shared/chat/styles/message-body.scss

@@ -0,0 +1,5 @@
+converse-chat-message-body {
+    audio {
+        width: 100%;
+    }
+}

+ 7 - 4
src/shared/components/rich-text.js

@@ -6,6 +6,8 @@ export default class RichText extends CustomElement {
 
     static get properties () {
         return {
+            embed_audio: { type: Boolean },
+            embed_videos: { type: Boolean },
             mentions: { type: Array },
             nick: { type: String },
             offset: { type: Number },
@@ -13,7 +15,6 @@ export default class RichText extends CustomElement {
             onImgLoad: { type: Function },
             render_styling: { type: Boolean },
             show_images: { type: Boolean },
-            embed_videos: { type: Boolean },
             show_me_message: { type: Boolean },
             text: { type: String },
         }
@@ -21,22 +22,24 @@ export default class RichText extends CustomElement {
 
     constructor () {
         super();
-        this.offset = 0;
+        this.embed_audio = false;
+        this.embed_videos = false;
         this.mentions = [];
+        this.offset = 0;
         this.render_styling = false;
         this.show_images = false;
-        this.embed_videos = false;
         this.show_me_message = false;
     }
 
     render () {
         const options = {
+            embed_audio: this.embed_audio,
+            embed_videos: this.embed_videos,
             nick: this.nick,
             onImgClick: this.onImgClick,
             onImgLoad: this.onImgLoad,
             render_styling: this.render_styling,
             show_images: this.show_images,
-            embed_videos: this.embed_videos,
             show_me_message: this.show_me_message,
         }
         return renderRichText(this.text, this.offset, this.mentions, options);

+ 1 - 1
src/shared/directives/styling.js

@@ -14,7 +14,7 @@ class StylingDirective extends Directive {
             txt,
             offset,
             mentions,
-            Object.assign(options, { 'show_images': false, 'embed_videos': false })
+            Object.assign(options, { 'show_images': false, 'embed_videos': false, 'embed_audio': false })
         );
         return html`${until(transform(t), html`${t}`)}`;
     }

+ 10 - 3
src/shared/rich-text.js

@@ -1,7 +1,8 @@
 import URI from 'urijs';
 import log from '@converse/headless/log';
+import tpl_audio from 'templates/audio.js';
 import tpl_image from 'templates/image.js';
-import tpl_video from '../templates/video.js';
+import tpl_video from 'templates/video.js';
 import { _converse, api } from '@converse/headless/core';
 import { containsDirectives, getDirectiveAndLength, getDirectiveTemplate, isQuoteDirective } from './styling.js';
 import {
@@ -13,8 +14,11 @@ import {
 import {
     filterQueryParamsFromURL,
     getHyperlinkTemplate,
-    isImageURL,
+    getURI,
+    isAudioDomainAllowed,
+    isAudioURL,
     isImageDomainAllowed,
+    isImageURL,
     isVideoDomainAllowed,
     isVideoURL
 } from 'utils/html';
@@ -66,6 +70,8 @@ export class RichText extends String {
      */
     constructor (text, offset = 0, mentions = [], options = {}) {
         super(text);
+        this.embed_audio = options?.embed_audio;
+        this.embed_videos = options?.embed_videos;
         this.mentions = mentions;
         this.nick = options?.nick;
         this.offset = offset;
@@ -76,7 +82,6 @@ export class RichText extends String {
         this.references = [];
         this.render_styling = options?.render_styling;
         this.show_images = options?.show_images;
-        this.embed_videos = options?.embed_videos;
     }
 
     /**
@@ -114,6 +119,8 @@ export class RichText extends String {
                 });
             } else if (this.embed_videos && isVideoURL(url_text) && isVideoDomainAllowed(url_text)) {
                 template = tpl_video({ 'url': filtered_url });
+            } else if (this.embed_audio && isAudioURL(url_text) && isAudioDomainAllowed(url_text)) {
+                template = tpl_audio(filtered_url);
             } else {
                 template = getHyperlinkTemplate(filtered_url);
             }

+ 4 - 6
src/templates/audio.js

@@ -1,7 +1,5 @@
-import { html } from "lit";
+import { html } from 'lit';
 
-
-export default (o) => html`
-    <audio controls src="${o.url}"></audio>
-    <a target="_blank" rel="noopener" href="${o.url}">${o.label_download}</a>
-`;
+export default (url) => {
+    return html`<audio controls src="${url}"></audio><a target="_blank" rel="noopener" href="${url}">${url}</a>`;
+}

+ 18 - 8
src/utils/html.js

@@ -94,6 +94,20 @@ export function isImageURL (url) {
     return regex?.test(url) || isURLWithImageExtension(url);
 }
 
+export function isAudioDomainAllowed (url) {
+    const embed_audio = api.settings.get('embed_audio');
+    if (!Array.isArray(embed_audio)) {
+        return embed_audio;
+    }
+    try {
+        const audio_domain = getURI(url).domain();
+        return embed_audio.includes(audio_domain);
+    } catch (error) {
+        log.debug(error);
+        return false;
+    }
+}
+
 export function isVideoDomainAllowed (url) {
     const embed_videos = api.settings.get('embed_videos');
     if (!Array.isArray(embed_videos)) {
@@ -122,7 +136,7 @@ export function isImageDomainAllowed (url) {
     }
 }
 
-function getFileName (uri) {
+export function getFileName (uri) {
     try {
         return decodeURI(uri.filename());
     } catch (error) {
@@ -131,12 +145,8 @@ function getFileName (uri) {
     }
 }
 
-function renderAudioURL (_converse, uri) {
-    const { __ } = _converse;
-    return tpl_audio({
-        'url': uri.toString(),
-        'label_download': __('Download audio file "%1$s"', getFileName(uri))
-    });
+function renderAudioURL (url) {
+    return tpl_audio(url);
 }
 
 function renderImageURL (_converse, uri) {
@@ -170,7 +180,7 @@ u.getOOBURLMarkup = function (_converse, url) {
     if (u.isVideoURL(uri)) {
         return tpl_video({ url });
     } else if (u.isAudioURL(uri)) {
-        return renderAudioURL(_converse, uri);
+        return renderAudioURL(url);
     } else if (u.isImageURL(uri)) {
         return renderImageURL(_converse, uri);
     } else {