Identify media URLs during message parsing
This commit is contained in:
parent
b90a435833
commit
f2aa39e1c3
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -24600,6 +24600,7 @@
|
|||
},
|
||||
"@converse/skeletor": {
|
||||
"version": "git+ssh://git@github.com/conversejs/skeletor.git#f354bc530493a17d031f6f9c524cc34e073908e3",
|
||||
"integrity": "sha512-BqifISxYDtkQeJxSkxOgUl/Z0vFT9+ePYKFVzwXQLjxjBQp05xdw1+WkE+t8BnEiAXkoGKAEOv04Ezg3D3jgIw==",
|
||||
"from": "@converse/skeletor@conversejs/skeletor#f354bc530493a17d031f6f9c524cc34e073908e3",
|
||||
"requires": {
|
||||
"lit-html": "^2.0.0-rc.2",
|
||||
|
|
|
@ -9,6 +9,7 @@ import { _converse, api, converse } from "../../core.js";
|
|||
import { getOpenPromise } from '@converse/openpromise';
|
||||
import { initStorage } from '@converse/headless/shared/utils.js';
|
||||
import { debouncedPruneHistory, pruneHistory } from '@converse/headless/shared/chat/utils.js';
|
||||
import { getMediaURLs } from '@converse/headless/shared/parsers';
|
||||
import { parseMessage } from './parsers.js';
|
||||
import { sendMarker } from '@converse/headless/shared/actions';
|
||||
|
||||
|
@ -869,7 +870,7 @@ const ChatBox = ModelWithContact.extend({
|
|||
body,
|
||||
is_spoiler,
|
||||
origin_id
|
||||
});
|
||||
}, getMediaURLs(text));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
getCorrectionAttributes,
|
||||
getEncryptionAttributes,
|
||||
getErrorAttributes,
|
||||
getMediaURLs,
|
||||
getOutOfBandAttributes,
|
||||
getReceiptId,
|
||||
getReferences,
|
||||
|
@ -215,5 +216,10 @@ export async function parseMessage (stanza, _converse) {
|
|||
* *Hook* which allows plugins to add additional parsing
|
||||
* @event _converse#parseMessage
|
||||
*/
|
||||
return api.hook('parseMessage', stanza, attrs);
|
||||
attrs = await api.hook('parseMessage', stanza, attrs);
|
||||
|
||||
// We call this after the hook, to allow plugins to decrypt encrypted
|
||||
// messages, since we need to parse the message text to determine whether
|
||||
// there are media urls.
|
||||
return Object.assign(attrs, getMediaURLs(attrs.is_encrypted ? attrs.plaintext : attrs.body));
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { _converse, api, converse } from '../../core.js';
|
|||
import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
|
||||
import { getOpenPromise } from '@converse/openpromise';
|
||||
import { initStorage } from '@converse/headless/shared/utils.js';
|
||||
import { isArchived } from '@converse/headless/shared/parsers';
|
||||
import { isArchived, getMediaURLs } from '@converse/headless/shared/parsers';
|
||||
import { parseMUCMessage, parseMUCPresence } from './parsers.js';
|
||||
import { sendMarker } from '@converse/headless/shared/actions';
|
||||
|
||||
|
@ -981,7 +981,7 @@ const ChatRoomMixin = {
|
|||
'nick': this.get('nick'),
|
||||
'sender': 'me',
|
||||
'type': 'groupchat'
|
||||
});
|
||||
}, getMediaURLs(text));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
getCorrectionAttributes,
|
||||
getEncryptionAttributes,
|
||||
getErrorAttributes,
|
||||
getMediaURLs,
|
||||
getOpenGraphMetadata,
|
||||
getOutOfBandAttributes,
|
||||
getReceiptId,
|
||||
|
@ -184,9 +185,10 @@ export async function parseMUCMessage (stanza, chatbox, _converse) {
|
|||
getOpenGraphMetadata(stanza),
|
||||
getRetractionAttributes(stanza, original_stanza),
|
||||
getModerationAttributes(stanza),
|
||||
getEncryptionAttributes(stanza, _converse)
|
||||
getEncryptionAttributes(stanza, _converse),
|
||||
);
|
||||
|
||||
|
||||
await api.emojis.initialize();
|
||||
attrs = Object.assign(
|
||||
{
|
||||
|
@ -213,11 +215,17 @@ export async function parseMUCMessage (stanza, chatbox, _converse) {
|
|||
}
|
||||
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
|
||||
attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from_muc || attrs.from}`] || u.getUniqueId();
|
||||
|
||||
/**
|
||||
* *Hook* which allows plugins to add additional parsing
|
||||
* @event _converse#parseMUCMessage
|
||||
*/
|
||||
return api.hook('parseMUCMessage', stanza, attrs);
|
||||
attrs = await api.hook('parseMUCMessage', stanza, attrs);
|
||||
|
||||
// We call this after the hook, to allow plugins to decrypt encrypted
|
||||
// messages, since we need to parse the message text to determine whether
|
||||
// there are media urls.
|
||||
return Object.assign(attrs, getMediaURLs(attrs.is_encrypted ? attrs.plaintext : attrs.body));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import URI from 'urijs';
|
||||
import dayjs from 'dayjs';
|
||||
import log from '@converse/headless/log';
|
||||
import sizzle from 'sizzle';
|
||||
import { Strophe } from 'strophe.js/src/strophe';
|
||||
import { _converse, api } from '@converse/headless/core';
|
||||
import { decodeHTMLEntities } from '@converse/headless/shared/utils';
|
||||
import { rejectMessage } from '@converse/headless/shared/actions';
|
||||
import {
|
||||
isAudioDomainAllowed,
|
||||
isAudioURL,
|
||||
isImageDomainAllowed,
|
||||
isImageURL,
|
||||
isVideoDomainAllowed,
|
||||
isVideoURL
|
||||
} from '@converse/headless/utils/url.js';
|
||||
|
||||
const { NS } = Strophe;
|
||||
|
||||
|
@ -166,6 +176,34 @@ export function getOpenGraphMetadata (stanza) {
|
|||
return {};
|
||||
}
|
||||
|
||||
|
||||
export function getMediaURLs (text) {
|
||||
const objs = [];
|
||||
if (!text) {
|
||||
return {};
|
||||
}
|
||||
const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi };
|
||||
try {
|
||||
URI.withinString(
|
||||
text,
|
||||
(url, start, end) => {
|
||||
objs.push({ url, start, end });
|
||||
return url;
|
||||
},
|
||||
parse_options
|
||||
);
|
||||
} catch (error) {
|
||||
log.debug(error);
|
||||
}
|
||||
const media_urls = objs.filter(o => {
|
||||
return (isImageURL(o.url) && isImageDomainAllowed(o.url)) ||
|
||||
(isVideoURL(o.url) && isVideoDomainAllowed(o.url)) ||
|
||||
(isAudioURL(o.url) && isAudioDomainAllowed(o.url));
|
||||
}).map(o => ({ 'start': o.start, 'end': o.end }));
|
||||
return media_urls.length ? { media_urls } : {};
|
||||
}
|
||||
|
||||
|
||||
export function getSpoilerAttributes (stanza) {
|
||||
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
|
||||
return {
|
||||
|
|
|
@ -148,4 +148,31 @@ describe("A Chat Message", function () {
|
|||
await u.waitUntil(() => view.querySelector('converse-chat-message-body .chat-image') === null);
|
||||
expect(true).toBe(true);
|
||||
}));
|
||||
|
||||
it("will allow the user to toggle visibility of rendered images",
|
||||
mock.initConverse(['chatBoxesFetched'], {'show_images_inline': true}, async function (_converse) {
|
||||
|
||||
await mock.waitForRoster(_converse, 'current');
|
||||
// let message = "https://i.imgur.com/Py9ifJE.mp4";
|
||||
const base_url = 'https://conversejs.org';
|
||||
const message = base_url+"/logo/conversejs-filled.svg";
|
||||
|
||||
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);
|
||||
const sel = '.chat-content .chat-msg:last .chat-msg__text';
|
||||
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
|
||||
|
||||
const actions_el = view.querySelector('converse-message-actions');
|
||||
await u.waitUntil(() => actions_el.textContent.includes('Hide media'));
|
||||
await u.waitUntil(() => view.querySelector('converse-chat-message-body img'));
|
||||
|
||||
actions_el.querySelector('.chat-msg__action-hide-previews').click();
|
||||
await u.waitUntil(() => actions_el.textContent.includes('Show media'));
|
||||
await u.waitUntil(() => !view.querySelector('converse-chat-message-body img'));
|
||||
|
||||
expect(view.querySelector('converse-chat-message-body').innerHTML.replace(/<!-.*?->/g, '').trim())
|
||||
.toBe(`<a target="_blank" rel="noopener" href="${message}">${message}</a>`)
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const { Strophe, sizzle, u } = converse.env;
|
||||
|
||||
describe("A Chat Message", function () {
|
||||
describe("A chat message containing video URLs", function () {
|
||||
|
||||
it("will render videos from their URLs", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
await mock.waitForRoster(_converse, 'current');
|
||||
|
@ -68,4 +68,31 @@ describe("A Chat Message", function () {
|
|||
`<video controls="" preload="metadata" src="${message}"></video>`+
|
||||
`<a target="_blank" rel="noopener" href="${message}">${message}</a>`);
|
||||
}));
|
||||
|
||||
it("will allow the user to toggle visibility of rendered videos",
|
||||
mock.initConverse(['chatBoxesFetched'], {'embed_videos': true}, async function (_converse) {
|
||||
|
||||
await mock.waitForRoster(_converse, 'current');
|
||||
// let message = "https://i.imgur.com/Py9ifJE.mp4";
|
||||
const base_url = 'https://conversejs.org';
|
||||
const message = base_url+"/logo/conversejs-filled.mp4";
|
||||
|
||||
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);
|
||||
const sel = '.chat-content .chat-msg:last .chat-msg__text';
|
||||
await u.waitUntil(() => sizzle(sel).pop().innerHTML.replace(/<!-.*?->/g, '').trim() === message);
|
||||
|
||||
const actions_el = view.querySelector('converse-message-actions');
|
||||
await u.waitUntil(() => actions_el.textContent.includes('Hide media'));
|
||||
await u.waitUntil(() => view.querySelector('converse-chat-message-body video'));
|
||||
|
||||
actions_el.querySelector('.chat-msg__action-hide-previews').click();
|
||||
await u.waitUntil(() => actions_el.textContent.includes('Show media'));
|
||||
await u.waitUntil(() => !view.querySelector('converse-chat-message-body video'));
|
||||
|
||||
expect(view.querySelector('converse-chat-message-body').innerHTML.replace(/<!-.*?->/g, '').trim())
|
||||
.toBe(`<a target="_blank" rel="noopener" href="${message}">${message}</a>`)
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import log from '@converse/headless/log';
|
||||
import { CustomElement } from 'shared/components/element.js';
|
||||
import { __ } from 'i18n';
|
||||
import { _converse, api, converse } from "@converse/headless/core";
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
import { html } from 'lit';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
|
||||
const { Strophe, u } = converse.env;
|
||||
|
||||
|
||||
class MessageActions extends CustomElement {
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
correcting: { type: Boolean },
|
||||
|
@ -18,12 +16,12 @@ class MessageActions extends CustomElement {
|
|||
is_retracted: { type: Boolean },
|
||||
message_type: { type: String },
|
||||
model: { type: Object },
|
||||
unfurls: { type: Number }
|
||||
}
|
||||
unfurls: { type: Number },
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
return html`${ until(this.renderActions(), '') }`;
|
||||
return html`${until(this.renderActions(), '')}`;
|
||||
}
|
||||
|
||||
async renderActions () {
|
||||
|
@ -38,7 +36,8 @@ class MessageActions extends CustomElement {
|
|||
if (items.length) {
|
||||
return html`<converse-dropdown
|
||||
class="chat-msg__actions ${should_drop_up ? 'dropup dropup--left' : 'dropleft'}"
|
||||
.items=${ items }></converse-dropdown>`;
|
||||
.items=${items}
|
||||
></converse-dropdown>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
@ -47,10 +46,12 @@ class MessageActions extends CustomElement {
|
|||
static getActionsDropdownItem (o) {
|
||||
return html`
|
||||
<button class="chat-msg__action ${o.button_class}" @click=${o.handler}>
|
||||
<converse-icon class="${o.icon_class}"
|
||||
path-prefix="${api.settings.get("assets_path")}"
|
||||
<converse-icon
|
||||
class="${o.icon_class}"
|
||||
path-prefix="${api.settings.get('assets_path')}"
|
||||
color="var(--text-color-lighten-15-percent)"
|
||||
size="1em"></converse-icon>
|
||||
size="1em"
|
||||
></converse-icon>
|
||||
${o.i18n_text}
|
||||
</button>
|
||||
`;
|
||||
|
@ -81,8 +82,8 @@ class MessageActions extends CustomElement {
|
|||
}
|
||||
const retraction_warning = __(
|
||||
'Be aware that other XMPP/Jabber clients (and servers) may ' +
|
||||
'not yet support retractions and that this message may not ' +
|
||||
'be removed everywhere.'
|
||||
'not yet support retractions and that this message may not ' +
|
||||
'be removed everywhere.'
|
||||
);
|
||||
const messages = [__('Are you sure you want to retract this message?')];
|
||||
if (api.settings.get('show_retraction_warning')) {
|
||||
|
@ -119,8 +120,8 @@ class MessageActions extends CustomElement {
|
|||
async onMUCMessageRetractButtonClicked () {
|
||||
const retraction_warning = __(
|
||||
'Be aware that other XMPP/Jabber clients (and servers) may ' +
|
||||
'not yet support retractions and that this message may not ' +
|
||||
'be removed everywhere.'
|
||||
'not yet support retractions and that this message may not ' +
|
||||
'be removed everywhere.'
|
||||
);
|
||||
|
||||
if (this.model.mayBeRetracted()) {
|
||||
|
@ -142,7 +143,7 @@ class MessageActions extends CustomElement {
|
|||
} else {
|
||||
let messages = [
|
||||
__('You are about to retract this message.'),
|
||||
__('You may optionally include a message, explaining the reason for the retraction.')
|
||||
__('You may optionally include a message, explaining the reason for the retraction.'),
|
||||
];
|
||||
if (api.settings.get('show_retraction_warning')) {
|
||||
messages = [messages[0], retraction_warning, messages[1]];
|
||||
|
@ -171,12 +172,20 @@ class MessageActions extends CustomElement {
|
|||
if (this.hide_url_previews) {
|
||||
this.model.save({
|
||||
'hide_url_previews': false,
|
||||
'url_preview_transition': 'fade-in'
|
||||
'url_preview_transition': 'fade-in',
|
||||
});
|
||||
} else {
|
||||
this.model.set('url_preview_transition', 'fade-out');
|
||||
const ogp_metadata = this.model.get('ogp_metadata') || [];
|
||||
const unfurls_to_show = api.settings.get('muc_show_ogp_unfurls') && ogp_metadata.length;
|
||||
if (unfurls_to_show) {
|
||||
this.model.set('url_preview_transition', 'fade-out');
|
||||
} else {
|
||||
this.model.save({
|
||||
'hide_url_previews': true,
|
||||
'url_preview_transition': 'fade-in',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getActionButtons () {
|
||||
|
@ -187,10 +196,10 @@ class MessageActions extends CustomElement {
|
|||
'handler': ev => this.onMessageEditButtonClicked(ev),
|
||||
'button_class': 'chat-msg__action-edit',
|
||||
'icon_class': 'fa fa-pencil-alt',
|
||||
'name': 'edit'
|
||||
'name': 'edit',
|
||||
});
|
||||
}
|
||||
const may_be_moderated = this.model.get('type') === 'groupchat' && await this.model.mayBeModerated();
|
||||
const may_be_moderated = this.model.get('type') === 'groupchat' && (await this.model.mayBeModerated());
|
||||
const retractable = !this.is_retracted && (this.model.mayBeRetracted() || may_be_moderated);
|
||||
if (retractable) {
|
||||
buttons.push({
|
||||
|
@ -198,7 +207,7 @@ class MessageActions extends CustomElement {
|
|||
'handler': ev => this.onMessageRetractButtonClicked(ev),
|
||||
'button_class': 'chat-msg__action-retract',
|
||||
'icon_class': 'fas fa-trash-alt',
|
||||
'name': 'retract'
|
||||
'name': 'retract',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -208,24 +217,25 @@ class MessageActions extends CustomElement {
|
|||
return [];
|
||||
}
|
||||
const ogp_metadata = this.model.get('ogp_metadata') || [];
|
||||
const chatbox = this.model.collection.chatbox;
|
||||
if (chatbox.get('type') === _converse.CHATROOMS_TYPE &&
|
||||
api.settings.get('muc_show_ogp_unfurls') &&
|
||||
ogp_metadata.length) {
|
||||
const unfurls_to_show = api.settings.get('muc_show_ogp_unfurls') && ogp_metadata.length;
|
||||
const media_to_show = this.model.get('media_urls')?.length;
|
||||
|
||||
if (unfurls_to_show || media_to_show) {
|
||||
let title;
|
||||
const hidden_preview = this.hide_url_previews;
|
||||
if (ogp_metadata.length > 1) {
|
||||
title = hidden_preview ? __('Show URL previews') : __('Hide URL previews');
|
||||
} else {
|
||||
} else if (ogp_metadata.length === 1) {
|
||||
title = hidden_preview ? __('Show URL preview') : __('Hide URL preview');
|
||||
} else {
|
||||
title = hidden_preview ? __('Show media') : __('Hide media');
|
||||
}
|
||||
buttons.push({
|
||||
'i18n_text': title,
|
||||
'handler': ev => this.onHidePreviewsButtonClicked(ev),
|
||||
'button_class': 'chat-msg__action-hide-previews',
|
||||
'icon_class': this.hide_url_previews ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||
'name': 'hide'
|
||||
'name': 'hide',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,12 @@ export default class MessageBody extends CustomElement {
|
|||
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object },
|
||||
is_me_message: { type: Boolean },
|
||||
show_images: { type: Boolean },
|
||||
embed_videos: { type: Boolean },
|
||||
embed_audio: { type: Boolean },
|
||||
embed_videos: { type: Boolean },
|
||||
hide_url_previews: { type: Boolean },
|
||||
is_me_message: { type: Boolean },
|
||||
model: { type: Object },
|
||||
show_images: { type: Boolean },
|
||||
text: { type: String },
|
||||
}
|
||||
}
|
||||
|
@ -33,14 +34,15 @@ export default class MessageBody extends CustomElement {
|
|||
const callback = () => this.model.collection?.trigger('rendered', this.model);
|
||||
const offset = 0;
|
||||
const mentions = this.model.get('references');
|
||||
|
||||
const options = {
|
||||
'embed_audio': this.embed_audio,
|
||||
'embed_videos': this.embed_videos,
|
||||
'embed_audio': !this.hide_url_previews && this.embed_audio,
|
||||
'embed_videos': !this.hide_url_previews && this.embed_videos,
|
||||
'nick': this.model.collection.chatbox.get('nick'),
|
||||
'onImgClick': (ev) => this.onImgClick(ev),
|
||||
'onImgLoad': () => this.onImgLoad(),
|
||||
'render_styling': !this.model.get('is_unstyled') && api.settings.get('allow_message_styling'),
|
||||
'show_images': this.show_images,
|
||||
'show_images': !this.hide_url_previews && this.show_images,
|
||||
'show_me_message': true
|
||||
}
|
||||
return renderRichText(this.text, offset, mentions, options, callback);
|
||||
|
|
|
@ -260,10 +260,11 @@ export default class Message extends CustomElement {
|
|||
<converse-chat-message-body
|
||||
class="chat-msg__text ${this.model.get('is_only_emojis') ? 'chat-msg__text--larger' : ''} ${spoiler_classes}"
|
||||
.model="${this.model}"
|
||||
?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')}"
|
||||
?hide_url_previews=${this.model.get('hide_url_previews')}
|
||||
?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>` : '' }
|
||||
|
|
Loading…
Reference in New Issue
Block a user