diff --git a/docs/source/plugin_development.rst b/docs/source/plugin_development.rst index a6feae307d..21112d7be6 100644 --- a/docs/source/plugin_development.rst +++ b/docs/source/plugin_development.rst @@ -33,7 +33,7 @@ modular and self-contained way, without having to change other files. This ensures that plugins are fully optional (one of the design goals of Converse) and can be removed from the main build without breaking the app. -For example, the ``converse-omemo``, +For example, the ``converse-omemo-views``, ``converse-rosterview``, ``converse-dragresize``, ``converse-minimize``, ``converse-muc`` and ``converse-muc-views`` plugins can all be removed from the build without breaking the app. diff --git a/src/converse.js b/src/converse.js index e93ee169a1..355ac63ce1 100644 --- a/src/converse.js +++ b/src/converse.js @@ -28,7 +28,7 @@ import "./plugins/muc-views/index.js"; // Views related to MUC import "./plugins/minimize/index.js"; // Allows chat boxes to be minimized import "./plugins/notifications/index.js"; import "./plugins/profile/index.js"; -import "./plugins/omemo/index.js"; +import "./plugins/omemo-views/index.js"; import "./plugins/push/index.js"; // XEP-0357 Push Notifications import "./plugins/register/index.js"; // XEP-0077 In-band registration import "./plugins/roomslist/index.js"; // Show currently open chat rooms diff --git a/src/plugins/omemo/api.js b/src/headless/plugins/omemo/api.js similarity index 100% rename from src/plugins/omemo/api.js rename to src/headless/plugins/omemo/api.js diff --git a/src/plugins/omemo/consts.js b/src/headless/plugins/omemo/consts.js similarity index 100% rename from src/plugins/omemo/consts.js rename to src/headless/plugins/omemo/consts.js diff --git a/src/plugins/omemo/device.js b/src/headless/plugins/omemo/device.js similarity index 100% rename from src/plugins/omemo/device.js rename to src/headless/plugins/omemo/device.js diff --git a/src/plugins/omemo/devicelist.js b/src/headless/plugins/omemo/devicelist.js similarity index 100% rename from src/plugins/omemo/devicelist.js rename to src/headless/plugins/omemo/devicelist.js diff --git a/src/plugins/omemo/devicelists.js b/src/headless/plugins/omemo/devicelists.js similarity index 100% rename from src/plugins/omemo/devicelists.js rename to src/headless/plugins/omemo/devicelists.js diff --git a/src/plugins/omemo/devices.js b/src/headless/plugins/omemo/devices.js similarity index 100% rename from src/plugins/omemo/devices.js rename to src/headless/plugins/omemo/devices.js diff --git a/src/plugins/omemo/index.js b/src/headless/plugins/omemo/index.js similarity index 69% rename from src/plugins/omemo/index.js rename to src/headless/plugins/omemo/index.js index b1f37bcdbe..f1f1148e53 100644 --- a/src/plugins/omemo/index.js +++ b/src/headless/plugins/omemo/index.js @@ -2,33 +2,29 @@ * @copyright The Converse.js contributors * @license Mozilla Public License (MPLv2) */ -import './fingerprints.js'; -import './profile.js'; -import 'shared/modals/user-details.js'; import ConverseMixins from './mixins/converse.js'; +import omemo_api from './api.js'; import Device from './device.js'; import DeviceList from './devicelist.js'; import DeviceLists from './devicelists.js'; import Devices from './devices.js'; import OMEMOStore from './store.js'; -import log from '@converse/headless/log'; -import omemo_api from './api.js'; import { _converse, api, converse } from '@converse/headless/core'; + import { createOMEMOMessageStanza, encryptFile, - getOMEMOToolbarButton, getOutgoingMessageAttributes, handleEncryptedFiles, handleMessageSendError, initOMEMO, omemo, onChatBoxesInitialized, - onChatInitialized, + onChatBoxInitialized, parseEncryptedMessage, - registerPEPPushHandler, + registerPEPPushHandler setEncryptedFileURL, -} from './utils.js'; +} from './utils.js' const { Strophe } = converse.env; @@ -42,18 +38,18 @@ Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO + '.bundles'); converse.plugins.add('converse-omemo', { enabled (_converse) { - return ( - window.libsignal && - _converse.config.get('trusted') && - !api.settings.get('clear_cache_on_logout') && - !_converse.api.settings.get('blacklisted_plugins').includes('converse-omemo') - ); + return ( + window.libsignal && + _converse.config.get('trusted') && + !api.settings.get('clear_cache_on_logout') && + !_converse.api.settings.get('blacklisted-plugins').includes('converse-omemo-views') + ); }, - dependencies: ['converse-chatview', 'converse-pubsub', 'converse-profile'], + dependencies: ['converse-chat', 'converse-pubsub'], - initialize () { - api.settings.extend({ 'omemo_default': false }); + initialize() { + api.settings.extend({ 'omemo-default': false }); api.promises.add(['OMEMOInitialized']); _converse.NUM_PREKEYS = 100; // Set here so that tests can override @@ -70,6 +66,8 @@ converse.plugins.add('converse-omemo', { /******************** Event Handlers ********************/ api.waitUntil('chatBoxesInitialized').then(onChatBoxesInitialized); + api.listen.on('chatBoxInitialized', onChatBoxInitialized); + api.listen.on('getOutgoingMessageAttributes', getOutgoingMessageAttributes); api.listen.on('createMessageStanza', async (chat, data) => { @@ -87,26 +85,11 @@ converse.plugins.add('converse-omemo', { api.listen.on('parseMessage', parseEncryptedMessage); api.listen.on('parseMUCMessage', parseEncryptedMessage); - api.listen.on('chatBoxViewInitialized', onChatInitialized); - api.listen.on('chatRoomViewInitialized', onChatInitialized); - api.listen.on('connected', registerPEPPushHandler); - api.listen.on('getToolbarButtons', getOMEMOToolbarButton); api.listen.on('statusInitialized', initOMEMO); api.listen.on('addClientFeatures', () => api.disco.own.features.add(`${Strophe.NS.OMEMO_DEVICELIST}+notify`)); - api.listen.on('afterMessageBodyTransformed', handleEncryptedFiles); - - api.listen.on('userDetailsModalInitialized', contact => { - const jid = contact.get('jid'); - _converse.generateFingerprints(jid).catch(e => log.error(e)); - }); - - api.listen.on('profileModalInitialized', () => { - _converse.generateFingerprints(_converse.bare_jid).catch(e => log.error(e)); - }); - api.listen.on('clearSession', () => { delete _converse.omemo_store if (_converse.shouldClearCache() && _converse.devicelists) { diff --git a/src/plugins/omemo/mixins/converse.js b/src/headless/plugins/omemo/mixins/converse.js similarity index 100% rename from src/plugins/omemo/mixins/converse.js rename to src/headless/plugins/omemo/mixins/converse.js diff --git a/src/plugins/omemo/store.js b/src/headless/plugins/omemo/store.js similarity index 99% rename from src/plugins/omemo/store.js rename to src/headless/plugins/omemo/store.js index 3ffa279602..c1e748cb8c 100644 --- a/src/plugins/omemo/store.js +++ b/src/headless/plugins/omemo/store.js @@ -5,7 +5,7 @@ import log from '@converse/headless/log'; import range from 'lodash-es/range'; import omit from 'lodash-es/omit'; import { Model } from '@converse/skeletor/src/model.js'; -import { generateDeviceID } from './utils.js'; +import { generateDeviceID } from '@converse/headless/plugins/omemo/utils.js'; import { _converse, api, converse } from '@converse/headless/core'; const { Strophe, $build, u } = converse.env; diff --git a/src/plugins/omemo/utils.js b/src/headless/plugins/omemo/utils.js similarity index 85% rename from src/plugins/omemo/utils.js rename to src/headless/plugins/omemo/utils.js index 8a462310b0..19b8493674 100644 --- a/src/plugins/omemo/utils.js +++ b/src/headless/plugins/omemo/utils.js @@ -2,19 +2,12 @@ import concat from 'lodash-es/concat'; import difference from 'lodash-es/difference'; import log from '@converse/headless/log'; -import tpl_audio from 'templates/audio.js'; -import tpl_file from 'templates/file.js'; -import tpl_image from 'templates/image.js'; -import tpl_video from 'templates/video.js'; import { KEY_ALGO, UNTRUSTED, TAG_LENGTH } from './consts.js'; import { MIMETYPES_MAP } from 'utils/file.js'; import { __ } from 'i18n'; import { _converse, converse, api } from '@converse/headless/core'; -import { html } from 'lit'; +import { getURI } from '@converse/headless/utils/url.js'; import { initStorage } from '@converse/headless/utils/storage.js'; -import { isError } from '@converse/headless/utils/core.js'; -import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js'; -import { until } from 'lit/directives/until.js'; import { appendArrayBuffer, arrayBufferToBase64, @@ -25,7 +18,7 @@ import { stringToArrayBuffer } from '@converse/headless/utils/arraybuffer.js'; -const { Strophe, URI, sizzle, u } = converse.env; +const { Strophe, sizzle, u } = converse.env; export function formatFingerprint (fp) { fp = fp.replace(/^05/, ''); @@ -197,36 +190,26 @@ async function getAndDecryptFile (uri) { } } -function getTemplateForObjectURL (uri, obj_url, richtext) { - if (isError(obj_url)) { - return html`

