diff --git a/src/converse.js b/src/converse.js index f4834263b..4b89f548e 100644 --- a/src/converse.js +++ b/src/converse.js @@ -36,7 +36,7 @@ import "./plugins/singleton.js"; /* END: Removable components */ import { _converse, converse } from "@converse/headless/core"; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; _converse.CustomElement = CustomElement; diff --git a/src/modals/templates/occupant.js b/src/modals/templates/occupant.js index 45e8ee96b..c9668ef89 100644 --- a/src/modals/templates/occupant.js +++ b/src/modals/templates/occupant.js @@ -1,6 +1,6 @@ import { html } from "lit-html"; import { modal_close_button, modal_header_close_button } from "./buttons.js" -import { renderAvatar } from '../../templates/directives/avatar'; +import { renderAvatar } from 'shared/directives/avatar'; export default (o) => { diff --git a/src/modals/templates/profile.js b/src/modals/templates/profile.js index c03a299cd..02d190c8e 100644 --- a/src/modals/templates/profile.js +++ b/src/modals/templates/profile.js @@ -1,4 +1,4 @@ -import "components/image-picker.js"; +import "shared/components/image-picker.js"; import spinner from "templates/spinner.js"; import { __ } from 'i18n'; import { _converse, converse } from "@converse/headless/core"; diff --git a/src/modals/templates/user-details.js b/src/modals/templates/user-details.js index 567b6826d..b93d73d1a 100644 --- a/src/modals/templates/user-details.js +++ b/src/modals/templates/user-details.js @@ -1,7 +1,7 @@ -import { __ } from '../../i18n'; -import { html } from "lit-html"; -import avatar from "../../templates/avatar.js"; -import { modal_close_button, modal_header_close_button } from "./buttons.js" +import avatar from 'shared/templates/avatar.js'; +import { __ } from 'i18n'; +import { html } from 'lit-html'; +import { modal_close_button, modal_header_close_button } from './buttons.js' const device_fingerprint = (o) => { diff --git a/src/plugins/bookmark-views/form.js b/src/plugins/bookmark-views/form.js index 68f3fceff..02b87ab51 100644 --- a/src/plugins/bookmark-views/form.js +++ b/src/plugins/bookmark-views/form.js @@ -1,5 +1,5 @@ import tpl_muc_bookmark_form from './templates/form.js'; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; import { _converse, api } from "@converse/headless/core"; diff --git a/src/plugins/chatview/templates/chat-head.js b/src/plugins/chatview/templates/chat-head.js index d7d974d60..64d182d23 100644 --- a/src/plugins/chatview/templates/chat-head.js +++ b/src/plugins/chatview/templates/chat-head.js @@ -1,6 +1,6 @@ import { _converse } from '@converse/headless/core'; import { html } from "lit-html"; -import { renderAvatar } from 'templates/directives/avatar.js'; +import { renderAvatar } from 'shared/directives/avatar.js'; import { until } from 'lit-html/directives/until.js'; diff --git a/src/plugins/controlbox/index.js b/src/plugins/controlbox/index.js index bb7b58912..ad94a53a5 100644 --- a/src/plugins/controlbox/index.js +++ b/src/plugins/controlbox/index.js @@ -3,7 +3,7 @@ * @copyright 2020, the Converse.js contributors * @license Mozilla Public License (MPLv2) */ -import "../../components/brand-heading"; +import "shared/components/brand-heading"; import "../chatview/index.js"; import './loginpanel.js'; import './navback.js'; diff --git a/src/plugins/controlbox/navback.js b/src/plugins/controlbox/navback.js index 453120fbd..5541d1968 100644 --- a/src/plugins/controlbox/navback.js +++ b/src/plugins/controlbox/navback.js @@ -1,5 +1,5 @@ import tpl_controlbox_navback from "./templates/navback.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { api } from "@converse/headless/core"; diff --git a/src/plugins/controlbox/templates/loginpanel.js b/src/plugins/controlbox/templates/loginpanel.js index 052c421d9..f421ac8f9 100644 --- a/src/plugins/controlbox/templates/loginpanel.js +++ b/src/plugins/controlbox/templates/loginpanel.js @@ -1,4 +1,4 @@ -import 'components/brand-heading.js'; +import 'shared/components/brand-heading.js'; import tpl_spinner from 'templates/spinner.js'; import { __ } from 'i18n'; import { _converse, api } from "@converse/headless/core"; diff --git a/src/plugins/controlbox/toggle.js b/src/plugins/controlbox/toggle.js index 277426a3a..b0432554f 100644 --- a/src/plugins/controlbox/toggle.js +++ b/src/plugins/controlbox/toggle.js @@ -1,5 +1,5 @@ import tpl_controlbox_toggle from "./templates/toggle.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { _converse, api } from "@converse/headless/core"; import { showControlBox } from './utils.js'; diff --git a/src/plugins/dragresize/components/dragresize.js b/src/plugins/dragresize/components/dragresize.js index c8c02800b..2335cc5b0 100644 --- a/src/plugins/dragresize/components/dragresize.js +++ b/src/plugins/dragresize/components/dragresize.js @@ -1,5 +1,5 @@ import tpl_dragresize from "../templates/dragresize.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; class ConverseDragResize extends CustomElement { diff --git a/src/plugins/minimize/components/minimized-chat.js b/src/plugins/minimize/components/minimized-chat.js index 8cb2086b4..b334b2f34 100644 --- a/src/plugins/minimize/components/minimized-chat.js +++ b/src/plugins/minimize/components/minimized-chat.js @@ -1,5 +1,5 @@ import tpl_trimmed_chat from "../templates/trimmed_chat.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { api, _converse } from "@converse/headless/core"; import { maximize } from '../utils.js'; diff --git a/src/plugins/muc-views/adhoc-commands.js b/src/plugins/muc-views/adhoc-commands.js index d20bbc7db..3ed9c1bc7 100644 --- a/src/plugins/muc-views/adhoc-commands.js +++ b/src/plugins/muc-views/adhoc-commands.js @@ -1,7 +1,7 @@ import 'shared/autocomplete/index.js'; import log from "@converse/headless/log"; import tpl_adhoc from './templates/ad-hoc.js'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { api, converse } from "@converse/headless/core"; import { fetchCommandForm } from './utils.js'; diff --git a/src/plugins/muc-views/chatarea.js b/src/plugins/muc-views/chatarea.js index 11ef09ab7..5e49d4fb0 100644 --- a/src/plugins/muc-views/chatarea.js +++ b/src/plugins/muc-views/chatarea.js @@ -1,6 +1,6 @@ import debounce from 'lodash-es/debounce'; import tpl_muc_chatarea from './templates/muc-chatarea.js'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from '@converse/headless/core'; diff --git a/src/plugins/muc-views/config-form.js b/src/plugins/muc-views/config-form.js index ac4878edb..02aecc2e8 100644 --- a/src/plugins/muc-views/config-form.js +++ b/src/plugins/muc-views/config-form.js @@ -1,6 +1,6 @@ import log from "@converse/headless/log"; import tpl_muc_config_form from "./templates/muc-config-form.js"; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; diff --git a/src/plugins/muc-views/destroyed.js b/src/plugins/muc-views/destroyed.js index 26e68d931..0bcb8baa1 100644 --- a/src/plugins/muc-views/destroyed.js +++ b/src/plugins/muc-views/destroyed.js @@ -1,5 +1,5 @@ import tpl_muc_destroyed from './templates/muc-destroyed.js'; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; import { _converse, api } from "@converse/headless/core"; diff --git a/src/plugins/muc-views/disconnected.js b/src/plugins/muc-views/disconnected.js index b51182407..87a88ce1d 100644 --- a/src/plugins/muc-views/disconnected.js +++ b/src/plugins/muc-views/disconnected.js @@ -1,5 +1,5 @@ import tpl_muc_disconnect from './templates/muc-disconnect.js'; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; import { __ } from 'i18n'; import { _converse, api } from "@converse/headless/core"; diff --git a/src/plugins/muc-views/nickname-form.js b/src/plugins/muc-views/nickname-form.js index 28f6cd746..7ca99e58e 100644 --- a/src/plugins/muc-views/nickname-form.js +++ b/src/plugins/muc-views/nickname-form.js @@ -1,5 +1,5 @@ import tpl_muc_nickname_form from './templates/muc-nickname-form.js'; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; import { _converse, api } from "@converse/headless/core"; class MUCNicknameForm extends CustomElement { diff --git a/src/plugins/muc-views/password-form.js b/src/plugins/muc-views/password-form.js index 255667653..ad21942c6 100644 --- a/src/plugins/muc-views/password-form.js +++ b/src/plugins/muc-views/password-form.js @@ -1,5 +1,5 @@ import tpl_muc_password_form from "./templates/muc-password-form.js"; -import { CustomElement } from 'components/element'; +import { CustomElement } from 'shared/components/element'; import { _converse, api } from "@converse/headless/core"; diff --git a/src/plugins/muc-views/sidebar.js b/src/plugins/muc-views/sidebar.js index 8b333a53a..508ff5dfb 100644 --- a/src/plugins/muc-views/sidebar.js +++ b/src/plugins/muc-views/sidebar.js @@ -1,6 +1,6 @@ import 'shared/autocomplete/index.js'; import tpl_muc_sidebar from "./templates/muc-sidebar.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { _converse, api, converse } from "@converse/headless/core"; const { u } = converse.env; diff --git a/src/plugins/muc-views/templates/muc-head.js b/src/plugins/muc-views/templates/muc-head.js index 998a14cb4..2f7c4c4df 100644 --- a/src/plugins/muc-views/templates/muc-head.js +++ b/src/plugins/muc-views/templates/muc-head.js @@ -1,5 +1,5 @@ -import 'components/dropdown.js'; -import 'shared/rich-text.js'; +import 'shared/components/dropdown.js'; +import 'shared/components/rich-text.js'; import { __ } from 'i18n'; import { _converse } from "@converse/headless/core"; import { html } from "lit-html"; diff --git a/src/plugins/profile/statusview.js b/src/plugins/profile/statusview.js index 674b3324f..29cb675b4 100644 --- a/src/plugins/profile/statusview.js +++ b/src/plugins/profile/statusview.js @@ -1,8 +1,8 @@ +import UserSettingsModal from 'modals/user-settings'; +import tpl_profile from './templates/profile.js'; import { ElementViewWithAvatar } from 'shared/avatar.js'; -import UserSettingsModal from "modals/user-settings"; -import tpl_profile from "./templates/profile.js"; import { __ } from 'i18n'; -import { _converse, api } from "@converse/headless/core"; +import { _converse, api } from '@converse/headless/core'; import { render } from 'lit-html'; diff --git a/src/plugins/rootview/root.js b/src/plugins/rootview/root.js index e149ea267..0b2f83e44 100644 --- a/src/plugins/rootview/root.js +++ b/src/plugins/rootview/root.js @@ -1,6 +1,6 @@ import tpl_root from "./templates/root.js"; import { api } from '@converse/headless/core'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; /** diff --git a/src/plugins/rootview/templates/root.js b/src/plugins/rootview/templates/root.js index 6c87edadc..0066bbfb5 100644 --- a/src/plugins/rootview/templates/root.js +++ b/src/plugins/rootview/templates/root.js @@ -1,4 +1,4 @@ -import 'components/font-awesome.js'; +import 'shared/components/font-awesome.js'; import { api } from '@converse/headless/core'; import { html } from 'lit-html'; diff --git a/src/plugins/rosterview/contactview.js b/src/plugins/rosterview/contactview.js index 40a351199..3faf85028 100644 --- a/src/plugins/rosterview/contactview.js +++ b/src/plugins/rosterview/contactview.js @@ -2,7 +2,7 @@ import log from "@converse/headless/log"; import tpl_pending_contact from "./templates/pending_contact.js"; import tpl_requesting_contact from "./templates/requesting_contact.js"; import tpl_roster_item from "./templates/roster_item.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; diff --git a/src/plugins/rosterview/templates/roster_item.js b/src/plugins/rosterview/templates/roster_item.js index 47e02efe2..28e283cbd 100644 --- a/src/plugins/rosterview/templates/roster_item.js +++ b/src/plugins/rosterview/templates/roster_item.js @@ -1,7 +1,7 @@ import { __ } from 'i18n'; import { api } from "@converse/headless/core"; import { html } from "lit-html"; -import { renderAvatar } from 'templates/directives/avatar'; +import { renderAvatar } from 'shared/directives/avatar'; export default (o) => { const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', o.display_name, o.jid); diff --git a/src/shared/autocomplete/component.js b/src/shared/autocomplete/component.js index a05b9656f..347392972 100644 --- a/src/shared/autocomplete/component.js +++ b/src/shared/autocomplete/component.js @@ -1,5 +1,5 @@ import AutoComplete from './autocomplete.js'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { FILTER_CONTAINS, FILTER_STARTSWITH } from './utils.js'; import { api } from '@converse/headless/core'; import { html } from 'lit-element'; diff --git a/src/shared/avatar.js b/src/shared/avatar.js index 4ec681fce..a31c07d21 100644 --- a/src/shared/avatar.js +++ b/src/shared/avatar.js @@ -1,5 +1,5 @@ -import { ElementView } from "@converse/skeletor/src/element"; -import tpl_avatar from 'templates/avatar.js'; +import tpl_avatar from 'shared/templates/avatar.js'; +import { ElementView } from '@converse/skeletor/src/element'; import { View } from '@converse/skeletor/src/view'; import { converse } from '@converse/headless/core'; diff --git a/src/shared/chat/chat-content.js b/src/shared/chat/chat-content.js index 1d97f58e1..71d3c9ded 100644 --- a/src/shared/chat/chat-content.js +++ b/src/shared/chat/chat-content.js @@ -1,6 +1,6 @@ import "./message-history"; import debounce from 'lodash/debounce'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { _converse, api } from "@converse/headless/core"; import { html } from 'lit-element'; diff --git a/src/shared/chat/emoji-picker-content.js b/src/shared/chat/emoji-picker-content.js index a1ac64c5e..6c19611e1 100644 --- a/src/shared/chat/emoji-picker-content.js +++ b/src/shared/chat/emoji-picker-content.js @@ -1,4 +1,4 @@ -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { _converse, converse, api } from "@converse/headless/core"; import { html } from "lit-element"; import { tpl_all_emojis, tpl_search_results } from "./templates/emoji-picker.js"; diff --git a/src/shared/chat/emoji-picker.js b/src/shared/chat/emoji-picker.js index c1bae744c..fd4c3d670 100644 --- a/src/shared/chat/emoji-picker.js +++ b/src/shared/chat/emoji-picker.js @@ -1,8 +1,8 @@ import "./emoji-picker-content.js"; import DOMNavigator from "shared/dom-navigator"; import debounce from 'lodash/debounce'; -import { BaseDropdown } from "components/dropdown.js"; -import { CustomElement } from 'components/element.js'; +import { BaseDropdown } from "shared/components/dropdown.js"; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; import { html } from "lit-element"; diff --git a/src/shared/chat/help-messages.js b/src/shared/chat/help-messages.js index 20f9c70ee..e448401b7 100644 --- a/src/shared/chat/help-messages.js +++ b/src/shared/chat/help-messages.js @@ -1,6 +1,6 @@ -import 'components/icons.js'; +import 'shared/components/icons.js'; import xss from 'xss/dist/xss'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { api } from '@converse/headless/core'; import { html } from 'lit-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; diff --git a/src/shared/chat/message-actions.js b/src/shared/chat/message-actions.js index fc9642bb8..dadd2a234 100644 --- a/src/shared/chat/message-actions.js +++ b/src/shared/chat/message-actions.js @@ -1,5 +1,5 @@ import log from '@converse/headless/log'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; import { html } from 'lit-element'; diff --git a/src/shared/chat/message-body.js b/src/shared/chat/message-body.js index 539ab9b9f..bf8a517f2 100644 --- a/src/shared/chat/message-body.js +++ b/src/shared/chat/message-body.js @@ -1,7 +1,7 @@ import 'shared/registry.js'; import ImageModal from 'modals/image.js'; import renderRichText from 'shared/directives/rich-text.js'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { api } from "@converse/headless/core"; diff --git a/src/shared/chat/message-history.js b/src/shared/chat/message-history.js index acc34f694..879fb191d 100644 --- a/src/shared/chat/message-history.js +++ b/src/shared/chat/message-history.js @@ -1,7 +1,7 @@ import "./message"; import dayjs from 'dayjs'; import tpl_new_day from "templates/new_day.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { _converse, api } from "@converse/headless/core"; import { html } from 'lit-element'; import { repeat } from 'lit-html/directives/repeat.js'; diff --git a/src/shared/chat/message.js b/src/shared/chat/message.js index 9f72d2f06..664c58ab8 100644 --- a/src/shared/chat/message.js +++ b/src/shared/chat/message.js @@ -1,7 +1,7 @@ -import 'shared/registry'; -import 'components/dropdown.js'; import './message-actions.js'; import './message-body.js'; +import 'shared/components/dropdown.js'; +import 'shared/registry'; import MessageVersionsModal from 'modals/message-versions.js'; import OccupantModal from 'modals/occupant.js'; import UserDetailsModal from 'modals/user-details.js'; @@ -9,12 +9,12 @@ import dayjs from 'dayjs'; import filesize from 'filesize'; import tpl_message from './templates/message.js'; import tpl_spinner from 'templates/spinner.js'; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from '@converse/headless/core'; import { getDerivedMessageProps } from './message-history'; import { html } from 'lit-element'; -import { renderAvatar } from 'templates/directives/avatar'; +import { renderAvatar } from 'shared/directives/avatar'; const { Strophe } = converse.env; const u = converse.env.utils; diff --git a/src/shared/chat/templates/message.js b/src/shared/chat/templates/message.js index 3ffb42b57..02761fc6c 100644 --- a/src/shared/chat/templates/message.js +++ b/src/shared/chat/templates/message.js @@ -1,7 +1,7 @@ import 'shared/chat/unfurl'; import { __ } from 'i18n'; import { html } from "lit-html"; -import { renderAvatar } from 'templates/directives/avatar'; +import { renderAvatar } from 'shared/directives/avatar'; export default (o) => { diff --git a/src/shared/chat/toolbar.js b/src/shared/chat/toolbar.js index 5873028a1..27c89eed0 100644 --- a/src/shared/chat/toolbar.js +++ b/src/shared/chat/toolbar.js @@ -1,5 +1,5 @@ import "./emoji-picker.js"; -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; import { html } from 'lit-element'; diff --git a/src/shared/chat/unfurl.js b/src/shared/chat/unfurl.js index 7640d0ce3..f9f310fdf 100644 --- a/src/shared/chat/unfurl.js +++ b/src/shared/chat/unfurl.js @@ -1,4 +1,4 @@ -import { CustomElement } from 'components/element.js'; +import { CustomElement } from 'shared/components/element.js'; import { _converse, api } from "@converse/headless/core"; import tpl_unfurl from './templates/unfurl.js'; diff --git a/src/components/brand-byline.js b/src/shared/components/brand-byline.js similarity index 100% rename from src/components/brand-byline.js rename to src/shared/components/brand-byline.js diff --git a/src/components/brand-heading.js b/src/shared/components/brand-heading.js similarity index 100% rename from src/components/brand-heading.js rename to src/shared/components/brand-heading.js diff --git a/src/components/brand-logo.js b/src/shared/components/brand-logo.js similarity index 100% rename from src/components/brand-logo.js rename to src/shared/components/brand-logo.js diff --git a/src/components/dropdown.js b/src/shared/components/dropdown.js similarity index 98% rename from src/components/dropdown.js rename to src/shared/components/dropdown.js index ccd0a4a16..823340568 100644 --- a/src/components/dropdown.js +++ b/src/shared/components/dropdown.js @@ -1,4 +1,4 @@ -import DOMNavigator from "../shared/dom-navigator.js"; +import DOMNavigator from "shared/dom-navigator.js"; import { CustomElement } from './element.js'; import { converse, api } from "@converse/headless/core"; import { html } from 'lit-element'; diff --git a/src/components/element.js b/src/shared/components/element.js similarity index 100% rename from src/components/element.js rename to src/shared/components/element.js diff --git a/src/components/font-awesome.js b/src/shared/components/font-awesome.js similarity index 100% rename from src/components/font-awesome.js rename to src/shared/components/font-awesome.js diff --git a/src/components/icons.js b/src/shared/components/icons.js similarity index 100% rename from src/components/icons.js rename to src/shared/components/icons.js diff --git a/src/components/image-picker.js b/src/shared/components/image-picker.js similarity index 93% rename from src/components/image-picker.js rename to src/shared/components/image-picker.js index 0a7796760..90ee29297 100644 --- a/src/components/image-picker.js +++ b/src/shared/components/image-picker.js @@ -1,7 +1,7 @@ import { CustomElement } from './element.js'; -import { __ } from '../i18n'; +import { __ } from 'i18n'; import { html } from 'lit-element'; -import { renderAvatar } from "../templates/directives/avatar.js"; +import { renderAvatar } from "shared/directives/avatar.js"; import { api } from "@converse/headless/core"; const i18n_alt_avatar = __('Your avatar image'); diff --git a/src/shared/components/rich-text.js b/src/shared/components/rich-text.js new file mode 100644 index 000000000..fee3b1b9d --- /dev/null +++ b/src/shared/components/rich-text.js @@ -0,0 +1,43 @@ +import renderRichText from 'shared/directives/rich-text.js'; +import { CustomElement } from 'shared/components/element.js'; +import { api } from "@converse/headless/core"; + +export default class RichText extends CustomElement { + + static get properties () { + return { + mentions: { type: Array }, + nick: { type: String }, + offset: { type: Number }, + onImgClick: { type: Function }, + onImgLoad: { type: Function }, + render_styling: { type: Boolean }, + show_images: { type: Boolean }, + show_me_message: { type: Boolean }, + text: { type: String }, + } + } + + constructor () { + super(); + this.offset = 0; + this.mentions = []; + this.render_styling = false; + this.show_images = false; + this.show_me_message = false; + } + + render () { + const options = { + nick: this.nick, + onImgClick: this.onImgClick, + onImgLoad: this.onImgLoad, + render_styling: this.render_styling, + show_images: this.show_images, + show_me_message: this.show_me_message, + } + return renderRichText(this.text, this.offset, this.mentions, options); + } +} + +api.elements.define('converse-rich-text', RichText); diff --git a/src/templates/directives/avatar.js b/src/shared/directives/avatar.js similarity index 86% rename from src/templates/directives/avatar.js rename to src/shared/directives/avatar.js index 73e60461a..7bb855b18 100644 --- a/src/templates/directives/avatar.js +++ b/src/shared/directives/avatar.js @@ -1,4 +1,4 @@ -import tpl_avatar from '../avatar.js'; +import tpl_avatar from 'shared/templates/avatar.js'; import { directive } from "lit-html"; diff --git a/src/templates/directives/image.js b/src/shared/directives/image.js similarity index 100% rename from src/templates/directives/image.js rename to src/shared/directives/image.js diff --git a/src/templates/directives/retraction.js b/src/shared/directives/retraction.js similarity index 100% rename from src/templates/directives/retraction.js rename to src/shared/directives/retraction.js diff --git a/src/shared/directives/rich-text.js b/src/shared/directives/rich-text.js index 5f08f8fec..0e330b2c2 100644 --- a/src/shared/directives/rich-text.js +++ b/src/shared/directives/rich-text.js @@ -1,4 +1,4 @@ -import { RichText } from 'shared/message/text.js'; +import { RichText } from 'shared/rich-text.js'; import { directive, html } from "lit-html"; import { until } from 'lit-html/directives/until.js'; diff --git a/src/templates/directives/styling.js b/src/shared/directives/styling.js similarity index 90% rename from src/templates/directives/styling.js rename to src/shared/directives/styling.js index d8d3f0ac8..64b68e198 100644 --- a/src/templates/directives/styling.js +++ b/src/shared/directives/styling.js @@ -1,4 +1,4 @@ -import { RichText } from '../../shared/message/text.js'; +import { RichText } from 'shared/rich-text.js'; import { directive, html } from 'lit-html'; import { until } from 'lit-html/directives/until.js'; diff --git a/src/shared/message/text.js b/src/shared/message/text.js deleted file mode 100644 index 5d6a88fa2..000000000 --- a/src/shared/message/text.js +++ /dev/null @@ -1,323 +0,0 @@ -import URI from 'urijs'; -import log from '@converse/headless/log'; -import { _converse, api, converse } from '@converse/headless/core'; -import { containsDirectives, getDirectiveAndLength, getDirectiveTemplate, isQuoteDirective } from './styling.js'; -import { convertASCII2Emoji, getCodePointReferences, getEmojiMarkup, getShortnameReferences } from '@converse/headless/plugins/emoji/index.js'; -import { html } from 'lit-html'; - -const u = converse.env.utils; - -const isString = (s) => typeof s === 'string'; - -// We don't render more than two line-breaks, replace extra line-breaks with -// the zero-width whitespace character -const collapseLineBreaks = text => text.replace(/\n\n+/g, m => `\n${"\u200B".repeat(m.length-2)}\n`); - -const tpl_mention_with_nick = (o) => html`${o.mention}`; -const tpl_mention = (o) => html`${o.mention}`; - - -/** - * @class RichText - * A String subclass that is used to render rich text (i.e. text that contains - * hyperlinks, images, mentions, styling etc.). - * - * The "rich" parts of the text is represented by lit-html TemplateResult - * objects which are added via the {@link RichText.addTemplateResult} - * method and saved as metadata. - * - * By default Converse adds TemplateResults to support emojis, hyperlinks, - * images, map URIs and mentions. - * - * 3rd party plugins can listen for the `beforeMessageBodyTransformed` - * and/or `afterMessageBodyTransformed` events and then call - * `addTemplateResult` on the RichText instance in order to add their own - * rich features. - */ -export class RichText extends String { - - /** - * Create a new {@link RichText} instance. - * @param { String } text - The text to be annotated - * @param { Integer } offset - The offset of this particular piece of text - * from the start of the original message text. This is necessary because - * RichText instances can be nested when templates call directives - * which create new RichText instances (as happens with XEP-393 styling directives). - * @param { Array } mentions - An array of mention references - * @param { Object } options - * @param { String } options.nick - The current user's nickname (only relevant if the message is in a XEP-0045 MUC) - * @param { Boolean } options.render_styling - Whether XEP-0393 message styling should be applied to the message - * @param { Boolean } options.show_images - Whether image URLs should be rendered as tags. - * @param { Boolean } options.show_me_message - Whether /me messages should be rendered differently - * @param { Function } options.onImgClick - Callback for when an inline rendered image has been clicked - * @param { Function } options.onImgLoad - Callback for when an inline rendered image has been loaded - */ - constructor (text, offset=0, mentions=[], options={}) { - super(text); - this.mentions = mentions; - this.nick = options?.nick; - this.offset = offset; - this.onImgClick = options?.onImgClick; - this.onImgLoad = options?.onImgLoad; - this.options = options; - this.payload = []; - this.references = []; - this.render_styling = options?.render_styling; - this.show_images = options?.show_images; - } - - /** - * Look for `http` URIs and return templates that render them as URL links - * @param { String } text - * @param { Integer } offset - The index of the passed in text relative to - * the start of the message body text. - */ - addHyperlinks (text, offset) { - const objs = []; - try { - const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi }; - URI.withinString(text, (url, start, end) => { - objs.push({url, start, end}) - return url; - } , parse_options); - } catch (error) { - log.debug(error); - return; - } - objs.forEach(url_obj => { - const url_text = text.slice(url_obj.start, url_obj.end); - const filtered_url = u.filterQueryParamsFromURL(url_text); - this.addTemplateResult( - url_obj.start+offset, - url_obj.end+offset, - this.show_images && u.isImageURL(url_text) && u.isImageDomainAllowed(url_text) ? - u.convertToImageTag(filtered_url, this.onImgLoad, this.onImgClick) : - u.convertUrlToHyperlink(filtered_url), - ); - }); - } - - /** - * Look for `geo` URIs and return templates that render them as URL links - * @param { String } text - * @param { Integer } offset - The index of the passed in text relative to - * the start of the message body text. - */ - addMapURLs (text, offset) { - const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g; - const matches = text.matchAll(regex); - for (const m of matches) { - this.addTemplateResult( - m.index+offset, - m.index+m[0].length+offset, - u.convertUrlToHyperlink(m[0].replace(regex, _converse.geouri_replacement)) - ); - } - } - - /** - * Look for emojis (shortnames or unicode) and add templates for rendering them. - * @param { String } text - * @param { Integer } offset - The index of the passed in text relative to - * the start of the message body text. - */ - addEmojis (text, offset) { - const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())]; - references.forEach(e => { - this.addTemplateResult( - e.begin+offset, - e.end+offset, - getEmojiMarkup(e, {'add_title_wrapper': true}) - ); - }); - } - - /** - * Look for mentions included as XEP-0372 references and add templates for - * rendering them. - * @param { String } text - * @param { Integer } local_offset - The index of the passed in text relative to - * the start of this RichText instance (which is not necessarily the same as the - * offset from the start of the original message stanza's body text). - */ - addMentions (text, local_offset) { - const full_offset = local_offset+this.offset; - this.mentions?.forEach(ref => { - const begin = Number(ref.begin)-full_offset; - if (begin < 0 || begin >= full_offset+text.length) { - return; - } - const end = Number(ref.end)-full_offset; - const mention = text.slice(begin, end); - if (mention === this.nick) { - this.addTemplateResult( - begin+local_offset, - end+local_offset, - tpl_mention_with_nick({mention}) - ); - } else { - this.addTemplateResult( - begin+local_offset, - end+local_offset, - tpl_mention({mention}) - ); - } - }); - } - - /** - * Look for XEP-0393 styling directives and add templates for rendering - * them. - */ - addStyling () { - let i = 0; - const references = []; - if (containsDirectives(this)) { - while (i < this.length) { - const { d, length } = getDirectiveAndLength(this, i); - if (d && length) { - const is_quote = isQuoteDirective(d); - const end = i+length; - const slice_end = is_quote ? end : end-d.length; - let slice_begin = d === '```' ? i+d.length+1 : i+d.length; - if (is_quote && this[slice_begin] === ' ') { - // Trim leading space inside codeblock - slice_begin += 1; - } - const offset = slice_begin; - const text = this.slice(slice_begin, slice_end); - references.push({ - 'begin': i, - 'template': getDirectiveTemplate(d, text, offset, this.mentions, this.options), - end, - }); - i = end; - } - i++; - } - } - references.forEach(ref => this.addTemplateResult(ref.begin, ref.end, ref.template)); - } - - trimMeMessage () { - if (this.offset === 0) { - // Subtract `/me ` from 3rd person messages - if (this.isMeCommand()) { - this.payload[0] = this.payload[0].substring(4); - } - } - } - - - /** - * Look for plaintext (i.e. non-templated) sections of this RichText - * instance and add references via the passed in function. - * @param { Function } func - */ - addAnnotations (func) { - const payload = this.marshall(); - let idx = 0; // The text index of the element in the payload - for (const text of payload) { - if (!text) { - continue - } else if (isString(text)) { - func.call(this, text, idx); - idx += text.length; - } else { - idx = text.end; - } - } - } - - - /** - * Parse the text and add template references for rendering the "rich" parts. - * - * @param { RichText } text - * @param { Boolean } show_images - Should URLs of images be rendered as `` tags? - * @param { Function } onImgLoad - * @param { Function } onImgClick - **/ - async addTemplates() { - /** - * Synchronous event which provides a hook for transforming a chat message's body text - * before the default transformations have been applied. - * @event _converse#beforeMessageBodyTransformed - * @param { RichText } text - A {@link RichText } instance. You - * can call {@link RichText#addTemplateResult } on it in order to - * add TemplateResult objects meant to render rich parts of the message. - * @example _converse.api.listen.on('beforeMessageBodyTransformed', (view, text) => { ... }); - */ - await api.trigger('beforeMessageBodyTransformed', this, {'Synchronous': true}); - - this.render_styling && this.addStyling(); - this.addAnnotations(this.addMentions); - this.addAnnotations(this.addHyperlinks); - this.addAnnotations(this.addMapURLs); - - await api.emojis.initialize(); - this.addAnnotations(this.addEmojis); - - /** - * Synchronous event which provides a hook for transforming a chat message's body text - * after the default transformations have been applied. - * @event _converse#afterMessageBodyTransformed - * @param { RichText } text - A {@link RichText } instance. You - * can call {@link RichText#addTemplateResult} on it in order to - * add TemplateResult objects meant to render rich parts of the message. - * @example _converse.api.listen.on('afterMessageBodyTransformed', (view, text) => { ... }); - */ - await api.trigger('afterMessageBodyTransformed', this, {'Synchronous': true}); - - this.payload = this.marshall(); - this.options.show_me_message && this.trimMeMessage(); - this.payload = this.payload.map(item => isString(item) ? item : item.template); - } - - /** - * The "rich" markup parts of a chat message are represented by lit-html - * TemplateResult objects. - * - * This method can be used to add new template results to this message's - * text. - * - * @method RichText.addTemplateResult - * @param { Number } begin - The starting index of the plain message text - * which is being replaced with markup. - * @param { Number } end - The ending index of the plain message text - * which is being replaced with markup. - * @param { Object } template - The lit-html TemplateResult instance - */ - addTemplateResult (begin, end, template) { - this.references.push({begin, end, template}); - } - - isMeCommand () { - const text = this.toString(); - if (!text) { - return false; - } - return text.startsWith('/me '); - } - - /** - * Take the annotations and return an array of text and TemplateResult - * instances to be rendered to the DOM. - * @method RichText#marshall - */ - marshall () { - let list = [this.toString()]; - this.references - .sort((a, b) => b.begin - a.begin) - .forEach(ref => { - const text = list.shift(); - list = [ - text.slice(0, ref.begin), - ref, - text.slice(ref.end), - ...list - ]; - }); - return list.reduce((acc, i) => isString(i) ? [...acc, convertASCII2Emoji(collapseLineBreaks(i))] : [...acc, i], []); - } -} diff --git a/src/shared/rich-text.js b/src/shared/rich-text.js index 3992a6903..5d6a88fa2 100644 --- a/src/shared/rich-text.js +++ b/src/shared/rich-text.js @@ -1,43 +1,323 @@ -import renderRichText from 'shared/directives/rich-text.js'; -import { CustomElement } from 'components/element.js'; -import { api } from "@converse/headless/core"; +import URI from 'urijs'; +import log from '@converse/headless/log'; +import { _converse, api, converse } from '@converse/headless/core'; +import { containsDirectives, getDirectiveAndLength, getDirectiveTemplate, isQuoteDirective } from './styling.js'; +import { convertASCII2Emoji, getCodePointReferences, getEmojiMarkup, getShortnameReferences } from '@converse/headless/plugins/emoji/index.js'; +import { html } from 'lit-html'; -export default class RichText extends CustomElement { +const u = converse.env.utils; - static get properties () { - return { - mentions: { type: Array }, - nick: { type: String }, - offset: { type: Number }, - onImgClick: { type: Function }, - onImgLoad: { type: Function }, - render_styling: { type: Boolean }, - show_images: { type: Boolean }, - show_me_message: { type: Boolean }, - text: { type: String }, +const isString = (s) => typeof s === 'string'; + +// We don't render more than two line-breaks, replace extra line-breaks with +// the zero-width whitespace character +const collapseLineBreaks = text => text.replace(/\n\n+/g, m => `\n${"\u200B".repeat(m.length-2)}\n`); + +const tpl_mention_with_nick = (o) => html`${o.mention}`; +const tpl_mention = (o) => html`${o.mention}`; + + +/** + * @class RichText + * A String subclass that is used to render rich text (i.e. text that contains + * hyperlinks, images, mentions, styling etc.). + * + * The "rich" parts of the text is represented by lit-html TemplateResult + * objects which are added via the {@link RichText.addTemplateResult} + * method and saved as metadata. + * + * By default Converse adds TemplateResults to support emojis, hyperlinks, + * images, map URIs and mentions. + * + * 3rd party plugins can listen for the `beforeMessageBodyTransformed` + * and/or `afterMessageBodyTransformed` events and then call + * `addTemplateResult` on the RichText instance in order to add their own + * rich features. + */ +export class RichText extends String { + + /** + * Create a new {@link RichText} instance. + * @param { String } text - The text to be annotated + * @param { Integer } offset - The offset of this particular piece of text + * from the start of the original message text. This is necessary because + * RichText instances can be nested when templates call directives + * which create new RichText instances (as happens with XEP-393 styling directives). + * @param { Array } mentions - An array of mention references + * @param { Object } options + * @param { String } options.nick - The current user's nickname (only relevant if the message is in a XEP-0045 MUC) + * @param { Boolean } options.render_styling - Whether XEP-0393 message styling should be applied to the message + * @param { Boolean } options.show_images - Whether image URLs should be rendered as tags. + * @param { Boolean } options.show_me_message - Whether /me messages should be rendered differently + * @param { Function } options.onImgClick - Callback for when an inline rendered image has been clicked + * @param { Function } options.onImgLoad - Callback for when an inline rendered image has been loaded + */ + constructor (text, offset=0, mentions=[], options={}) { + super(text); + this.mentions = mentions; + this.nick = options?.nick; + this.offset = offset; + this.onImgClick = options?.onImgClick; + this.onImgLoad = options?.onImgLoad; + this.options = options; + this.payload = []; + this.references = []; + this.render_styling = options?.render_styling; + this.show_images = options?.show_images; + } + + /** + * Look for `http` URIs and return templates that render them as URL links + * @param { String } text + * @param { Integer } offset - The index of the passed in text relative to + * the start of the message body text. + */ + addHyperlinks (text, offset) { + const objs = []; + try { + const parse_options = { 'start': /\b(?:([a-z][a-z0-9.+-]*:\/\/)|xmpp:|mailto:|www\.)/gi }; + URI.withinString(text, (url, start, end) => { + objs.push({url, start, end}) + return url; + } , parse_options); + } catch (error) { + log.debug(error); + return; + } + objs.forEach(url_obj => { + const url_text = text.slice(url_obj.start, url_obj.end); + const filtered_url = u.filterQueryParamsFromURL(url_text); + this.addTemplateResult( + url_obj.start+offset, + url_obj.end+offset, + this.show_images && u.isImageURL(url_text) && u.isImageDomainAllowed(url_text) ? + u.convertToImageTag(filtered_url, this.onImgLoad, this.onImgClick) : + u.convertUrlToHyperlink(filtered_url), + ); + }); + } + + /** + * Look for `geo` URIs and return templates that render them as URL links + * @param { String } text + * @param { Integer } offset - The index of the passed in text relative to + * the start of the message body text. + */ + addMapURLs (text, offset) { + const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g; + const matches = text.matchAll(regex); + for (const m of matches) { + this.addTemplateResult( + m.index+offset, + m.index+m[0].length+offset, + u.convertUrlToHyperlink(m[0].replace(regex, _converse.geouri_replacement)) + ); } } - constructor () { - super(); - this.offset = 0; - this.mentions = []; - this.render_styling = false; - this.show_images = false; - this.show_me_message = false; + /** + * Look for emojis (shortnames or unicode) and add templates for rendering them. + * @param { String } text + * @param { Integer } offset - The index of the passed in text relative to + * the start of the message body text. + */ + addEmojis (text, offset) { + const references = [...getShortnameReferences(text.toString()), ...getCodePointReferences(text.toString())]; + references.forEach(e => { + this.addTemplateResult( + e.begin+offset, + e.end+offset, + getEmojiMarkup(e, {'add_title_wrapper': true}) + ); + }); } - render () { - const options = { - nick: this.nick, - onImgClick: this.onImgClick, - onImgLoad: this.onImgLoad, - render_styling: this.render_styling, - show_images: this.show_images, - show_me_message: this.show_me_message, + /** + * Look for mentions included as XEP-0372 references and add templates for + * rendering them. + * @param { String } text + * @param { Integer } local_offset - The index of the passed in text relative to + * the start of this RichText instance (which is not necessarily the same as the + * offset from the start of the original message stanza's body text). + */ + addMentions (text, local_offset) { + const full_offset = local_offset+this.offset; + this.mentions?.forEach(ref => { + const begin = Number(ref.begin)-full_offset; + if (begin < 0 || begin >= full_offset+text.length) { + return; + } + const end = Number(ref.end)-full_offset; + const mention = text.slice(begin, end); + if (mention === this.nick) { + this.addTemplateResult( + begin+local_offset, + end+local_offset, + tpl_mention_with_nick({mention}) + ); + } else { + this.addTemplateResult( + begin+local_offset, + end+local_offset, + tpl_mention({mention}) + ); + } + }); + } + + /** + * Look for XEP-0393 styling directives and add templates for rendering + * them. + */ + addStyling () { + let i = 0; + const references = []; + if (containsDirectives(this)) { + while (i < this.length) { + const { d, length } = getDirectiveAndLength(this, i); + if (d && length) { + const is_quote = isQuoteDirective(d); + const end = i+length; + const slice_end = is_quote ? end : end-d.length; + let slice_begin = d === '```' ? i+d.length+1 : i+d.length; + if (is_quote && this[slice_begin] === ' ') { + // Trim leading space inside codeblock + slice_begin += 1; + } + const offset = slice_begin; + const text = this.slice(slice_begin, slice_end); + references.push({ + 'begin': i, + 'template': getDirectiveTemplate(d, text, offset, this.mentions, this.options), + end, + }); + i = end; + } + i++; + } } - return renderRichText(this.text, this.offset, this.mentions, options); + references.forEach(ref => this.addTemplateResult(ref.begin, ref.end, ref.template)); + } + + trimMeMessage () { + if (this.offset === 0) { + // Subtract `/me ` from 3rd person messages + if (this.isMeCommand()) { + this.payload[0] = this.payload[0].substring(4); + } + } + } + + + /** + * Look for plaintext (i.e. non-templated) sections of this RichText + * instance and add references via the passed in function. + * @param { Function } func + */ + addAnnotations (func) { + const payload = this.marshall(); + let idx = 0; // The text index of the element in the payload + for (const text of payload) { + if (!text) { + continue + } else if (isString(text)) { + func.call(this, text, idx); + idx += text.length; + } else { + idx = text.end; + } + } + } + + + /** + * Parse the text and add template references for rendering the "rich" parts. + * + * @param { RichText } text + * @param { Boolean } show_images - Should URLs of images be rendered as `` tags? + * @param { Function } onImgLoad + * @param { Function } onImgClick + **/ + async addTemplates() { + /** + * Synchronous event which provides a hook for transforming a chat message's body text + * before the default transformations have been applied. + * @event _converse#beforeMessageBodyTransformed + * @param { RichText } text - A {@link RichText } instance. You + * can call {@link RichText#addTemplateResult } on it in order to + * add TemplateResult objects meant to render rich parts of the message. + * @example _converse.api.listen.on('beforeMessageBodyTransformed', (view, text) => { ... }); + */ + await api.trigger('beforeMessageBodyTransformed', this, {'Synchronous': true}); + + this.render_styling && this.addStyling(); + this.addAnnotations(this.addMentions); + this.addAnnotations(this.addHyperlinks); + this.addAnnotations(this.addMapURLs); + + await api.emojis.initialize(); + this.addAnnotations(this.addEmojis); + + /** + * Synchronous event which provides a hook for transforming a chat message's body text + * after the default transformations have been applied. + * @event _converse#afterMessageBodyTransformed + * @param { RichText } text - A {@link RichText } instance. You + * can call {@link RichText#addTemplateResult} on it in order to + * add TemplateResult objects meant to render rich parts of the message. + * @example _converse.api.listen.on('afterMessageBodyTransformed', (view, text) => { ... }); + */ + await api.trigger('afterMessageBodyTransformed', this, {'Synchronous': true}); + + this.payload = this.marshall(); + this.options.show_me_message && this.trimMeMessage(); + this.payload = this.payload.map(item => isString(item) ? item : item.template); + } + + /** + * The "rich" markup parts of a chat message are represented by lit-html + * TemplateResult objects. + * + * This method can be used to add new template results to this message's + * text. + * + * @method RichText.addTemplateResult + * @param { Number } begin - The starting index of the plain message text + * which is being replaced with markup. + * @param { Number } end - The ending index of the plain message text + * which is being replaced with markup. + * @param { Object } template - The lit-html TemplateResult instance + */ + addTemplateResult (begin, end, template) { + this.references.push({begin, end, template}); + } + + isMeCommand () { + const text = this.toString(); + if (!text) { + return false; + } + return text.startsWith('/me '); + } + + /** + * Take the annotations and return an array of text and TemplateResult + * instances to be rendered to the DOM. + * @method RichText#marshall + */ + marshall () { + let list = [this.toString()]; + this.references + .sort((a, b) => b.begin - a.begin) + .forEach(ref => { + const text = list.shift(); + list = [ + text.slice(0, ref.begin), + ref, + text.slice(ref.end), + ...list + ]; + }); + return list.reduce((acc, i) => isString(i) ? [...acc, convertASCII2Emoji(collapseLineBreaks(i))] : [...acc, i], []); } } - -api.elements.define('converse-rich-text', RichText); diff --git a/src/shared/message/styling.js b/src/shared/styling.js similarity index 98% rename from src/shared/message/styling.js rename to src/shared/styling.js index ac05ad766..175738334 100644 --- a/src/shared/message/styling.js +++ b/src/shared/styling.js @@ -5,7 +5,7 @@ * @todo Other parsing helpers can be made more abstract and placed here. */ import { html } from 'lit-element'; -import { renderStylingDirectiveBody } from '../../templates/directives/styling.js'; +import { renderStylingDirectiveBody } from 'shared/directives/styling.js'; const styling_directives = ['*', '_', '~', '`', '```', '>']; diff --git a/src/templates/avatar.js b/src/shared/templates/avatar.js similarity index 100% rename from src/templates/avatar.js rename to src/shared/templates/avatar.js diff --git a/src/templates/image.js b/src/templates/image.js index 95e2c5d6e..0d8c573f4 100644 --- a/src/templates/image.js +++ b/src/templates/image.js @@ -1,4 +1,4 @@ import { html } from "lit-html"; -import { renderImage } from "./directives/image.js"; +import { renderImage } from "shared/directives/image.js"; export default (o) => html`${renderImage(o.url, o.url, o.onLoad, o.onClick)}`;