diff --git a/CHANGELOG.md b/CHANGELOG.md index 90419e10..f8fb88fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ TODO: replace commit_id by a tag in build-conversejs +### New features + * #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames +* Fullscreen chat: now uses a custom page (in other words: when opening the chat in a new tab, you will have the Peertube menu). ## 8.4.0 diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts index db3d1349..56682db4 100644 --- a/client/admin-plugin-client-plugin.ts +++ b/client/admin-plugin-client-plugin.ts @@ -1,22 +1,14 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { Video } from '@peertube/peertube-types' import type { ProsodyListRoomsResult } from 'shared/lib/types' +import { getBaseRoute } from './utils/uri' interface ActionPluginSettingsParams { npmName: string } -function register ({ registerHook, registerSettingsScript, peertubeHelpers }: RegisterClientOptions): void { - function getBaseRoute (): string { - // NB: this will come with Peertube > 3.2.1 (3.3.0?) - if (peertubeHelpers.getBaseRouterRoute) { - return peertubeHelpers.getBaseRouterRoute() - } - // We are guessing the route with the correct plugin version with this trick: - const staticBase = peertubeHelpers.getBaseStaticRoute() - // we can't use '/plugins/livechat/router', because the loaded html page needs correct relative paths. - return staticBase.replace(/\/static.*$/, '/router') - } +function register (clientOptions: RegisterClientOptions): void { + const { registerHook, registerSettingsScript, peertubeHelpers } = clientOptions registerHook({ target: 'action:admin-plugin-settings.init', @@ -30,7 +22,7 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re diagButtons.forEach(diagButton => { if (diagButton.hasAttribute('href')) { return } // TODO: use a modal instead of a target=_blank - diagButton.setAttribute('href', getBaseRoute() + '/settings/diagnostic') + diagButton.setAttribute('href', getBaseRoute(clientOptions) + '/settings/diagnostic') diagButton.setAttribute('target', '_blank') }) console.log('[peertube-plugin-livechat] Initializing prosody-list-rooms button') @@ -71,7 +63,7 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re container.textContent = '...' listRoomsButton.after(container) - const response = await fetch(getBaseRoute() + '/webchat/prosody-list-rooms', { + const response = await fetch(getBaseRoute(clientOptions) + '/webchat/prosody-list-rooms', { method: 'GET', headers: peertubeHelpers.getAuthHeader() }) @@ -168,7 +160,8 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re // Here we have a channel chat room // The backend should have added informations here // (because the Peertube API can't work with channelId...) - const href = getBaseRoute() + '/webchat/room/' + encodeURIComponent(localpart) + '?forcetype=1' + const href = getBaseRoute(clientOptions) + + '/webchat/room/' + encodeURIComponent(localpart) + '?forcetype=1' if (room.channel?.name) { aEl.href = href // here we know that the channel still exists, so we can open the webchat. const aVideoEl = document.createElement('a') @@ -183,7 +176,8 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re } else if (/^[a-zA-A0-9-]+$/.test(localpart)) { // localpart must be a video uuid. const uuid = localpart - const href = getBaseRoute() + '/webchat/room/' + encodeURIComponent(uuid) + '?forcetype=1' + const href = getBaseRoute(clientOptions) + + '/webchat/room/' + encodeURIComponent(uuid) + '?forcetype=1' const p = fetch('/api/v1/videos/' + uuid, { method: 'GET', headers: peertubeHelpers.getAuthHeader() diff --git a/client/common/configuration/register.ts b/client/common/configuration/register.ts index d4c7f9b7..1d77316f 100644 --- a/client/common/configuration/register.ts +++ b/client/common/configuration/register.ts @@ -1,6 +1,9 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' +import type { InitConverseJSParams } from 'shared/lib/types' import { renderConfigurationHome } from './templates/home' import { renderConfigurationChannel } from './templates/channel' +import { getBaseRoute } from '../../utils/uri' +import { loadConverseJS } from '../../utils/conversejs' /** * Registers stuff related to the user's configuration pages. @@ -12,6 +15,42 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro const settings = await peertubeHelpers.getSettings() if (settings['disable-channel-configuration']) { return } + registerClientRoute({ + route: 'livechat/room', + onMount: async ({ rootEl }) => { + try { + const urlParams = new URLSearchParams(window.location.search) + const roomKey = urlParams.get('room') + if (!roomKey) { + throw new Error('missing room parameter') + } + + const response = await fetch( + getBaseRoute(clientOptions) + '/api/configuration/room/' + encodeURIComponent(roomKey), + { + method: 'GET', + headers: peertubeHelpers.getAuthHeader() + } + ) + if (!response.ok) { + throw new Error('Can\'t get channel configuration options.') + } + + const converseJSParams: InitConverseJSParams = await (response).json() + await loadConverseJS(converseJSParams) + + rootEl.innerHTML = `
+
+
` + + window.initConverse(converseJSParams) + } catch (err) { + console.error('[peertube-plugin-livechat] ' + (err as string)) + rootEl.innerText = await peertubeHelpers.translate(LOC_NOT_FOUND) + } + } + }) + registerClientRoute({ route: 'livechat/configuration', onMount: async ({ rootEl }) => { diff --git a/client/common/configuration/templates/logic/channel.ts b/client/common/configuration/templates/logic/channel.ts index 34ffecb8..5c98ece9 100644 --- a/client/common/configuration/templates/logic/channel.ts +++ b/client/common/configuration/templates/logic/channel.ts @@ -1,6 +1,6 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { ChannelConfiguration, ChannelConfigurationOptions } from 'shared/lib/types' -import { getBaseRoute } from '../../../../videowatch/uri' +import { getBaseRoute } from '../../../../utils/uri' /** * Returns the data that can be feed into the template view diff --git a/client/utils/conversejs.ts b/client/utils/conversejs.ts new file mode 100644 index 00000000..328233a0 --- /dev/null +++ b/client/utils/conversejs.ts @@ -0,0 +1,58 @@ +import type { InitConverseJSParams } from 'shared/lib/types' + +// declare global { +// interface Window { +// converse?: { +// initialize: (args: any) => void +// plugins: { +// add: (name: string, plugin: any) => void +// } +// } +// } +// } + +declare global { + interface Window { + converse?: any + initConverse: Function + } +} + +async function loadCSS (url: string): Promise { + return new Promise((resolve, reject) => { + const css = document.createElement('link') + css.onerror = () => reject(new URIError(`CSS ${url} didn't load correctly.`)) + css.onload = () => resolve() + css.setAttribute('type', 'text/css') + css.setAttribute('rel', 'stylesheet') + css.setAttribute('href', url) + document.head.appendChild(css) + }) +} + +async function loadScript (url: string): Promise { + return new Promise((resolve, reject) => { + const script = document.createElement('script') + script.onerror = () => reject(new URIError(`Script ${url} didn't load correctly.`)) + script.onload = () => resolve() + script.async = true + script.src = url + document.head.appendChild(script) + }) +} + +async function loadConverseJS (converseJSParams: InitConverseJSParams): Promise { + if (!window.converse) { + await Promise.all([ + loadCSS(converseJSParams.staticBaseUrl + 'conversejs/converse.min.css'), + loadScript(converseJSParams.staticBaseUrl + 'conversejs/converse.min.js') + ]) + } + if (!window.initConverse) { + await loadScript(converseJSParams.staticBaseUrl + 'static/builtin.js') + } +} + +export { + loadConverseJS +} diff --git a/client/utils/uri.ts b/client/utils/uri.ts new file mode 100644 index 00000000..e39b73b1 --- /dev/null +++ b/client/utils/uri.ts @@ -0,0 +1,18 @@ +import type { RegisterClientOptions } from '@peertube/peertube-types/client' + +function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent: boolean = false): string { + if (permanent) { + return '/plugins/livechat/router' + } + // NB: this will come with Peertube > 3.2.1 (3.3.0?) + if (peertubeHelpers.getBaseRouterRoute) { + return peertubeHelpers.getBaseRouterRoute() + } + // We are guessing the route with the correct plugin version with this trick: + const staticBase = peertubeHelpers.getBaseStaticRoute() + return staticBase.replace(/\/static.*$/, '/router') +} + +export { + getBaseRoute +} diff --git a/client/videowatch/uri.ts b/client/videowatch/uri.ts index ff1c565b..6f15895d 100644 --- a/client/videowatch/uri.ts +++ b/client/videowatch/uri.ts @@ -1,6 +1,7 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { Video } from '@peertube/peertube-types' import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors' +import { getBaseRoute } from '../utils/uri' import { logger } from './logger' import { computeAutoColors } from './colors' @@ -11,19 +12,6 @@ interface UriOptions { permanent?: boolean } -function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent: boolean = false): string { - if (permanent) { - return '/plugins/livechat/router' - } - // NB: this will come with Peertube > 3.2.1 (3.3.0?) - if (peertubeHelpers.getBaseRouterRoute) { - return peertubeHelpers.getBaseRouterRoute() - } - // We are guessing the route with the correct plugin version with this trick: - const staticBase = peertubeHelpers.getBaseStaticRoute() - return staticBase.replace(/\/static.*$/, '/router') -} - function getIframeUri ( registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {} ): string | null { @@ -98,7 +86,6 @@ export type { UriOptions } export { - getBaseRoute, getIframeUri, getXMPPAddr } diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts index 57a7d896..e3dfaae7 100644 --- a/server/lib/conversejs/params.ts +++ b/server/lib/conversejs/params.ts @@ -1,5 +1,5 @@ import type { RegisterServerOptions, MVideoThumbnail, SettingEntries } from '@peertube/peertube-types' -import type { ConverseJSTheme, InitConverseJSParams } from '../../../shared/lib/types' +import type { ConverseJSTheme, InitConverseJSParams, InitConverseJSParamsError } from '../../../shared/lib/types' import type { RegisterServerOptionsV5 } from '../helpers' import type { LiveChatJSONLDAttributeV1 } from '../federation/types' import { getChannelInfosById, getChannelNameById } from '../database/channel' @@ -11,12 +11,6 @@ import { getBaseRouterRoute, getBaseStaticRoute } from '../helpers' import { getProsodyDomain } from '../prosody/config/domain' import { getBoshUri, getWSUri } from '../uri/webchat' -interface InitConverseJSParamsError { - isError: true - code: 404 | 403 | 500 - message: string -} - interface GetConverseJSParamsParams { readonly?: boolean | 'noscroll' transparent?: boolean diff --git a/server/lib/routers/api/configuration.ts b/server/lib/routers/api/configuration.ts index e42e72b1..9440c007 100644 --- a/server/lib/routers/api/configuration.ts +++ b/server/lib/routers/api/configuration.ts @@ -10,10 +10,24 @@ import { storeChannelConfigurationOptions } from '../../configuration/channel/storage' import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize' +import { getConverseJSParams } from '../../../lib/conversejs/params' async function initConfigurationApiRouter (options: RegisterServerOptions, router: Router): Promise { const logger = options.peertubeHelpers.logger + router.get('/configuration/room/:roomKey', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction): Promise => { + const roomKey = req.params.roomKey + const initConverseJSParam = await getConverseJSParams(options, roomKey, {}) + if (('isError' in initConverseJSParam) && initConverseJSParam.isError) { + res.sendStatus(initConverseJSParam.code) + return + } + res.status(200) + res.json(initConverseJSParam) + } + )) + router.get('/configuration/channel/:channelId', asyncMiddleware([ checkConfigurationEnabledMiddleware(options), getCheckConfigurationChannelMiddleware(options), diff --git a/shared/lib/types.ts b/shared/lib/types.ts index f694d1c9..ed5d439d 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -21,6 +21,12 @@ interface InitConverseJSParams { forceDefaultHideMucParticipants?: boolean } +interface InitConverseJSParamsError { + isError: true + code: 404 | 403 | 500 + message: string +} + interface ProsodyListRoomsResultError { ok: false error: string @@ -87,6 +93,7 @@ interface ChannelConfiguration { export type { ConverseJSTheme, InitConverseJSParams, + InitConverseJSParamsError, ProsodyListRoomsResult, ProsodyListRoomsResultRoom, ChannelInfos,