Moderator notes WIP (#144)

This commit is contained in:
John Livingston 2024-07-30 01:24:19 +02:00
parent 20cb668e09
commit eb76e7ebb9
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
12 changed files with 398 additions and 10 deletions

View File

@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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)

View File

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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)

View File

@ -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'],

View File

@ -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 {

View File

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* 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%;
}
}
}
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* 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;
}
}

View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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`
<div draggable="true" class="note-line">
<div class="note-description">${note.get('description') ?? ''}</div>
<button class="note-action" title="${__('Edit')}"
@click=${el.toggleEdit}
>
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
</button>
<button class="note-action" title="${i18nDelete}"
@click=${el.deleteNote}
>
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
</button>
</div>`
: html`
<div class="note-line">
<form class="converse-form" @submit=${el.saveNote}>
${_tplNoteForm(note)}
<fieldset class="form-group">
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
<input type="button" class="btn btn-secondary button-cancel"
value="${__('Cancel')}" @click=${el.toggleEdit}
/>
</fieldset>
</form>
</div>`
}
function _tplNoteForm (note) {
// eslint-disable-next-line no-undef
const i18nNoteDesc = __(LOC_moderator_note_description)
return html`<fieldset class="form-group">
<textarea
class="form-control" name="description"
placeholder="${i18nNoteDesc}"
>${note ? note.get('description') : ''}</textarea>
</fieldset>`
}
export function tplMucCreateNoteForm (notesEl) {
const i18nOk = __('Ok')
const i18nCancel = __('Cancel')
return html`
<form class="notes-create-note converse-form" @submit=${notesEl.submitCreateNote}>
${_tplNoteForm(undefined)}
<fieldset class="form-group">
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
<input type="button" class="btn btn-secondary button-cancel"
value="${i18nCancel}" @click=${notesEl.closeCreateNoteForm}
/>
${!notesEl.create_note_error_message
? ''
: html`<div class="invalid-feedback d-block">${notesEl.create_note_error_message}</div>`
}
</fieldset>
</form>`
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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`<livechat-converse-muc-note .model=${note}></livechat-converse-muc-note>`
})
}`
}
function tplCreateButton (el) {
// eslint-disable-next-line no-undef
const i18nCreateNote = __(LOC_moderator_note_create)
return html`
<div class="notes-actions">
<button class="notes-action" title="${i18nCreateNote}" @click=${el.openCreateNoteForm}>
<converse-icon class="fa fa-plus" size="1em"></converse-icon>
</button>
</div>`
}

View File

@ -86,7 +86,6 @@ function _initChatRoomNotes (mucModel) {
xmlns: XMLNS_NOTE,
collection: mucModel.notes,
fields: {
name: String,
description: String
},
attributes: {

View File

@ -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

View File

@ -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?'

View File

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