diff --git a/CHANGES.md b/CHANGES.md index 451f0828aa..6d8f5401f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ ### Github Issues - #122: Set horizontal layout direction based on the language -- #317: Add the ability to render audio streams. New config option [show_self_in_roster](https://conversejs.org/docs/html/configuration.html#show-self-in-roster) +- #317: Add the ability to render audio streams. New config option [fetch_url_headers](https://conversejs.org/docs/html/configuration.html#fetch-url-headers) - #698: Add support for MUC private messages - #1021: Message from non-roster contacts don't appear in fullscreen view_mode - #1038: Support setting node config manually diff --git a/src/plugins/chatview/tests/message-audio.js b/src/plugins/chatview/tests/message-audio.js index b266835aab..95a943e9bd 100644 --- a/src/plugins/chatview/tests/message-audio.js +++ b/src/plugins/chatview/tests/message-audio.js @@ -46,25 +46,6 @@ describe("A Chat Message", function () { expect(msg.querySelector('audio').src).toEqual(message); })); - xit("will render audio stream", - mock.initConverse(['chatBoxesFetched'], - { fetch_url_headers: true }, - async function (_converse) { - - await mock.waitForRoster(_converse, 'current', 1); - const message = 'https://differentdrumz.radioca.st/stream/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); - 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( - ``+ - `${message}`); - })); - it("will render Spotify player for Spotify URLs", mock.initConverse(['chatBoxesFetched'], { embed_3rd_party_media_players: true, view_mode: 'fullscreen' }, diff --git a/src/plugins/chatview/tests/message-videos.js b/src/plugins/chatview/tests/message-videos.js index dfa388e5bc..509f5e4304 100644 --- a/src/plugins/chatview/tests/message-videos.js +++ b/src/plugins/chatview/tests/message-videos.js @@ -16,17 +16,13 @@ describe("A chat message containing video URLs", function () { await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-content video').length, 1000) let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); - expect(msg.innerHTML.replace(//g, '').trim()).toEqual( - ``+ - `${message}`); + expect(msg.querySelector('video').src).toEqual(message); message += "?param1=val1¶m2=val2"; await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-content video').length === 2, 1000); msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); - expect(msg.innerHTML.replace(//g, '').trim()).toEqual( - ``+ - `${Strophe.xmlescape(message)}`); + expect(msg.querySelector('video').src).toEqual(message); })); it("will not render videos if render_media is false", @@ -64,9 +60,7 @@ describe("A chat message containing video URLs", function () { await mock.sendMessage(view, message); await u.waitUntil(() => view.querySelectorAll('.chat-content video').length, 1000) const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); - expect(msg.innerHTML.replace(//g, '').trim()).toEqual( - ``+ - `${message}`); + expect(msg.querySelector('video').src).toEqual(message); })); it("will allow the user to toggle visibility of rendered videos", diff --git a/src/plugins/chatview/tests/oob.js b/src/plugins/chatview/tests/oob.js index cbabad362b..8a9de76e30 100644 --- a/src/plugins/chatview/tests/oob.js +++ b/src/plugins/chatview/tests/oob.js @@ -82,9 +82,7 @@ describe("A Chat Message", function () { expect(msg.classList.length).toBe(1); expect(msg.textContent).toEqual('Have you seen this funny video?'); const media = view.querySelector('.chat-msg .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "").replace(//g, '')).toEqual( - ``+ - `${Strophe.xmlescape(url)}`); + expect(media.querySelector('video').getAttribute('src')).toBe(url); // If the and contents is the same, don't duplicate. stanza = u.toStanza(` diff --git a/src/shared/directives/image.js b/src/shared/directives/image.js index 14f7a21660..cabb5d641d 100644 --- a/src/shared/directives/image.js +++ b/src/shared/directives/image.js @@ -7,25 +7,46 @@ import { getHyperlinkTemplate } from 'utils/html.js'; const { URI } = converse.env; const { isURLWithImageExtension } = u; - class ImageDirective extends AsyncDirective { - - render (src, href, onLoad, onClick) { - return href ? - html`${ this.renderImage(src, href, onLoad, onClick) }` : - this.renderImage(src, href, onLoad, onClick); + /** + * @param {string} src - The source URL of the image. + * @param {string} [href] - The optional hyperlink for the image. + * @param {Function} [onLoad] - Callback function to be called once the image has loaded. + * @param {Function} [onClick] - Callback function to be called once the image has been clicked. + * @returns {import('lit').TemplateResult} + */ + render(src, href, onLoad, onClick) { + return href + ? html`${this.renderImage(src, href, onLoad, onClick)}` + : this.renderImage(src, href, onLoad, onClick); } - renderImage (src, href, onLoad, onClick) { + /** + * @param {string} src - The source URL of the image. + * @param {string} [href] - The optional hyperlink for the image. + * @param {Function} [onLoad] - Callback function to be called once the image has loaded. + * @param {Function} [onClick] - Callback function to be called once the image has been clicked. + * @returns {import('lit').TemplateResult} + */ + renderImage(src, href, onLoad, onClick) { return html` this.onError(src, href, onLoad, onClick)} - @load="${onLoad}"/>`; + loading="lazy" + src="${src}" + @click=${onClick} + @error=${() => this.onError(src, href, onLoad, onClick)} + @load="${onLoad}"/>`; } - onError (src, href, onLoad, onClick) { + /** + * Handles errors that occur during image loading. + * @param {string} src - The source URL of the image that failed to load. + * @param {string} [href] - The optional hyperlink for the image. + * @param {Function} [onLoad] - Callback function to be called once the image has loaded. + * @param {Function} [onClick] - Callback function to be called once the image has been clicked. + */ + onError(src, href, onLoad, onClick) { if (isURLWithImageExtension(src)) { href && this.setValue(getHyperlinkTemplate(href)); } else { diff --git a/src/shared/texture/texture.js b/src/shared/texture/texture.js index 2d45849dc9..fcf170fa33 100644 --- a/src/shared/texture/texture.js +++ b/src/shared/texture/texture.js @@ -153,7 +153,7 @@ export class Texture extends String { } else { if (this.shouldRenderMedia(url_text, 'audio') && api.settings.get('fetch_url_headers')) { const headers = await getHeaders(url_text); - if (headers.get('content-type')?.startsWith('audio')) { + if (headers?.get('content-type')?.startsWith('audio')) { template = tplAudio(filtered_url, this.hide_media_urls, headers.get('Icy-Name')); } } diff --git a/src/templates/video.js b/src/templates/video.js index fd80311ed0..caa68eb65f 100644 --- a/src/templates/video.js +++ b/src/templates/video.js @@ -4,7 +4,10 @@ import { html } from 'lit'; * @param {string} url * @param {boolean} [hide_url] */ -export default (url, hide_url) => - html`${hide_url - ? '' - : html`${url}`}`; +export default (url, hide_url) => { + const { hostname } = new URL(url); + return html`
+ + ${hide_url ? '' : html`${hostname}`} +
`; +} diff --git a/src/types/shared/directives/image.d.ts b/src/types/shared/directives/image.d.ts index f30c5f4382..378225a5c5 100644 --- a/src/types/shared/directives/image.d.ts +++ b/src/types/shared/directives/image.d.ts @@ -7,11 +7,32 @@ * @param { Function } onLoad - A callback function to be called once the image has loaded. * @param { Function } onClick - A callback function to be called once the image has been clicked. */ -export const renderImage: (src?: any, href?: any, onLoad?: any, onClick?: any) => import("lit/async-directive.js").DirectiveResult; +export const renderImage: (src: string, href?: string, onLoad?: Function, onClick?: Function) => import("lit/async-directive.js").DirectiveResult; declare class ImageDirective extends AsyncDirective { - render(src: any, href: any, onLoad: any, onClick: any): import("lit").TemplateResult<1>; - renderImage(src: any, href: any, onLoad: any, onClick: any): import("lit").TemplateResult<1>; - onError(src: any, href: any, onLoad: any, onClick: any): void; + /** + * @param {string} src - The source URL of the image. + * @param {string} [href] - The optional hyperlink for the image. + * @param {Function} [onLoad] - Callback function to be called once the image has loaded. + * @param {Function} [onClick] - Callback function to be called once the image has been clicked. + * @returns {import('lit').TemplateResult} + */ + render(src: string, href?: string, onLoad?: Function, onClick?: Function): import("lit").TemplateResult; + /** + * @param {string} src - The source URL of the image. + * @param {string} [href] - The optional hyperlink for the image. + * @param {Function} [onLoad] - Callback function to be called once the image has loaded. + * @param {Function} [onClick] - Callback function to be called once the image has been clicked. + * @returns {import('lit').TemplateResult} + */ + renderImage(src: string, href?: string, onLoad?: Function, onClick?: Function): import("lit").TemplateResult; + /** + * Handles errors that occur during image loading. + * @param {string} src - The source URL of the image that failed to load. + * @param {string} [href] - The optional hyperlink for the image. + * @param {Function} [onLoad] - Callback function to be called once the image has loaded. + * @param {Function} [onClick] - Callback function to be called once the image has been clicked. + */ + onError(src: string, href?: string, onLoad?: Function, onClick?: Function): void; } import { AsyncDirective } from 'lit/async-directive.js'; export {};