diff --git a/conversejs/custom/plugins/poll/components/poll-form-view.js b/conversejs/custom/plugins/poll/components/poll-form-view.js new file mode 100644 index 00000000..df171595 --- /dev/null +++ b/conversejs/custom/plugins/poll/components/poll-form-view.js @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only +import { XMLNS_POLL } from '../constants.js' +import { tplPollForm } from '../templates/poll-form.js' +import { CustomElement } from 'shared/components/element.js' +import { converse, api } from '@converse/headless/core' +import { webForm2xForm } from '@converse/headless/utils/form' +import { __ } from 'i18n' +import '../styles/poll-form.scss' +const $iq = converse.env.$iq +const u = converse.env.utils +const sizzle = converse.env.sizzle +const Strophe = converse.env.Strophe + +export default class MUCPollFormView extends CustomElement { + static get properties () { + return { + model: { type: Object, attribute: true }, + modal: { type: Object, attribute: true }, + form_fields: { type: Object, attribute: false }, + alert_message: { type: Object, attribute: false }, + title: { type: String, attribute: false }, + instructions: { type: String, attribute: false } + } + } + + async initialize () { + this.alert_message = undefined + if (!this.model) { + this.alert_message = __('Error') + return + } + try { + const stanza = await this._fetchPollForm() + const query = stanza.querySelector('query') + const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0] + if (!xform) { + throw Error('Missing xform in stanza') + } + + this.title = xform.querySelector('title')?.textContent ?? '' + this.instructions = xform.querySelector('instructions')?.textContent ?? '' + this.form_fields = Array.from(xform.querySelectorAll('field')).map(field => { + return u.xForm2TemplateResult(field, stanza) + }) + } catch (err) { + console.error(err) + this.alert_message = __('Error') + } + } + + render () { + return tplPollForm(this) + } + + _fetchPollForm () { + return api.sendIQ( + $iq({ + to: this.model.get('jid'), + type: 'get' + }).c('query', { xmlns: XMLNS_POLL }) + ) + } + + async formSubmit (ev) { + ev.preventDefault() + try { + this.alert_message = undefined + const form = ev.target + const inputs = sizzle(':input:not([type=button]):not([type=submit])', form) + + const iq = $iq({ + type: 'set', + id: u.getUniqueId() + }).c('query', { xmlns: XMLNS_POLL }) + + iq.c('x', { xmlns: Strophe.NS.XFORM, type: 'submit' }) + + const xmlNodes = inputs.map(i => webForm2xForm(i)).filter(n => n) + xmlNodes.forEach(n => iq.cnode(n).up()) + + await api.sendIQ(iq) + + if (this.modal) { + this.modal.hide() + } + } catch (err) { + console.error(err) + this.alert_message = __('Error') + } + } +} + +api.elements.define('livechat-converse-poll-form', MUCPollFormView) diff --git a/conversejs/custom/plugins/poll/index.js b/conversejs/custom/plugins/poll/index.js index 798b1037..bd345204 100644 --- a/conversejs/custom/plugins/poll/index.js +++ b/conversejs/custom/plugins/poll/index.js @@ -5,6 +5,8 @@ import { _converse, converse } from '../../../src/headless/core.js' import { getHeadingButtons } from './utils.js' // import { XMLNS_POLL } from './constants.js' +import './modals/poll-form.js' +import './components/poll-form-view.js' converse.plugins.add('livechat-converse-poll', { dependencies: ['converse-muc', 'converse-disco'], diff --git a/conversejs/custom/plugins/poll/modals/poll-form.js b/conversejs/custom/plugins/poll/modals/poll-form.js new file mode 100644 index 00000000..137e65d0 --- /dev/null +++ b/conversejs/custom/plugins/poll/modals/poll-form.js @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { __ } from 'i18n' +import BaseModal from 'plugins/modal/modal.js' +import { api } from '@converse/headless/core' +import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js' +import { html } from 'lit' +import 'livechat-external-login-content.js' + +class PollFormModal extends BaseModal { + initialize () { + super.initialize() + } + + renderModal () { + return html`` + } + + getModalTitle () { + // eslint-disable-next-line no-undef + return __(LOC_new_poll) + } + + renderModalFooter () { + return html` + + ` + } +} + +api.elements.define('livechat-converse-poll-form-modal', PollFormModal) diff --git a/conversejs/custom/plugins/poll/styles/poll-form.scss b/conversejs/custom/plugins/poll/styles/poll-form.scss new file mode 100644 index 00000000..6476d183 --- /dev/null +++ b/conversejs/custom/plugins/poll/styles/poll-form.scss @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +livechat-converse-poll-form-modal { + /* Special case: when the form is in a modal */ + + .converse-form { + max-height: 50vh; + overflow-y: scroll; + } +} diff --git a/conversejs/custom/plugins/poll/templates/poll-form.js b/conversejs/custom/plugins/poll/templates/poll-form.js new file mode 100644 index 00000000..5316f099 --- /dev/null +++ b/conversejs/custom/plugins/poll/templates/poll-form.js @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { html } from 'lit' +import { __ } from 'i18n' + +export function tplPollForm (el) { + const i18nOk = __('Ok') + + return html` + ${el.alert_message ? html`
${el.alert_message}
` : ''} + ${ + el.form_fields + ? html` +
el.formSubmit(ev)}> +

