This commit is contained in:
matty 2024-07-16 15:02:04 -04:00
commit b043e1e9c4
43 changed files with 291 additions and 110 deletions

View File

@ -1,5 +1,18 @@
# Changelog # Changelog
## 11.0.0 (Not Released Yet)
### New features
* Updating ConverseJS, to use upstream (v11 WIP). This comes with many improvments and new features.
## 10.3.1
### Minor changes and fixes
* Moderation delay: fix accessibility on the timer shown to moderators.
* Fix «create new poll» icon.
## 10.3.0 ## 10.3.0
### New features ### New features

View File

@ -15,32 +15,21 @@ set -x
# Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use. # Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use.
# Defaults values: # Defaults values:
CONVERSE_VERSION="v10.1.6" CONVERSE_VERSION="v11.0.0"
CONVERSE_REPO="https://github.com/conversejs/converse.js.git" 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. # You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
CONVERSE_COMMIT="" # 2024-07-15: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="46313ad92c1a861bcb50b9653859cfa9a960ae4a"
# 2024-07-15, FIXME: the following commit includes a quick fix for Converse/#3443, waiting for upstream to be fixed.
CONVERSE_COMMIT="7d65ef8d30a1f3949dbc590b6d27a9d786bf819f"
# 2014-01-16: we are using a custom version, to wait for some PR to be apply upstream. # It is possible to use another repository, if we want some customization that are not upstream (yet):
# This version includes following changes: # CONVERSE_VERSION="livechat"
# - #converse.js/3300: Adding the maxWait option for `debouncedPruneHistory` # # CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
# - #converse.js/3302: debounce MUC sidebar rendering
# - Fix: refresh the MUC sidebar when participants collection is sorted
# - Fix: MUC occupant list does not sort itself on nicknames or roles changes
# - Fix inconsistency between browsers on textarea outlines
# - Fix: room information not correctly refreshed when modifications are made by other users
# This version already includes following changes that will not be merged in ConverseJS upstream:
# - Don't load vCards for all room occupants when the right menu is closed
# - Changing the default avatar, for something very light (to mitigate blinking effect when vCards are loaded)
# - Custom settings livechat_load_all_vcards for the readonly mode
# - Adding "users" icon in the menu toggle button
# - Removing unecessary plugins: headless/pubsub, minimize, notifications, profile, omemo, push, roomlist, dragresize.
# - Destroy room: remove the challenge, and the new JID
# - New config option [colorize_username](https://conversejs.org/docs/html/configuration.html#colorize_username)
# - New loadEmojis hook, to customize emojis at runtime.
# - Fix custom emojis path when assets_path is not the default path.
CONVERSE_VERSION="livechat-10.1.0"
# CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js" CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
# 2024-07-15, fix MUC save.
CONVERSE_COMMIT="58c682b9ba09038beb961e9d8f804c270408ea69"
CONVERSE_COMMIT="bbee0e4e8d2dc43636385cf4cca34f3604f59520"
rootdir="$(pwd)" rootdir="$(pwd)"
src_dir="$rootdir/conversejs" src_dir="$rootdir/conversejs"

View File

@ -8,12 +8,11 @@
* @description This files will override the original ConverseJS index.js file. * @description This files will override the original ConverseJS index.js file.
*/ */
import '@converse/headless'
import './i18n/index.js' import './i18n/index.js'
import 'shared/registry.js' import 'shared/registry.js'
import { CustomElement } from 'shared/components/element' import { CustomElement } from 'shared/components/element'
import { VIEW_PLUGINS } from './shared/constants.js' import { VIEW_PLUGINS } from './shared/constants.js'
import { _converse, converse } from '@converse/headless/core' import { _converse, converse } from '@converse/headless'
import 'shared/styles/index.scss' import 'shared/styles/index.scss'
@ -50,6 +49,9 @@ import '../custom/plugins/terms/index.js'
import '../custom/plugins/poll/index.js' import '../custom/plugins/poll/index.js'
/* END: Removable components */ /* END: Removable components */
// Running some specific livechat patches:
import '../custom/livechat-patch-vcard.js'
import { CORE_PLUGINS } from './headless/shared/constants.js' import { CORE_PLUGINS } from './headless/shared/constants.js'
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js' import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
// We must add our custom plugins to CORE_PLUGINS (so it is white listed): // We must add our custom plugins to CORE_PLUGINS (so it is white listed):
@ -61,7 +63,7 @@ CORE_PLUGINS.push('livechat-converse-poll')
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const) // (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous') ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
_converse.CustomElement = CustomElement _converse.exports.CustomElement = CustomElement
const initialize = converse.initialize const initialize = converse.initialize

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless/core.js' import { api } from '@converse/headless/index.js'
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js' import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
// Here we are patching the vCard plugin, to add some specific optimizations.
import { _converse, api } from '@converse/headless/index.js'
import {
onOccupantAvatarChanged,
setVCardOnModel,
setVCardOnOccupant
} from '@converse/headless/plugins/vcard/utils.js'
const pluginDefinition = _converse.pluggable.plugins['converse-vcard']
const originalInitialize = pluginDefinition.initialize
pluginDefinition.initialize = function initialize () {
const previousListeners = _converse._events.chatRoomInitialized ?? []
originalInitialize.apply(this)
_converse.api.settings.extend({
livechat_load_all_vcards: false
})
// Now we must detect the new chatRoomInitialized listener, and remove it:
const listenersToRemove = []
for (const def of _converse._events.chatRoomInitialized ?? []) {
if (def.callback && !previousListeners.includes(def.callback)) {
listenersToRemove.push(def.callback)
}
}
for (const callback of listenersToRemove) {
console.debug('Livechat patching vcard: we must remove this listener', callback)
api.listen.not('chatRoomInitialized', callback)
}
// Adding the new listener:
api.listen.on('chatRoomInitialized', (m) => {
console.debug('Patched version of the vcard chatRoomInitialized event.')
setVCardOnModel(m)
// loadAll: when in readonly mode (ie: OBS integration), always load all avatars.
const loadAll = api.settings.get('livechat_load_all_vcards') === true
let hiddenOccupants = m.get('hidden_occupants')
if (hiddenOccupants !== true || loadAll) {
m.occupants.forEach(setVCardOnOccupant)
}
m.listenTo(m.occupants, 'add', (occupant) => {
if (hiddenOccupants !== true || loadAll) {
setVCardOnOccupant(occupant)
}
})
m.on('change:hidden_occupants', () => {
hiddenOccupants = m.get('hidden_occupants')
if (hiddenOccupants !== true || loadAll) {
m.occupants.forEach(setVCardOnOccupant)
}
})
m.listenTo(m.occupants, 'change:image_hash', o => onOccupantAvatarChanged(o))
})
}

