Add support for rendering unfurls via Prosody's mod_ogp
See here: https://modules.prosody.im/mod_ogp.html
This commit is contained in:
parent
c69eb6e1bf
commit
16edc2954d
@ -17,6 +17,7 @@
|
||||
- New configuration setting: [send_chat_markers](https://conversejs.org/docs/html/configuration.html#send-chat-markers)
|
||||
- #1823: New config options [mam_request_all_pages](https://conversejs.org/docs/html/configuration.html#mam-request-all-pages)
|
||||
- Use the MUC stanza id when sending XEP-0333 markers
|
||||
- Add support for rendering unfurls via [mod_ogp](https://modules.prosody.im/mod_ogp.html)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
@ -53,6 +53,7 @@ module.exports = function(config) {
|
||||
{ pattern: "spec/markers.js", type: 'module' },
|
||||
{ pattern: "spec/rai.js", type: 'module' },
|
||||
{ pattern: "spec/muc_messages.js", type: 'module' },
|
||||
{ pattern: "spec/unfurls.js", type: 'module' },
|
||||
{ pattern: "spec/muc-mentions.js", type: 'module' },
|
||||
{ pattern: "spec/me-messages.js", type: 'module' },
|
||||
{ pattern: "spec/mentions.js", type: 'module' },
|
||||
|
@ -9,6 +9,12 @@
|
||||
}
|
||||
}
|
||||
.message {
|
||||
|
||||
.card--unfurl {
|
||||
margin: 1em 0;
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
.show-msg-author-modal {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
@import "bootstrap/scss/input-group";
|
||||
@import "bootstrap/scss/custom-forms";
|
||||
@import "bootstrap/scss/nav";
|
||||
@import "bootstrap/scss/card";
|
||||
@import "bootstrap/scss/badge";
|
||||
@import "bootstrap/scss/alert";
|
||||
@import "bootstrap/scss/media";
|
||||
|
@ -59,7 +59,8 @@ describe("A Groupchat Message", function () {
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const nick = 'romeo';
|
||||
await mock.openAndEnterChatRoom(_converse, muc_jid, nick);
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
let presence = u.toStanza(`
|
||||
<presence xmlns="jabber:client" to="${_converse.jid}" from="${muc_jid}/romeo">
|
||||
|
103
spec/unfurls.js
Normal file
103
spec/unfurls.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*global mock, converse */
|
||||
|
||||
const { u } = converse.env;
|
||||
|
||||
describe("A Groupchat Message", function () {
|
||||
|
||||
it("will render an unfurl based on OGP data", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
|
||||
const nick = 'romeo';
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await mock.openAndEnterChatRoom(_converse, muc_jid, nick);
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
|
||||
const message_stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client" type="groupchat" from="${muc_jid}/arzu" xml:lang="en" to="${_converse.jid}" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04">
|
||||
<body>https://www.youtube.com/watch?v=dQw4w9WgXcQ</body>
|
||||
<active xmlns="http://jabber.org/protocol/chatstates"/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04"/>
|
||||
<stanza-id xmlns="urn:xmpp:sid:0" by="${muc_jid}" id="8f7613cc-27d4-40ca-9488-da25c4baf92a"/>
|
||||
<markable xmlns="urn:xmpp:chat-markers:0"/>
|
||||
</message>`);
|
||||
_converse.connection._dataRecv(mock.createRequest(message_stanza));
|
||||
const el = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
|
||||
expect(el.textContent).toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||
|
||||
const metadata_stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client" from="${muc_jid}" to="${_converse.jid}">
|
||||
<apply-to xmlns="urn:xmpp:fasten:0" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04">
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:site_name" content="YouTube" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:url" content="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:title" content="Rick Astley - Never Gonna Give You Up (Video)" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image" content="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image:width" content="1280" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image:height" content="720" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:description" content="Rick Astley&#39;s official music video for "Never Gonna Give You Up" Listen to Rick Astley: https://RickAstley.lnk.to/_listenYD Subscribe to the official Rick Ast..." />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:type" content="video.other" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:url" content="https://www.youtube.com/embed/dQw4w9WgXcQ" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:secure_url" content="https://www.youtube.com/embed/dQw4w9WgXcQ" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:type" content="text/html" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:width" content="1280" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:height" content="720" />
|
||||
</apply-to>
|
||||
</message>`);
|
||||
_converse.connection._dataRecv(mock.createRequest(metadata_stanza));
|
||||
|
||||
const unfurl = await u.waitUntil(() => view.querySelector('converse-message-unfurl'));
|
||||
expect(unfurl.querySelector('.card-img-top').getAttribute('src')).toBe('https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg');
|
||||
done();
|
||||
}));
|
||||
|
||||
it("will render multiple unfurls based on OGP data", mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
|
||||
const nick = 'romeo';
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await mock.openAndEnterChatRoom(_converse, muc_jid, nick);
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
|
||||
const message_stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client" type="groupchat" from="${muc_jid}/arzu" xml:lang="en" to="${_converse.jid}" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04">
|
||||
<body>Check out https://www.youtube.com/watch?v=dQw4w9WgXcQ and https://duckduckgo.com</body>
|
||||
<active xmlns="http://jabber.org/protocol/chatstates"/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04"/>
|
||||
<stanza-id xmlns="urn:xmpp:sid:0" by="${muc_jid}" id="8f7613cc-27d4-40ca-9488-da25c4baf92a"/>
|
||||
<markable xmlns="urn:xmpp:chat-markers:0"/>
|
||||
</message>`);
|
||||
_converse.connection._dataRecv(mock.createRequest(message_stanza));
|
||||
const el = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
|
||||
expect(el.textContent).toBe('Check out https://www.youtube.com/watch?v=dQw4w9WgXcQ and https://duckduckgo.com');
|
||||
|
||||
let metadata_stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client" from="${muc_jid}" to="${_converse.jid}">
|
||||
<apply-to xmlns="urn:xmpp:fasten:0" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04">
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:site_name" content="YouTube" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:url" content="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:title" content="Rick Astley - Never Gonna Give You Up (Video)" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image" content="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image:width" content="1280" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image:height" content="720" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:description" content="Rick Astley&#39;s official music video for "Never Gonna Give You Up" Listen to Rick Astley: https://RickAstley.lnk.to/_listenYD Subscribe to the official Rick Ast..." />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:type" content="video.other" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:url" content="https://www.youtube.com/embed/dQw4w9WgXcQ" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:secure_url" content="https://www.youtube.com/embed/dQw4w9WgXcQ" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:type" content="text/html" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:width" content="1280" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:video:height" content="720" />
|
||||
</apply-to>
|
||||
</message>`);
|
||||
_converse.connection._dataRecv(mock.createRequest(metadata_stanza));
|
||||
|
||||
metadata_stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client" from="${muc_jid}" to="${_converse.jid}">
|
||||
<apply-to xmlns="urn:xmpp:fasten:0" id="eda6c790-b4f3-4c07-b5e2-13fff99e6c04">
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:url" content="https://duckduckgo.com/" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:site_name" content="DuckDuckGo" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:image" content="https://duckduckgo.com/assets/logo_social-media.png" />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:title" content="DuckDuckGo - Privacy, simplified." />
|
||||
<meta xmlns="http://www.w3.org/1999/xhtml" property="og:description" content="The Internet privacy company that empowers you to seamlessly take control of your personal information online, without any tradeoffs." />
|
||||
</apply-to>
|
||||
</message>`);
|
||||
_converse.connection._dataRecv(mock.createRequest(metadata_stanza));
|
||||
|
||||
await u.waitUntil(() => view.querySelectorAll('converse-message-unfurl').length === 2);
|
||||
done();
|
||||
}));
|
||||
});
|
@ -47,6 +47,7 @@ const tpl_message = (o) => html`
|
||||
spoiler_hint=${o.spoiler_hint || ''}
|
||||
subject=${o.subject || ''}
|
||||
time=${o.time}
|
||||
unfurl_metadata=${o.unfurl_metadata}
|
||||
username=${o.username}></converse-chat-message>
|
||||
`;
|
||||
|
||||
|
@ -58,6 +58,7 @@ export default class Message extends CustomElement {
|
||||
spoiler_hint: { type: String },
|
||||
subject: { type: String },
|
||||
time: { type: String },
|
||||
unfurl_metadata: { type: String },
|
||||
username: { type: String }
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ Strophe.addNamespace('STYLING', 'urn:xmpp:styling:0');
|
||||
Strophe.addNamespace('VCARD', 'vcard-temp');
|
||||
Strophe.addNamespace('VCARDUPDATE', 'vcard-temp:x:update');
|
||||
Strophe.addNamespace('XFORM', 'jabber:x:data');
|
||||
Strophe.addNamespace('XHTML', 'http://www.w3.org/1999/xhtml');
|
||||
|
||||
/**
|
||||
* Custom error for indicating timeouts
|
||||
|
@ -1,9 +1,12 @@
|
||||
import ModelWithContact from './model-with-contact.js';
|
||||
import filesize from "filesize";
|
||||
import isMatch from "lodash/isMatch";
|
||||
import isObject from "lodash/isObject";
|
||||
import log from '@converse/headless/log';
|
||||
import pick from "lodash/pick";
|
||||
import { Model } from '@converse/skeletor/src/model.js';
|
||||
import { _converse, api, converse } from "../../core.js";
|
||||
import { find, isMatch, isObject, pick } from "lodash-es";
|
||||
import { getOpenGraphMetadata } from '@converse/headless/shared/parsers';
|
||||
import { parseMessage } from './parsers.js';
|
||||
import { sendMarker } from '@converse/headless/shared/actions';
|
||||
|
||||
@ -11,6 +14,24 @@ const { Strophe, $msg } = converse.env;
|
||||
|
||||
const u = converse.env.utils;
|
||||
|
||||
const METADATA_ATTRIBUTES = [
|
||||
"og:description",
|
||||
"og:image",
|
||||
"og:image:height",
|
||||
"og:image:width",
|
||||
"og:site_name",
|
||||
"og:title",
|
||||
"og:type",
|
||||
"og:url",
|
||||
"og:video:height",
|
||||
"og:video:secure_url",
|
||||
"og:video:tag",
|
||||
"og:video:type",
|
||||
"og:video:url",
|
||||
"og:video:width"
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Represents an open/ongoing chat conversation.
|
||||
*
|
||||
@ -468,6 +489,24 @@ const ChatBox = ModelWithContact.extend({
|
||||
return false;
|
||||
},
|
||||
|
||||
handleMetadataFastening (stanza) {
|
||||
const attrs = getOpenGraphMetadata(stanza);
|
||||
if (attrs.ogp_for_id) {
|
||||
if (attrs.ogp_for_id) {
|
||||
const message = this.messages.findWhere({'origin_id': attrs.ogp_for_id});
|
||||
if (message) {
|
||||
const list = message.get('ogp_metadata') || [];
|
||||
list.push(pick(attrs, METADATA_ATTRIBUTES));
|
||||
message.save('ogp_metadata', list);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines whether the passed in message attributes represent a
|
||||
* message which corrects a previously received message, or an
|
||||
@ -524,7 +563,7 @@ const ChatBox = ModelWithContact.extend({
|
||||
this.getMessageBodyQueryAttrs(attrs)
|
||||
].filter(s => s);
|
||||
const msgs = this.messages.models;
|
||||
return find(msgs, m => queries.reduce((out, q) => (out || isMatch(m.attributes, q)), false));
|
||||
return msgs.find(m => queries.reduce((out, q) => (out || isMatch(m.attributes, q)), false));
|
||||
},
|
||||
|
||||
getOriginIdQueryAttrs (attrs) {
|
||||
|
@ -495,6 +495,7 @@ const ChatRoomMixin = {
|
||||
*/
|
||||
async handleMessageStanza (stanza) {
|
||||
if (stanza.getAttribute('type') !== 'groupchat') {
|
||||
this.handleMetadataFastening(stanza);
|
||||
this.handleForwardedMentions(stanza);
|
||||
return;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import dayjs from 'dayjs';
|
||||
import sizzle from 'sizzle';
|
||||
import { Strophe } from 'strophe.js/src/strophe';
|
||||
import { _converse, api } from '@converse/headless/core';
|
||||
import { decodeHTMLEntities } from 'shared/utils';
|
||||
import { rejectMessage } from '@converse/headless/shared/actions';
|
||||
|
||||
const { NS } = Strophe;
|
||||
@ -120,6 +121,24 @@ export function getCorrectionAttributes (stanza, original_stanza) {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getOpenGraphMetadata (stanza) {
|
||||
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
||||
if (fastening) {
|
||||
const applies_to_id = fastening.getAttribute('id');
|
||||
const meta = sizzle(`> meta[xmlns="${Strophe.NS.XHTML}"]`, fastening);
|
||||
return meta.reduce((acc, el) => {
|
||||
const property = el.getAttribute('property');
|
||||
if (property) {
|
||||
acc[property] = decodeHTMLEntities(el.getAttribute('content') || '');
|
||||
}
|
||||
return acc;
|
||||
}, {
|
||||
'ogp_for_id': applies_to_id,
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getSpoilerAttributes (stanza) {
|
||||
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
|
||||
return {
|
||||
|
15
src/shared/chat/templates/unfurl.js
Normal file
15
src/shared/chat/templates/unfurl.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { html } from 'lit-element';
|
||||
import { converse } from "@converse/headless/core";
|
||||
const u = converse.env.utils;
|
||||
|
||||
export default (o) => {
|
||||
return html`<div class="card card--unfurl">
|
||||
<a href="${o.url}" target="_blank" rel="noopener">
|
||||
<img class="card-img-top" src="${o.image}" @load=${o.onload}/>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<a href="${o.url}" target="_blank" rel="noopener"><h5 class="card-title">${o.title}</h5></a>
|
||||
<p class="card-text">${u.addHyperlinks(o.description)}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
34
src/shared/chat/unfurl.js
Normal file
34
src/shared/chat/unfurl.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { CustomElement } from 'components/element.js';
|
||||
import { _converse, api } from "@converse/headless/core";
|
||||
import tpl_unfurl from './templates/unfurl.js';
|
||||
|
||||
|
||||
export default class MessageUnfurl extends CustomElement {
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
description: { type: String },
|
||||
image: { type: String },
|
||||
jid: { type: String },
|
||||
title: { type: String },
|
||||
url: { type: String },
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return tpl_unfurl(Object.assign({
|
||||
'onload': () => this.onImageLoad()
|
||||
}, {
|
||||
description: this.description,
|
||||
image: this.image,
|
||||
title: this.title,
|
||||
url: this.url
|
||||
}));
|
||||
}
|
||||
|
||||
onImageLoad () {
|
||||
_converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown();
|
||||
}
|
||||
}
|
||||
|
||||
api.elements.define('converse-message-unfurl', MessageUnfurl);
|
12
src/shared/utils.js
Normal file
12
src/shared/utils.js
Normal file
@ -0,0 +1,12 @@
|
||||
import xss from 'xss/dist/xss';
|
||||
|
||||
const element = document.createElement('div');
|
||||
|
||||
export function decodeHTMLEntities (str) {
|
||||
if (str && typeof str === 'string') {
|
||||
element.innerHTML = xss.filterXSS(str);
|
||||
str = element.textContent;
|
||||
element.textContent = '';
|
||||
}
|
||||
return str;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { html } from "lit-html";
|
||||
import 'shared/chat/unfurl';
|
||||
import { __ } from '../i18n';
|
||||
import { html } from "lit-html";
|
||||
import { renderAvatar } from './../templates/directives/avatar';
|
||||
|
||||
|
||||
@ -39,6 +40,14 @@ export default (o) => {
|
||||
?is_retracted="${o.is_retracted}"
|
||||
message_type="${o.message_type}"></converse-message-actions>
|
||||
</div>
|
||||
|
||||
${ o.model.get('ogp_metadata')?.map(m =>
|
||||
html`<converse-message-unfurl
|
||||
jid="${o.jid}"
|
||||
description="${m['og:description']}"
|
||||
title="${m['og:title']}"
|
||||
image="${m['og:image']}"
|
||||
url="${m['og:url']}"></converse-message-unfurl>`) }
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user