parent
27bc548552
commit
e675c853f3
@ -3,6 +3,7 @@
|
|||||||
## 8.0.0 (Unreleased)
|
## 8.0.0 (Unreleased)
|
||||||
|
|
||||||
- #1083: Add support for XEP-0393 Message Styling
|
- #1083: Add support for XEP-0393 Message Styling
|
||||||
|
- #1182: Add support for XEP-0454 OMEMO Media sharing
|
||||||
- #2275: Allow punctuation to immediately precede a mention
|
- #2275: Allow punctuation to immediately precede a mention
|
||||||
- #2348: `auto_join_room` not showing the room in `fullscreen` `view_mode`.
|
- #2348: `auto_join_room` not showing the room in `fullscreen` `view_mode`.
|
||||||
- #2400: Fixes infinite loop bug when appending .png to allowed image urls
|
- #2400: Fixes infinite loop bug when appending .png to allowed image urls
|
||||||
|
@ -108,7 +108,7 @@ In embedded mode, Converse can be embedded into an element in the DOM.
|
|||||||
- [XEP-0424](https://xmpp.org/extensions/xep-0424.html) Message Retractions
|
- [XEP-0424](https://xmpp.org/extensions/xep-0424.html) Message Retractions
|
||||||
- [XEP-0425](https://xmpp.org/extensions/xep-0425.html) Message Moderation
|
- [XEP-0425](https://xmpp.org/extensions/xep-0425.html) Message Moderation
|
||||||
- [XEP-0437](https://xmpp.org/extensions/xep-0437.html) Room Activity Indicators
|
- [XEP-0437](https://xmpp.org/extensions/xep-0437.html) Room Activity Indicators
|
||||||
|
- [XEP-0454](https://xmpp.org/extensions/xep-0454.html) OMEMO Media sharing
|
||||||
|
|
||||||
## Integration into other servers and frameworks
|
## Integration into other servers and frameworks
|
||||||
|
|
||||||
|
@ -224,15 +224,23 @@ const MessageMixin = {
|
|||||||
|
|
||||||
uploadFile () {
|
uploadFile () {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = async () => {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
log.info('Status: ' + xhr.status);
|
log.info('Status: ' + xhr.status);
|
||||||
if (xhr.status === 200 || xhr.status === 201) {
|
if (xhr.status === 200 || xhr.status === 201) {
|
||||||
this.save({
|
let attrs = {
|
||||||
'upload': _converse.SUCCESS,
|
'upload': _converse.SUCCESS,
|
||||||
'oob_url': this.get('get'),
|
'oob_url': this.get('get'),
|
||||||
'message': this.get('get')
|
'message': this.get('get'),
|
||||||
});
|
'body': this.get('get'),
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* *Hook* which allows plugins to change the attributes
|
||||||
|
* saved on the message once a file has been uploaded.
|
||||||
|
* @event _converse#afterFileUploaded
|
||||||
|
*/
|
||||||
|
attrs = await api.hook('afterFileUploaded', this, attrs);
|
||||||
|
this.save(attrs);
|
||||||
} else {
|
} else {
|
||||||
xhr.onerror();
|
xhr.onerror();
|
||||||
}
|
}
|
||||||
|
@ -1002,6 +1002,13 @@ const ChatBox = ModelWithContact.extend({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Array.from(files).forEach(async file => {
|
Array.from(files).forEach(async file => {
|
||||||
|
/**
|
||||||
|
* *Hook* which allows plugins to transform files before they'll be
|
||||||
|
* uploaded. The main use-case is to encrypt the files.
|
||||||
|
* @event _converse#beforeFileUpload
|
||||||
|
*/
|
||||||
|
file = await api.hook('beforeFileUpload', this, file);
|
||||||
|
|
||||||
if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
|
if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
|
||||||
return this.createMessage({
|
return this.createMessage({
|
||||||
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
|
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
|
||||||
|
@ -19,6 +19,7 @@ import omemo_api from './api.js';
|
|||||||
import { OMEMOEnabledChatBox } from './mixins/chatbox.js';
|
import { OMEMOEnabledChatBox } from './mixins/chatbox.js';
|
||||||
import { _converse, api, converse } from '@converse/headless/core';
|
import { _converse, api, converse } from '@converse/headless/core';
|
||||||
import {
|
import {
|
||||||
|
encryptFile,
|
||||||
getOMEMOToolbarButton,
|
getOMEMOToolbarButton,
|
||||||
handleEncryptedFiles,
|
handleEncryptedFiles,
|
||||||
initOMEMO,
|
initOMEMO,
|
||||||
@ -26,6 +27,7 @@ import {
|
|||||||
onChatBoxesInitialized,
|
onChatBoxesInitialized,
|
||||||
onChatInitialized,
|
onChatInitialized,
|
||||||
parseEncryptedMessage,
|
parseEncryptedMessage,
|
||||||
|
setEncryptedFileURL,
|
||||||
registerPEPPushHandler,
|
registerPEPPushHandler,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
@ -72,6 +74,9 @@ converse.plugins.add('converse-omemo', {
|
|||||||
/******************** Event Handlers ********************/
|
/******************** Event Handlers ********************/
|
||||||
api.waitUntil('chatBoxesInitialized').then(onChatBoxesInitialized);
|
api.waitUntil('chatBoxesInitialized').then(onChatBoxesInitialized);
|
||||||
|
|
||||||
|
api.listen.on('afterFileUploaded', (msg, attrs) => msg.file.xep454_ivkey ? setEncryptedFileURL(msg, attrs) : attrs);
|
||||||
|
api.listen.on('beforeFileUpload', (chat, file) => chat.get('omemo_active') ? encryptFile(file) : file);
|
||||||
|
|
||||||
api.listen.on('parseMessage', parseEncryptedMessage);
|
api.listen.on('parseMessage', parseEncryptedMessage);
|
||||||
api.listen.on('parseMUCMessage', parseEncryptedMessage);
|
api.listen.on('parseMUCMessage', parseEncryptedMessage);
|
||||||
|
|
||||||
|
@ -1229,13 +1229,13 @@ describe("The OMEMO module", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
view.model.save({'omemo_supported': false});
|
view.model.save({'omemo_supported': false});
|
||||||
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
|
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')?.dataset.disabled === "true");
|
||||||
icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
icon = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo converse-icon'));
|
||||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||||
|
|
||||||
view.model.save({'omemo_supported': true});
|
view.model.save({'omemo_supported': true});
|
||||||
await u.waitUntil(() => !toolbar.querySelector('.toggle-omemo').disabled);
|
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')?.dataset.disabled === "false");
|
||||||
icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
||||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||||
@ -1270,7 +1270,7 @@ describe("The OMEMO module", function() {
|
|||||||
let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||||
expect(view.model.get('omemo_active')).toBe(undefined);
|
expect(view.model.get('omemo_active')).toBe(undefined);
|
||||||
expect(view.model.get('omemo_supported')).toBe(true);
|
expect(view.model.get('omemo_supported')).toBe(true);
|
||||||
await u.waitUntil(() => !toggle.disabled);
|
await u.waitUntil(() => toggle.dataset.disabled === "false");
|
||||||
|
|
||||||
let icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
let icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
||||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||||
@ -1278,7 +1278,7 @@ describe("The OMEMO module", function() {
|
|||||||
|
|
||||||
toggle.click();
|
toggle.click();
|
||||||
toggle = toolbar.querySelector('.toggle-omemo');
|
toggle = toolbar.querySelector('.toggle-omemo');
|
||||||
expect(!!toggle.disabled).toBe(false);
|
expect(toggle.dataset.disabled).toBe("false");
|
||||||
expect(view.model.get('omemo_active')).toBe(true);
|
expect(view.model.get('omemo_active')).toBe(true);
|
||||||
expect(view.model.get('omemo_supported')).toBe(true);
|
expect(view.model.get('omemo_supported')).toBe(true);
|
||||||
|
|
||||||
@ -1330,7 +1330,7 @@ describe("The OMEMO module", function() {
|
|||||||
expect(view.model.get('omemo_active')).toBe(true);
|
expect(view.model.get('omemo_active')).toBe(true);
|
||||||
toggle = toolbar.querySelector('.toggle-omemo');
|
toggle = toolbar.querySelector('.toggle-omemo');
|
||||||
expect(toggle === null).toBe(false);
|
expect(toggle === null).toBe(false);
|
||||||
expect(!!toggle.disabled).toBe(false);
|
expect(toggle.dataset.disabled).toBe("false");
|
||||||
expect(view.model.get('omemo_supported')).toBe(true);
|
expect(view.model.get('omemo_supported')).toBe(true);
|
||||||
|
|
||||||
await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
|
await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
|
||||||
@ -1340,23 +1340,23 @@ describe("The OMEMO module", function() {
|
|||||||
// anonymous or semi-anonymous
|
// anonymous or semi-anonymous
|
||||||
view.model.features.save({'nonanonymous': false, 'semianonymous': true});
|
view.model.features.save({'nonanonymous': false, 'semianonymous': true});
|
||||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
|
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||||
|
|
||||||
view.model.features.save({'nonanonymous': true, 'semianonymous': false});
|
view.model.features.save({'nonanonymous': true, 'semianonymous': false});
|
||||||
await u.waitUntil(() => view.model.get('omemo_supported'));
|
await u.waitUntil(() => view.model.get('omemo_supported'));
|
||||||
await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
|
await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
|
||||||
expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
||||||
expect(!!view.querySelector('.toggle-omemo').disabled).toBe(false);
|
expect(view.querySelector('.toggle-omemo').dataset.disabled).toBe("false");
|
||||||
|
|
||||||
// Test that the button gets disabled when the room becomes open
|
// Test that the button gets disabled when the room becomes open
|
||||||
view.model.features.save({'membersonly': false, 'open': true});
|
view.model.features.save({'membersonly': false, 'open': true});
|
||||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
|
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||||
|
|
||||||
view.model.features.save({'membersonly': true, 'open': false});
|
view.model.features.save({'membersonly': true, 'open': false});
|
||||||
await u.waitUntil(() => view.model.get('omemo_supported'));
|
await u.waitUntil(() => view.model.get('omemo_supported'));
|
||||||
await u.waitUntil(() => !view.querySelector('.toggle-omemo').disabled);
|
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "false");
|
||||||
|
|
||||||
expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||||
expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
||||||
@ -1404,7 +1404,7 @@ describe("The OMEMO module", function() {
|
|||||||
"Encrypted chat will no longer be possible in this grouchat."
|
"Encrypted chat will no longer be possible in this grouchat."
|
||||||
);
|
);
|
||||||
|
|
||||||
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
|
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||||
icon = view.querySelector('.toggle-omemo converse-icon');
|
icon = view.querySelector('.toggle-omemo converse-icon');
|
||||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||||
|
@ -31,50 +31,73 @@ const KEY_ALGO = {
|
|||||||
'length': 128
|
'length': 128
|
||||||
};
|
};
|
||||||
|
|
||||||
export const omemo = {
|
|
||||||
async encryptMessage (plaintext) {
|
|
||||||
// The client MUST use fresh, randomly generated key/IV pairs
|
|
||||||
// with AES-128 in Galois/Counter Mode (GCM).
|
|
||||||
|
|
||||||
// For GCM a 12 byte IV is strongly suggested as other IV lengths
|
async function encryptMessage (plaintext) {
|
||||||
// will require additional calculations. In principle any IV size
|
// The client MUST use fresh, randomly generated key/IV pairs
|
||||||
// can be used as long as the IV doesn't ever repeat. NIST however
|
// with AES-128 in Galois/Counter Mode (GCM).
|
||||||
// suggests that only an IV size of 12 bytes needs to be supported
|
|
||||||
// by implementations.
|
|
||||||
//
|
|
||||||
// https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
|
|
||||||
const iv = crypto.getRandomValues(new window.Uint8Array(12)),
|
|
||||||
key = await crypto.subtle.generateKey(KEY_ALGO, true, ['encrypt', 'decrypt']),
|
|
||||||
algo = {
|
|
||||||
'name': 'AES-GCM',
|
|
||||||
'iv': iv,
|
|
||||||
'tagLength': TAG_LENGTH
|
|
||||||
},
|
|
||||||
encrypted = await crypto.subtle.encrypt(algo, key, stringToArrayBuffer(plaintext)),
|
|
||||||
length = encrypted.byteLength - ((128 + 7) >> 3),
|
|
||||||
ciphertext = encrypted.slice(0, length),
|
|
||||||
tag = encrypted.slice(length),
|
|
||||||
exported_key = await crypto.subtle.exportKey('raw', key);
|
|
||||||
|
|
||||||
return {
|
// For GCM a 12 byte IV is strongly suggested as other IV lengths
|
||||||
'key': exported_key,
|
// will require additional calculations. In principle any IV size
|
||||||
'tag': tag,
|
// can be used as long as the IV doesn't ever repeat. NIST however
|
||||||
'key_and_tag': appendArrayBuffer(exported_key, tag),
|
// suggests that only an IV size of 12 bytes needs to be supported
|
||||||
'payload': arrayBufferToBase64(ciphertext),
|
// by implementations.
|
||||||
'iv': arrayBufferToBase64(iv)
|
//
|
||||||
};
|
// https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
|
||||||
},
|
const iv = crypto.getRandomValues(new window.Uint8Array(12));
|
||||||
|
const key = await crypto.subtle.generateKey(KEY_ALGO, true, ['encrypt', 'decrypt']);
|
||||||
async decryptMessage (obj) {
|
const algo = {
|
||||||
const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']);
|
|
||||||
const cipher = appendArrayBuffer(base64ToArrayBuffer(obj.payload), obj.tag);
|
|
||||||
const algo = {
|
|
||||||
'name': 'AES-GCM',
|
'name': 'AES-GCM',
|
||||||
'iv': base64ToArrayBuffer(obj.iv),
|
'iv': iv,
|
||||||
'tagLength': TAG_LENGTH
|
'tagLength': TAG_LENGTH
|
||||||
};
|
};
|
||||||
return arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher));
|
const encrypted = await crypto.subtle.encrypt(algo, key, stringToArrayBuffer(plaintext));
|
||||||
}
|
const length = encrypted.byteLength - ((128 + 7) >> 3);
|
||||||
|
const ciphertext = encrypted.slice(0, length);
|
||||||
|
const tag = encrypted.slice(length);
|
||||||
|
const exported_key = await crypto.subtle.exportKey('raw', key);
|
||||||
|
return {
|
||||||
|
'key': exported_key,
|
||||||
|
'tag': tag,
|
||||||
|
'key_and_tag': appendArrayBuffer(exported_key, tag),
|
||||||
|
'payload': arrayBufferToBase64(ciphertext),
|
||||||
|
'iv': arrayBufferToBase64(iv)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptMessage (obj) {
|
||||||
|
const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']);
|
||||||
|
const cipher = appendArrayBuffer(base64ToArrayBuffer(obj.payload), obj.tag);
|
||||||
|
const algo = {
|
||||||
|
'name': 'AES-GCM',
|
||||||
|
'iv': base64ToArrayBuffer(obj.iv),
|
||||||
|
'tagLength': TAG_LENGTH
|
||||||
|
};
|
||||||
|
return arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const omemo = {
|
||||||
|
decryptMessage,
|
||||||
|
encryptMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function encryptFile (file) {
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt']);
|
||||||
|
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, }, key, await file.arrayBuffer());
|
||||||
|
const exported_key = await window.crypto.subtle.exportKey('raw', key);
|
||||||
|
const encrypted_file = new File([encrypted], file.name, { type: file.type, lastModified: file.lastModified });
|
||||||
|
encrypted_file.xep454_ivkey = arrayBufferToHex(iv) + arrayBufferToHex(exported_key);
|
||||||
|
return encrypted_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEncryptedFileURL (message, attrs) {
|
||||||
|
const url = attrs.oob_url.replace(/^https?:/, 'aesgcm:') + '#' + message.file.xep454_ivkey;
|
||||||
|
return Object.assign(attrs, {
|
||||||
|
'oob_url': null, // Since only the body gets encrypted, we don't set the oob_url
|
||||||
|
'message': url,
|
||||||
|
'body': url
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptFile (iv, key, cipher) {
|
async function decryptFile (iv, key, cipher) {
|
||||||
@ -646,14 +669,19 @@ export function getOMEMOToolbarButton (toolbar_el, buttons) {
|
|||||||
'order to support OMEMO encrypted messages'
|
'order to support OMEMO encrypted messages'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let color;
|
||||||
|
if (model.get('omemo_supported')) {
|
||||||
|
color = model.get('omemo_active') ? `var(--info-color)` : `var(--error-color)`;
|
||||||
|
} else {
|
||||||
|
color = `var(--muc-toolbar-btn-disabled-color)`;
|
||||||
|
}
|
||||||
buttons.push(html`
|
buttons.push(html`
|
||||||
<button class="toggle-omemo" title="${title}" ?disabled=${!model.get('omemo_supported')} @click=${toggleOMEMO}>
|
<button class="toggle-omemo" title="${title}" data-disabled=${!model.get('omemo_supported')} @click=${toggleOMEMO}>
|
||||||
<converse-icon
|
<converse-icon
|
||||||
class="fa ${model.get('omemo_active') ? `fa-lock` : `fa-unlock`}"
|
class="fa ${model.get('omemo_active') ? `fa-lock` : `fa-unlock`}"
|
||||||
path-prefix="${api.settings.get('assets_path')}"
|
path-prefix="${api.settings.get('assets_path')}"
|
||||||
size="1em"
|
size="1em"
|
||||||
color="${model.get('omemo_active') ? `var(--info-color)` : `var(--error-color)`}"
|
color="${color}"
|
||||||
></converse-icon>
|
></converse-icon>
|
||||||
</button>
|
</button>
|
||||||
`);
|
`);
|
||||||
|
@ -13,11 +13,11 @@ import { CustomElement } from 'shared/components/element.js';
|
|||||||
import { __ } from 'i18n';
|
import { __ } from 'i18n';
|
||||||
import { _converse, api, converse } from '@converse/headless/core';
|
import { _converse, api, converse } from '@converse/headless/core';
|
||||||
import { getHats } from './utils.js';
|
import { getHats } from './utils.js';
|
||||||
|
import { getOOBURLMarkup } from 'utils/html.js';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { renderAvatar } from 'shared/directives/avatar';
|
import { renderAvatar } from 'shared/directives/avatar';
|
||||||
|
|
||||||
const { Strophe, dayjs } = converse.env;
|
const { Strophe, dayjs } = converse.env;
|
||||||
const u = converse.env.utils;
|
|
||||||
|
|
||||||
|
|
||||||
export default class Message extends CustomElement {
|
export default class Message extends CustomElement {
|
||||||
@ -70,7 +70,7 @@ export default class Message extends CustomElement {
|
|||||||
return '';
|
return '';
|
||||||
} else if (this.show_spinner) {
|
} else if (this.show_spinner) {
|
||||||
return tpl_spinner();
|
return tpl_spinner();
|
||||||
} else if (this.model.get('file') && !this.model.get('oob_url')) {
|
} else if (this.model.get('file') && this.model.get('upload') !== _converse.SUCCESS) {
|
||||||
return this.renderFileProgress();
|
return this.renderFileProgress();
|
||||||
} else if (['error', 'info'].includes(this.model.get('type'))) {
|
} else if (['error', 'info'].includes(this.model.get('type'))) {
|
||||||
return this.renderInfoMessage();
|
return this.renderInfoMessage();
|
||||||
@ -105,6 +105,10 @@ export default class Message extends CustomElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFileProgress () {
|
renderFileProgress () {
|
||||||
|
if (!this.model.file) {
|
||||||
|
// Can happen when file upload failed and page was reloaded
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const i18n_uploading = __('Uploading file:');
|
const i18n_uploading = __('Uploading file:');
|
||||||
const filename = this.model.file.name;
|
const filename = this.model.file.name;
|
||||||
const size = filesize(this.model.file.size);
|
const size = filesize(this.model.file.size);
|
||||||
@ -264,7 +268,7 @@ export default class Message extends CustomElement {
|
|||||||
${ (this.model.get('received') && !this.model.isMeCommand() && !is_groupchat_message) ? html`<span class="fa fa-check chat-msg__receipt"></span>` : '' }
|
${ (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>` : '' }
|
${ (this.model.get('edited')) ? html`<i title="${ i18n_edited }" class="fa fa-edit chat-msg__edit-modal" @click=${this.showMessageVersionsModal}></i>` : '' }
|
||||||
</span>
|
</span>
|
||||||
${ this.model.get('oob_url') ? html`<div class="chat-msg__media">${u.getOOBURLMarkup(_converse, this.model.get('oob_url'))}</div>` : '' }
|
${ this.model.get('oob_url') ? html`<div class="chat-msg__media">${getOOBURLMarkup(this.model.get('oob_url'))}</div>` : '' }
|
||||||
<div class="chat-msg__error">${ this.model.get('error_text') || this.model.get('error') }</div>
|
<div class="chat-msg__error">${ this.model.get('error_text') || this.model.get('error') }</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ function getFileName (uri) {
|
|||||||
* @param { String } url
|
* @param { String } url
|
||||||
* @returns { String }
|
* @returns { String }
|
||||||
*/
|
*/
|
||||||
u.getOOBURLMarkup = function (_converse, url) {
|
export function getOOBURLMarkup (url) {
|
||||||
const uri = getURI(url);
|
const uri = getURI(url);
|
||||||
if (uri === null) {
|
if (uri === null) {
|
||||||
return url;
|
return url;
|
||||||
@ -175,7 +175,7 @@ u.getOOBURLMarkup = function (_converse, url) {
|
|||||||
} else {
|
} else {
|
||||||
return tpl_file(uri.toString(), getFileName(uri));
|
return tpl_file(uri.toString(), getFileName(uri));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the height of the passed in DOM element,
|
* Return the height of the passed in DOM element,
|
||||||
@ -598,7 +598,8 @@ Object.assign(u, {
|
|||||||
isImageURL,
|
isImageURL,
|
||||||
isImageDomainAllowed,
|
isImageDomainAllowed,
|
||||||
isURLWithImageExtension,
|
isURLWithImageExtension,
|
||||||
isVideoURL
|
isVideoURL,
|
||||||
|
getOOBURLMarkup,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default u;
|
export default u;
|
||||||
|
Loading…
Reference in New Issue
Block a user