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`
+
+
+
`
+}
+
+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`
+ `
+}
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 = {};