${obj_url.message}

`; - } - - const file_url = uri.toString(); - if (isImageURL(file_url)) { - return tpl_image({ - 'src': obj_url, - 'onClick': richtext.onImgClick, - 'onLoad': richtext.onImgLoad - }); - } else if (isAudioURL(file_url)) { - return tpl_audio(obj_url); - } else if (isVideoURL(file_url)) { - return tpl_video(obj_url); - } else { - return tpl_file(obj_url, uri.filename()); - } - +export const omemo = { + decryptMessage, + encryptMessage, + formatFingerprint } -function addEncryptedFiles(text, offset, richtext) { +export function processEncryptedFiles (text) { const objs = []; try { const parse_options = { 'start': /\b(aesgcm:\/\/)/gi }; URI.withinString( text, (url, start, end) => { - objs.push({ url, start, end }); + const uri = getURI(text.slice(o.start, o.end)); + objs.push({ + uri, + start, + end, + obj_url: getAndDecryptFile(uri); // this is a promise + }); return url; }, parse_options @@ -235,21 +218,8 @@ function addEncryptedFiles(text, offset, richtext) { log.debug(error); return; } - objs.forEach(o => { - const uri = getURI(text.slice(o.start, o.end)); - const promise = getAndDecryptFile(uri) - .then(obj_url => getTemplateForObjectURL(uri, obj_url, richtext)); - - const template = html`${until(promise, '')}`; - richtext.addTemplateResult(o.start + offset, o.end + offset, template); - }); -} -export function handleEncryptedFiles (richtext) { - if (!_converse.config.get('trusted')) { - return; - } - richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext)); + return objs; } /** @@ -306,24 +276,17 @@ export function onChatBoxesInitialized () { }); } -export function onChatInitialized (el) { - el.listenTo(el.model.messages, 'add', message => { +export function onChatBoxInitialized(model) { + model.listenTo(model.messages, 'add', message => { if (message.get('is_encrypted') && !message.get('is_error')) { - el.model.save('omemo_supported', true); + model.save('omemo_supported', true); } }); - el.listenTo(el.model, 'change:omemo_supported', () => { - if (!el.model.get('omemo_supported') && el.model.get('omemo_active')) { - el.model.set('omemo_active', false); - } else { - // Manually trigger an update, setting omemo_active to - // false above will automatically trigger one. - el.querySelector('converse-chat-toolbar')?.requestUpdate(); + model.listenTo(model, 'change:omemo_supported', () => { + if (!model.get('omemo_supported') && model.get('omemo_active')) { + model.set('omemo_active', false); } }); - el.listenTo(el.model, 'change:omemo_active', () => { - el.querySelector('converse-chat-toolbar').requestUpdate(); - }); } export function getSessionCipher (jid, id) { @@ -331,6 +294,7 @@ export function getSessionCipher (jid, id) { return new window.libsignal.SessionCipher(_converse.omemo_store, address); } + function getJIDForDecryption (attrs) { const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from; if (!from_jid) { @@ -459,6 +423,7 @@ export function addKeysToMessageStanza (stanza, dicts, iv) { return Promise.resolve(stanza); } + /** * Given an XML element representing a user's OMEMO bundle, parse it * and return a map. @@ -676,6 +641,7 @@ export async function initOMEMO (reconnecting) { api.trigger('OMEMOInitialized'); } + async function onOccupantAdded (chatroom, occupant) { if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) { return; @@ -710,73 +676,6 @@ async function checkOMEMOSupported (chatbox) { } } -function toggleOMEMO (ev) { - ev.stopPropagation(); - ev.preventDefault(); - const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar'); - if (!toolbar_el.model.get('omemo_supported')) { - let messages; - if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) { - messages = [ - __( - 'Cannot use end-to-end encryption in this groupchat, ' + - 'either the groupchat has some anonymity or not all participants support OMEMO.' - ) - ]; - } else { - messages = [ - __( - "Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.", - toolbar_el.model.contact.getDisplayName() - ) - ]; - } - return api.alert('error', __('Error'), messages); - } - toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') }); -} - -export function getOMEMOToolbarButton (toolbar_el, buttons) { - const model = toolbar_el.model; - const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; - let title; - if (model.get('omemo_supported')) { - const i18n_plaintext = __('Messages are being sent in plaintext'); - const i18n_encrypted = __('Messages are sent encrypted'); - title = model.get('omemo_active') ? i18n_encrypted : i18n_plaintext; - } else if (is_muc) { - title = __( - 'This groupchat needs to be members-only and non-anonymous in ' + - 'order to support OMEMO encrypted messages' - ); - } else { - title = __('OMEMO encryption is not supported'); - } - - let color; - if (model.get('omemo_supported')) { - if (model.get('omemo_active')) { - color = is_muc ? `var(--muc-color)` : `var(--chat-toolbar-btn-color)`; - } else { - color = `var(--error-color)`; - } - } else { - color = `var(--muc-toolbar-btn-disabled-color)`; - } - buttons.push(html` - - `); - return buttons; -} - - async function getBundlesAndBuildSessions (chatbox) { const no_devices_err = __('Sorry, no devices found to which we can send an OMEMO encrypted message.'); let devices; @@ -858,9 +757,3 @@ export async function createOMEMOMessageStanza (chat, data) { stanza.c('encryption', { 'xmlns': Strophe.NS.EME, namespace: Strophe.NS.OMEMO }); return { message, stanza }; } - -export const omemo = { - decryptMessage, - encryptMessage, - formatFingerprint -} diff --git a/src/plugins/omemo/errors.js b/src/plugins/omemo-views/errors.js similarity index 100% rename from src/plugins/omemo/errors.js rename to src/plugins/omemo-views/errors.js diff --git a/src/plugins/omemo/fingerprints.js b/src/plugins/omemo-views/fingerprints.js similarity index 100% rename from src/plugins/omemo/fingerprints.js rename to src/plugins/omemo-views/fingerprints.js diff --git a/src/plugins/omemo-views/index.js b/src/plugins/omemo-views/index.js new file mode 100644 index 0000000000..c3dadc403f --- /dev/null +++ b/src/plugins/omemo-views/index.js @@ -0,0 +1,44 @@ +/** + * @copyright The Converse.js contributors + * @license Mozilla Public License (MPLv2) + */ +import './fingerprints.js'; +import './profile.js'; +import 'shared/modals/user-details.js'; +import log from '@converse/headless/log'; +import { _converse, api, converse } from '@converse/headless/core'; +import { + getOMEMOToolbarButton, + onChatInitialized, +} from './utils.js'; + +converse.plugins.add('converse-omemo-views', { + enabled (_converse) { + return ( + window.libsignal && + _converse.config.get('trusted') && + !api.settings.get('clear_cache_on_logout') && + !_converse.api.settings.get('blacklisted_plugins').includes('converse-omemo-views') + ); + }, + + dependencies: ['converse-chatview', 'converse-pubsub', 'converse-profile', 'converse-omemo'], + + initialize () { + api.listen.on('chatBoxViewInitialized', onChatInitialized); + api.listen.on('chatRoomViewInitialized', onChatInitialized); + + api.listen.on('getToolbarButtons', getOMEMOToolbarButton); + + api.listen.on('userDetailsModalInitialized', contact => { + const jid = contact.get('jid'); + _converse.generateFingerprints(jid).catch(e => log.error(e)); + }); + + api.listen.on('profileModalInitialized', () => { + _converse.generateFingerprints(_converse.bare_jid).catch(e => log.error(e)); + }); + + api.listen.on('afterMessageBodyTransformed', handleEncryptedFiles); + } +}); diff --git a/src/plugins/omemo/profile.js b/src/plugins/omemo-views/profile.js similarity index 100% rename from src/plugins/omemo/profile.js rename to src/plugins/omemo-views/profile.js diff --git a/src/plugins/omemo/templates/fingerprints.js b/src/plugins/omemo-views/templates/fingerprints.js similarity index 96% rename from src/plugins/omemo/templates/fingerprints.js rename to src/plugins/omemo-views/templates/fingerprints.js index c30493dda4..3563ebdf6b 100644 --- a/src/plugins/omemo/templates/fingerprints.js +++ b/src/plugins/omemo-views/templates/fingerprints.js @@ -1,6 +1,6 @@ import { __ } from 'i18n'; import { html } from 'lit'; -import { formatFingerprint } from '../utils.js'; +import { formatFingerprint } from '@converse/headless/plugins/omemo/utils.js'; const device_fingerprint = (el, device) => { const i18n_trusted = __('Trusted'); diff --git a/src/plugins/omemo/templates/profile.js b/src/plugins/omemo-views/templates/profile.js similarity index 97% rename from src/plugins/omemo/templates/profile.js rename to src/plugins/omemo-views/templates/profile.js index 42278c13b1..e75d7a80f4 100644 --- a/src/plugins/omemo/templates/profile.js +++ b/src/plugins/omemo-views/templates/profile.js @@ -1,5 +1,5 @@ import spinner from "templates/spinner.js"; -import { formatFingerprint } from 'plugins/omemo/utils.js'; +import { formatFingerprint } from '@converse/headless/plugins/omemo/utils.js'; import { html } from "lit"; import { __ } from 'i18n'; diff --git a/src/plugins/omemo/tests/corrections.js b/src/plugins/omemo-views/tests/corrections.js similarity index 100% rename from src/plugins/omemo/tests/corrections.js rename to src/plugins/omemo-views/tests/corrections.js diff --git a/src/plugins/omemo/tests/media-sharing.js b/src/plugins/omemo-views/tests/media-sharing.js similarity index 100% rename from src/plugins/omemo/tests/media-sharing.js rename to src/plugins/omemo-views/tests/media-sharing.js diff --git a/src/plugins/omemo/tests/muc.js b/src/plugins/omemo-views/tests/muc.js similarity index 100% rename from src/plugins/omemo/tests/muc.js rename to src/plugins/omemo-views/tests/muc.js diff --git a/src/plugins/omemo/tests/omemo.js b/src/plugins/omemo-views/tests/omemo.js similarity index 100% rename from src/plugins/omemo/tests/omemo.js rename to src/plugins/omemo-views/tests/omemo.js diff --git a/src/plugins/omemo-views/utils.js b/src/plugins/omemo-views/utils.js new file mode 100644 index 0000000000..194b05bff8 --- /dev/null +++ b/src/plugins/omemo-views/utils.js @@ -0,0 +1,137 @@ +/* global libsignal */ +import log from '@converse/headless/log'; +import tpl_audio from 'templates/audio.js'; +import tpl_file from 'templates/file.js'; +import tpl_image from 'templates/image.js'; +import tpl_video from 'templates/video.js'; +import { __ } from 'i18n'; +import { _converse, converse, api } from '@converse/headless/core'; +import { html } from 'lit'; +import { isError } from '@converse/headless/utils/core.js'; +import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js'; +import { processEncryptedFiles } from '@converse/headless/plugins/omemo/utils.js' +import { until } from 'lit/directives/until.js'; + +const { u } = converse.env; + +function getTemplateForObjectURL (uri, obj_url, richtext) { + if (isError(obj_url)) { + return html`

