Moderator notes WIP (#144)

This commit is contained in:
John Livingston 2024-07-31 15:53:19 +02:00
parent e81a7c90b8
commit a46425d51f
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
15 changed files with 183 additions and 31 deletions

View File

@ -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-07-30: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="9ddf6e7b7a83fdc04c8b55f0f470e59c09283a39"
# 2024-07-31: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="2f8cfc02d04bb6191b3b9facb706e475836279f5"
# # 2024-07-31: testing the jcbrand/bootstrap5 branch
# CONVERSE_COMMIT="e5edeec997d53a8720470a49685be123e8688e1c"

View File

@ -0,0 +1,31 @@
async function openNotes () {
const appElement = document.querySelector('livechat-converse-muc-note-app')
if (!appElement) {
throw new Error('Cant find Note App Element')
}
await appElement.showApp()
await appElement.updateComplete // waiting for the app to be open
const notesElement = appElement.querySelector('livechat-converse-muc-notes')
if (!notesElement) {
throw new Error('Cant find Notes Element')
}
await notesElement.updateComplete
return notesElement
}
async function openCreateNoteForm (occupant) {
const notesElement = await openNotes()
await notesElement.openCreateNoteForm(undefined, occupant)
}
async function searchNotesAbout (occupant) {
const notesElement = await openNotes()
await notesElement.filterNotes({ occupant })
}
export default {
openNotes,
openCreateNoteForm,
searchNotesAbout
}

View File

@ -13,7 +13,8 @@ export default class MUCNoteView extends CustomElement {
static get properties () {
return {
model: { type: Object, attribute: true },
edit: { type: Boolean, attribute: false }
edit: { type: Boolean, attribute: false },
is_ocupant_filter: { type: Boolean, attribute: true }
}
}

View File

@ -15,7 +15,8 @@ export default class MUCNotesView extends DraggablesCustomElement {
model: { type: Object, attribute: true },
create_note_error_message: { type: String, attribute: false },
create_note_opened: { type: Boolean, attribute: false },
create_note_for_occupant: { type: Object, attribute: false }
create_note_about_occupant: { type: Object, attribute: false },
occupant_filter: { type: Object, attribute: false }
}
}
@ -45,7 +46,11 @@ export default class MUCNotesView extends DraggablesCustomElement {
async openCreateNoteForm (ev, occupant) {
ev?.preventDefault?.()
this.create_note_opened = true
this.create_note_for_occupant = occupant ?? undefined
this.create_note_about_occupant = occupant ?? undefined
if (this.create_note_about_occupant === undefined && this.occupant_filter) {
// if we have a current filter, we can use it for the new note.
this.create_note_about_occupant = this.occupant_filter
}
await this.updateComplete
const textarea = this.querySelector('.notes-create-note textarea[name="description"]')
if (textarea) {
@ -56,7 +61,11 @@ export default class MUCNotesView extends DraggablesCustomElement {
closeCreateNoteForm (ev) {
ev?.preventDefault?.()
this.create_note_opened = false
this.create_note_for_occupant = undefined
this.create_note_about_occupant = undefined
}
filterNotes (filters) {
this.occupant_filter = filters?.occupant || undefined
}
async submitCreateNote (ev) {

View File

@ -7,6 +7,7 @@ import { XMLNS_NOTE } from './constants.js'
import { ChatRoomNote } from './note.js'
import { ChatRoomNotes } from './notes.js'
import { initOrDestroyChatRoomNotes, getHeadingButtons, getMessageActionButtons } from './utils.js'
import notesApi from './api.js'
import './components/muc-note-app-view.js'
import './components/muc-notes-view.js'
@ -30,6 +31,10 @@ converse.plugins.add('livechat-converse-notes', {
livechat_note_app_restore: false // should we open the app by default if it was previously oppened?
})
Object.assign(_converse.api, {
livechat_notes: notesApi
})
_converse.api.listen.on('chatRoomInitialized', muc => {
muc.session.on('change:connection_status', _session => {
// When joining a room, initializing the Notes object (if user has access),

View File

@ -27,6 +27,8 @@
}
& > ul {
font-weight: lighter;
font-size: 0.75em;
list-style: none;
text-align: right;
}

View File

@ -20,8 +20,11 @@
column-gap: 0.25em;
width: 100%;
.note-description {
.note-content {
flex-grow: 2;
}
.note-description {
white-space: pre-wrap;
}

View File

@ -18,4 +18,21 @@
padding-left: 0.25em;
padding-right: 0.25em;
}
.notes-filters {
border: 1px solid var(--chatroom-head-bg-color);
border-radius: 4px;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
margin: 0.25em 0;
padding: 0.25em;
column-gap: 0.25em;
width: 100%;
livechat-converse-muc-note-occupant {
flex-grow: 2;
}
}
}

View File

@ -27,7 +27,7 @@ export function tplMucNoteOccupant (el, occupant) {
</a>
${
el.full_display
? html`<ul>
? html`<ul aria-hidden="true">
${jid ? html`<li title=${__('XMPP Address')}>${jid}</li>` : ''}
${occupantId ? html`<li title=${__('Occupant Id')}>${occupantId}</li>` : ''}
</ul>`

View File

@ -2,27 +2,44 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless'
import { html } from 'lit'
import { __ } from 'i18n'
export function tplMucNote (el, note) {
// eslint-disable-next-line no-undef
const i18nDelete = __(LOC_moderator_note_delete)
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_search_for_participant)
const aboutOccupant = note.getAboutOccupant()
return !el.edit
? html`
<div draggable="true" class="note-line draggables-line">
<div class="note-description">${note.get('description') ?? ''}</div>
<div class="note-content">
${
aboutOccupant
? html`
<livechat-converse-muc-note-occupant
.full_display=${el.is_ocupant_filter}
.model=${aboutOccupant}
></livechat-converse-muc-note-occupant>`
: ''
}
<div class="note-description">${note.get('description') ?? ''}</div>
</div>
${
aboutOccupant && el.is_ocupant_filter
? ''
: html`
<button type="button" class="note-action" @click=${ev => {
ev.preventDefault()
api.livechat_notes.searchNotesAbout(aboutOccupant)
}}>
<converse-icon class="fa fa-magnifying-glass" size="1em" title=${i18nSearch}></converse-icon>
</button>`
}
<button class="note-action" title="${__('Edit')}"
@click=${el.toggleEdit}
>

View File

@ -7,6 +7,57 @@ import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n'
import { tplMucCreateNoteForm } from './muc-note'
function tplFilters (el) {
const filterOccupant = el.occupant_filter
if (!filterOccupant) { return '' }
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_filters)
return html`
<div class="notes-filters">
<converse-icon class="fa fa-magnifying-glass" size="1em" title=${i18nSearch}></converse-icon>
${
filterOccupant
? html`<livechat-converse-muc-note-occupant
full_display=${true}
.model=${filterOccupant}
></livechat-converse-muc-note-occupant>`
: ''
}
<button class="notes-action" @click=${(ev) => {
ev?.preventDefault()
el.filterNotes({})
}} title="${__('Close')}">
<converse-icon class="fa fa-times" size="1em"></converse-icon>
</button>
</div>
<hr/>
`
}
function isFiltered (el, note) {
const filterOccupant = el.occupant_filter
if (!filterOccupant) { return false }
const noteOccupant = note.getAboutOccupant()
// there is an occupant filter, so if current note has no associated occupant, we can pass.
if (!noteOccupant) { return true }
if (noteOccupant === filterOccupant) {
// Yes!
return false
}
// We will also test for nickname, so that we can found anonymous users
// (they can have multiple associated occupants)
if (filterOccupant.get('nick') && filterOccupant.get('nick') === noteOccupant.get('nick')) {
return false
}
return true
}
export default function tplMucNotes (el, notes) {
if (!notes) { // if user loses rights
return html`` // FIXME: add a message like "you dont have access"?
@ -14,11 +65,17 @@ export default function tplMucNotes (el, notes) {
return html`
${
el.create_note_opened ? tplMucCreateNoteForm(el, el.create_note_for_occupant) : tplCreateButton(el)
el.create_note_opened ? tplMucCreateNoteForm(el, el.create_note_about_occupant) : tplCreateButton(el)
}
${tplFilters(el)}
${
repeat(notes, (note) => note.get('id'), (note) => {
return html`<livechat-converse-muc-note .model=${note}></livechat-converse-muc-note>`
return isFiltered(el, note)
? ''
: html`<livechat-converse-muc-note
.model=${note}
.is_ocupant_filter=${!!el.occupant_filter}
></livechat-converse-muc-note>`
})
}`
}

View File

@ -51,29 +51,30 @@ export function getMessageActionButtons (messageActionsEl, buttons) {
if (messageModel.occupant) {
// eslint-disable-next-line no-undef
const i18nCreate = __(LOC_moderator_note_create_for_participant)
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_search_for_participant)
buttons.push({
i18n_text: i18nCreate,
handler: async (ev) => {
ev.preventDefault()
const appElement = document.querySelector('livechat-converse-muc-note-app')
if (!appElement) {
throw new Error('Cant find Note App Element')
}
await appElement.showApp()
await appElement.updateComplete // waiting for the app to be open
const notesElement = appElement.querySelector('livechat-converse-muc-notes')
if (!notesElement) {
throw new Error('Cant find Notes Element')
}
await notesElement.updateComplete
notesElement.openCreateNoteForm(undefined, messageModel.occupant)
await api.livechat_notes.openCreateNoteForm(messageModel.occupant)
},
button_class: '',
icon_class: 'fa fa-note-sticky',
name: 'muc-note-create-for-occupant'
})
buttons.push({
i18n_text: i18nSearch,
handler: async (ev) => {
ev.preventDefault()
await api.livechat_notes.searchNotesAbout(messageModel.occupant)
},
button_class: '',
icon_class: 'fa fa-magnifying-glass',
name: 'muc-note-search-for-occupant'
})
}
return buttons

View File

@ -33,6 +33,11 @@ export default () => {
<symbol id="icon-note-sticky" viewBox="0 0 448 512">
<path d="M64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l224 0 0-80c0-17.7 14.3-32 32-32l80 0 0-224c0-8.8-7.2-16-16-16L64 80zM288 480L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32l320 0c35.3 0 64 28.7 64 64l0 224 0 5.5c0 17-6.7 33.3-18.7 45.3l-90.5 90.5c-12 12-28.3 18.7-45.3 18.7l-5.5 0z"/>
</symbol>
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<symbol id="icon-magnifying-glass" viewBox="0 0 512 512">
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/>
</symbol>
</svg>
`
}

View File

@ -56,7 +56,9 @@ const locKeys = [
'moderator_note_description',
'moderator_note_delete',
'moderator_note_delete_confirm',
'moderator_note_create_for_participant'
'moderator_note_create_for_participant',
'moderator_note_search_for_participant',
'moderator_note_filters'
]
module.exports = locKeys

View File

@ -600,4 +600,6 @@ 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?'
moderator_note_create_for_participant: 'Create a new note for this participant'
moderator_note_create_for_participant: 'Create a new note about this participant'
moderator_note_search_for_participant: 'Search notes about this participant'
moderator_note_filters: 'Search filters'