diff --git a/README.md b/README.md
index b9d7a08ca..1757f62e1 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,7 @@ In embedded mode, Converse can be embedded into an element in the DOM.
- A [plugin architecture](https://conversejs.org/docs/html/plugin_development.html) based on [pluggable.js](https://conversejs.github.io/pluggable.js/)
- Chat statuses (online, busy, away, offline)
- Anonymous logins, see the [anonymous login demo](https://conversejs.org/demo/anonymous.html)
+- URL Previews (requires server support, for example [mod_ogp](https://modules.prosody.im/mod_ogp.html)
- Translated into over 30 languages
### Supported XMPP Extensions
diff --git a/sass/_core.scss b/sass/_core.scss
index d77e94d96..3a134ad29 100644
--- a/sass/_core.scss
+++ b/sass/_core.scss
@@ -2,7 +2,7 @@
opacity: 0; /* make things invisible upon start */
animation-name: fadein;
animation-fill-mode: forwards;
- animation-duration: 0.75s;
+ animation-duration: 0.5s;
animation-timing-function: ease;
}
diff --git a/spec/unfurls.js b/spec/unfurls.js
index 018e2d23a..cd88dbce5 100644
--- a/spec/unfurls.js
+++ b/spec/unfurls.js
@@ -261,4 +261,46 @@ describe("A Groupchat Message", function () {
done();
}));
+ it("lets the user hide an unfurl",
+ mock.initConverse(['chatBoxesFetched'],
+ {'show_images_inline': []},
+ 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(`
+
+ https://www.youtube.com/watch?v=dQw4w9WgXcQ
+
+
+
+
+ `);
+ _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(`
+
+
+
+
+
+
+
+
+ `);
+ _converse.connection._dataRecv(mock.createRequest(metadata_stanza));
+
+ await u.waitUntil(() => view.querySelector('converse-message-unfurl'));
+ const button = await u.waitUntil(() => view.querySelector('.chat-msg__content .chat-msg__action-hide-previews'));
+ button.click();
+ await u.waitUntil(() => view.querySelector('converse-message-unfurl') === null, 750);
+ button.click();
+ await u.waitUntil(() => view.querySelector('converse-message-unfurl'), 750);
+ done();
+ }));
});
diff --git a/src/components/message-actions.js b/src/components/message-actions.js
index 63b5217fe..2e26f2825 100644
--- a/src/components/message-actions.js
+++ b/src/components/message-actions.js
@@ -12,11 +12,13 @@ class MessageActions extends CustomElement {
static get properties () {
return {
- model: { type: Object },
- editable: { type: Boolean },
correcting: { type: Boolean },
- message_type: { type: String },
+ editable: { type: Boolean },
+ hide_url_previews: { type: Boolean },
is_retracted: { type: Boolean },
+ message_type: { type: String },
+ model: { type: Object },
+ unfurls: { type: Number }
}
}
@@ -147,7 +149,7 @@ class MessageActions extends CustomElement {
}
onMessageRetractButtonClicked (ev) {
- ev.preventDefault();
+ ev?.preventDefault?.();
const chatbox = this.model.collection.chatbox;
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
this.onMUCMessageRetractButtonClicked();
@@ -156,6 +158,19 @@ class MessageActions extends CustomElement {
}
}
+ onHidePreviewsButtonClicked (ev) {
+ ev?.preventDefault?.();
+ if (this.hide_url_previews) {
+ this.model.save({
+ 'hide_url_previews': false,
+ 'url_preview_transition': 'fade-in'
+ });
+ } else {
+ this.model.set('url_preview_transition', 'fade-out');
+ }
+
+ }
+
async getActionButtons () {
const buttons = [];
if (this.editable) {
@@ -178,6 +193,29 @@ class MessageActions extends CustomElement {
'name': 'retract'
});
}
+
+ 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) {
+
+ let title;
+ const hidden_preview = this.hide_url_previews;
+ if (ogp_metadata.length > 1) {
+ title = hidden_preview ? __('Show URL previews') : __('Hide URL previews');
+ } else {
+ title = hidden_preview ? __('Show URL preview') : __('Hide URL preview');
+ }
+ 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'
+ });
+ }
+
/**
* *Hook* which allows plugins to add more message action buttons
* @event _converse#getMessageActionButtons
diff --git a/src/shared/chat/message.js b/src/shared/chat/message.js
index fffe77eee..18701c482 100644
--- a/src/shared/chat/message.js
+++ b/src/shared/chat/message.js
@@ -7,7 +7,7 @@ import OccupantModal from 'modals/occupant.js';
import UserDetailsModal from 'modals/user-details.js';
import dayjs from 'dayjs';
import filesize from 'filesize';
-import tpl_chat_message from './templates/message.js';
+import tpl_message from './templates/message.js';
import tpl_spinner from 'templates/spinner.js';
import { CustomElement } from 'components/element.js';
import { __ } from 'i18n';
@@ -126,7 +126,7 @@ export default class Message extends CustomElement {
}
renderChatMessage () {
- return tpl_chat_message(this);
+ return tpl_message(this);
}
shouldShowAvatar () {
@@ -145,6 +145,15 @@ export default class Message extends CustomElement {
};
}
+ onUnfurlAnimationEnd () {
+ if (this.model.get('url_preview_transition') === 'fade-out') {
+ this.model.save({
+ 'hide_url_previews': !this.model.get('hide_url_previews'),
+ 'url_preview_transition': 'fade-in'
+ });
+ }
+ }
+
async onRetryClicked () {
this.show_spinner = true;
await api.trigger(this.retry_event_id, {'synchronous': true});
diff --git a/src/shared/chat/templates/message.js b/src/shared/chat/templates/message.js
index 764d178b9..3ffb42b57 100644
--- a/src/shared/chat/templates/message.js
+++ b/src/shared/chat/templates/message.js
@@ -38,16 +38,20 @@ export default (o) => {
?correcting="${o.correcting}"
?editable="${o.editable}"
?is_retracted="${o.is_retracted}"
+ ?hide_url_previews="${o.model.get('hide_url_previews')}"
+ unfurls="${o.model.get('ogp_metadata')?.length}"
message_type="${o.message_type}">
- ${ o.model.get('ogp_metadata')?.map(m =>
+ ${ !o.model.get('hide_url_previews') ? o.model.get('ogp_metadata')?.map(m =>
html``) }
+ url="${m['og:url'] || ''}">`) : '' }
`;
}