From eb76e7ebb91e398a66ba05ef9903382a54811495 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 30 Jul 2024 01:24:19 +0200 Subject: [PATCH] Moderator notes WIP (#144) --- .../plugins/notes/components/muc-note-view.js | 109 ++++++++++++++++++ .../notes/components/muc-notes-view.js | 95 +++++++++++++++ conversejs/custom/plugins/notes/index.js | 2 + conversejs/custom/plugins/notes/notes.js | 19 ++- .../custom/plugins/notes/styles/muc-note.scss | 40 +++++++ .../plugins/notes/styles/muc-notes.scss | 21 ++++ .../plugins/notes/templates/muc-note.js | 71 ++++++++++++ .../plugins/notes/templates/muc-notes.js | 35 ++++++ conversejs/custom/plugins/notes/utils.js | 1 - conversejs/loc.keys.js | 7 +- languages/en.yml | 5 + .../mod_pubsub_peertubelivechat.lua | 3 - 12 files changed, 398 insertions(+), 10 deletions(-) create mode 100644 conversejs/custom/plugins/notes/components/muc-note-view.js create mode 100644 conversejs/custom/plugins/notes/components/muc-notes-view.js create mode 100644 conversejs/custom/plugins/notes/styles/muc-note.scss create mode 100644 conversejs/custom/plugins/notes/styles/muc-notes.scss create mode 100644 conversejs/custom/plugins/notes/templates/muc-note.js create mode 100644 conversejs/custom/plugins/notes/templates/muc-notes.js diff --git a/conversejs/custom/plugins/notes/components/muc-note-view.js b/conversejs/custom/plugins/notes/components/muc-note-view.js new file mode 100644 index 00000000..cdcb4e15 --- /dev/null +++ b/conversejs/custom/plugins/notes/components/muc-note-view.js @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { CustomElement } from 'shared/components/element.js' +import { api } from '@converse/headless' +import { tplMucNote } from '../templates/muc-note' +import { __ } from 'i18n' + +import '../styles/muc-note.scss' + +export default class MUCNoteView extends CustomElement { + static get properties () { + return { + model: { type: Object, attribute: true }, + edit: { type: Boolean, attribute: false } + } + } + + async initialize () { + this.edit = false + if (!this.model) { + return + } + + this.listenTo(this.model, 'change', () => this.requestUpdate()) + } + + render () { + return tplMucNote(this, this.model) + } + + shouldUpdate (changedProperties) { + if (!super.shouldUpdate(...arguments)) { return false } + // When a note is currently edited, and another users change the order, + // it could refresh losing the current form. + // To avoid this, we cancel update here. + // Note: of course, if 'edit' is part of the edited properties, we must update anyway + // (it means we just leaved the form) + if (this.edit && !changedProperties.has('edit')) { + console.info('Canceling an update on note, because it is currently edited', this) + return false + } + return true + } + + async saveNote (ev) { + ev?.preventDefault?.() + + const description = ev.target.description.value + + if ((description ?? '') === '') { return } + + try { + this.querySelectorAll('input[type=submit]').forEach(el => { + el.setAttribute('disabled', true) + el.classList.add('disabled') + }) + + const note = this.model + note.set('description', description) + await note.saveItem() + + this.edit = false + this.requestUpdate() // In case we cancel another update in shouldUpdate + } catch (err) { + console.error(err) + } finally { + this.querySelectorAll('input[type=submit]').forEach(el => { + el.removeAttribute('disabled') + el.classList.remove('disabled') + }) + } + } + + async deleteNote (ev) { + ev?.preventDefault?.() + + // eslint-disable-next-line no-undef + const i18nConfirmDelete = __(LOC_moderator_note_delete_confirm) + + const result = await api.confirm(i18nConfirmDelete) + if (!result) { return } + + try { + await this.model.deleteItem() + } catch (err) { + api.alert( + 'error', __('Error'), [__('Error')] + ) + } + } + + async toggleEdit () { + this.edit = !this.edit + if (this.edit) { + await this.updateComplete + const textarea = this.querySelector('textarea[name="description"]') + if (textarea) { + textarea.focus() + // Placing cursor at the end: + textarea.selectionStart = textarea.value.length + textarea.selectionEnd = textarea.selectionStart + } + } + } +} + +api.elements.define('livechat-converse-muc-note', MUCNoteView) diff --git a/conversejs/custom/plugins/notes/components/muc-notes-view.js b/conversejs/custom/plugins/notes/components/muc-notes-view.js new file mode 100644 index 00000000..902bb356 --- /dev/null +++ b/conversejs/custom/plugins/notes/components/muc-notes-view.js @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { CustomElement } from 'shared/components/element.js' +import { api } from '@converse/headless' +import tplMucNotes from '../templates/muc-notes' +import { __ } from 'i18n' + +import '../styles/muc-notes.scss' + +export default class MUCNotesView extends CustomElement { + currentDraggedNote = null + + static get properties () { + return { + model: { type: Object, attribute: true }, + create_note_error_message: { type: String, attribute: false }, + create_note_opened: { type: Boolean, attribute: false } + } + } + + async initialize () { + this.create_note_error_message = '' + + if (!this.model) { + return + } + + // Adding or removing a new note: we must update. + this.listenTo(this.model, 'add', () => this.requestUpdate()) + this.listenTo(this.model, 'remove', () => this.requestUpdate()) + this.listenTo(this.model, 'sort', () => this.requestUpdate()) + + // this._handleDragStartBinded = this._handleDragStart.bind(this) + // this._handleDragOverBinded = this._handleDragOver.bind(this) + // this._handleDragLeaveBinded = this._handleDragLeave.bind(this) + // this._handleDragEndBinded = this._handleDragEnd.bind(this) + // this._handleDropBinded = this._handleDrop.bind(this) + } + + render () { + return tplMucNotes(this, this.model) + } + + async openCreateNoteForm (ev) { + ev?.preventDefault?.() + this.create_note_opened = true + await this.updateComplete + const textarea = this.querySelector('.notes-create-note textarea[name="description"]') + if (textarea) { + textarea.focus() + } + } + + closeCreateNoteForm (ev) { + ev?.preventDefault?.() + this.create_note_opened = false + } + + async submitCreateNote (ev) { + ev.preventDefault() + + const description = ev.target.description.value + if (this.create_note_error_message) { + this.create_note_error_message = '' + } + + if ((description ?? '') === '') { return } + + try { + this.querySelectorAll('input[type=submit]').forEach(el => { + el.setAttribute('disabled', true) + el.classList.add('disabled') + }) + + await this.model.createNote({ + description: description + }) + + this.closeCreateNoteForm() + } catch (err) { + console.error(err) + // eslint-disable-next-line no-undef + this.create_note_error_message = __(LOC_moderator_notes_create_error) + } finally { + this.querySelectorAll('input[type=submit]').forEach(el => { + el.removeAttribute('disabled') + el.classList.remove('disabled') + }) + } + } +} + +api.elements.define('livechat-converse-muc-notes', MUCNotesView) diff --git a/conversejs/custom/plugins/notes/index.js b/conversejs/custom/plugins/notes/index.js index 7f3127af..a1bd5d66 100644 --- a/conversejs/custom/plugins/notes/index.js +++ b/conversejs/custom/plugins/notes/index.js @@ -9,6 +9,8 @@ import { ChatRoomNotes } from './notes.js' import { initOrDestroyChatRoomNotes, getHeadingButtons, getMessageActionButtons } from './utils.js' import './components/muc-note-app-view.js' +import './components/muc-notes-view.js' +import './components/muc-note-view.js' converse.plugins.add('livechat-converse-notes', { dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'], diff --git a/conversejs/custom/plugins/notes/notes.js b/conversejs/custom/plugins/notes/notes.js index 32a4d9e3..e83b2ad9 100644 --- a/conversejs/custom/plugins/notes/notes.js +++ b/conversejs/custom/plugins/notes/notes.js @@ -27,11 +27,20 @@ class ChatRoomNotes extends Collection { this.on('change:order', () => this.sort()) } - // async createNote (data) { - // console.log('Creating note...') - // await this.chatroom.NoteManager.createItem(this, Object.assign({}, data)) - // console.log('Note created.') - // } + async createNote (data) { + data = Object.assign({}, data) + + if (!data.order) { + data.order = 0 + Math.max( + 0, + ...(this.map(n => n.get('order') ?? 0).filter(o => !isNaN(o))) + ) + } + + console.log('Creating note...') + await this.chatroom.noteManager.createItem(this, data) + console.log('Note created.') + } } export { diff --git a/conversejs/custom/plugins/notes/styles/muc-note.scss b/conversejs/custom/plugins/notes/styles/muc-note.scss new file mode 100644 index 00000000..d3f698d0 --- /dev/null +++ b/conversejs/custom/plugins/notes/styles/muc-note.scss @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +.conversejs { + livechat-converse-muc-note { + padding: 0; + width: 100%; + + .note-line { + border: 1px solid var(--chatroom-head-bg-color); + border-radius: 4px; + display: flex; + flex-flow: row nowrap; + justify-content: space-around; + margin: 0.25em 0; + padding: 0.25em; + column-gap: 0.25em; + width: 100%; + + .note-description { + flex-grow: 2; + white-space: pre-wrap; + } + + .note-action { + background: unset; + border: 0; + padding-left: 0.25em; + padding-right: 0.25em; + } + + form { + width: 100%; + } + } + } +} diff --git a/conversejs/custom/plugins/notes/styles/muc-notes.scss b/conversejs/custom/plugins/notes/styles/muc-notes.scss new file mode 100644 index 00000000..8cea37b8 --- /dev/null +++ b/conversejs/custom/plugins/notes/styles/muc-notes.scss @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +.conversejs { + .notes-actions { + display: flex; + flex-flow: row nowrap; + justify-content: right; + width: 100%; + } + + .notes-action { + background: unset; + border: 0; + padding-left: 0.25em; + padding-right: 0.25em; + } +} diff --git a/conversejs/custom/plugins/notes/templates/muc-note.js b/conversejs/custom/plugins/notes/templates/muc-note.js new file mode 100644 index 00000000..dde1ffec --- /dev/null +++ b/conversejs/custom/plugins/notes/templates/muc-note.js @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { html } from 'lit' +import { __ } from 'i18n' + +export function tplMucNote (el, note) { + // eslint-disable-next-line no-undef + const i18nDelete = __(LOC_moderator_note_delete) + + return !el.edit + ? html` +
+
${note.get('description') ?? ''}
+ + +
` + : html` +
+
+ ${_tplNoteForm(note)} +
+ + +
+
+
` +} + +function _tplNoteForm (note) { + // eslint-disable-next-line no-undef + const i18nNoteDesc = __(LOC_moderator_note_description) + + return html`
+ +
` +} + +export function tplMucCreateNoteForm (notesEl) { + const i18nOk = __('Ok') + const i18nCancel = __('Cancel') + + return html` +
+ ${_tplNoteForm(undefined)} +
+ + + ${!notesEl.create_note_error_message + ? '' + : html`
${notesEl.create_note_error_message}
` + } +
+
` +} diff --git a/conversejs/custom/plugins/notes/templates/muc-notes.js b/conversejs/custom/plugins/notes/templates/muc-notes.js new file mode 100644 index 00000000..8a0aa363 --- /dev/null +++ b/conversejs/custom/plugins/notes/templates/muc-notes.js @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { html } from 'lit' +import { repeat } from 'lit/directives/repeat.js' +import { __ } from 'i18n' +import { tplMucCreateNoteForm } from './muc-note' + +export default function tplMucNotes (el, notes) { + if (!notes) { // if user loses rights + return html`` // FIXME: add a message like "you dont have access"? + } + + return html` + ${ + el.create_note_opened ? tplMucCreateNoteForm(el) : tplCreateButton(el) + } + ${ + repeat(notes, (note) => note.get('id'), (note) => { + return html`` + }) + }` +} + +function tplCreateButton (el) { + // eslint-disable-next-line no-undef + const i18nCreateNote = __(LOC_moderator_note_create) + return html` +
+ +
` +} diff --git a/conversejs/custom/plugins/notes/utils.js b/conversejs/custom/plugins/notes/utils.js index e425d9e3..e06d4e93 100644 --- a/conversejs/custom/plugins/notes/utils.js +++ b/conversejs/custom/plugins/notes/utils.js @@ -86,7 +86,6 @@ function _initChatRoomNotes (mucModel) { xmlns: XMLNS_NOTE, collection: mucModel.notes, fields: { - name: String, description: String }, attributes: { diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js index bb811c3f..9be7414d 100644 --- a/conversejs/loc.keys.js +++ b/conversejs/loc.keys.js @@ -50,7 +50,12 @@ const locKeys = [ 'poll_is_over', 'poll_choice_invalid', 'poll_anonymous_vote_ok', - 'moderator_notes' + 'moderator_notes', + 'moderator_notes_create_error', + 'moderator_note_create', + 'moderator_note_description', + 'moderator_note_delete', + 'moderator_note_delete_confirm' ] module.exports = locKeys diff --git a/languages/en.yml b/languages/en.yml index b0133c1e..376a24b5 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -595,3 +595,8 @@ livechat_configuration_channel_anonymize_moderation_desc: | When this is enabled, moderation actions will be anonymized, to avoid disclosing who is banning/kicking/… occupants. moderator_notes: Moderator notes +moderator_notes_create_error: 'Error when saving the note' +moderator_note_create: 'Create a new note' +moderator_note_description: 'Description' +moderator_note_delete: 'Delete note' +moderator_note_delete_confirm: 'Are you sure you want to delete this note?' diff --git a/prosody-modules/mod_pubsub_peertubelivechat/mod_pubsub_peertubelivechat.lua b/prosody-modules/mod_pubsub_peertubelivechat/mod_pubsub_peertubelivechat.lua index 5b004436..95afee23 100644 --- a/prosody-modules/mod_pubsub_peertubelivechat/mod_pubsub_peertubelivechat.lua +++ b/prosody-modules/mod_pubsub_peertubelivechat/mod_pubsub_peertubelivechat.lua @@ -48,9 +48,6 @@ local lib_pubsub = module:require "pubsub"; local mod_muc = module:depends"muc"; local get_room_from_jid = mod_muc.get_room_from_jid; -local muc_util = module:require "muc/util"; -local valid_roles = muc_util.valid_roles; - -- room_jid => object passed to module:add_items() local mep_service_items = {};