parent
3ef0541886
commit
4591633400
30
conversejs/custom/plugins/poll/components/poll-view.js
Normal file
30
conversejs/custom/plugins/poll/components/poll-view.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { tplPoll } from '../templates/poll.js'
|
||||||
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
|
import { api } from '@converse/headless/core'
|
||||||
|
import '../styles/poll.scss'
|
||||||
|
|
||||||
|
export default class MUCPollView extends CustomElement {
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
model: { type: Object, attribute: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize () {
|
||||||
|
if (!this.model) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.listenTo(this.model, 'change:current_poll', () => this.requestUpdate())
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const currentPoll = this.model?.get('current_poll')
|
||||||
|
return tplPoll(this.model, currentPoll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.elements.define('livechat-converse-muc-poll', MUCPollView)
|
@ -3,3 +3,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
export const XMLNS_POLL = 'http://jabber.org/protocol/muc#x-poll'
|
export const XMLNS_POLL = 'http://jabber.org/protocol/muc#x-poll'
|
||||||
|
export const XMLNS_POLL_MESSAGE = 'http://jabber.org/protocol/muc#x-poll-message'
|
||||||
|
export const POLL_MESSAGE_TAG = 'x-poll'
|
||||||
|
export const POLL_QUESTION_TAG = 'x-poll-question'
|
||||||
|
export const POLL_CHOICE_TAG = 'x-poll-choice'
|
||||||
|
@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
import { _converse, converse } from '../../../src/headless/core.js'
|
import { _converse, converse } from '../../../src/headless/core.js'
|
||||||
import { getHeadingButtons } from './utils.js'
|
import { getHeadingButtons } from './utils.js'
|
||||||
// import { XMLNS_POLL } from './constants.js'
|
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
||||||
import './modals/poll-form.js'
|
import './modals/poll-form.js'
|
||||||
|
import './components/poll-view.js'
|
||||||
import './components/poll-form-view.js'
|
import './components/poll-form-view.js'
|
||||||
|
|
||||||
|
const { sizzle } = converse.env
|
||||||
|
|
||||||
converse.plugins.add('livechat-converse-poll', {
|
converse.plugins.add('livechat-converse-poll', {
|
||||||
dependencies: ['converse-muc', 'converse-disco'],
|
dependencies: ['converse-muc', 'converse-disco'],
|
||||||
|
|
||||||
@ -19,5 +22,64 @@ converse.plugins.add('livechat-converse-poll', {
|
|||||||
// })
|
// })
|
||||||
// adding the poll actions in the MUC heading buttons:
|
// adding the poll actions in the MUC heading buttons:
|
||||||
_converse.api.listen.on('getHeadingButtons', getHeadingButtons)
|
_converse.api.listen.on('getHeadingButtons', getHeadingButtons)
|
||||||
|
|
||||||
|
_converse.api.listen.on('parseMUCMessage', (stanza, attrs) => {
|
||||||
|
// Checking if there is any poll data in the message.
|
||||||
|
const poll = sizzle(POLL_MESSAGE_TAG, stanza)?.[0]
|
||||||
|
if (!poll) {
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
const question = sizzle(POLL_QUESTION_TAG, poll)?.[0]
|
||||||
|
const choices = sizzle(POLL_CHOICE_TAG, poll)
|
||||||
|
if (!question || !choices.length) {
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
const endDate = poll.hasAttribute('end')
|
||||||
|
? new Date(1000 * parseInt(poll.getAttribute('end')))
|
||||||
|
: null
|
||||||
|
|
||||||
|
const currentPoll = {
|
||||||
|
question: question.textContent,
|
||||||
|
id: poll.getAttribute('id'),
|
||||||
|
votes: parseInt(poll.getAttribute('votes') ?? 0),
|
||||||
|
over: poll.hasAttribute('over'),
|
||||||
|
endDate: endDate,
|
||||||
|
choices: choices.map(c => {
|
||||||
|
return {
|
||||||
|
label: c.textContent,
|
||||||
|
choice: c.getAttribute('choice'),
|
||||||
|
votes: parseInt(c.getAttribute('votes') ?? 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign(
|
||||||
|
attrs,
|
||||||
|
{
|
||||||
|
current_poll: currentPoll
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
overrides: {
|
||||||
|
ChatRoom: {
|
||||||
|
onMessage: function onMessage (attrs) {
|
||||||
|
if (!attrs.current_poll) {
|
||||||
|
return this.__super__.onMessage(attrs)
|
||||||
|
}
|
||||||
|
// We intercept poll messages, so they won't show up in the chat as classic messages.
|
||||||
|
if (attrs.is_delayed) {
|
||||||
|
console.info('Got a delayed poll message, just dropping')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('Got a poll message, setting it as the current_poll')
|
||||||
|
this.set('current_poll', attrs.current_poll)
|
||||||
|
// this will be displayed by the livechat-converse-muc-poll custom element,
|
||||||
|
// which is inserted in the DOM by the muc.js template overload.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
76
conversejs/custom/plugins/poll/styles/poll.scss
Normal file
76
conversejs/custom/plugins/poll/styles/poll.scss
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.conversejs {
|
||||||
|
livechat-converse-muc-poll {
|
||||||
|
background-color: var(--peertube-main-background);
|
||||||
|
color: var(--peertube-main-foreground);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
border: 1px solid var(--peertube-menu-background);
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
p.livechat-poll-question {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.livechat-poll-end {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.livechat-poll-choice-label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livechat-progress-bar {
|
||||||
|
background-color: var(--peertube-menu-background);
|
||||||
|
border: 1px solid var(--peertube-menu-background);
|
||||||
|
color: var(--peertube-menu-foreground);
|
||||||
|
height: 1.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
background-color: var(--peertube-button-background);
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: inline;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
conversejs/custom/plugins/poll/templates/poll.js
Normal file
79
conversejs/custom/plugins/poll/templates/poll.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { html } from 'lit'
|
||||||
|
import { repeat } from 'lit/directives/repeat.js'
|
||||||
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
|
function _tplPollEnd (el, currentPoll) {
|
||||||
|
if (!currentPoll.endDate) {
|
||||||
|
return html``
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const i18nPollEnd = __(LOC_poll_end)
|
||||||
|
return html`<p class="livechat-poll-end">
|
||||||
|
${i18nPollEnd}
|
||||||
|
${currentPoll.endDate.toLocaleString()}
|
||||||
|
</p>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function _tplChoice (el, currentPoll, choice) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const i18nChoiceN = __(LOC_poll_choice_n).replace('{{N}}', choice.choice)
|
||||||
|
|
||||||
|
const votes = choice.votes
|
||||||
|
const totalVotes = currentPoll.votes
|
||||||
|
const percent = totalVotes ? (100 * votes / totalVotes).toFixed(2) : '0.00'
|
||||||
|
return html`
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
${
|
||||||
|
currentPoll.over
|
||||||
|
? html`${i18nChoiceN}`
|
||||||
|
: html`
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
|
@click=${ev => {
|
||||||
|
ev.preventDefault()
|
||||||
|
if (currentPoll.over) { return }
|
||||||
|
// TODO
|
||||||
|
console.info('User has voted for choice: ', choice)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${i18nChoiceN}
|
||||||
|
</button>`
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="livechat-poll-choice-label">
|
||||||
|
${choice.label}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="livechat-progress-bar">
|
||||||
|
<div
|
||||||
|
role="progressbar"
|
||||||
|
style="width: ${percent}%;"
|
||||||
|
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
||||||
|
></div>
|
||||||
|
<p>
|
||||||
|
${votes}/${totalVotes}
|
||||||
|
(${percent}%)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tplPoll (el, currentPoll) {
|
||||||
|
if (!currentPoll) {
|
||||||
|
return html``
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<div>
|
||||||
|
<p class="livechat-poll-question">${currentPoll.question}</p>
|
||||||
|
<table><tbody>
|
||||||
|
${repeat(currentPoll.choices ?? [], (c) => c.choice, (c) => _tplChoice(el, currentPoll, c))}
|
||||||
|
</tbody></table>
|
||||||
|
${_tplPollEnd(el, currentPoll)}
|
||||||
|
</div>`
|
||||||
|
}
|
@ -21,6 +21,7 @@ export default (o) => {
|
|||||||
</converse-muc-heading>
|
</converse-muc-heading>
|
||||||
<livechat-converse-muc-terms .model=${o.model} termstype="global"></livechat-converse-muc-terms>
|
<livechat-converse-muc-terms .model=${o.model} termstype="global"></livechat-converse-muc-terms>
|
||||||
<livechat-converse-muc-terms .model=${o.model} termstype="muc"></livechat-converse-muc-terms>
|
<livechat-converse-muc-terms .model=${o.model} termstype="muc"></livechat-converse-muc-terms>
|
||||||
|
<livechat-converse-muc-poll .model=${o.model}></livechat-converse-muc-poll>
|
||||||
<div class="chat-body chatroom-body row no-gutters">${getChatRoomBodyTemplate(o)}</div>`
|
<div class="chat-body chatroom-body row no-gutters">${getChatRoomBodyTemplate(o)}</div>`
|
||||||
: ''}
|
: ''}
|
||||||
</div>`
|
</div>`
|
||||||
|
@ -42,7 +42,9 @@ const locKeys = [
|
|||||||
'poll_anonymous_results',
|
'poll_anonymous_results',
|
||||||
'poll_choice_n',
|
'poll_choice_n',
|
||||||
'poll_title',
|
'poll_title',
|
||||||
'poll_instructions'
|
'poll_instructions',
|
||||||
|
'poll_end',
|
||||||
|
'poll'
|
||||||
]
|
]
|
||||||
|
|
||||||
module.exports = locKeys
|
module.exports = locKeys
|
||||||
|
@ -565,9 +565,11 @@ livechat_configuration_channel_terms_desc: |
|
|||||||
You can configure a "terms & conditions" message that will be shown to users joining your chatrooms.
|
You can configure a "terms & conditions" message that will be shown to users joining your chatrooms.
|
||||||
|
|
||||||
new_poll: Create a new poll
|
new_poll: Create a new poll
|
||||||
|
poll: Poll
|
||||||
poll_title: New poll
|
poll_title: New poll
|
||||||
poll_instructions: Complete and submit this form to create a new poll. This will end and replace any existing poll.
|
poll_instructions: Complete and submit this form to create a new poll. This will end and replace any existing poll.
|
||||||
poll_question: Question
|
poll_question: Question
|
||||||
poll_duration: Poll duration (in minutes)
|
poll_duration: Poll duration (in minutes)
|
||||||
poll_anonymous_results: Anonymous results
|
poll_anonymous_results: Anonymous results
|
||||||
poll_choice_n: Choice {{N}}
|
poll_choice_n: Choice {{N}}
|
||||||
|
poll_end: 'Poll ends at:'
|
||||||
|
@ -62,7 +62,7 @@ local function build_poll_message(room, message_id, is_end_message)
|
|||||||
}):up();
|
}):up();
|
||||||
|
|
||||||
-- now we must add some custom XML data, so that compatible clients can display the poll as they want:
|
-- now we must add some custom XML data, so that compatible clients can display the poll as they want:
|
||||||
-- <x-poll xmlns="http://jabber.org/protocol/muc#x-poll-message" id="I9UWyoxsz4BN" votes="1" over="">
|
-- <x-poll xmlns="http://jabber.org/protocol/muc#x-poll-message" id="I9UWyoxsz4BN" votes="1" end="1719842224" over="">
|
||||||
-- <x-poll-question>Poll question</x-poll-question>
|
-- <x-poll-question>Poll question</x-poll-question>
|
||||||
-- <x-poll-choice choice="1" votes="0">Choice 1 label</x-poll-choice>
|
-- <x-poll-choice choice="1" votes="0">Choice 1 label</x-poll-choice>
|
||||||
-- <x-poll-choice choice="2" votes="1">Choice 2 label</x-poll-choice>
|
-- <x-poll-choice choice="2" votes="1">Choice 2 label</x-poll-choice>
|
||||||
@ -74,6 +74,7 @@ local function build_poll_message(room, message_id, is_end_message)
|
|||||||
id = current_poll.poll_id,
|
id = current_poll.poll_id,
|
||||||
votes = "" .. total
|
votes = "" .. total
|
||||||
};
|
};
|
||||||
|
message_attrs["end"] = string.format("%i", current_poll.end_timestamp);
|
||||||
if current_poll.already_ended then
|
if current_poll.already_ended then
|
||||||
message_attrs["over"] = "";
|
message_attrs["over"] = "";
|
||||||
end
|
end
|
||||||
@ -175,9 +176,23 @@ local function remove_specific_tags_from_groupchat(event)
|
|||||||
end);
|
end);
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- when a new session is opened, we must send the current poll to the client
|
||||||
|
local function handle_new_occupant_session(event)
|
||||||
|
local room = event.room;
|
||||||
|
if not room._data.current_poll then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
if room._data.current_poll.already_ended then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
schedule_poll_update_message(room.jid);
|
||||||
|
-- FIXME: for now we just schedule a new poll update. But we should only send a message to the new occupant.
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
poll_start_message = poll_start_message;
|
poll_start_message = poll_start_message;
|
||||||
poll_end_message = poll_end_message;
|
poll_end_message = poll_end_message;
|
||||||
schedule_poll_update_message = schedule_poll_update_message;
|
schedule_poll_update_message = schedule_poll_update_message;
|
||||||
remove_specific_tags_from_groupchat = remove_specific_tags_from_groupchat;
|
remove_specific_tags_from_groupchat = remove_specific_tags_from_groupchat;
|
||||||
|
handle_new_occupant_session = handle_new_occupant_session;
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,8 @@ local xmlns_poll = module:require("constants").xmlns_poll;
|
|||||||
local send_form = module:require("form").send_form;
|
local send_form = module:require("form").send_form;
|
||||||
local process_form = module:require("form").process_form;
|
local process_form = module:require("form").process_form;
|
||||||
local handle_groupchat = module:require("poll").handle_groupchat;
|
local handle_groupchat = module:require("poll").handle_groupchat;
|
||||||
local remove_specific_tags_from_groupchat = module:require("message").remove_specific_tags_from_groupchat
|
local remove_specific_tags_from_groupchat = module:require("message").remove_specific_tags_from_groupchat;
|
||||||
|
local handle_new_occupant_session = module:require("message").handle_new_occupant_session;
|
||||||
local room_restored = module:require("poll").room_restored;
|
local room_restored = module:require("poll").room_restored;
|
||||||
|
|
||||||
-- new poll creation, get form
|
-- new poll creation, get form
|
||||||
@ -85,7 +86,11 @@ module:hook("muc-occupant-groupchat", handle_groupchat, 1000);
|
|||||||
-- security check: we must remove all specific tags, to be sure nobody tries to spoof polls!
|
-- security check: we must remove all specific tags, to be sure nobody tries to spoof polls!
|
||||||
module:hook("muc-occupant-groupchat", remove_specific_tags_from_groupchat, 1000);
|
module:hook("muc-occupant-groupchat", remove_specific_tags_from_groupchat, 1000);
|
||||||
|
|
||||||
|
|
||||||
-- when a room is restored (after a server restart for example),
|
-- when a room is restored (after a server restart for example),
|
||||||
-- we must resume any current poll
|
-- we must resume any current poll
|
||||||
module:hook("muc-room-restored", room_restored);
|
module:hook("muc-room-restored", room_restored);
|
||||||
|
|
||||||
|
-- when a new session is opened, we must send the current poll to the client
|
||||||
|
-- Note: it should be in the MAM. But it is easier for clients to ignore delayed messages
|
||||||
|
-- when displaying polls (to ignore old polls).
|
||||||
|
module:hook("muc-occupant-session-new", handle_new_occupant_session);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user