Poll WIP (#231):

* front end poll WIP
* backend fix
This commit is contained in:
John Livingston 2024-07-01 17:45:11 +02:00
parent 3ef0541886
commit 4591633400
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
10 changed files with 281 additions and 5 deletions

View 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)

View File

@ -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'

View File

@ -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.
}
}
}
})

View 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;
}
}
}
}
}

View 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>`
}

View File

@ -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>`

View File

@ -42,7 +42,9 @@ const locKeys = [
'poll_anonymous_results',
'poll_choice_n',
'poll_title',
'poll_instructions'
'poll_instructions',
'poll_end',
'poll'
]
module.exports = locKeys

View File

@ -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:'

View File

@ -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;
};

View File

@ -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);