New features: announcements WIP (#518):

* Front-end implementation finished.
* Refactoring.
This commit is contained in:
John Livingston 2024-09-12 11:17:44 +02:00
parent 8944bb95d8
commit d92bf9073e
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
5 changed files with 197 additions and 104 deletions

View File

@ -13,13 +13,6 @@
--livechat-announcement-color: #000; --livechat-announcement-color: #000;
--livechat-announcement-background-color: #dbf2d8; --livechat-announcement-background-color: #dbf2d8;
--livechat-announcement-border-color: #2ab218; --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-highlight {
@ -28,15 +21,39 @@
--livechat-announcement-border-color: #3075e5; --livechat-announcement-border-color: #3075e5;
} }
&.livechat-warning {
--livechat-announcement-color: #000;
--livechat-announcement-background-color: #fadede;
--livechat-announcement-border-color: #e03e3e;
}
&.livechat-announcement, &.livechat-announcement,
&.livechat-highlight { &.livechat-highlight,
&.livechat-warning {
converse-chat-message-body { converse-chat-message-body {
border: 2px solid var(--livechat-announcement-border-color); border: 2px solid var(--livechat-announcement-border-color);
color: var(--livechat-announcement-color); color: var(--livechat-announcement-color);
background-color: var(--livechat-announcement-background-color); background-color: var(--livechat-announcement-background-color);
min-width: 50%; 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;
}
}
} }

View File

@ -4,6 +4,8 @@
// Important note: loc segments that are declared here must also be in loc.keys.js (for now). // 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_STANDARD: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT: string declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT: string declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING: string

View File

@ -2,6 +2,10 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
interface Current {
announcementType: string | undefined
}
/** /**
* livechat announcements ConverseJS plugin: * livechat announcements ConverseJS plugin:
* with this plugin, moderators can send highlighted/announcements messages. * with this plugin, moderators can send highlighted/announcements messages.
@ -15,108 +19,60 @@ export const livechatAnnouncementsPlugin = {
dependencies: ['converse-muc', 'converse-muc-views'], dependencies: ['converse-muc', 'converse-muc-views'],
initialize: function (this: any) { initialize: function (this: any) {
const _converse = this._converse const _converse = this._converse
const { __ } = _converse
// This is a closure variable, to get the current form status when sending a message. // This is a closure variable, to get the current form status when sending a message.
let currentAnnouncementType: string | undefined const current: Current = {
announcementType: undefined
}
// Overloading the MUCMessageForm to handle the announcement type field (if present). overrideMUCMessageForm(_converse, current)
_converse.api.listen.on('getToolbarButtons', getToolbarButtons.bind(this))
_converse.api.listen.on('chatRoomInitialized', (muc: any) => onAffiliationChange(_converse, muc))
_converse.api.listen.on('getOutgoingMessageAttributes', (chatbox: any, attrs: any) => {
return onGetOutgoingMessageAttributes(current, _converse, chatbox, attrs)
})
_converse.api.listen.on('createMessageStanza', createMessageStanza)
_converse.api.listen.on('parseMUCMessage', parseMUCMessage)
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'] const MUCMessageForm = _converse.api.elements.registry['converse-muc-message-form']
if (MUCMessageForm) { if (MUCMessageForm) {
class MUCMessageFormloaded extends MUCMessageForm { class MUCMessageFormloaded extends MUCMessageForm {
async onFormSubmitted (ev?: Event): Promise<void> { async onFormSubmitted (ev?: Event): Promise<void> {
const announcementSelect = this.querySelector('[name=livechat-announcements]') const announcementSelect = this.querySelector('[name=livechat-announcements]')
currentAnnouncementType = announcementSelect?.selectedOptions?.[0]?.value || undefined current.announcementType = announcementSelect?.selectedOptions?.[0]?.value || undefined
try { try {
await super.onFormSubmitted(ev) await super.onFormSubmitted(ev)
if (announcementSelect) { announcementSelect.selectedIndex = 0 } // set back to default if (announcementSelect) { announcementSelect.selectedIndex = 0 } // set back to default
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
currentAnnouncementType = undefined current.announcementType = undefined
} }
} }
_converse.api.elements.define('converse-muc-message-form', MUCMessageFormloaded) _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)
}
}
} }
/**
* 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<typeof getToolbarButtons>[1] { function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Parameters<typeof getToolbarButtons>[1] {
const _converse = this._converse const _converse = this._converse
const mucModel = toolbarEl.model const mucModel = toolbarEl.model
@ -131,15 +87,26 @@ function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Paramete
const { __ } = _converse const { __ } = _converse
const { html } = window.converse.env const { html } = window.converse.env
const i18n = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE)
const i18nStandard = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD) const i18nStandard = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD)
const i18nAnnouncement = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT) const i18nAnnouncement = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT)
const i18nHighlight = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT) const i18nHighlight = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT)
const i18nWarning = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING)
const select = html`<select name="livechat-announcements"> const select = html`<span class="livechat-announcements-form form-inline">
<label for="livechat-announcements-select">${i18n}</label>
<select
name="livechat-announcements"
id="livechat-announcements-select"
class="form-control form-control-sm"
title=${i18n}
>
<option value="">${i18nStandard}</option> <option value="">${i18nStandard}</option>
<option value="announcement">${i18nAnnouncement}</option>
<option value="highlight">${i18nHighlight}</option> <option value="highlight">${i18nHighlight}</option>
</select>` <option value="announcement">${i18nAnnouncement}</option>
<option value="warning">${i18nWarning}</option>
</select>
</span>`
if (_converse.api.settings.get('visible_toolbar_buttons').emoji) { if (_converse.api.settings.get('visible_toolbar_buttons').emoji) {
// Emojis should be the first entry, so adding select in second place. // 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 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<typeof onGetOutgoingMessageAttributes>[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<Parameters<typeof createMessageStanza>[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<typeof parseMUCMessage>[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)
}
}

View File

@ -64,9 +64,11 @@ const locKeys = [
'message_search', 'message_search',
'message_search_original_nick', 'message_search_original_nick',
'emoji_only_info', 'emoji_only_info',
'announcements_message_type',
'announcements_message_type_standard', 'announcements_message_type_standard',
'announcements_message_type_announcement', 'announcements_message_type_announcement',
'announcements_message_type_highlight' 'announcements_message_type_highlight',
'announcements_message_type_warning'
] ]
module.exports = locKeys module.exports = locKeys

View File

@ -671,6 +671,8 @@ livechat_configuration_channel_no_duplicate_delay_label: Time interval
livechat_configuration_channel_no_duplicate_delay_desc: | livechat_configuration_channel_no_duplicate_delay_desc: |
The interval, in seconds, during which a user can't send again the same message. 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_announcement: Announcement
announcements_message_type_highlight: Highlighted message announcements_message_type_highlight: Highlight
announcements_message_type_warning: Warning