From d92bf9073edb244388b3dc86a8c123bf0275a98a Mon Sep 17 00:00:00 2001 From: John Livingston Date: Thu, 12 Sep 2024 11:17:44 +0200 Subject: [PATCH] New features: announcements WIP (#518): * Front-end implementation finished. * Refactoring. --- .../custom/shared/styles/_announcements.scss | 35 ++- conversejs/lib/@types/global.d.ts | 2 + .../lib/plugins/livechat-announcements.ts | 254 +++++++++++------- conversejs/loc.keys.js | 4 +- languages/en.yml | 6 +- 5 files changed, 197 insertions(+), 104 deletions(-) diff --git a/conversejs/custom/shared/styles/_announcements.scss b/conversejs/custom/shared/styles/_announcements.scss index 12b5b863..deeddd5c 100644 --- a/conversejs/custom/shared/styles/_announcements.scss +++ b/conversejs/custom/shared/styles/_announcements.scss @@ -13,13 +13,6 @@ --livechat-announcement-color: #000; --livechat-announcement-background-color: #dbf2d8; --livechat-announcement-border-color: #2ab218; - - converse-chat-message-body::first-line { - // Different color for the title line - color: #FFF; - background-color: #2ab218; - text-align: center; - } } &.livechat-highlight { @@ -28,15 +21,39 @@ --livechat-announcement-border-color: #3075e5; } + &.livechat-warning { + --livechat-announcement-color: #000; + --livechat-announcement-background-color: #fadede; + --livechat-announcement-border-color: #e03e3e; + } + &.livechat-announcement, - &.livechat-highlight { + &.livechat-highlight, + &.livechat-warning { converse-chat-message-body { border: 2px solid var(--livechat-announcement-border-color); color: var(--livechat-announcement-color); background-color: var(--livechat-announcement-background-color); min-width: 50%; - padding: 2em; + padding: 1em; } } } + + .livechat-announcements-form { + label { + // only for screen readers + border: 0 !important; + clip: rect(1px, 1px, 1px, 1px) !important; + /* stylelint-disable-next-line property-no-vendor-prefix */ + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; + } + } } diff --git a/conversejs/lib/@types/global.d.ts b/conversejs/lib/@types/global.d.ts index a15031a8..4c24d9e1 100644 --- a/conversejs/lib/@types/global.d.ts +++ b/conversejs/lib/@types/global.d.ts @@ -4,6 +4,8 @@ // Important note: loc segments that are declared here must also be in loc.keys.js (for now). +declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE: string declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD: string declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT: string declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT: string +declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING: string diff --git a/conversejs/lib/plugins/livechat-announcements.ts b/conversejs/lib/plugins/livechat-announcements.ts index 2d4a8a3d..8c52cb67 100644 --- a/conversejs/lib/plugins/livechat-announcements.ts +++ b/conversejs/lib/plugins/livechat-announcements.ts @@ -2,6 +2,10 @@ // // SPDX-License-Identifier: AGPL-3.0-only +interface Current { + announcementType: string | undefined +} + /** * livechat announcements ConverseJS plugin: * with this plugin, moderators can send highlighted/announcements messages. @@ -15,108 +19,60 @@ export const livechatAnnouncementsPlugin = { dependencies: ['converse-muc', 'converse-muc-views'], initialize: function (this: any) { const _converse = this._converse - const { __ } = _converse // This is a closure variable, to get the current form status when sending a message. - let currentAnnouncementType: string | undefined - - // Overloading the MUCMessageForm to handle the announcement type field (if present). - const MUCMessageForm = _converse.api.elements.registry['converse-muc-message-form'] - if (MUCMessageForm) { - class MUCMessageFormloaded extends MUCMessageForm { - async onFormSubmitted (ev?: Event): Promise { - const announcementSelect = this.querySelector('[name=livechat-announcements]') - currentAnnouncementType = announcementSelect?.selectedOptions?.[0]?.value || undefined - try { - await super.onFormSubmitted(ev) - if (announcementSelect) { announcementSelect.selectedIndex = 0 } // set back to default - } catch (err) { - console.log(err) - } - currentAnnouncementType = undefined - } - } - _converse.api.elements.define('converse-muc-message-form', MUCMessageFormloaded) + const current: Current = { + announcementType: undefined } - // Toolbar: adding the announcement type field (if user has rights). + overrideMUCMessageForm(_converse, current) + _converse.api.listen.on('getToolbarButtons', getToolbarButtons.bind(this)) - // When current user affiliation changes, we must refresh the toolbar. - _converse.api.listen.on('chatRoomInitialized', (muc: any) => { - muc.occupants.on('change:affiliation', (occupant: any) => { - if (occupant.get('jid') !== _converse.bare_jid) { // only for myself - return - } - document.querySelectorAll('converse-chat-toolbar').forEach(e => (e as any).requestUpdate?.()) - }) - }) + _converse.api.listen.on('chatRoomInitialized', (muc: any) => onAffiliationChange(_converse, muc)) _converse.api.listen.on('getOutgoingMessageAttributes', (chatbox: any, attrs: any) => { - // For outgoing message, adding the announcement type if there is a current one. - if (!currentAnnouncementType) { return attrs } - - attrs.livechat_announcement_type = currentAnnouncementType - if (currentAnnouncementType === 'announcement') { - attrs.body = '* ' + __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT) + ' * \n' + attrs.body - } - return attrs + return onGetOutgoingMessageAttributes(current, _converse, chatbox, attrs) }) - _converse.api.listen.on('createMessageStanza', async (chat: any, data: any) => { - // Outgoing messages: adding an attribute on body for announcements. - const { message, stanza } = data - const announcementType = message.get('livechat_announcement_type') - if (!announcementType) { - return data - } + _converse.api.listen.on('createMessageStanza', createMessageStanza) - stanza.tree().querySelector('message body')?.setAttribute('x-livechat-announcement-type', announcementType) - return data - }) + _converse.api.listen.on('parseMUCMessage', parseMUCMessage) - _converse.api.listen.on('parseMUCMessage', (stanza: any, attrs: any) => { - // Incoming messages: checking if there is an announcement attribute - const { sizzle } = window.converse.env - const body = sizzle('message body', stanza)?.[0] - if (!body) { return attrs } - - const announcementType = body.getAttribute('x-livechat-announcement-type') - if (!announcementType) { return attrs } - - // Note: we don't check the value here. Will be done in getExtraMessageClasses. - // Moreover, the backend server will ensure that only admins/owners can send this attribute. - attrs.livechat_announcement_type = announcementType - return attrs - }) - - // Overloading the Message class to add CSS for announcements. - const Message = _converse.api.elements.registry['converse-chat-message'] - if (Message) { - class MessageOverloaded extends Message { - getExtraMessageClasses (this: any): string { - // Adding CSS class if the message is an announcement. - let extraClasses = super.getExtraMessageClasses() ?? '' - const announcementType = this.model.get('livechat_announcement_type') - if (!announcementType) { - return extraClasses - } - switch (announcementType) { - case 'announcement': - extraClasses += ' livechat-announcement' - break - case 'highlight': - extraClasses += ' livechat-highlight' - break - } - return extraClasses - } - } - _converse.api.elements.define('converse-chat-message', MessageOverloaded) - } + overrideMessage(_converse) } } +/** + * Overloads the MUCMessageForm to handle the announcement type field (if present) when sending a message. + */ +function overrideMUCMessageForm (_converse: any, current: Current): void { + const MUCMessageForm = _converse.api.elements.registry['converse-muc-message-form'] + if (MUCMessageForm) { + class MUCMessageFormloaded extends MUCMessageForm { + async onFormSubmitted (ev?: Event): Promise { + const announcementSelect = this.querySelector('[name=livechat-announcements]') + current.announcementType = announcementSelect?.selectedOptions?.[0]?.value || undefined + try { + await super.onFormSubmitted(ev) + if (announcementSelect) { announcementSelect.selectedIndex = 0 } // set back to default + } catch (err) { + console.log(err) + } + current.announcementType = undefined + } + } + _converse.api.elements.define('converse-muc-message-form', MUCMessageFormloaded) + } +} + +/** + * Adds the announcement selector in the toolbar for owner/admin. + * @param this the plugin + * @param toolbarEl the toolbar element + * @param buttons the button list + * @returns the updated "button" list + */ function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Parameters[1] { const _converse = this._converse const mucModel = toolbarEl.model @@ -131,15 +87,26 @@ function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Paramete const { __ } = _converse const { html } = window.converse.env + const i18n = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE) const i18nStandard = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD) const i18nAnnouncement = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT) const i18nHighlight = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT) + const i18nWarning = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING) - const select = html`` + const select = html` + + + ` if (_converse.api.settings.get('visible_toolbar_buttons').emoji) { // Emojis should be the first entry, so adding select in second place. @@ -154,3 +121,106 @@ function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Paramete } return buttons } + +/** + * Refreshed the toolbar when current user affiliation changes. + * @param _converse _converse object + * @param muc the current muc + */ +function onAffiliationChange (_converse: any, muc: any): void { + muc.occupants.on('change:affiliation', (occupant: any) => { + if (occupant.get('jid') !== _converse.bare_jid) { // only for myself + return + } + document.querySelectorAll('converse-chat-toolbar').forEach(e => (e as any).requestUpdate?.()) + }) +} + +/** + * For outgoing message, adding the announcement type if there is a current one. + * @param current current object + * @param _converse _converse object + * @param chatbox the chatbox + * @param attrs message attributes + * @returns + */ +function onGetOutgoingMessageAttributes ( + current: Current, + _converse: any, + chatbox: any, + attrs: any +): Parameters[3] { + if (!current.announcementType) { return attrs } + + const { __ } = _converse + attrs.livechat_announcement_type = current.announcementType + if (current.announcementType === 'announcement') { + attrs.body = '* ' + __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT) + ' * \n' + attrs.body + } else if (current.announcementType === 'warning') { + attrs.body = '* ' + __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING) + ' *\n' + attrs.body + } + return attrs +} + +/** + * Outgoing messages: adding an attribute on body for announcements. + * @param chat + * @param data + */ +async function createMessageStanza ( + chat: any, + data: any +): Promise[1]> { + const { message, stanza } = data + const announcementType = message.get('livechat_announcement_type') + if (!announcementType) { + return data + } + + stanza.tree().querySelector('message body')?.setAttribute('x-livechat-announcement-type', announcementType) + return data +} + +/** + * Incoming messages: checking if there is an announcement attribute, and adding it in computed attributes. + * @param stanza + * @param attrs + */ +function parseMUCMessage (stanza: any, attrs: any): Parameters[1] { + const { sizzle } = window.converse.env + const body = sizzle('message body', stanza)?.[0] + if (!body) { return attrs } + + const announcementType = body.getAttribute('x-livechat-announcement-type') + if (!announcementType) { return attrs } + + // Note: we don't check the value here. Will be done in getExtraMessageClasses. + // Moreover, the backend server will ensure that only admins/owners can send this attribute. + attrs.livechat_announcement_type = announcementType + return attrs +} + +/** + * Overloading the Message class to add CSS for announcements. + * @param _converse + */ +function overrideMessage (_converse: any): void { + const Message = _converse.api.elements.registry['converse-chat-message'] + if (Message) { + class MessageOverloaded extends Message { + getExtraMessageClasses (this: any): string { + // Adding CSS class if the message is an announcement. + let extraClasses = super.getExtraMessageClasses() ?? '' + const announcementType: string | undefined = this.model.get('livechat_announcement_type') + if (!announcementType) { + return extraClasses + } + if (['announcement', 'highlight', 'warning'].includes(announcementType)) { + extraClasses += ' livechat-' + announcementType + } + return extraClasses + } + } + _converse.api.elements.define('converse-chat-message', MessageOverloaded) + } +} diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js index 1a1f0d4e..019c63bd 100644 --- a/conversejs/loc.keys.js +++ b/conversejs/loc.keys.js @@ -64,9 +64,11 @@ const locKeys = [ 'message_search', 'message_search_original_nick', 'emoji_only_info', + 'announcements_message_type', 'announcements_message_type_standard', 'announcements_message_type_announcement', - 'announcements_message_type_highlight' + 'announcements_message_type_highlight', + 'announcements_message_type_warning' ] module.exports = locKeys diff --git a/languages/en.yml b/languages/en.yml index d4ba3f5c..81cce586 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -671,6 +671,8 @@ livechat_configuration_channel_no_duplicate_delay_label: Time interval livechat_configuration_channel_no_duplicate_delay_desc: | The interval, in seconds, during which a user can't send again the same message. -announcements_message_type_standard: Standard message +announcements_message_type: Message type +announcements_message_type_standard: Standard announcements_message_type_announcement: Announcement -announcements_message_type_highlight: Highlighted message +announcements_message_type_highlight: Highlight +announcements_message_type_warning: Warning