View File

@ -4,7 +4,7 @@
import { XMLNS_POLL } from '../constants.js' import { XMLNS_POLL } from '../constants.js'
import { tplPollForm } from '../templates/poll-form.js' import { tplPollForm } from '../templates/poll-form.js'
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { converse, api } from '@converse/headless/core' import { converse, api, parsers } from '@converse/headless'
import { webForm2xForm } from '@converse/headless/utils/form' import { webForm2xForm } from '@converse/headless/utils/form'
import { __ } from 'i18n' import { __ } from 'i18n'
import '../styles/poll-form.scss' import '../styles/poll-form.scss'
@ -18,7 +18,6 @@ export default class MUCPollFormView extends CustomElement {
return { return {
model: { type: Object, attribute: true }, model: { type: Object, attribute: true },
modal: { type: Object, attribute: true }, modal: { type: Object, attribute: true },
form_fields: { type: Object, attribute: false },
alert_message: { type: Object, attribute: false }, alert_message: { type: Object, attribute: false },
title: { type: String, attribute: false }, title: { type: String, attribute: false },
instructions: { type: String, attribute: false } instructions: { type: String, attribute: false }
@ -27,6 +26,8 @@ export default class MUCPollFormView extends CustomElement {
_fieldTranslationMap = new Map() _fieldTranslationMap = new Map()
xform = undefined
async initialize () { async initialize () {
this.alert_message = undefined this.alert_message = undefined
if (!this.model) { if (!this.model) {
@ -36,20 +37,18 @@ export default class MUCPollFormView extends CustomElement {
try { try {
this._initFieldTranslations() this._initFieldTranslations()
const stanza = await this._fetchPollForm() const stanza = await this._fetchPollForm()
const query = stanza.querySelector('query') const xform = parsers.parseXForm(stanza)
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0]
if (!xform) { if (!xform) {
throw Error('Missing xform in stanza') throw Error('Missing xform in stanza')
} }
xform.fields?.map(f => this._translateField(f))
this.xform = xform
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? '' this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? '' this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
this.form_fields = Array.from(xform.querySelectorAll('field')).map(field => {
this._translateField(field)
return u.xForm2TemplateResult(field, stanza)
})
} catch (err) { } catch (err) {
console.error(err) console.error(err)
this.alert_message = __('Error') this.alert_message = __('Error')
@ -86,10 +85,10 @@ export default class MUCPollFormView extends CustomElement {
} }
_translateField (field) { _translateField (field) {
const v = field.getAttribute('var') const v = field.var
const label = this._fieldTranslationMap.get(v) const label = this._fieldTranslationMap.get(v)
if (label) { if (label) {
field.setAttribute('label', label) field.label = label
} }
} }

View File

@ -4,7 +4,7 @@
import { tplPoll } from '../templates/poll.js' import { tplPoll } from '../templates/poll.js'
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { converse, _converse, api } from '@converse/headless/core' import { converse, _converse, api } from '@converse/headless'
import '../styles/poll.scss' import '../styles/poll.scss'
export default class MUCPollView extends CustomElement { export default class MUCPollView extends CustomElement {

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse } from '../../../src/headless/core.js' import { _converse, converse } from '../../../src/headless/index.js'
import { getHeadingButtons } from './utils.js' import { getHeadingButtons } from './utils.js'
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js' import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -4,7 +4,7 @@
import { __ } from 'i18n' import { __ } from 'i18n'
import BaseModal from 'plugins/modal/modal.js' import BaseModal from 'plugins/modal/modal.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js' import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
import { html } from 'lit' import { html } from 'lit'

View File

@ -5,6 +5,10 @@
import { converseLocalizedHelpUrl } from '../../../shared/lib/help' import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { html } from 'lit' import { html } from 'lit'
import { __ } from 'i18n' import { __ } from 'i18n'
import { converse } from '@converse/headless'
const u = converse.env.utils
export function tplPollForm (el) { export function tplPollForm (el) {
const i18nOk = __('Ok') const i18nOk = __('Ok')
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@ -13,10 +17,18 @@ export function tplPollForm (el) {
page: 'documentation/user/streamers/polls' page: 'documentation/user/streamers/polls'
}) })
let formFieldTemplates
if (el.xform) {
const fields = el.xform.fields
formFieldTemplates = fields.map(field => {
return u.xFormField2TemplateResult(field)
})
}
return html` return html`
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''} ${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
${ ${
el.form_fields formFieldTemplates
? html` ? html`
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}> <form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
<p class="title"> <p class="title">
@ -30,7 +42,7 @@ export function tplPollForm (el) {
<p class="form-help instructions">${el.instructions}</p> <p class="form-help instructions">${el.instructions}</p>
<div class="form-errors hidden"></div> <div class="form-errors hidden"></div>
${el.form_fields} ${formFieldTemplates}
<fieldset class="buttons form-group"> <fieldset class="buttons form-group">
<input type="submit" class="btn btn-primary" value="${i18nOk}" /> <input type="submit" class="btn btn-primary" value="${i18nOk}" />

View File

@ -63,7 +63,7 @@ function _tplChoice (el, currentPoll, choice, canVote) {
<div class="livechat-progress-bar"> <div class="livechat-progress-bar">
<div <div
role="progressbar" role="progressbar"
style="width: ${percent}%;" style=${'width: ' + percent + '%;'}
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
></div> ></div>
<p> <p>

View File

@ -3,12 +3,12 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { XMLNS_POLL } from './constants.js' import { XMLNS_POLL } from './constants.js'
import { _converse, api } from '../../../src/headless/core.js' import { _converse, api } from '../../../src/headless/index.js'
import { __ } from 'i18n' import { __ } from 'i18n'
export function getHeadingButtons (view, buttons) { export function getHeadingButtons (view, buttons) {
const muc = view.model const muc = view.model
if (muc.get('type') !== _converse.CHATROOMS_TYPE) { if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC. // only on MUC.
return buttons return buttons
} }
@ -32,7 +32,7 @@ export function getHeadingButtons (view, buttons) {
api.modal.show('livechat-converse-poll-form-modal', { model: muc }) api.modal.show('livechat-converse-poll-form-modal', { model: muc })
}, },
a_class: '', a_class: '',
icon_class: 'fa-list-check', // FIXME icon_class: 'fa-square-poll-horizontal',
name: 'muc-create-poll' name: 'muc-create-poll'
}) })

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse, api } from '../../../src/headless/core.js' import { _converse, converse, api } from '../../../src/headless/index.js'
/** /**
* This plugin computes the available width of converse-root, and adds classes * This plugin computes the available width of converse-root, and adds classes

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { tplMUCTaskApp } from '../templates/muc-task-app.js' import { tplMUCTaskApp } from '../templates/muc-task-app.js'

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import tplMucTaskList from '../templates/muc-task-list' import tplMucTaskList from '../templates/muc-task-list'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import tplMucTaskLists from '../templates/muc-task-lists' import tplMucTaskLists from '../templates/muc-task-lists'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { tplMucTask } from '../templates/muc-task' import { tplMucTask } from '../templates/muc-task'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse } from '../../../src/headless/core.js' import { _converse, converse } from '../../../src/headless/index.js'
import { ChatRoomTaskLists } from './task-lists.js' import { ChatRoomTaskLists } from './task-lists.js'
import { ChatRoomTaskList } from './task-list.js' import { ChatRoomTaskList } from './task-list.js'
import { ChatRoomTasks } from './tasks.js' import { ChatRoomTasks } from './tasks.js'
@ -18,9 +18,14 @@ converse.plugins.add('livechat-converse-tasks', {
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'], dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
initialize () { initialize () {
_converse.ChatRoomTaskLists = ChatRoomTaskLists Object.assign(
_converse.ChatRoomTaskList = ChatRoomTaskList _converse.exports,
_converse.ChatRoomTasks = ChatRoomTasks {
ChatRoomTaskLists,
ChatRoomTaskList,
ChatRoomTasks
}
)
_converse.api.settings.extend({ _converse.api.settings.extend({
livechat_task_app_enabled: false, livechat_task_app_enabled: false,

View File

@ -4,7 +4,7 @@
import BaseModal from 'plugins/modal/modal.js' import BaseModal from 'plugins/modal/modal.js'
import tplPickTaskList from './templates/pick-task-list.js' import tplPickTaskList from './templates/pick-task-list.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { __ } from 'i18n' import { __ } from 'i18n'
export default class PickTaskListModal extends BaseModal { export default class PickTaskListModal extends BaseModal {

View File

@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
/** /**
* A chat room task list. * A chat room task list.
* @class * @class
* @namespace _converse.ChatRoomTaskList * @namespace _converse.exports.ChatRoomTaskList
* @memberof _converse * @memberof _converse
*/ */
class ChatRoomTaskList extends Model { class ChatRoomTaskList extends Model {

View File

@ -7,9 +7,9 @@ import { ChatRoomTaskList } from './task-list'
import { initStorage } from '@converse/headless/utils/storage.js' import { initStorage } from '@converse/headless/utils/storage.js'
/** /**
* A list of {@link _converse.ChatRoomTaskList} instances, representing task lists associated to a MUC. * A list of {@link _converse.exports.ChatRoomTaskList} instances, representing task lists associated to a MUC.
* @class * @class
* @namespace _converse.ChatRoomTaskLists * @namespace _converse.exports.ChatRoomTaskLists
* @memberOf _converse * @memberOf _converse
*/ */
class ChatRoomTaskLists extends Collection { class ChatRoomTaskLists extends Collection {

View File

@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
/** /**
* A chat room task. * A chat room task.
* @class * @class
* @namespace _converse.ChatRoomTask * @namespace _converse.exports.ChatRoomTask
* @memberof _converse * @memberof _converse
*/ */
class ChatRoomTask extends Model { class ChatRoomTask extends Model {

View File

@ -7,9 +7,9 @@ import { ChatRoomTask } from './task'
import { initStorage } from '@converse/headless/utils/storage.js' import { initStorage } from '@converse/headless/utils/storage.js'
/** /**
* A list of {@link _converse.ChatRoomTask} instances, representing all tasks associated to a MUC. * A list of {@link _converse.exports.ChatRoomTask} instances, representing all tasks associated to a MUC.
* @class * @class
* @namespace _converse.ChatRoomTasks * @namespace _converse.exports.ChatRoomTasks
* @memberOf _converse * @memberOf _converse
*/ */
class ChatRoomTasks extends Collection { class ChatRoomTasks extends Collection {

View File

@ -4,12 +4,12 @@
import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js' import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js'
import { PubSubManager } from '../../shared/lib/pubsub-manager.js' import { PubSubManager } from '../../shared/lib/pubsub-manager.js'
import { converse, _converse, api } from '../../../src/headless/core.js' import { converse, _converse, api } from '../../../src/headless/index.js'
import { __ } from 'i18n' import { __ } from 'i18n'
export function getHeadingButtons (view, buttons) { export function getHeadingButtons (view, buttons) {
const muc = view.model const muc = view.model
if (muc.get('type') !== _converse.CHATROOMS_TYPE) { if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC. // only on MUC.
return buttons return buttons
} }
@ -74,8 +74,8 @@ function _initChatRoomTaskLists (mucModel) {
return return
} }
mucModel.tasklists = new _converse.ChatRoomTaskLists(undefined, { chatroom: mucModel }) mucModel.tasklists = new _converse.exports.ChatRoomTaskLists(undefined, { chatroom: mucModel })
mucModel.tasks = new _converse.ChatRoomTasks(undefined, { chatroom: mucModel }) mucModel.tasks = new _converse.exports.ChatRoomTasks(undefined, { chatroom: mucModel })
mucModel.taskManager = new PubSubManager( mucModel.taskManager = new PubSubManager(
mucModel.get('jid'), mucModel.get('jid'),
@ -127,7 +127,7 @@ function _destroyChatRoomTaskLists (mucModel) {
} }
export function initOrDestroyChatRoomTaskLists (mucModel) { export function initOrDestroyChatRoomTaskLists (mucModel) {
if (mucModel.get('type') !== _converse.CHATROOMS_TYPE) { if (mucModel.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC. // only on MUC.
return _destroyChatRoomTaskLists(mucModel) return _destroyChatRoomTaskLists(mucModel)
} }

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { html } from 'lit' import { html } from 'lit'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { converse, api } from '../../../src/headless/core.js' import { converse, api } from '../../../src/headless/index.js'
import './components/muc-terms.js' import './components/muc-terms.js'
const { sizzle } = converse.env const { sizzle } = converse.env

View File

@ -4,7 +4,7 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
import { html } from 'lit' import { html } from 'lit'
import tplIcons from '../../../src/shared/templates/icons.js' import tplIcons from '../../../src/shared/components/templates/icons.js'
export default () => { export default () => {
// Here we are adding some additonal icons to ConverseJS defaults // Here we are adding some additonal icons to ConverseJS defaults
@ -23,6 +23,11 @@ export default () => {
<symbol id="icon-circle-question" viewBox="0 0 448 512"> <symbol id="icon-circle-question" viewBox="0 0 448 512">
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/> <path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/>
</symbol> </symbol>
<!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<symbol id="icon-square-poll-horizontal" viewBox="0 0 448 512">
<path d="M448 96c0-35.3-28.7-64-64-64L64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320zM256 160c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0c17.7 0 32 14.3 32 32zm64 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l-192 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l192 0zM192 352c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0c17.7 0 32 14.3 32 32z"/>
</symbol>
</svg> </svg>
` `
} }

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { converse, _converse, api } from '../../../src/headless/core.js' import { converse, _converse, api } from '../../../src/headless/index.js'
const { $build, Strophe, $iq, sizzle } = converse.env const { $build, Strophe, $iq, sizzle } = converse.env
/** /**
@ -50,7 +50,7 @@ export class PubSubManager {
async start () { async start () {
// FIXME: handle errors. Find a way to display to user that this failed. // FIXME: handle errors. Find a way to display to user that this failed.
this.stanzaHandler = _converse.connection.addHandler( this.stanzaHandler = api.connection.get().addHandler(
(message) => { (message) => {
try { try {
this._handleMessage(message) this._handleMessage(message)
@ -79,7 +79,7 @@ export class PubSubManager {
// Note: no need to unsubscribe from the pubsub node, the backend will do when users leave the room. // Note: no need to unsubscribe from the pubsub node, the backend will do when users leave the room.
if (this.stanzaHandler) { if (this.stanzaHandler) {
_converse.connection.deleteHandler(this.stanzaHandler) api.connection.get().deleteHandler(this.stanzaHandler)
this.stanzaHandler = undefined this.stanzaHandler = undefined
} }
} }

View File

@ -4,7 +4,7 @@
import { __ } from 'i18n' import { __ } from 'i18n'
import BaseModal from 'plugins/modal/modal.js' import BaseModal from 'plugins/modal/modal.js'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { html } from 'lit' import { html } from 'lit'
import 'livechat-external-login-content.js' import 'livechat-external-login-content.js'

View File

@ -5,7 +5,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit' import { html } from 'lit'
import { api } from '@converse/headless/core.js' import { api } from '@converse/headless/index.js'
export default () => html` export default () => html`
<div class="inner-content converse-brand row"> <div class="inner-content converse-brand row">

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { _converse, api } from '@converse/headless/core' import { _converse, api } from '@converse/headless'
import { __ } from 'i18n' import { __ } from 'i18n'
import { html } from 'lit' import { html } from 'lit'

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { __ } from 'i18n' import { __ } from 'i18n'
import { _converse, api } from '@converse/headless/core' import { _converse, api } from '@converse/headless'
import { html } from 'lit' import { html } from 'lit'
import tplMucBottomPanel from '../../src/plugins/muc-views/templates/muc-bottom-panel.js' import tplMucBottomPanel from '../../src/plugins/muc-views/templates/muc-bottom-panel.js'
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
@ -79,7 +79,7 @@ class SlowMode extends CustomElement {
api.elements.define('livechat-slow-mode', SlowMode) api.elements.define('livechat-slow-mode', SlowMode)
const tplSlowMode = (o) => { const tplSlowMode = (o) => {
if (!o.can_edit) { return html`` } if (!o.can_post) { return html`` }
return html`<livechat-slow-mode jid=${o.model.get('jid')}>` return html`<livechat-slow-mode jid=${o.model.get('jid')}>`
} }
@ -128,17 +128,9 @@ const tplViewerMode = (o) => {
} }
export default (o) => { export default (o) => {
// ConverseJS 10.x does not handle properly the visitor role in unmoderated rooms.
// See https://github.com/conversejs/converse.js/issues/3428 for more info.
// So we will do a dirty hack here to fix this case.
// Note: ConverseJS 11.x has changed the code, and could be fixed more cleanly (or will be fixed if #3428 is fixed).
if (o.can_edit && o.model.getOwnRole() === 'visitor') {
o.can_edit = false
}
let mutedAnonymousMessage let mutedAnonymousMessage
if ( if (
!o.can_edit && !o.can_post &&
o.model.features?.get?.('x_peertubelivechat_mute_anonymous') && o.model.features?.get?.('x_peertubelivechat_mute_anonymous') &&
_converse.api.settings.get('livechat_specific_is_anonymous') === true _converse.api.settings.get('livechat_specific_is_anonymous') === true
) { ) {

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import tplMUCChatarea from '../../src/plugins/muc-views/templates/muc-chatarea.js' import tplMUCChatarea from '../../src/plugins/muc-views/templates/muc-chatarea.js'
import { html } from 'lit' import { html } from 'lit'

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit' import { html } from 'lit'
import { api } from '@converse/headless/core' import { api } from '@converse/headless'
import { until } from 'lit/directives/until.js' import { until } from 'lit/directives/until.js'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { unsafeHTML } from 'lit/directives/unsafe-html.js' import { unsafeHTML } from 'lit/directives/unsafe-html.js'

View File

@ -44,7 +44,7 @@ module.exports = merge(prod, {
'../../templates/background_logo.js$': path.resolve(__dirname, 'custom/templates/background_logo.js'), '../../templates/background_logo.js$': path.resolve(__dirname, 'custom/templates/background_logo.js'),
'./templates/muc-chatarea.js': path.resolve('custom/templates/muc-chatarea.js'), './templates/muc-chatarea.js': path.resolve('custom/templates/muc-chatarea.js'),
'../templates/icons.js': path.resolve(__dirname, 'custom/shared/components/font-awesome.js'), './templates/icons.js': path.resolve(__dirname, 'custom/shared/components/font-awesome.js'),
'shared/styles/index.scss$': path.resolve(__dirname, 'custom/shared/styles/livechat.scss'), 'shared/styles/index.scss$': path.resolve(__dirname, 'custom/shared/styles/livechat.scss'),

View File

@ -97,6 +97,7 @@ function defaultConverseParams (
pruning_behavior: 'unscrolled', pruning_behavior: 'unscrolled',
colorize_username: true, colorize_username: true,
send_chat_markers: [], send_chat_markers: [],
reuse_scram_keys: false, // for now we don't use this.
// This is a specific settings, that is used in ConverseJS customization, to force avatars loading in readonly mode. // This is a specific settings, that is used in ConverseJS customization, to force avatars loading in readonly mode.
livechat_load_all_vcards: !!forceReadonly, livechat_load_all_vcards: !!forceReadonly,

View File

@ -16,7 +16,7 @@ export const livechatMiniMucHeadPlugin = {
}) })
_converse.api.listen.on('getHeadingButtons', (view: any, buttons: any[]) => { _converse.api.listen.on('getHeadingButtons', (view: any, buttons: any[]) => {
if (view.model.get('type') !== _converse.CHATROOMS_TYPE) { if (view.model.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC. // only on MUC.
return buttons return buttons
} }

View File

@ -15,7 +15,7 @@ export const livechatSpecificsPlugin = {
}) })
_converse.api.listen.on('getHeadingButtons', (view: any, buttons: any[]) => { _converse.api.listen.on('getHeadingButtons', (view: any, buttons: any[]) => {
if (view.model.get('type') !== _converse.CHATROOMS_TYPE) { if (view.model.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC. // only on MUC.
return buttons return buttons
} }
@ -53,6 +53,71 @@ export const livechatSpecificsPlugin = {
return buttons return buttons
}) })
_converse.api.listen.on('getToolbarButtons', (toolbarEl: any, buttons: any[]) => {
// We will replace the toggle occupant button, to change its appearance.
// First, we must find it. We search from the end, because usually it is the last one.
let toggleOccupantButton: any
for (const button of buttons.reverse()) {
if (button.strings?.find((s: string) => s.includes('toggle_occupants'))) { // searching the classname
console.debug('[livechatSpecificsPlugin] found the toggle occupants button', button)
toggleOccupantButton = button
break
}
}
if (!toggleOccupantButton) {
console.debug('[livechatSpecificsPlugin] Did not found the toggle occupants button')
return buttons
}
buttons = buttons.filter(b => b !== toggleOccupantButton)
// Replacing by the new button...
// Note: we don't need to test conditions, we know the button was here.
const i18nHideOccupants = _converse.__('Hide participants')
const i18nShowOccupants = _converse.__('Show participants')
const html = window.converse.env.html
const icon = toolbarEl.hidden_occupants
? html`<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa fa-angle-double-left"
size="1em">
</converse-icon>
<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa users"
size="1em">
</converse-icon>`
: html`<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa users"
size="1em">
</converse-icon>
<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa fa-angle-double-right"
size="1em">
</converse-icon>`
buttons.push(html`
<button class="toggle_occupants right"
title="${toolbarEl.hidden_occupants ? i18nShowOccupants : i18nHideOccupants}"
@click=${toolbarEl.toggleOccupants}>
${icon}
</button>`
)
return buttons
})
// Overriding the MUCHeading custom element, to customize the destroyMUC function:
const MUCHeading = _converse.api.elements.registry['converse-muc-heading']
if (MUCHeading) {
class MUCHeadingOverloaded extends MUCHeading {
async destroy (ev: Event): Promise<void> {
ev.preventDefault()
await destroyMUC(_converse, this.model) // here we call a custom version of destroyMUC
}
}
_converse.api.elements.define('converse-muc-heading', MUCHeadingOverloaded)
}
_converse.api.listen.on('chatRoomViewInitialized', function (this: any, _model: any): void { _converse.api.listen.on('chatRoomViewInitialized', function (this: any, _model: any): void {
// Remove the spinner if present... // Remove the spinner if present...
document.getElementById('livechat-loading-spinner')?.remove() document.getElementById('livechat-loading-spinner')?.remove()
@ -117,7 +182,7 @@ export const livechatSpecificsPlugin = {
}, },
overrides: { overrides: {
ChatRoom: { ChatRoom: {
getActionInfoMessage: function (this: any, code: string, nick: string, actor: any): any { getActionInfoMessage: function getActionInfoMessage (this: any, code: string, nick: string, actor: any): any {
if (code === '303') { if (code === '303') {
// When there is numerous anonymous users joining at the same time, // When there is numerous anonymous users joining at the same time,
// they can all change their nicknames at the same time, generating a log of action messages. // they can all change their nicknames at the same time, generating a log of action messages.
@ -130,6 +195,12 @@ export const livechatSpecificsPlugin = {
} }
} }
return this.__super__.getActionInfoMessage(code, nick, actor) return this.__super__.getActionInfoMessage(code, nick, actor)
},
canPostMessages: function canPostMessages (this: any) {
// ConverseJS does not handle properly the visitor role in unmoderated rooms.
// See https://github.com/conversejs/converse.js/issues/3428 for more info.
// FIXME: if #3428 is fixed, remove this override.
return this.isEntered() && this.getOwnRole() !== 'visitor'
} }
}, },
ChatRoomMessage: { ChatRoomMessage: {
@ -183,3 +254,26 @@ function getOpenPromise (): any {
) )
return promise return promise
} }
async function destroyMUC (_converse: any, model: any): Promise<void> {
const __ = _converse.__
const messages = [__('Are you sure you want to destroy this groupchat?')]
// Note: challenge and newjid make no sens for peertube-plugin-livechat,
// we remove them comparing to the original function.
let fields = [
{
name: 'reason',
label: __('Optional reason for destroying this groupchat'),
placeholder: __('Reason'),
value: undefined
}
]
try {
fields = await _converse.api.confirm(__('Confirm'), messages, fields)
const reason = fields.filter(f => f.name === 'reason').pop()?.value
const newjid = undefined
return model.sendDestroyIQ(reason, newjid).then(() => model.close())
} catch (e) {
console.error(e)
}
}

View File

@ -17,11 +17,11 @@ export const livechatViewerModePlugin = {
livechat_external_auth_reconnect_mode: undefined livechat_external_auth_reconnect_mode: undefined
}) })
const originalGetDefaultMUCNickname = _converse.getDefaultMUCNickname const originalGetDefaultMUCNickname = _converse.exports.getDefaultMUCNickname
if (!originalGetDefaultMUCNickname) { if (!originalGetDefaultMUCNickname) {
console.error('[livechatViewerModePlugin] getDefaultMUCNickname is not initialized.') console.error('[livechatViewerModePlugin] getDefaultMUCNickname is not initialized.')
} else { } else {
Object.assign(_converse, { Object.assign(_converse.exports, {
getDefaultMUCNickname: function (this: any): any { getDefaultMUCNickname: function (this: any): any {
if (!_converse.api.settings.get('livechat_enable_viewer_mode')) { if (!_converse.api.settings.get('livechat_enable_viewer_mode')) {
return originalGetDefaultMUCNickname.apply(this, arguments) return originalGetDefaultMUCNickname.apply(this, arguments)

View File

@ -42,7 +42,7 @@ export const moderationDelayPlugin = {
// Ok... We will add some info about how many remains... // Ok... We will add some info about how many remains...
r.pretty_time = window.converse.env.html` r.pretty_time = window.converse.env.html`
${r.pretty_time}&nbsp;-&nbsp;${Math.round(remains)} ${r.pretty_time}<span aria-hidden="true">&nbsp;-&nbsp;${Math.round(remains)}</span>
` `
// and we must update in 1 second... // and we must update in 1 second...
setTimeout(() => this.requestUpdate(), 1000) setTimeout(() => this.requestUpdate(), 1000)

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "peertube-plugin-livechat", "name": "peertube-plugin-livechat",
"version": "10.3.0", "version": "10.3.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "peertube-plugin-livechat", "name": "peertube-plugin-livechat",
"version": "10.3.0", "version": "10.3.1",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@xmpp/jid": "^0.13.1", "@xmpp/jid": "^0.13.1",

View File

@ -1,7 +1,7 @@
{ {
"name": "peertube-plugin-livechat", "name": "peertube-plugin-livechat",
"description": "NCTV fork of the peertube-plugin-livechat plugin, containing styling and other shit. This will be maintained with upstream.", "description": "NCTV fork of the peertube-plugin-livechat plugin, containing styling and other shit. This will be maintained with upstream.",
"version": "10.3.0", "version": "10.3.1",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"author": { "author": {
"name": "Matty Boombalatty", "name": "Matty Boombalatty",

View File

@ -8,9 +8,11 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-07-10 16:54+0200\n" "POT-Creation-Date: 2024-07-10 16:54+0200\n"
"PO-Revision-Date: 2024-07-05 19:12+0000\n" "PO-Revision-Date: 2024-07-12 09:10+0000\n"
"Last-Translator: Victor Hampel <v.hampel@users.noreply.weblate.framasoft.org>\n" "Last-Translator: Victor Hampel <v.hampel@users.noreply.weblate.framasoft.org>"
"Language-Team: German <https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/>\n" "\n"
"Language-Team: German <https://weblate.framasoft.org/projects/"
"peertube-livechat/peertube-plugin-livechat-documentation/de/>\n"
"Language: de\n" "Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -3272,25 +3274,30 @@ msgstr "Um den Wert für einen bereits bestehenden Raum zu ändern, öffnen Sie
#: support/documentation/content/en/documentation/user/streamers/moderation_delay.md #: support/documentation/content/en/documentation/user/streamers/moderation_delay.md
msgid "Currently, this feature has one known bug: users that join the chat will get all messages, even messages that are still pending for other participants. However, messages sent after they joined will be delayed correctly." msgid "Currently, this feature has one known bug: users that join the chat will get all messages, even messages that are still pending for other participants. However, messages sent after they joined will be delayed correctly."
msgstr "" msgstr ""
"Derzeit gibt es bei dieser Funktion einen bekannten Fehler: Benutzer, die "
"dem Chat beitreten, erhalten alle Nachrichten, auch solche, die noch für "
"andere Teilnehmer ausstehen. Allerdings werden Nachrichten, die nach dem "
"Beitritt gesendet werden, korrekt verzögert."
#. type: Title ## #. type: Title ##
#: support/documentation/content/en/documentation/user/streamers/moderation_delay.md #: support/documentation/content/en/documentation/user/streamers/moderation_delay.md
#, fuzzy, no-wrap #, no-wrap
#| msgid "Share the chat"
msgid "In the chat" msgid "In the chat"
msgstr "Teilen Sie den Chat" msgstr "Im Chat"
#. type: Plain text #. type: Plain text
#: support/documentation/content/en/documentation/user/streamers/moderation_delay.md #: support/documentation/content/en/documentation/user/streamers/moderation_delay.md
msgid "As a moderator, you will see the remaining time (in seconds) before the message is broadcasted, just besides the message datetime." msgid "As a moderator, you will see the remaining time (in seconds) before the message is broadcasted, just besides the message datetime."
msgstr "" msgstr ""
"Als Moderator sehen Sie neben dem Datum der Nachricht auch die verbleibende "
"Zeit (in Sekunden), bevor die Nachricht veröffentlicht wird."
#. type: Plain text #. type: Plain text
#: support/documentation/content/en/documentation/user/streamers/moderation_delay.md #: support/documentation/content/en/documentation/user/streamers/moderation_delay.md
#, fuzzy
#| msgid "![Channel configuration / Moderation delay](/peertube-plugin-livechat/images/moderation_delay_channel_option.png?classes=shadow,border&height=400px)"
msgid "![Moderation delay timer](/peertube-plugin-livechat/images/moderation_delay_timer.png?classes=shadow,border)" msgid "![Moderation delay timer](/peertube-plugin-livechat/images/moderation_delay_timer.png?classes=shadow,border)"
msgstr "![Kanalkonfiguration / Moderationsverzögerung](/peertube-plugin-livechat/images/moderation_delay_channel_option.png?classes=shadow,border&height=400px)" msgstr ""
"![Moderationsverzögerungstimer](/peertube-plugin-livechat/images/"
"moderation_delay_timer.png?classes=shadow,border)"
#. type: Yaml Front Matter Hash Value: description #. type: Yaml Front Matter Hash Value: description
#: build/documentation/pot_in/documentation/user/streamers/moderation.md #: build/documentation/pot_in/documentation/user/streamers/moderation.md
@ -3337,10 +3344,11 @@ msgstr "Über das [Chat Dropdown Menü](/peertube-plugin-livechat/de/documentati
#. type: Plain text #. type: Plain text
#: build/documentation/pot_in/documentation/user/streamers/moderation.md #: build/documentation/pot_in/documentation/user/streamers/moderation.md
#, fuzzy
#| msgid "The video owner will be owner of the chat room. This means he can configure the room, delete it, promote other users as admins, ..."
msgid "The video owner will be owner of the chat room. This means they can configure the room, delete it, promote other users as admins, ..." msgid "The video owner will be owner of the chat room. This means they can configure the room, delete it, promote other users as admins, ..."
msgstr "Der Videobesitzer ist der Besitzer des Chatraums. Das bedeutet, er kann den Raum konfigurieren, löschen, andere Benutzer als Administratoren befördern, ..." msgstr ""
"Der Videobesitzer ist der Besitzer des Chatraums. Das bedeutet, er kann den "
"Raum konfigurieren, löschen, andere Benutzer als Administratoren befördern, "
"..."
#. type: Plain text #. type: Plain text
#: build/documentation/pot_in/documentation/user/streamers/moderation.md #: build/documentation/pot_in/documentation/user/streamers/moderation.md