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`
+ `
+ : ''
+ }`
+}
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