${obj_url.message}

`; + } + + const file_url = uri.toString(); + if (isImageURL(file_url)) { + return tpl_image({ + 'src': obj_url, + 'onClick': richtext.onImgClick, + 'onLoad': richtext.onImgLoad + }); + } else if (isAudioURL(file_url)) { + return tpl_audio(obj_url); + } else if (isVideoURL(file_url)) { + return tpl_video(obj_url); + } else { + return tpl_file(obj_url, uri.filename()); + } + +} + +function addEncryptedFiles(text, offset, richtext) { + const objs = processEncryptedFiles(text); + + objs.forEach(o => { + o.obj_url.then(obj_url => getTemplateForObjectURL(o.uri, obj_url, richtext)); + + const template = html`${until(o.obj_url, '')}`; + richtext.addTemplateResult(o.start + offset, o.end + offset, template); + }); +} + +export function handleEncryptedFiles (richtext) { + if (!_converse.config.get('trusted')) { + return; + } + richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext)); +} + +export function onChatInitialized (el) { + el.listenTo(el.model, 'change:omemo_supported', () => { + if (el.model.get('omemo_supported') || !el.model.get('omemo_active')) { + // Do nothing; the model will be updated by + // onChatBoxInitialized in the headless plugin + } else { + // Manually trigger an update, setting omemo_active to + // false above will automatically trigger one. + el.querySelector('converse-chat-toolbar')?.requestUpdate(); + } + }); + el.listenTo(el.model, 'change:omemo_active', () => { + el.querySelector('converse-chat-toolbar').requestUpdate(); + }); +} + +function toggleOMEMO (ev) { + ev.stopPropagation(); + ev.preventDefault(); + const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar'); + if (!toolbar_el.model.get('omemo_supported')) { + let messages; + if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) { + messages = [ + __( + 'Cannot use end-to-end encryption in this groupchat, ' + + 'either the groupchat has some anonymity or not all participants support OMEMO.' + ) + ]; + } else { + messages = [ + __( + "Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.", + toolbar_el.model.contact.getDisplayName() + ) + ]; + } + return api.alert('error', __('Error'), messages); + } + toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') }); +} + +export function getOMEMOToolbarButton (toolbar_el, buttons) { + const model = toolbar_el.model; + const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; + let title; + if (model.get('omemo_supported')) { + const i18n_plaintext = __('Messages are being sent in plaintext'); + const i18n_encrypted = __('Messages are sent encrypted'); + title = model.get('omemo_active') ? i18n_encrypted : i18n_plaintext; + } else if (is_muc) { + title = __( + 'This groupchat needs to be members-only and non-anonymous in ' + + 'order to support OMEMO encrypted messages' + ); + } else { + title = __('OMEMO encryption is not supported'); + } + + let color; + if (model.get('omemo_supported')) { + if (model.get('omemo_active')) { + color = is_muc ? `var(--muc-color)` : `var(--chat-toolbar-btn-color)`; + } else { + color = `var(--error-color)`; + } + } else { + color = `var(--muc-toolbar-btn-disabled-color)`; + } + buttons.push(html` + + `); + return buttons; +} diff --git a/src/plugins/profile/templates/profile_modal.js b/src/plugins/profile/templates/profile_modal.js index da9f2a8234..ed10498919 100644 --- a/src/plugins/profile/templates/profile_modal.js +++ b/src/plugins/profile/templates/profile_modal.js @@ -51,7 +51,7 @@ export default (el) => { ` ); - if (_converse.pluggable.plugins['converse-omemo']?.enabled(_converse)) { + if (_converse.pluggable.plugins['converse-omemo-views']?.enabled(_converse)) { navigation_tabs.push( html`