diff --git a/conversejs/custom/plugins/poll/index.js b/conversejs/custom/plugins/poll/index.js index 1f4953b7..2f32be8d 100644 --- a/conversejs/custom/plugins/poll/index.js +++ b/conversejs/custom/plugins/poll/index.js @@ -74,8 +74,11 @@ converse.plugins.add('livechat-converse-poll', { } // We will also translate some strings here. - // eslint-disable-next-line no-undef - const body = (attrs.body ?? '').replace(LOC_poll_is_over, __(LOC_poll_is_over)) + const body = (attrs.body ?? '') + // eslint-disable-next-line no-undef + .replace(LOC_poll_is_over, __(LOC_poll_is_over)) + // eslint-disable-next-line no-undef + .replace(LOC_poll_vote_instructions_xmpp, __(LOC_poll_vote_instructions)) // changing instructions on the fly return Object.assign( attrs, @@ -93,15 +96,11 @@ converse.plugins.add('livechat-converse-poll', { if (!attrs.current_poll) { return this.__super__.onMessage(attrs) } + // We intercept poll messages, to show the banner. - // Note: we also show poll end messages in the chat, so that the user don't loose the result. - if (attrs.is_delayed || attrs.is_archived) { - if (attrs.current_poll.over) { - console.info('Got a delayed/archived poll message for an poll that is over, just displaying in the chat') - return this.__super__.onMessage(attrs) - } - console.info('Got a delayed/archived poll message, just dropping') - return + // We just drop archived messages, to not show the banner for finished polls. + if (attrs.is_archived) { + return this.__super__.onMessage(attrs) } console.info('Got a poll message, setting it as the current_poll') @@ -109,11 +108,7 @@ converse.plugins.add('livechat-converse-poll', { // which is inserted in the DOM by the muc.js template overload. this.set('current_poll', attrs.current_poll) - if (attrs.current_poll.over) { - console.info('The poll is over, displaying the message in the chat') - return this.__super__.onMessage(attrs) - } - // Dropping the message. + return this.__super__.onMessage(attrs) } } } diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js index c209e67d..7a6f9283 100644 --- a/conversejs/loc.keys.js +++ b/conversejs/loc.keys.js @@ -46,6 +46,7 @@ const locKeys = [ 'poll_end', 'poll', 'poll_vote_instructions', + 'poll_vote_instructions_xmpp', 'poll_is_over', 'poll_choice_invalid', 'poll_anonymous_vote_ok' diff --git a/languages/en.yml b/languages/en.yml index 9dde54d3..34211eb4 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -575,6 +575,8 @@ poll_choice_n: 'Choice {{N}}:' poll_end: 'Poll ends at:' poll_vote_instructions: | To vote, click on your choice or send a message with an exclamation mark followed by your choice number (Example: !1). +poll_vote_instructions_xmpp: | + Send a message with an exclamation mark followed by your choice number to vote. Example: !1 poll_is_over: This poll is now over. poll_choice_invalid: This choice is not valid. poll_anonymous_vote_ok: Your vote is taken into account. Votes are anonymous, they will not be shown to other participants. diff --git a/prosody-modules/mod_muc_poll/message.lib.lua b/prosody-modules/mod_muc_poll/message.lib.lua index ea17b88a..93612a0b 100644 --- a/prosody-modules/mod_muc_poll/message.lib.lua +++ b/prosody-modules/mod_muc_poll/message.lib.lua @@ -20,13 +20,12 @@ local scheduled_updates = {}; local string_poll_over = module:get_option_string("poll_string_over") or "This poll is now over."; local string_poll_vote_instructions = module:get_option_string("poll_string_vote_instructions") or "Send a message with an exclamation mark followed by your choice number to vote. Example: !1"; --- construct the poll message stanza -local function build_poll_message(room, message_id, is_end_message) +-- Build the content for poll start and end messages (that will go to the message ) +local function build_poll_message_content(room, is_end_message) local current_poll = room._data.current_poll; if not current_poll then return nil; end - local from = current_poll.occupant_nick; -- this is in fact room.jid/nickname local content = current_poll["muc#roompoll_question"] .. "\n"; @@ -41,7 +40,8 @@ local function build_poll_message(room, message_id, is_end_message) for _, choice_desc in ipairs(current_poll.choices_ordered) do local choice, label = choice_desc.number, choice_desc.label; content = content .. choice .. ': ' .. label; - if total > 0 then + -- if vote over, and at least 1 vote, we add the results. + if is_end_message and total > 0 then local nb = current_poll.votes_by_choices[choice] or 0; local percent = string.format("%.2f", nb * 100 / total); content = content .. " (" .. nb .. "/" .. total .. " = " .. percent .. "%)"; @@ -53,10 +53,23 @@ local function build_poll_message(room, message_id, is_end_message) content = content .. string_poll_vote_instructions .. "\n"; end + return content; +end + +-- construct the poll message stanza. +-- Note: content can be nil, for updates messages. +local function build_poll_message(room, content) + local current_poll = room._data.current_poll; + if not current_poll then + return nil; + end + + local from = current_poll.occupant_nick; -- this is in fact room.jid/nickname + local msg = st.message({ type = "groupchat", from = from, - id = message_id + id = id.long() }, content); msg:tag("occupant-id", { @@ -64,6 +77,14 @@ local function build_poll_message(room, message_id, is_end_message) id = current_poll.occupant_id }):up(); + if content == nil then + -- No content, this is an update message. + -- Adding some hints (XEP-0334): + msg:tag("no-copy", { xmlns = "urn:xmpp:hints" }):up(); + msg:tag("no-store", { xmlns = "urn:xmpp:hints" }):up(); + msg:tag("no-permanent-store", { xmlns = "urn:xmpp:hints" }):up(); + end + -- now we must add some custom XML data, so that compatible clients can display the poll as they want: -- -- Poll question @@ -72,6 +93,11 @@ local function build_poll_message(room, message_id, is_end_message) -- Choice 3 label -- Choice 4 label -- + local total = 0; + for choice, nb in pairs(current_poll.votes_by_choices) do + total = total + nb; + end + local message_attrs = { xmlns = xmlns_poll_message, id = current_poll.poll_id, @@ -85,6 +111,7 @@ local function build_poll_message(room, message_id, is_end_message) for _, choice_desc in ipairs(current_poll.choices_ordered) do local choice, label = choice_desc.number, choice_desc.label; local nb = current_poll.votes_by_choices[choice] or 0; + total = total + nb; msg:text_tag(poll_choice_tag, label, { votes = "" .. nb, choice = choice @@ -101,10 +128,9 @@ local function poll_start_message(room) return nil; end module:log("debug", "Sending the start message for room %s poll", room.jid); - local message_id = id.medium(); - local msg = build_poll_message(room, message_id, false); + local content = build_poll_message_content(room, false); + local msg = build_poll_message(room, content); room:broadcast_message(msg); - return message_id; end -- Send the poll update message @@ -118,17 +144,9 @@ local function send_poll_update_message(room) end module:log("debug", "Sending an update message for room %s poll", room.jid); - local message_id = id.medium(); -- generate a new id - local msg = build_poll_message(room, message_id, false); - - -- the update message is a message (see XEP-0308). - msg:tag('replace', { - xmlns = xmlns_replace; - id = room._data.current_poll.start_message_id; - }):up(); + local msg = build_poll_message(room, nil); room:broadcast_message(msg); - return message_id; end -- Schedule an update of the start message. @@ -162,10 +180,9 @@ local function poll_end_message(room) timer.stop(scheduled_updates[room.jid]); scheduled_updates[room.jid] = nil; end - local message_id = id.medium(); -- generate a new id - local msg = build_poll_message(room, message_id, true); + local content = build_poll_message_content(room, true); + local msg = build_poll_message(room, content); room:broadcast_message(msg); - return message_id; end -- security check: we must remove all specific tags, to be sure nobody tries to spoof polls! @@ -187,14 +204,23 @@ 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; + local occupant = event.occupant; + local origin = event.origin; + if not occupant then + return; + end 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. + + -- Sending an update message to the new occupant. + module:log("debug", "Sending a poll update message to new occupant %s", occupant.jid); + local msg = build_poll_message(room, nil); + msg.attr.to = occupant.jid; + origin.send(msg); end return { diff --git a/prosody-modules/mod_muc_poll/mod_muc_poll.lua b/prosody-modules/mod_muc_poll/mod_muc_poll.lua index 44e376fd..1ef491ca 100644 --- a/prosody-modules/mod_muc_poll/mod_muc_poll.lua +++ b/prosody-modules/mod_muc_poll/mod_muc_poll.lua @@ -96,4 +96,4 @@ 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); +module:hook("muc-occupant-session-new", handle_new_occupant_session, 10); -- must be after subject (20, see Prosody code) diff --git a/prosody-modules/mod_muc_poll/poll.lib.lua b/prosody-modules/mod_muc_poll/poll.lib.lua index 471d17cc..03ab69ad 100644 --- a/prosody-modules/mod_muc_poll/poll.lib.lua +++ b/prosody-modules/mod_muc_poll/poll.lib.lua @@ -128,7 +128,7 @@ local function create_poll(room, fields, occupant) return tonumber(a.number) < tonumber(b.number); end); - room._data.current_poll.start_message_id = poll_start_message(room); + poll_start_message(room); schedule_poll_end(room.jid, room._data.current_poll.end_timestamp); end diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index a95b3b38..88a3d9c7 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -540,6 +540,7 @@ class ProsodyConfigContent { this.muc.set('poll_string_over', loc('poll_is_over')) this.muc.set('poll_string_invalid_choice', loc('poll_choice_invalid')) this.muc.set('poll_string_anonymous_vote_ok', loc('poll_anonymous_vote_ok')) + this.muc.set('poll_string_vote_instructions', loc('poll_vote_instructions_xmpp')) } addMucAdmins (jids: string[]): void {