diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc1112b..0bfdbb19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ TODO: replace commit_id by a tag in build-conversejs +**Breaking changes**: + +* if you were adding custom CSS to livechat iframe, it could be broken, as the livechat is no more included in an iframe. Your custom styles are now added on a `div` element. + ### New features * #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames diff --git a/assets/styles/style.scss b/assets/styles/style.scss index c388572d..bb0bd0c8 100644 --- a/assets/styles/style.scss +++ b/assets/styles/style.scss @@ -67,7 +67,7 @@ } } -#peertube-plugin-livechat-container iframe { +#peertube-plugin-livechat-container converse-root { border: 1px solid black; min-height: 30vh; height: 100%; diff --git a/client/common-client-plugin.ts b/client/common-client-plugin.ts index e8434cd9..a4660355 100644 --- a/client/common-client-plugin.ts +++ b/client/common-client-plugin.ts @@ -15,7 +15,7 @@ async function register (clientOptions: RegisterClientOptions): Promise { console.warn( '[peertube-plugin-livechat navigation-end] ' + 'It seems that action:router.navigation-end was called after action:video-watch.video.loaded. ' + - 'No removing the chat from the DOM.' + 'Not removing the chat from the DOM.' ) return } diff --git a/client/common/configuration/register.ts b/client/common/configuration/register.ts index 01adfeda..81e74612 100644 --- a/client/common/configuration/register.ts +++ b/client/common/configuration/register.ts @@ -1,9 +1,7 @@ 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' +import { displayConverseJS } from '../../utils/conversejs' /** * Registers stuff related to the user's configuration pages. @@ -29,33 +27,7 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro container.classList.add('livechat-embed-fullpage') rootEl.append(container) - const converseRoot = document.createElement('converse-root') - converseRoot.classList.add('theme-peertube') - container.append(converseRoot) - - const spinner = document.createElement('div') - spinner.classList.add('livechat-spinner') - spinner.setAttribute('id', 'livechat-loading-spinner') - spinner.innerHTML = '
' - container.prepend(spinner) - // spinner will be removed by a converse plugin - - const authHeader = peertubeHelpers.getAuthHeader() - - 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) - window.initConverse(converseJSParams, 'peertube-fullpage', authHeader ?? null) + await displayConverseJS(clientOptions, container, roomKey, 'peertube-fullpage') } catch (err) { console.error('[peertube-plugin-livechat] ' + (err as string)) // FIXME: do a better error page. diff --git a/client/utils/conversejs.ts b/client/utils/conversejs.ts index 41e7fd6d..02348a27 100644 --- a/client/utils/conversejs.ts +++ b/client/utils/conversejs.ts @@ -1,6 +1,7 @@ -import type { InitConverseJSParams } from 'shared/lib/types' +import type { RegisterClientOptions } from '@peertube/peertube-types/client' +import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types' import { computeAutoColors } from './colors' - +import { getBaseRoute } from './uri' // FIXME // declare global { // interface Window { @@ -109,6 +110,50 @@ async function loadConverseJS (converseJSParams: InitConverseJSParams): Promise< } } -export { - loadConverseJS +/** + * Loads the chat in the given container. + * @param clientOptions Peertube client options + * @param container the dom element where to insert the chat + * @param roomKey the room to join + * @param chatIncludeMode the include mode + */ +async function displayConverseJS ( + clientOptions: RegisterClientOptions, + container: HTMLElement, + roomKey: string, + chatIncludeMode: ChatPeertubeIncludeMode +): Promise { + const peertubeHelpers = clientOptions.peertubeHelpers + + const converseRoot = document.createElement('converse-root') + converseRoot.classList.add('theme-peertube') + container.append(converseRoot) + + const spinner = document.createElement('div') + spinner.classList.add('livechat-spinner') + spinner.setAttribute('id', 'livechat-loading-spinner') + spinner.innerHTML = '
' + container.prepend(spinner) + // spinner will be removed by a converse plugin + + const authHeader = peertubeHelpers.getAuthHeader() + + 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) + window.initConverse(converseJSParams, chatIncludeMode, authHeader ?? null) +} + +export { + displayConverseJS } diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts index dd1e6bbc..aa57c09e 100644 --- a/client/videowatch-client-plugin.ts +++ b/client/videowatch-client-plugin.ts @@ -6,7 +6,7 @@ import { logger } from './utils/logger' import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG } from './videowatch/buttons' import { displayButton, displayButtonOptions } from './videowatch/button' import { shareChatUrl } from './videowatch/share' -import { getIframeUri } from './videowatch/uri' +import { displayConverseJS } from './utils/conversejs' interface VideoWatchLoadedHookOptions { videojs: any @@ -68,7 +68,7 @@ function guessIamIModerator (_registerOptions: RegisterClientOptions): boolean { function register (registerOptions: RegisterClientOptions): void { const { registerHook, peertubeHelpers } = registerOptions - let settings: any = {} + let settings: any = {} // will be loaded later async function insertChatDom ( container: HTMLElement, video: Video, showOpenBlank: boolean, showShareUrlButton: boolean @@ -77,7 +77,7 @@ function register (registerOptions: RegisterClientOptions): void { const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, { page: 'documentation/user/viewers' }) - const p = new Promise((resolve, reject) => { + const p = new Promise((resolve) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all([ peertubeHelpers.translate(LOC_OPEN_CHAT), @@ -92,11 +92,6 @@ function register (registerOptions: RegisterClientOptions): void { const labelShareUrl = labels[3] const labelHelp = labels[4] - const iframeUri = getIframeUri(registerOptions, settings, video) - if (!iframeUri) { - return reject(new Error('No uri, cant display the buttons.')) - } - const buttonContainer = document.createElement('div') buttonContainer.classList.add('peertube-plugin-livechat-buttons') container.append(buttonContainer) @@ -107,7 +102,9 @@ function register (registerOptions: RegisterClientOptions): void { buttonContainer, name: 'open', label: labelOpen, - callback: () => openChat(video), + callback: () => { + openChat(video).then(() => {}, () => {}) + }, icon: openChatSVG, additionalClasses: [] }) @@ -118,7 +115,7 @@ function register (registerOptions: RegisterClientOptions): void { label: labelOpenBlank, callback: () => { closeChat() - window.open(iframeUri) + window.open('/p/livechat/room?room=' + encodeURIComponent(video.uuid)) }, icon: openBlankChatSVG, additionalClasses: [] @@ -176,45 +173,49 @@ function register (registerOptions: RegisterClientOptions): void { return p } - function openChat (video: Video): void | boolean { + async function openChat (video: Video): Promise { if (!video) { logger.log('No video.') return false } logger.info(`Trying to load the chat for video ${video.uuid}.`) - const iframeUri = getIframeUri(registerOptions, settings, video) - if (!iframeUri) { - logger.error('Incorrect iframe uri') + // here the room key is always the video uuid, a backend API will translate to channel id if relevant. + const roomkey = video.uuid + if (!roomkey) { + logger.error('Can\'t get room xmpp addr') return false } + const additionalStyles = settings['chat-style'] || '' logger.info('Opening the chat...') const container = document.getElementById('peertube-plugin-livechat-container') - if (!container) { - logger.error('Cant found the livechat container.') - return false - } - if (container.querySelector('iframe')) { - logger.error('Seems that there is already an iframe in the container.') - return false - } + try { + if (!container) { + logger.error('Cant found the livechat container.') + return false + } - // Creating the iframe... - const iframe = document.createElement('iframe') - iframe.setAttribute('src', iframeUri) - iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms') - iframe.setAttribute('frameborder', '0') - if (additionalStyles) { - iframe.setAttribute('style', additionalStyles) - } - container.append(iframe) - container.setAttribute('peertube-plugin-livechat-state', 'open') + if (container.querySelector('converse-root')) { + logger.error('Seems that there is already a ConverseJS in the container.') + return false + } - // Hacking styles... - hackStyles(true) + // Loading converseJS... + await displayConverseJS(registerOptions, container, roomkey, 'peertube-video') + + if (additionalStyles) { + container.setAttribute('style', additionalStyles) + } + container.setAttribute('peertube-plugin-livechat-state', 'open') + + // Hacking styles... + hackStyles(true) + } catch (err) { + + } } function closeChat (): void { @@ -223,8 +224,12 @@ function register (registerOptions: RegisterClientOptions): void { logger.error('Cant close livechat, container not found.') return } - container.querySelectorAll('iframe') - .forEach(dom => dom.remove()) + + // Disconnecting ConverseJS + if (window.converse?.livechatDisconnect) { window.converse.livechatDisconnect() } + + // Removing from the DOM + container.childNodes.forEach(dom => dom.remove()) container.setAttribute('peertube-plugin-livechat-state', 'closed') @@ -232,7 +237,7 @@ function register (registerOptions: RegisterClientOptions): void { hackStyles(false) } - function initChat (video: Video): void { + async function initChat (video: Video): Promise { if (!video) { logger.error('No video provided') return @@ -254,15 +259,15 @@ function register (registerOptions: RegisterClientOptions): void { container.setAttribute('peertube-plugin-livechat-current-url', window.location.href) placeholder.append(container) - peertubeHelpers.getSettings().then((s: any) => { - settings = s + try { + settings = await peertubeHelpers.getSettings() logger.log('Checking if this video should have a chat...') if (settings['chat-no-anonymous'] === true && isAnonymousUser(registerOptions)) { logger.log('No chat for anonymous users') return } - if (!videoHasWebchat(s, video) && !videoHasRemoteWebchat(s, video)) { + if (!videoHasWebchat(settings, video) && !videoHasRemoteWebchat(settings, video)) { logger.log('This video has no webchat') return } @@ -279,18 +284,16 @@ function register (registerOptions: RegisterClientOptions): void { } } - insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton).then(() => { - if (settings['chat-auto-display']) { - openChat(video) - } else if (container) { - container.setAttribute('peertube-plugin-livechat-state', 'closed') - } - }, () => { - logger.error('insertChatDom has failed') - }) - }, () => { - logger.error('Cant get settings') - }) + await insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton) + if (settings['chat-auto-display']) { + await openChat(video) + } else if (container) { + container.setAttribute('peertube-plugin-livechat-state', 'closed') + } + } catch (err) { + logger.error('initChat has failed') + logger.error(err as string) + } } let savedMyPluginFlexGrow: string | undefined @@ -335,7 +338,7 @@ function register (registerOptions: RegisterClientOptions): void { logger.info('We are in a playlist, we will not use the webchat') return } - initChat(video) + initChat(video).then(() => {}, () => {}) } }) } diff --git a/conversejs/builtin.ts b/conversejs/builtin.ts index 8d29f54e..9546a659 100644 --- a/conversejs/builtin.ts +++ b/conversejs/builtin.ts @@ -1,4 +1,4 @@ -import type { InitConverseJSParams } from 'shared/lib/types' +import type { InitConverseJSParams, ChatIncludeMode } from 'shared/lib/types' import { inIframe } from './lib/utils' import { initDom } from './lib/dom' import { @@ -54,14 +54,6 @@ function initConversePlugins (peertubeEmbedded: boolean): void { } window.initConversePlugins = initConversePlugins -/** - * ChatIncludeMode: - * - chat-only: the chat is on a full page, without Peertube - * - peertube-fullpage: the chat is embedded in Peertube, in a full custom page - * - peertube-video: the chat is embedded in Peertube, beside a video - */ -type ChatIncludeMode = 'chat-only' | 'peertube-fullpage' | 'peertube-video' - /** * Init ConverseJS * @param initConverseParams ConverseJS init Params diff --git a/conversejs/custom/shared/styles/livechat.scss b/conversejs/custom/shared/styles/livechat.scss index 6919af56..1f29266a 100644 --- a/conversejs/custom/shared/styles/livechat.scss +++ b/conversejs/custom/shared/styles/livechat.scss @@ -2,9 +2,12 @@ @import "shared/styles/index"; @import "./peertubetheme"; -body.livechat-iframe #conversejs .chat-head { - // Hidding the chat-head when the plugin is displayed in an iframe. - display: none; +peertube-plugin-livechat-container, +body.livechat-iframe { + #conversejs .chat-head { + // Hidding the chat-head when the plugin is displayed in an iframe or besides a video. + display: none; + } } #conversejs-bg { diff --git a/conversejs/lib/dom.ts b/conversejs/lib/dom.ts index db947adc..859c09e3 100644 --- a/conversejs/lib/dom.ts +++ b/conversejs/lib/dom.ts @@ -4,7 +4,7 @@ function initDom ({ forceReadonly, transparent }: InitConverseJSParams, isInIfra const body = document.querySelector('body') if (isInIframe) { if (body) { - body.classList.add('livechat-iframe') + body.classList.add('livechat-iframe') // we need to keep this, for embedded chats in external websites // prevent horizontal scrollbar when in iframe. (don't know why, but does not work if done by CSS) body.style.overflowX = 'hidden' } diff --git a/shared/lib/types.ts b/shared/lib/types.ts index ed5d439d..6ba6e2c7 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -90,6 +90,16 @@ interface ChannelConfiguration { configuration: ChannelConfigurationOptions } +type ChatPeertubeIncludeMode = 'peertube-fullpage' | 'peertube-video' + +/** + * ChatIncludeMode: + * - chat-only: the chat is on a full page, without Peertube + * - peertube-fullpage: the chat is embedded in Peertube, in a full custom page + * - peertube-video: the chat is embedded in Peertube, beside a video + */ +type ChatIncludeMode = 'chat-only' | ChatPeertubeIncludeMode + export type { ConverseJSTheme, InitConverseJSParams, @@ -98,5 +108,7 @@ export type { ProsodyListRoomsResultRoom, ChannelInfos, ChannelConfigurationOptions, - ChannelConfiguration + ChannelConfiguration, + ChatIncludeMode, + ChatPeertubeIncludeMode }