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
|
||||
|
||||
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 { 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 './components/poll-view.js'
|
||||
import './components/poll-form-view.js'
|
||||
|
||||
const { sizzle } = converse.env
|
||||
|
||||
converse.plugins.add('livechat-converse-poll', {
|
||||
dependencies: ['converse-muc', 'converse-disco'],
|
||||
|
||||
@ -19,5 +22,64 @@ converse.plugins.add('livechat-converse-poll', {
|
||||
// })
|
||||
// adding the poll actions in the MUC heading buttons:
|
||||
_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>
|
||||
<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-poll .model=${o.model}></livechat-converse-muc-poll>
|
||||
<div class="chat-body chatroom-body row no-gutters">${getChatRoomBodyTemplate(o)}</div>`
|
||||
: ''}
|
||||
</div>`
|
||||
|
@ -42,7 +42,9 @@ const locKeys = [
|
||||
'poll_anonymous_results',
|
||||
'poll_choice_n',
|
||||
'poll_title',
|
||||
'poll_instructions'
|
||||
'poll_instructions',
|
||||
'poll_end',
|
||||
'poll'
|
||||
]
|
||||
|
||||
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.
|
||||
|
||||
new_poll: Create a new poll
|
||||
poll: 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_question: Question
|
||||
poll_duration: Poll duration (in minutes)
|
||||
poll_anonymous_results: Anonymous results
|
||||
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();
|
||||
|
||||
-- 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-choice choice="1" votes="0">Choice 1 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,
|
||||
votes = "" .. total
|
||||
};
|
||||
message_attrs["end"] = string.format("%i", current_poll.end_timestamp);
|
||||
if current_poll.already_ended then
|
||||
message_attrs["over"] = "";
|
||||
end
|
||||
@ -175,9 +176,23 @@ local function remove_specific_tags_from_groupchat(event)
|
||||
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 {
|
||||
poll_start_message = poll_start_message;
|
||||
poll_end_message = poll_end_message;
|
||||
schedule_poll_update_message = schedule_poll_update_message;
|
||||
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 process_form = module:require("form").process_form;
|
||||
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;
|
||||
|
||||
-- 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!
|
||||
module:hook("muc-occupant-groupchat", remove_specific_tags_from_groupchat, 1000);
|
||||
|
||||
|
||||
-- when a room is restored (after a server restart for example),
|
||||
-- we must resume any current poll
|
||||
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…
Reference in New Issue
Block a user