${o.text}
`;\n","import { html } from \"lit\";\n\nexport default (o) => html`\n \n \n`;\n","import { html } from \"lit\";\n\nexport default (o) => html`\n `;\n","import { html } from \"lit\";\n\nexport default (o) => html`\n \n`;\n","/**\n * @module dom-navigator\n * @description A class for navigating the DOM with the keyboard\n * This module started as a fork of Rubens Mariuzzo's dom-navigator.\n * @copyright Rubens Mariuzzo, JC Brand\n */\nimport u from '../utils/html';\nimport { converse } from \"@converse/headless/core\";\n\nconst { keycodes } = converse;\n\n\n/**\n * Indicates if a given element is fully visible in the viewport.\n * @param { Element } el The element to check.\n * @return { Boolean } True if the given element is fully visible in the viewport, otherwise false.\n */\nfunction inViewport(el) {\n const rect = el.getBoundingClientRect();\n return (\n rect.top >= 0 &&\n rect.left >= 0 &&\n rect.bottom <= window.innerHeight &&\n rect.right <= window.innerWidth\n );\n}\n\n/**\n * Return the absolute offset top of an element.\n * @param el { Element } The element.\n * @return { Number } The offset top.\n */\nfunction absoluteOffsetTop(el) {\n let offsetTop = 0;\n do {\n if (!isNaN(el.offsetTop)) {\n offsetTop += el.offsetTop;\n }\n } while ((el = el.offsetParent));\n return offsetTop;\n}\n\n/**\n * Return the absolute offset left of an element.\n * @param el { Element } The element.\n * @return { Number } The offset left.\n */\nfunction absoluteOffsetLeft(el) {\n let offsetLeft = 0;\n do {\n if (!isNaN(el.offsetLeft)) {\n offsetLeft += el.offsetLeft;\n }\n } while ((el = el.offsetParent));\n return offsetLeft;\n}\n\n\n/**\n * Adds the ability to navigate the DOM with the arrow keys\n * @class DOMNavigator\n */\nclass DOMNavigator {\n /**\n * Directions.\n * @returns {{left: string, up: string, right: string, down: string}}\n * @constructor\n */\n static get DIRECTION () {\n return {\n down: 'down',\n end: 'end',\n home: 'home',\n left: 'left',\n right: 'right',\n up: 'up'\n };\n }\n\n /**\n * The default options for the DOM navigator.\n * @returns {{\n * down: number,\n * getSelector: null,\n * jump_to_picked: null,\n * jump_to_picked_direction: null,\n * jump_to_picked_selector: string,\n * left: number,\n * onSelected: null,\n * right: number,\n * selected: string,\n * up: number\n * }}\n */\n static get DEFAULTS () {\n return {\n home: [`${keycodes.SHIFT}+${keycodes.UP_ARROW}`],\n end: [`${keycodes.SHIFT}+${keycodes.DOWN_ARROW}`],\n up: [keycodes.UP_ARROW],\n down: [keycodes.DOWN_ARROW],\n left: [\n keycodes.LEFT_ARROW,\n `${keycodes.SHIFT}+${keycodes.TAB}`\n ],\n right: [keycodes.RIGHT_ARROW, keycodes.TAB],\n getSelector: null,\n jump_to_picked: null,\n jump_to_picked_direction: null,\n jump_to_picked_selector: 'picked',\n onSelected: null,\n selected: 'selected',\n selector: 'li',\n };\n }\n\n static getClosestElement (els, getDistance) {\n const next = els.reduce((prev, curr) => {\n const current_distance = getDistance(curr);\n if (current_distance < prev.distance) {\n return {\n distance: current_distance,\n element: curr\n };\n }\n return prev;\n }, {\n distance: Infinity\n });\n return next.element;\n }\n\n /**\n * Create a new DOM Navigator.\n * @param { Element } container The container of the element to navigate.\n * @param { Object } options The options to configure the DOM navigator.\n * @param { Function } options.getSelector\n * @param { Number } [options.down] - The keycode for navigating down\n * @param { Number } [options.left] - The keycode for navigating left\n * @param { Number } [options.right] - The keycode for navigating right\n * @param { Number } [options.up] - The keycode for navigating up\n * @param { String } [options.selected] - The class that should be added to the currently selected DOM element.\n * @param { String } [options.jump_to_picked] - A selector, which if\n * matched by the next element being navigated to, based on the direction\n * given by `jump_to_picked_direction`, will cause navigation\n * to jump to the element that matches the `jump_to_picked_selector`.\n * For example, this is useful when navigating to tabs. You want to\n * immediately navigate to the currently active tab instead of just\n * navigating to the first tab.\n * @param { String } [options.jump_to_picked_selector=picked] - The selector\n * indicating the currently picked element to jump to.\n * @param { String } [options.jump_to_picked_direction] - The direction for\n * which jumping to the picked element should be enabled.\n * @param { Function } [options.onSelected] - The callback function which\n * should be called when en element gets selected.\n * @constructor\n */\n constructor (container, options) {\n this.doc = window.document;\n this.container = container;\n this.scroll_container = options.scroll_container || container;\n this.options = Object.assign({}, DOMNavigator.DEFAULTS, options);\n this.init();\n }\n\n /**\n * Initialize the navigator.\n */\n init () {\n this.selected = null;\n this.keydownHandler = null;\n this.elements = {};\n // Create hotkeys map.\n this.keys = {};\n this.options.down.forEach(key => (this.keys[key] = DOMNavigator.DIRECTION.down));\n this.options.end.forEach(key => (this.keys[key] = DOMNavigator.DIRECTION.end));\n this.options.home.forEach(key => (this.keys[key] = DOMNavigator.DIRECTION.home));\n this.options.left.forEach(key => (this.keys[key] = DOMNavigator.DIRECTION.left));\n this.options.right.forEach(key => (this.keys[key] = DOMNavigator.DIRECTION.right));\n this.options.up.forEach(key => (this.keys[key] = DOMNavigator.DIRECTION.up));\n }\n\n /**\n * Enable this navigator.\n */\n enable () {\n this.getElements();\n this.keydownHandler = event => this.handleKeydown(event);\n this.doc.addEventListener('keydown', this.keydownHandler);\n this.enabled = true;\n }\n\n /**\n * Disable this navigator.\n */\n disable () {\n if (this.keydownHandler) {\n this.doc.removeEventListener('keydown', this.keydownHandler);\n }\n this.unselect();\n this.elements = {};\n this.enabled = false;\n }\n\n /**\n * Destroy this navigator removing any event registered and any other data.\n */\n destroy () {\n this.disable();\n if (this.container.domNavigator) {\n delete this.container.domNavigator;\n }\n }\n\n /**\n * @param {'down'|'right'|'left'|'up'} direction\n * @returns { HTMLElement }\n */\n getNextElement (direction) {\n let el;\n if (direction === DOMNavigator.DIRECTION.home) {\n el = this.getElements(direction)[0];\n } else if (direction === DOMNavigator.DIRECTION.end) {\n el = Array.from(this.getElements(direction)).pop();\n } else if (this.selected) {\n if (direction === DOMNavigator.DIRECTION.right) {\n const els = this.getElements(direction);\n el = els.slice(els.indexOf(this.selected))[1];\n } else if (direction == DOMNavigator.DIRECTION.left) {\n const els = this.getElements(direction);\n el = els.slice(0, els.indexOf(this.selected)).pop() || this.selected;\n } else if (direction == DOMNavigator.DIRECTION.down) {\n const left = this.selected.offsetLeft;\n const top = this.selected.offsetTop + this.selected.offsetHeight;\n const els = this.elementsAfter(0, top);\n const getDistance = el => Math.abs(el.offsetLeft - left) + Math.abs(el.offsetTop - top);\n el = DOMNavigator.getClosestElement(els, getDistance);\n } else if (direction == DOMNavigator.DIRECTION.up) {\n const left = this.selected.offsetLeft;\n const top = this.selected.offsetTop - 1;\n const els = this.elementsBefore(Infinity, top);\n const getDistance = el => Math.abs(left - el.offsetLeft) + Math.abs(top - el.offsetTop);\n el = DOMNavigator.getClosestElement(els, getDistance);\n } else {\n throw new Error(\"getNextElement: invalid direction value\");\n }\n } else {\n if (direction === DOMNavigator.DIRECTION.right || direction === DOMNavigator.DIRECTION.down) {\n // If nothing is selected, we pretend that the first element is\n // selected, so we return the next.\n el = this.getElements(direction)[1];\n } else {\n el = this.getElements(direction)[0]\n }\n }\n\n if (this.options.jump_to_picked && el && el.matches(this.options.jump_to_picked) &&\n direction === this.options.jump_to_picked_direction\n ) {\n el = this.container.querySelector(this.options.jump_to_picked_selector) || el;\n }\n return el;\n }\n\n /**\n * Select the given element.\n * @param { Element } el The DOM element to select.\n * @param { string } [direction] The direction.\n */\n select (el, direction) {\n if (!el || el === this.selected) {\n return;\n }\n this.unselect();\n direction && this.scrollTo(el, direction);\n if (el.matches('input')) {\n el.focus();\n } else {\n u.addClass(this.options.selected, el);\n }\n this.selected = el;\n this.options.onSelected && this.options.onSelected(el);\n }\n\n /**\n * Remove the current selection\n */\n unselect () {\n if (this.selected) {\n u.removeClass(this.options.selected, this.selected);\n delete this.selected;\n }\n }\n\n /**\n * Scroll the container to an element.\n * @param { HTMLElement } el The destination element.\n * @param { String } direction The direction of the current navigation.\n * @return void.\n */\n scrollTo (el, direction) {\n if (!this.inScrollContainerViewport(el)) {\n const container = this.scroll_container;\n if (!container.contains(el)) {\n return;\n }\n switch (direction) {\n case DOMNavigator.DIRECTION.left:\n container.scrollLeft = el.offsetLeft - container.offsetLeft;\n container.scrollTop = el.offsetTop - container.offsetTop;\n break;\n case DOMNavigator.DIRECTION.up:\n container.scrollTop = el.offsetTop - container.offsetTop;\n break;\n case DOMNavigator.DIRECTION.right:\n container.scrollLeft = el.offsetLeft - container.offsetLeft - (container.offsetWidth - el.offsetWidth);\n container.scrollTop = el.offsetTop - container.offsetTop - (container.offsetHeight - el.offsetHeight);\n break;\n case DOMNavigator.DIRECTION.down:\n container.scrollTop = el.offsetTop - container.offsetTop - (container.offsetHeight - el.offsetHeight);\n break;\n }\n } else if (!inViewport(el)) {\n switch (direction) {\n case DOMNavigator.DIRECTION.left:\n document.body.scrollLeft = absoluteOffsetLeft(el) - document.body.offsetLeft;\n break;\n case DOMNavigator.DIRECTION.up:\n document.body.scrollTop = absoluteOffsetTop(el) - document.body.offsetTop;\n break;\n case DOMNavigator.DIRECTION.right:\n document.body.scrollLeft = absoluteOffsetLeft(el) - document.body.offsetLeft - (document.documentElement.clientWidth - el.offsetWidth);\n break;\n case DOMNavigator.DIRECTION.down:\n document.body.scrollTop = absoluteOffsetTop(el) - document.body.offsetTop - (document.documentElement.clientHeight - el.offsetHeight);\n break;\n }\n }\n }\n\n /**\n * Indicate if an element is in the container viewport.\n * @param { HTMLElement } el The element to check.\n * @return { Boolean } true if the given element is in the container viewport, otherwise false.\n */\n inScrollContainerViewport(el) {\n const container = this.scroll_container;\n // Check on left side.\n if (el.offsetLeft - container.scrollLeft < container.offsetLeft) {\n return false;\n }\n // Check on top side.\n if (el.offsetTop - container.scrollTop < container.offsetTop) {\n return false;\n }\n // Check on right side.\n if ((el.offsetLeft + el.offsetWidth - container.scrollLeft) > (container.offsetLeft + container.offsetWidth)) {\n return false;\n }\n // Check on down side.\n if ((el.offsetTop + el.offsetHeight - container.scrollTop) > (container.offsetTop + container.offsetHeight)) {\n return false;\n }\n return true;\n }\n\n /**\n * Find and store the navigable elements\n */\n getElements (direction) {\n const selector = this.options.getSelector ? this.options.getSelector(direction) : this.options.selector;\n if (!this.elements[selector]) {\n this.elements[selector] = Array.from(this.container.querySelectorAll(selector));\n }\n return this.elements[selector];\n }\n\n /**\n * Return an array of navigable elements after an offset.\n * @param { number } left The left offset.\n * @param { number } top The top offset.\n * @return { Array } An array of elements.\n */\n elementsAfter (left, top) {\n return this.getElements(DOMNavigator.DIRECTION.down).filter(el => el.offsetLeft >= left && el.offsetTop >= top);\n }\n\n /**\n * Return an array of navigable elements before an offset.\n * @param { number } left The left offset.\n * @param { number } top The top offset.\n * @return { Array } An array of elements.\n */\n elementsBefore (left, top) {\n return this.getElements(DOMNavigator.DIRECTION.up).filter(el => el.offsetLeft <= left && el.offsetTop <= top);\n }\n\n /**\n * Handle the key down event.\n * @param { Event } event The event object.\n */\n handleKeydown (ev) {\n const keys = keycodes;\n const direction = ev.shiftKey ? this.keys[`${keys.SHIFT}+${ev.which}`] : this.keys[ev.which];\n if (direction) {\n ev.preventDefault();\n ev.stopPropagation();\n const next = this.getNextElement(direction, ev);\n this.select(next, direction);\n }\n }\n}\n\nexport default DOMNavigator;\n","import DOMNavigator from \"shared/dom-navigator.js\";\nimport { CustomElement } from './element.js';\nimport { converse, api } from \"@converse/headless/core\";\nimport { html } from 'lit';\nimport { until } from 'lit/directives/until.js';\n\nconst u = converse.env.utils;\n\n\nexport class BaseDropdown extends CustomElement {\n\n firstUpdated () {\n this.menu = this.querySelector('.dropdown-menu');\n this.dropdown = this.firstElementChild;\n this.button = this.dropdown.querySelector('button');\n this.dropdown.addEventListener('click', ev => this.toggleMenu(ev));\n this.dropdown.addEventListener('keyup', ev => this.handleKeyUp(ev));\n document.addEventListener('click', ev => !this.contains(ev.composedPath()[0]) && this.hideMenu(ev));\n }\n\n hideMenu () {\n u.removeClass('show', this.menu);\n this.button?.setAttribute('aria-expanded', false);\n this.button?.blur();\n }\n\n showMenu () {\n u.addClass('show', this.menu);\n this.button.setAttribute('aria-expanded', true);\n }\n\n toggleMenu (ev) {\n ev.preventDefault();\n if (u.hasClass('show', this.menu)) {\n this.hideMenu();\n } else {\n this.showMenu();\n }\n }\n\n handleKeyUp (ev) {\n if (ev.keyCode === converse.keycodes.ESCAPE) {\n this.hideMenu();\n } else if (ev.keyCode === converse.keycodes.DOWN_ARROW && this.navigator && !this.navigator.enabled) {\n this.enableArrowNavigation(ev);\n }\n }\n}\n\n\nexport default class DropdownList extends BaseDropdown {\n\n static get properties () {\n return {\n 'icon_classes': { type: String },\n 'items': { type: Array }\n }\n }\n\n render () {\n const icon_classes = this.icon_classes || \"fa fa-bars\";\n return html`\n${this.model.get('moderation_reason')}` : '' }\n `;\n }\n\n renderMessageText () {\n const i18n_edited = __('This message has been edited');\n const i18n_show = __('Show more');\n const is_groupchat_message = (this.model.get('type') === 'groupchat');\n const i18n_show_less = __('Show less');\n\n const tpl_spoiler_hint = html`\n
${ o.status }
` : '' }\n `;\n}\n","import { __ } from 'i18n';\nimport { api } from \"@converse/headless/core\";\nimport { html } from \"lit\";\nimport { resetElementHeight } from '../utils.js';\n\n\nexport default (o) => {\n const label_message = o.composing_spoiler ? __('Hidden message') : __('Message');\n const label_spoiler_hint = __('Optional hint');\n const show_send_button = api.settings.get('show_send_button');\n\n return html`\n `;\n}\n","import tpl_message_form from './templates/message-form.js';\nimport { ElementView } from '@converse/skeletor/src/element.js';\nimport { __ } from 'i18n';\nimport { _converse, api, converse } from \"@converse/headless/core\";\nimport { parseMessageForCommands } from './utils.js';\n\nconst { u } = converse.env;\n\n\nexport default class MessageForm extends ElementView {\n\n async connectedCallback () {\n super.connectedCallback();\n this.model = _converse.chatboxes.get(this.getAttribute('jid'));\n await this.model.initialized;\n this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);\n this.render();\n }\n\n toHTML () {\n return tpl_message_form(\n Object.assign(this.model.toJSON(), {\n 'onDrop': ev => this.onDrop(ev),\n 'hint_value': this.querySelector('.spoiler-hint')?.value,\n 'message_value': this.querySelector('.chat-textarea')?.value,\n 'onChange': ev => this.model.set({'draft': ev.target.value}),\n 'onKeyDown': ev => this.onKeyDown(ev),\n 'onKeyUp': ev => this.onKeyUp(ev),\n 'onPaste': ev => this.onPaste(ev),\n 'viewUnreadMessages': ev => this.viewUnreadMessages(ev)\n })\n );\n }\n\n /**\n * Insert a particular string value into the textarea of this chat box.\n * @param {string} value - The value to be inserted.\n * @param {(boolean|string)} [replace] - Whether an existing value\n * should be replaced. If set to `true`, the entire textarea will\n * be replaced with the new value. If set to a string, then only\n * that string will be replaced *if* a position is also specified.\n * @param {integer} [position] - The end index of the string to be\n * replaced with the new value.\n */\n insertIntoTextArea (value, replace = false, correcting = false, position) {\n const textarea = this.querySelector('.chat-textarea');\n if (correcting) {\n u.addClass('correcting', textarea);\n } else {\n u.removeClass('correcting', textarea);\n }\n if (replace) {\n if (position && typeof replace == 'string') {\n textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>\n offset == position - replace.length ? value + ' ' : match\n );\n } else {\n textarea.value = value;\n }\n } else {\n let existing = textarea.value;\n if (existing && existing[existing.length - 1] !== ' ') {\n existing = existing + ' ';\n }\n textarea.value = existing + value + ' ';\n }\n const ev = document.createEvent('HTMLEvents');\n ev.initEvent('change', false, true);\n textarea.dispatchEvent(ev);\n u.placeCaretAtEnd(textarea);\n }\n\n onMessageCorrecting (message) {\n if (message.get('correcting')) {\n this.insertIntoTextArea(u.prefixMentions(message), true, true);\n } else {\n const currently_correcting = this.model.messages.findWhere('correcting');\n if (currently_correcting && currently_correcting !== message) {\n this.insertIntoTextArea(u.prefixMentions(message), true, true);\n } else {\n this.insertIntoTextArea('', true, false);\n }\n }\n }\n\n onEscapePressed (ev) {\n ev.preventDefault();\n const idx = this.model.messages.findLastIndex('correcting');\n const message = idx >= 0 ? this.model.messages.at(idx) : null;\n if (message) {\n message.save('correcting', false);\n }\n this.insertIntoTextArea('', true, false);\n }\n\n onPaste (ev) {\n ev.stopPropagation();\n if (ev.clipboardData.files.length !== 0) {\n ev.preventDefault();\n // Workaround for quirk in at least Firefox 60.7 ESR:\n // It seems that pasted files disappear from the event payload after\n // the event has finished, which apparently happens during async\n // processing in sendFiles(). So we copy the array here.\n this.model.sendFiles(Array.from(ev.clipboardData.files));\n return;\n }\n this.model.set({'draft': ev.clipboardData.getData('text/plain')});\n }\n\n onKeyUp (ev) {\n this.model.set({'draft': ev.target.value});\n }\n\n onKeyDown (ev) {\n if (ev.ctrlKey) {\n // When ctrl is pressed, no chars are entered into the textarea.\n return;\n }\n if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {\n if (ev.keyCode === converse.keycodes.TAB) {\n const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);\n if (value.startsWith(':')) {\n ev.preventDefault();\n ev.stopPropagation();\n this.model.trigger('emoji-picker-autocomplete', ev.target, value);\n }\n } else if (ev.keyCode === converse.keycodes.FORWARD_SLASH) {\n // Forward slash is used to run commands. Nothing to do here.\n return;\n } else if (ev.keyCode === converse.keycodes.ESCAPE) {\n return this.onEscapePressed(ev, this);\n } else if (ev.keyCode === converse.keycodes.ENTER) {\n return this.onFormSubmitted(ev);\n } else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {\n const textarea = this.querySelector('.chat-textarea');\n if (!textarea.value || u.hasClass('correcting', textarea)) {\n return this.model.editEarlierMessage();\n }\n } else if (\n ev.keyCode === converse.keycodes.DOWN_ARROW &&\n ev.target.selectionEnd === ev.target.value.length &&\n u.hasClass('correcting', this.querySelector('.chat-textarea'))\n ) {\n return this.model.editLaterMessage();\n }\n }\n if (\n [\n converse.keycodes.SHIFT,\n converse.keycodes.META,\n converse.keycodes.META_RIGHT,\n converse.keycodes.ESCAPE,\n converse.keycodes.ALT\n ].includes(ev.keyCode)\n ) {\n return;\n }\n if (this.model.get('chat_state') !== _converse.COMPOSING) {\n // Set chat state to composing if keyCode is not a forward-slash\n // (which would imply an internal command and not a message).\n this.model.setChatState(_converse.COMPOSING);\n }\n }\n\n parseMessageForCommands (text) {\n // Wrap util so that we can override in the MUC message-form component\n return parseMessageForCommands(this.model, text);\n }\n\n async onFormSubmitted (ev) {\n ev?.preventDefault?.();\n\n const textarea = this.querySelector('.chat-textarea');\n const message_text = textarea.value.trim();\n if (\n (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||\n !message_text.replace(/\\s/g, '').length\n ) {\n return;\n }\n if (!_converse.connection.authenticated) {\n const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');\n api.alert('error', __('Error'), err_msg);\n api.connection.reconnect();\n return;\n }\n let spoiler_hint,\n hint_el = {};\n if (this.model.get('composing_spoiler')) {\n hint_el = this.querySelector('form.sendXMPPMessage input.spoiler-hint');\n spoiler_hint = hint_el.value;\n }\n u.addClass('disabled', textarea);\n textarea.setAttribute('disabled', 'disabled');\n this.querySelector('converse-emoji-dropdown')?.hideMenu();\n\n const is_command = this.parseMessageForCommands(message_text);\n const message = is_command ? null : await this.model.sendMessage(message_text, spoiler_hint);\n if (is_command || message) {\n hint_el.value = '';\n textarea.value = '';\n u.removeClass('correcting', textarea);\n textarea.style.height = 'auto';\n this.model.set({'draft': ''});\n }\n if (api.settings.get('view_mode') === 'overlayed') {\n // XXX: Chrome flexbug workaround. The .chat-content area\n // doesn't resize when the textarea is resized to its original size.\n const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));\n const msgs_container = chatview.querySelector('.chat-content__messages');\n msgs_container.parentElement.style.display = 'none';\n }\n textarea.removeAttribute('disabled');\n u.removeClass('disabled', textarea);\n\n if (api.settings.get('view_mode') === 'overlayed') {\n // XXX: Chrome flexbug workaround.\n const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));\n const msgs_container = chatview.querySelector('.chat-content__messages');\n msgs_container.parentElement.style.display = '';\n }\n // Suppress events, otherwise superfluous CSN gets set\n // immediately after the message, causing rate-limiting issues.\n this.model.setChatState(_converse.ACTIVE, { 'silent': true });\n textarea.focus();\n }\n}\n\napi.elements.define('converse-message-form', MessageForm);\n","import './message-form.js';\nimport debounce from 'lodash-es/debounce';\nimport tpl_bottom_panel from './templates/bottom-panel.js';\nimport { ElementView } from '@converse/skeletor/src/element.js';\nimport { _converse, api } from '@converse/headless/core';\nimport { clearMessages } from './utils.js';\nimport { render } from 'lit';\n\nimport './styles/chat-bottom-panel.scss';\n\n\nexport default class ChatBottomPanel extends ElementView {\n events = {\n 'click .send-button': 'sendButtonClicked',\n 'click .toggle-clear': 'clearMessages'\n };\n\n async connectedCallback () {\n super.connectedCallback();\n this.debouncedRender = debounce(this.render, 100);\n this.model = _converse.chatboxes.get(this.getAttribute('jid'));\n await this.model.initialized;\n this.listenTo(this.model, 'change:num_unread', this.debouncedRender)\n this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker);\n\n this.addEventListener('focusin', ev => this.emitFocused(ev));\n this.addEventListener('focusout', ev => this.emitBlurred(ev));\n this.render();\n }\n\n render () {\n render(tpl_bottom_panel({\n 'model': this.model,\n 'viewUnreadMessages': ev => this.viewUnreadMessages(ev)\n }), this);\n }\n\n sendButtonClicked (ev) {\n this.querySelector('converse-message-form')?.onFormSubmitted(ev);\n }\n\n viewUnreadMessages (ev) {\n ev?.preventDefault?.();\n this.model.save({ 'scrolled': false });\n }\n\n emitFocused (ev) {\n _converse.chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev);\n }\n\n emitBlurred (ev) {\n _converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);\n }\n\n getToolbarOptions () { // eslint-disable-line class-methods-use-this\n return {};\n }\n\n onDrop (evt) {\n if (evt.dataTransfer.files.length == 0) {\n // There are no files to be dropped, so this isn’t a file\n // transfer operation.\n return;\n }\n evt.preventDefault();\n this.model.sendFiles(evt.dataTransfer.files);\n }\n\n onDragOver (ev) { // eslint-disable-line class-methods-use-this\n ev.preventDefault();\n }\n\n clearMessages (ev) {\n ev?.preventDefault?.();\n clearMessages(this.model);\n }\n\n async autocompleteInPicker (input, value) {\n await api.emojis.initialize();\n const emoji_picker = this.querySelector('converse-emoji-picker');\n if (emoji_picker) {\n emoji_picker.model.set({\n 'ac_position': input.selectionStart,\n 'autocompleting': value,\n 'query': value\n });\n const emoji_dropdown = this.querySelector('converse-emoji-dropdown');\n emoji_dropdown?.showMenu();\n }\n }\n}\n\napi.elements.define('converse-chat-bottom-panel', ChatBottomPanel);\n","import { __ } from 'i18n';\nimport { api } from '@converse/headless/core';\nimport { html } from 'lit';\n\n\nexport default (o) => {\n const unread_msgs = __('You have unread messages');\n const message_limit = api.settings.get('message_limit');\n const show_call_button = api.settings.get('visible_toolbar_buttons').call;\n const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji;\n const show_send_button = api.settings.get('show_send_button');\n const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;\n const show_toolbar = api.settings.get('show_toolbar');\n return html`\n ${ o.model.get('scrolled') && o.model.get('num_unread') ?\n html`${_converse.VERSION_NAME}
\n\n Open Source XMPP chat client\n brought to you by Opkode\n
\n\n Translate\n it into your own language\n
\n `\n : ''}\n `;\n }\n}\n\napi.elements.define('converse-brand-byline', ConverseBrandByline);\n","import { api } from '@converse/headless/core';\nimport { CustomElement } from './element.js';\nimport { html } from 'lit';\n\n\nexport class ConverseBrandLogo extends CustomElement {\n\n render () { // eslint-disable-line class-methods-use-this\n const is_fullscreen = api.settings.get('view_mode') === 'fullscreen';\n return html`\n \n \n \n \n converse.js\n ${is_fullscreen\n ? html`\n \n `\n : ''}\n \n \n \n `;\n }\n}\n\napi.elements.define('converse-brand-logo', ConverseBrandLogo);\n","import './brand-byline.js';\nimport './brand-logo.js';\nimport { CustomElement } from './element.js';\nimport { api } from '@converse/headless/core';\nimport { html } from 'lit-html';\n\n\nexport class ConverseBrandHeading extends CustomElement {\n\n render () { // eslint-disable-line class-methods-use-this\n return html`\n${i18n_disconnected}
` : '' }\n `;\n}\n\n\nexport default (o) => html`\n${ o.status }
` : '' }\n `;\n}\n","import BaseChatView from 'shared/chat/baseview.js';\nimport tpl_headlines from './templates/headlines.js';\nimport { _converse, api } from '@converse/headless/core';\n\n\nclass HeadlinesView extends BaseChatView {\n\n connectedCallback () {\n super.connectedCallback();\n this.initialize();\n }\n\n async initialize() {\n _converse.chatboxviews.add(this.jid, this);\n\n this.model = _converse.chatboxes.get(this.jid);\n this.model.disable_mam = true; // Don't do MAM queries for this box\n this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);\n this.listenTo(this.model, 'change:hidden', () => this.afterShown());\n this.listenTo(this.model, 'destroy', this.remove);\n this.listenTo(this.model, 'show', this.show);\n this.listenTo(this.model.messages, 'add', this.requestUpdate);\n this.listenTo(this.model.messages, 'remove', this.requestUpdate);\n this.listenTo(this.model.messages, 'reset', this.requestUpdate);\n\n await this.model.messages.fetched;\n this.model.maybeShow();\n /**\n * Triggered once the {@link _converse.HeadlinesBoxView} has been initialized\n * @event _converse#headlinesBoxViewInitialized\n * @type { _converse.HeadlinesBoxView }\n * @example _converse.api.listen.on('headlinesBoxViewInitialized', view => { ... });\n */\n api.trigger('headlinesBoxViewInitialized', this);\n }\n\n render () {\n return tpl_headlines(this.model);\n }\n\n async close (ev) {\n ev?.preventDefault?.();\n if (_converse.router.history.getFragment() === 'converse/chat?jid=' + this.model.get('jid')) {\n _converse.router.navigate('');\n }\n await this.model.close(ev);\n api.trigger('chatBoxClosed', this);\n return this;\n }\n\n getNotifications () { // eslint-disable-line class-methods-use-this\n // Override method in ChatBox. We don't show notifications for\n // headlines boxes.\n return [];\n }\n\n afterShown () { // eslint-disable-line class-methods-use-this\n return;\n }\n}\n\napi.elements.define('converse-headlines', HeadlinesView);\n","import '../heading.js';\nimport { html } from \"lit\";\n\nexport default (model) => html`\n${i18n_moved}
\n\n o.onSwitch(ev)}>${o.moved_jid}\n
`;\n}\n\nexport default (o) => {\n const i18n_non_existent = __('This groupchat no longer exists');\n const i18n_reason = __('The following reason was given: \"%1$s\"', o.reason || '');\n return html`\n${i18n_reason}
` : '' }\n ${ o.moved_jid ? tpl_moved(o) : '' }\n `;\n}\n","import tpl_muc_disconnect from './templates/muc-disconnect.js';\nimport { CustomElement } from 'shared/components/element';\nimport { __ } from 'i18n';\nimport { _converse, api } from \"@converse/headless/core\";\n\n\nclass MUCDisconnected extends CustomElement {\n\n static get properties () {\n return {\n 'jid': { type: String }\n }\n }\n\n connectedCallback () {\n super.connectedCallback();\n this.model = _converse.chatboxes.get(this.jid);\n }\n\n render () {\n const message = this.model.session.get('disconnection_message');\n if (!message) {\n return;\n }\n const messages = [message];\n const actor = this.model.session.get('disconnection_actor');\n if (actor) {\n messages.push(__('This action was done by %1$s.', actor));\n }\n const reason = this.model.session.get('disconnection_reason');\n if (reason) {\n messages.push(__('The reason given is: \"%1$s\".', reason));\n }\n return tpl_muc_disconnect(messages);\n }\n}\n\napi.elements.define('converse-muc-disconnected', MUCDisconnected);\n","import { html } from \"lit\";\n\n\nexport default (messages) => {\n return html`\n${m}
`) }\n${i18n_topic}: ${unsafeHTML(xss.filterXSS(o.subject.text, {'whiteList': {}}))}
\n${i18n_topic_author}: ${o.subject && o.subject.author}
\n `;\n}\n\n\nexport default (o) => {\n const i18n_address = __('Groupchat address (JID)');\n const i18n_archiving = __('Message archiving');\n const i18n_archiving_help = __('Messages are archived on the server');\n const i18n_desc = __('Description');\n const i18n_features = __('Features');\n const i18n_hidden = __('Hidden');\n const i18n_hidden_help = __('This groupchat is not publicly searchable');\n const i18n_members_help = __('This groupchat is restricted to members only');\n const i18n_members_only = __('Members only');\n const i18n_moderated = __('Moderated');\n const i18n_moderated_help = __('Participants entering this groupchat need to request permission to write');\n const i18n_name = __('Name');\n const i18n_no_pass_help = __('This groupchat does not require a password upon entry');\n const i18n_no_password_required = __('No password required');\n const i18n_not_anonymous = __('Not anonymous');\n const i18n_not_anonymous_help = __('All other groupchat participants can see your XMPP address');\n const i18n_not_moderated = __('Not moderated');\n const i18n_not_moderated_help = __('Participants entering this groupchat can write right away');\n const i18n_online_users = __('Online users');\n const i18n_open = __('Open');\n const i18n_open_help = __('Anyone can join this groupchat');\n const i18n_password_help = __('This groupchat requires a password before entry');\n const i18n_password_protected = __('Password protected');\n const i18n_persistent = __('Persistent');\n const i18n_persistent_help = __('This groupchat persists even if it\\'s unoccupied');\n const i18n_public = __('Public');\n const i18n_semi_anon = __('Semi-anonymous');\n const i18n_semi_anon_help = __('Only moderators can see your XMPP address');\n const i18n_temporary = __('Temporary');\n const i18n_temporary_help = __('This groupchat will disappear once the last person leaves');\n return html`\n\n
\n ${i18n_providers}\n ${i18n_providers_link}.\n
\n `;\n};\n\nconst tpl_fetch_form_buttons = () => {\n const i18n_register = __('Fetch registration form');\n const i18n_existing_account = __('Already have a chat account?');\n const i18n_login = __('Log in here');\n return html`\n \n${i18n_existing_account}
\n \n'+message+'
'\n );\n flash.classList.remove('hidden');\n }\n\n /**\n * Report back to the user any error messages received from the\n * XMPP server after attempted registration.\n * @private\n * @method _converse.RegisterPanel#reportErrors\n * @param { XMLElement } stanza - The IQ stanza received from the XMPP server\n */\n reportErrors (stanza) {\n const errors = stanza.querySelectorAll('error');\n errors.forEach(e => this.showValidationError(e.textContent));\n if (!errors.length) {\n const message = __('The provider rejected your registration attempt. '+\n 'Please check the values you entered for correctness.');\n this.showValidationError(message);\n }\n }\n\n renderProviderChoiceForm (ev) {\n if (ev && ev.preventDefault) { ev.preventDefault(); }\n _converse.connection._proto._abortAllRequests();\n _converse.connection.reset();\n this.render();\n }\n\n abortRegistration () {\n _converse.connection._proto._abortAllRequests();\n _converse.connection.reset();\n if ([FETCHING_FORM, REGISTRATION_FORM].includes(this.model.get('registration_status'))) {\n if (api.settings.get('registration_domain')) {\n this.fetchRegistrationForm(api.settings.get('registration_domain'));\n }\n } else {\n this.render();\n }\n }\n\n /**\n * Handler, when the user submits the registration form.\n * Provides form error feedback or starts the registration process.\n * @private\n * @method _converse.RegisterPanel#submitRegistrationForm\n * @param { HTMLElement } form - The HTML form that was submitted\n */\n submitRegistrationForm (form) {\n const has_empty_inputs = Array.from(this.querySelectorAll('input.required'))\n .reduce((result, input) => {\n if (input.value === '') {\n input.classList.add('error');\n return result + 1;\n }\n return result;\n }, 0);\n if (has_empty_inputs) { return; }\n\n const inputs = sizzle(':input:not([type=button]):not([type=submit])', form);\n const iq = $iq({'type': 'set', 'id': u.getUniqueId()})\n .c(\"query\", {xmlns:Strophe.NS.REGISTER});\n\n if (this.form_type === 'xform') {\n iq.c(\"x\", {xmlns: Strophe.NS.XFORM, type: 'submit'});\n\n const xml_nodes = inputs.map(i => utils.webForm2xForm(i)).filter(n => n);\n xml_nodes.forEach(n => iq.cnode(n).up());\n } else {\n inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));\n }\n _converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, \"iq\", null, null);\n _converse.connection.send(iq);\n this.setFields(iq.tree());\n }\n\n /* Stores the values that will be sent to the XMPP server during attempted registration.\n * @private\n * @method _converse.RegisterPanel#setFields\n * @param { XMLElement } stanza - the IQ stanza that will be sent to the XMPP server.\n */\n setFields (stanza) {\n const query = stanza.querySelector('query');\n const xform = sizzle(`x[xmlns=\"${Strophe.NS.XFORM}\"]`, query);\n if (xform.length > 0) {\n this._setFieldsFromXForm(xform.pop());\n } else {\n this._setFieldsFromLegacy(query);\n }\n }\n\n _setFieldsFromLegacy (query) {\n [].forEach.call(query.children, field => {\n if (field.tagName.toLowerCase() === 'instructions') {\n this.instructions = Strophe.getText(field);\n return;\n } else if (field.tagName.toLowerCase() === 'x') {\n if (field.getAttribute('xmlns') === 'jabber:x:oob') {\n this.urls.concat(sizzle('url', field).map(u => u.textContent));\n }\n return;\n }\n this.fields[field.tagName.toLowerCase()] = Strophe.getText(field);\n });\n this.form_type = 'legacy';\n }\n\n _setFieldsFromXForm (xform) {\n this.title = xform.querySelector('title')?.textContent;\n this.instructions = xform.querySelector('instructions')?.textContent;\n xform.querySelectorAll('field').forEach(field => {\n const _var = field.getAttribute('var');\n if (_var) {\n this.fields[_var.toLowerCase()] = field.querySelector('value')?.textContent ?? '';\n } else {\n // TODO: other option seems to be type=\"fixed\"\n log.warn(\"Found field we couldn't parse\");\n }\n });\n this.form_type = 'xform';\n }\n\n /**\n * Callback method that gets called when a return IQ stanza\n * is received from the XMPP server, after attempting to\n * register a new user.\n * @private\n * @method _converse.RegisterPanel#reportErrors\n * @param { XMLElement } stanza - The IQ stanza.\n */\n _onRegisterIQ (stanza) {\n if (stanza.getAttribute(\"type\") === \"error\") {\n log.error(\"Registration failed.\");\n this.reportErrors(stanza);\n\n let error = stanza.getElementsByTagName(\"error\");\n if (error.length !== 1) {\n _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, \"unknown\");\n return false;\n }\n error = error[0].firstElementChild.tagName.toLowerCase();\n if (error === 'conflict') {\n _converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error);\n } else if (error === 'not-acceptable') {\n _converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error);\n } else {\n _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error);\n }\n } else {\n _converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null);\n }\n return false;\n }\n}\n\napi.elements.define('converse-register-panel', RegisterPanel);\n","/**\n * @module converse-register\n * @description\n * This is a Converse.js plugin which add support for in-band registration\n * as specified in XEP-0077.\n * @copyright 2020, the Converse.js contributors\n * @license Mozilla Public License (MPLv2)\n */\nimport './panel.js';\nimport '../controlbox/index.js';\nimport { __ } from 'i18n';\nimport { _converse, api, converse } from '@converse/headless/core';\n\n// Strophe methods for building stanzas\nconst { Strophe } = converse.env;\n\n// Add Strophe Namespaces\nStrophe.addNamespace('REGISTER', 'jabber:iq:register');\n\n// Add Strophe Statuses\nconst i = Object.keys(Strophe.Status).reduce((max, k) => Math.max(max, Strophe.Status[k]), 0);\nStrophe.Status.REGIFAIL = i + 1;\nStrophe.Status.REGISTERED = i + 2;\nStrophe.Status.CONFLICT = i + 3;\nStrophe.Status.NOTACCEPTABLE = i + 5;\n\nconverse.plugins.add('converse-register', {\n\n dependencies: ['converse-controlbox'],\n\n enabled () {\n return true;\n },\n\n initialize () {\n _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';\n _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';\n _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';\n _converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE';\n\n api.settings.extend({\n 'allow_registration': true,\n 'domain_placeholder': __(' e.g. conversejs.org'), // Placeholder text shown in the domain input on the registration form\n 'providers_link': 'https://compliance.conversations.im/', // Link to XMPP providers shown on registration page\n 'registration_domain': ''\n });\n\n async function setActiveForm (value) {\n await api.waitUntil('controlBoxInitialized');\n const controlbox = _converse.chatboxes.get('controlbox');\n controlbox.set({ 'active-form': value });\n }\n _converse.router.route('converse/login', () => setActiveForm('login'));\n _converse.router.route('converse/register', () => setActiveForm('register'));\n\n\n api.listen.on('controlBoxInitialized', view => {\n view.model.on('change:active-form', view.showLoginOrRegisterForm, view);\n });\n }\n});\n","import xss from \"xss/dist/xss\";\nimport { __ } from 'i18n';\nimport { html } from \"lit\";\nimport { modal_header_close_button } from \"modals/templates/buttons.js\"\nimport { unsafeHTML } from \"lit/directives/unsafe-html.js\";\n\n\nconst nickname_input = (o) => {\n const i18n_nickname = __('Nickname');\n const i18n_required_field = __('This field is required');\n return html`\n${i18n_jid} ${o.jid}
\n${i18n_desc} ${o.desc}
\n${i18n_occ} ${o.occ}
\n${i18n_features}\n