${el.title}

+

${el.instructions}

+ + + ${el.form_fields} + + ${ + el.modal + ? html`` // no need for submit button, the modal will have one in the footer + : html`
+ +
` + } +
` + : '' + }` +} diff --git a/conversejs/custom/plugins/poll/utils.js b/conversejs/custom/plugins/poll/utils.js index acddfcab..9a4a3812 100644 --- a/conversejs/custom/plugins/poll/utils.js +++ b/conversejs/custom/plugins/poll/utils.js @@ -3,18 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import { XMLNS_POLL } from './constants.js' -import { converse, _converse, api } from '../../../src/headless/core.js' +import { _converse, api } from '../../../src/headless/core.js' import { __ } from 'i18n' -const $iq = converse.env.$iq - -function _fetchPollForm (mucModel) { - return api.sendIQ( - $iq({ - to: mucModel.get('jid'), - type: 'get' - }).c('query', { xmlns: XMLNS_POLL }) - ) -} export function getHeadingButtons (view, buttons) { const muc = view.model @@ -39,9 +29,7 @@ export function getHeadingButtons (view, buttons) { i18n_text: __(LOC_new_poll), handler: async (ev) => { ev.preventDefault() - const r = await _fetchPollForm(muc) - // FIXME - console.info('Received poll form', r) + api.modal.show('livechat-converse-poll-form-modal', { model: muc }) }, a_class: '', icon_class: 'fa-list-check', // FIXME diff --git a/prosody-modules/mod_muc_poll/mod_muc_poll.lua b/prosody-modules/mod_muc_poll/mod_muc_poll.lua index 6386d493..1e12d025 100644 --- a/prosody-modules/mod_muc_poll/mod_muc_poll.lua +++ b/prosody-modules/mod_muc_poll/mod_muc_poll.lua @@ -21,9 +21,63 @@ local function get_form_layout(room, stanza) table.insert(form, { name = "muc#roompoll_question"; + type = "text-single"; label = "Question"; + desc = "The poll question."; value = ""; }); + table.insert(form, { + name = "muc#roompoll_duration"; + type = "text-single"; + datatype = "xs:integer"; + range_min = 1; + label = "Poll duration (in minutes)"; + desc = "The number of minutes to run the poll."; + value = ""; + }); + table.insert(form, { + name = "muc#roompoll_anonymous"; + type = "text-single"; + label = "Anonymous results"; + desc = "By enabling this, user's votes won't be publicly shown in the room."; + value = ""; + }); + table.insert(form, { + name = "muc#roompoll_choice1"; + type = "text-single"; + label = "Choice 1"; + desc = ""; + value = ""; + }); + table.insert(form, { + name = "muc#roompoll_choice2"; + type = "text-single"; + label = "Choice 2"; + desc = ""; + value = ""; + }); + table.insert(form, { + name = "muc#roompoll_choice3"; + type = "text-single"; + label = "Choice 3"; + desc = ""; + value = ""; + }); + table.insert(form, { + name = "muc#roompoll_choice4"; + type = "text-single"; + label = "Choice 4"; + desc = ""; + value = ""; + }); + table.insert(form, { + name = "muc#roompoll_choice5"; + type = "text-single"; + label = "Choice 5"; + desc = ""; + value = ""; + }); + return form; end