From 8944bb95d830b7925da401a2ea81e7a17864c30e Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 11 Sep 2024 19:21:57 +0200 Subject: [PATCH] New features: announcements WIP (#518). --- CHANGELOG.md | 3 +- build-client.js | 11 +- conversejs/build-conversejs.sh | 6 +- conversejs/builtin.ts | 4 + .../custom/shared/styles/_announcements.scss | 42 +++++ conversejs/custom/shared/styles/livechat.scss | 1 + conversejs/lib/@types/global.d.ts | 9 + conversejs/lib/converse-params.ts | 3 +- .../lib/plugins/livechat-announcements.ts | 156 ++++++++++++++++++ conversejs/loc.keys.js | 5 +- languages/en.yml | 4 + 11 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 conversejs/custom/shared/styles/_announcements.scss create mode 100644 conversejs/lib/@types/global.d.ts create mode 100644 conversejs/lib/plugins/livechat-announcements.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6907a812..d5095abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 12.0.0 (Not Released Yet) TODO Before releasing: -* update ConverseJS with latest merges. +* update ConverseJS with latest merges (there are currently some known bugs). ### Importante Notes @@ -15,6 +15,7 @@ It also requires NodeJS 16 or superior (same as Peertube 5.2.0.) * #131: Emoji only mode. * #516: new option for the moderation bot: forbid duplicate messages. * #517: new option for the moderation bot: forbid messages with too many special characters. +* #518: moderators can send announcements and highlighted messages. ### Minor changes and fixes diff --git a/build-client.js b/build-client.js index 2748d406..461be46c 100644 --- a/build-client.js +++ b/build-client.js @@ -15,7 +15,7 @@ const clientFiles = [ 'admin-plugin-client-plugin' ] -function loadLocs() { +function loadLocs(globalFile) { // Loading english strings, so we can inject them as constants. const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json') if (!fs.existsSync(refFile)) { @@ -25,7 +25,6 @@ function loadLocs() { // Reading client/@types/global.d.ts, to have a list of needed localized strings. const r = {} - const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts') const globalFileContent = '' + fs.readFileSync(globalFile) const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm) for (const match of matches) { @@ -41,7 +40,7 @@ function loadLocs() { const define = Object.assign({ PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name), PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, '')) -}, loadLocs()) +}, loadLocs(path.resolve(__dirname, 'client', '@types', 'global.d.ts'))) const configs = clientFiles.map(f => ({ entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ], @@ -59,8 +58,14 @@ const configs = clientFiles.map(f => ({ outfile: path.resolve(__dirname, 'dist/client', f + '.js'), })) +const defineBuiltin = Object.assign( + {}, + loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts')) +) + configs.push({ entryPoints: ["./conversejs/builtin.ts"], + define: defineBuiltin, bundle: true, minify: true, sourcemap, diff --git a/conversejs/build-conversejs.sh b/conversejs/build-conversejs.sh index c0720784..d752b37d 100644 --- a/conversejs/build-conversejs.sh +++ b/conversejs/build-conversejs.sh @@ -18,8 +18,8 @@ set -x CONVERSE_VERSION="v11.0.0" CONVERSE_REPO="https://github.com/conversejs/converse.js.git" # You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches. -# 2024-09-02: using Converse upstream (v11 WIP). -CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2" +# 2024-09-11: using Converse upstream (v11 WIP). +CONVERSE_COMMIT="b5452466b90ff646e9ba5aa19e572a8ba958db83" # It is possible to use another repository, if we want some customization that are not upstream (yet): # CONVERSE_VERSION="livechat" @@ -30,7 +30,7 @@ CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2" # 2024-09-03: include badges short label and quick fix for sendMessage button CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js" CONVERSE_VERSION="livechat-12.0.0" -CONVERSE_COMMIT="a910586fa83bd64db7182add6fc4bbf71cef0ae8" +# CONVERSE_COMMIT="" rootdir="$(pwd)" src_dir="$rootdir/conversejs" diff --git a/conversejs/builtin.ts b/conversejs/builtin.ts index 28cfc320..19ff31f0 100644 --- a/conversejs/builtin.ts +++ b/conversejs/builtin.ts @@ -23,6 +23,7 @@ import { livechatViewerModePlugin } from './lib/plugins/livechat-viewer-mode' import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head' import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis' import { moderationDelayPlugin } from './lib/plugins/moderation-delay' +import { livechatAnnouncementsPlugin } from './lib/plugins/livechat-announcements' declare global { interface Window { @@ -37,6 +38,7 @@ declare global { html: Function sizzle: Function dayjs: Function + __: Function } } initConversePlugins: typeof initConversePlugins @@ -76,6 +78,8 @@ function initConversePlugins (peertubeEmbedded: boolean): void { converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin) converse.plugins.add('converse-moderation-delay', moderationDelayPlugin) + + converse.plugins.add('livechatAnnouncementsPlugin', livechatAnnouncementsPlugin) } window.initConversePlugins = initConversePlugins diff --git a/conversejs/custom/shared/styles/_announcements.scss b/conversejs/custom/shared/styles/_announcements.scss new file mode 100644 index 00000000..12b5b863 --- /dev/null +++ b/conversejs/custom/shared/styles/_announcements.scss @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// FIXME: this should be with the livechat-announcement plugin. +// But for now, there is no way to build scss from there. + +#conversejs { + .message.chat-msg { + &.livechat-announcement { + --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 { + --livechat-announcement-color: #000; + --livechat-announcement-background-color: #dce8fa; + --livechat-announcement-border-color: #3075e5; + } + + &.livechat-announcement, + &.livechat-highlight { + 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; + } + } + } +} diff --git a/conversejs/custom/shared/styles/livechat.scss b/conversejs/custom/shared/styles/livechat.scss index bd398c4e..cc3797e2 100644 --- a/conversejs/custom/shared/styles/livechat.scss +++ b/conversejs/custom/shared/styles/livechat.scss @@ -7,6 +7,7 @@ @import "./variables"; @import "shared/styles/index"; @import "./peertubetheme"; +@import "./announcements"; body.livechat-iframe { #conversejs .chat-head { diff --git a/conversejs/lib/@types/global.d.ts b/conversejs/lib/@types/global.d.ts new file mode 100644 index 00000000..a15031a8 --- /dev/null +++ b/conversejs/lib/@types/global.d.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +// Important note: loc segments that are declared here must also be in loc.keys.js (for now). + +declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD: string +declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT: string +declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT: string diff --git a/conversejs/lib/converse-params.ts b/conversejs/lib/converse-params.ts index cbf28ed8..e1c9927a 100644 --- a/conversejs/lib/converse-params.ts +++ b/conversejs/lib/converse-params.ts @@ -86,7 +86,8 @@ function defaultConverseParams ( 'livechatDisconnectOnUnloadPlugin', 'converse-slow-mode', 'livechatEmojis', - 'converse-moderation-delay' + 'converse-moderation-delay', + 'livechatAnnouncementsPlugin' ], show_retraction_warning: false, // No need to use this warning (except if we open to external clients?) muc_show_info_messages: mucShowInfoMessages, diff --git a/conversejs/lib/plugins/livechat-announcements.ts b/conversejs/lib/plugins/livechat-announcements.ts new file mode 100644 index 00000000..2d4a8a3d --- /dev/null +++ b/conversejs/lib/plugins/livechat-announcements.ts @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +/** + * livechat announcements ConverseJS plugin: + * with this plugin, moderators can send highlighted/announcements messages. + * + * Moderators will have a special select field in the chat toolbar, so that they can choose a messaging style. + * These special messages will have a first line with a generated title (for XMPP compatibility). + * They will also have a special attribute on the body tag. + * This attribute will be used to apply some CSS with this plugin. + */ +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) + } + + // Toolbar: adding the announcement type field (if user has rights). + _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('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 + }) + + _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 + } + + stanza.tree().querySelector('message body')?.setAttribute('x-livechat-announcement-type', announcementType) + return data + }) + + _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) + } + } +} + +function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Parameters[1] { + const _converse = this._converse + const mucModel = toolbarEl.model + if (!toolbarEl.is_groupchat) { + return buttons + } + const myself = mucModel.getOwnOccupant() + if (!myself || !['admin', 'owner'].includes(myself.get('affiliation') as string)) { + return buttons + } + + const { __ } = _converse + const { html } = window.converse.env + + const i18nStandard = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD) + const i18nAnnouncement = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT) + const i18nHighlight = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT) + + const select = html`` + + if (_converse.api.settings.get('visible_toolbar_buttons').emoji) { + // Emojis should be the first entry, so adding select in second place. + buttons = [ + buttons.shift(), + select, + ...buttons + ] + } else { + // Adding the select in first place. + buttons.unshift(select) + } + return buttons +} diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js index 997726db..1a1f0d4e 100644 --- a/conversejs/loc.keys.js +++ b/conversejs/loc.keys.js @@ -63,7 +63,10 @@ const locKeys = [ 'search_occupant_message', 'message_search', 'message_search_original_nick', - 'emoji_only_info' + 'emoji_only_info', + 'announcements_message_type_standard', + 'announcements_message_type_announcement', + 'announcements_message_type_highlight' ] module.exports = locKeys diff --git a/languages/en.yml b/languages/en.yml index 90574a22..d4ba3f5c 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -670,3 +670,7 @@ livechat_configuration_channel_no_duplicate_desc: | 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_announcement: Announcement +announcements_message_type_highlight: Highlighted message