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