Embeddeding chat without iframe besieds videos WIP

This commit is contained in:
John Livingston 2024-03-28 15:06:15 +01:00
parent ba52d4e3d8
commit 612a9f622d
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
10 changed files with 134 additions and 103 deletions

View File

@ -4,6 +4,10 @@
TODO: replace commit_id by a tag in build-conversejs 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 ### New features
* #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames * #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames

View File

@ -67,7 +67,7 @@
} }
} }
#peertube-plugin-livechat-container iframe { #peertube-plugin-livechat-container converse-root {
border: 1px solid black; border: 1px solid black;
min-height: 30vh; min-height: 30vh;
height: 100%; height: 100%;

View File

@ -15,7 +15,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
console.warn( console.warn(
'[peertube-plugin-livechat navigation-end] ' + '[peertube-plugin-livechat navigation-end] ' +
'It seems that action:router.navigation-end was called after action:video-watch.video.loaded. ' + '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 return
} }

View File

@ -1,9 +1,7 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import type { InitConverseJSParams } from 'shared/lib/types'
import { renderConfigurationHome } from './templates/home' import { renderConfigurationHome } from './templates/home'
import { renderConfigurationChannel } from './templates/channel' import { renderConfigurationChannel } from './templates/channel'
import { getBaseRoute } from '../../utils/uri' import { displayConverseJS } from '../../utils/conversejs'
import { loadConverseJS } from '../../utils/conversejs'
/** /**
* Registers stuff related to the user's configuration pages. * 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') container.classList.add('livechat-embed-fullpage')
rootEl.append(container) rootEl.append(container)
const converseRoot = document.createElement('converse-root') await displayConverseJS(clientOptions, container, roomKey, 'peertube-fullpage')
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 = '<div></div>'
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)
} catch (err) { } catch (err) {
console.error('[peertube-plugin-livechat] ' + (err as string)) console.error('[peertube-plugin-livechat] ' + (err as string))
// FIXME: do a better error page. // FIXME: do a better error page.

View File

@ -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 { computeAutoColors } from './colors'
import { getBaseRoute } from './uri'
// FIXME // FIXME
// declare global { // declare global {
// interface Window { // 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<void> {
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 = '<div></div>'
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
} }

View File

@ -6,7 +6,7 @@ import { logger } from './utils/logger'
import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG } from './videowatch/buttons' import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG } from './videowatch/buttons'
import { displayButton, displayButtonOptions } from './videowatch/button' import { displayButton, displayButtonOptions } from './videowatch/button'
import { shareChatUrl } from './videowatch/share' import { shareChatUrl } from './videowatch/share'
import { getIframeUri } from './videowatch/uri' import { displayConverseJS } from './utils/conversejs'
interface VideoWatchLoadedHookOptions { interface VideoWatchLoadedHookOptions {
videojs: any videojs: any
@ -68,7 +68,7 @@ function guessIamIModerator (_registerOptions: RegisterClientOptions): boolean {
function register (registerOptions: RegisterClientOptions): void { function register (registerOptions: RegisterClientOptions): void {
const { registerHook, peertubeHelpers } = registerOptions const { registerHook, peertubeHelpers } = registerOptions
let settings: any = {} let settings: any = {} // will be loaded later
async function insertChatDom ( async function insertChatDom (
container: HTMLElement, video: Video, showOpenBlank: boolean, showShareUrlButton: boolean container: HTMLElement, video: Video, showOpenBlank: boolean, showShareUrlButton: boolean
@ -77,7 +77,7 @@ function register (registerOptions: RegisterClientOptions): void {
const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, { const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, {
page: 'documentation/user/viewers' page: 'documentation/user/viewers'
}) })
const p = new Promise<void>((resolve, reject) => { const p = new Promise<void>((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
Promise.all([ Promise.all([
peertubeHelpers.translate(LOC_OPEN_CHAT), peertubeHelpers.translate(LOC_OPEN_CHAT),
@ -92,11 +92,6 @@ function register (registerOptions: RegisterClientOptions): void {
const labelShareUrl = labels[3] const labelShareUrl = labels[3]
const labelHelp = labels[4] 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') const buttonContainer = document.createElement('div')
buttonContainer.classList.add('peertube-plugin-livechat-buttons') buttonContainer.classList.add('peertube-plugin-livechat-buttons')
container.append(buttonContainer) container.append(buttonContainer)
@ -107,7 +102,9 @@ function register (registerOptions: RegisterClientOptions): void {
buttonContainer, buttonContainer,
name: 'open', name: 'open',
label: labelOpen, label: labelOpen,
callback: () => openChat(video), callback: () => {
openChat(video).then(() => {}, () => {})
},
icon: openChatSVG, icon: openChatSVG,
additionalClasses: [] additionalClasses: []
}) })
@ -118,7 +115,7 @@ function register (registerOptions: RegisterClientOptions): void {
label: labelOpenBlank, label: labelOpenBlank,
callback: () => { callback: () => {
closeChat() closeChat()
window.open(iframeUri) window.open('/p/livechat/room?room=' + encodeURIComponent(video.uuid))
}, },
icon: openBlankChatSVG, icon: openBlankChatSVG,
additionalClasses: [] additionalClasses: []
@ -176,45 +173,49 @@ function register (registerOptions: RegisterClientOptions): void {
return p return p
} }
function openChat (video: Video): void | boolean { async function openChat (video: Video): Promise<void | false> {
if (!video) { if (!video) {
logger.log('No video.') logger.log('No video.')
return false return false
} }
logger.info(`Trying to load the chat for video ${video.uuid}.`) logger.info(`Trying to load the chat for video ${video.uuid}.`)
const iframeUri = getIframeUri(registerOptions, settings, video) // here the room key is always the video uuid, a backend API will translate to channel id if relevant.
if (!iframeUri) { const roomkey = video.uuid
logger.error('Incorrect iframe uri') if (!roomkey) {
logger.error('Can\'t get room xmpp addr')
return false return false
} }
const additionalStyles = settings['chat-style'] || '' const additionalStyles = settings['chat-style'] || ''
logger.info('Opening the chat...') logger.info('Opening the chat...')
const container = document.getElementById('peertube-plugin-livechat-container') const container = document.getElementById('peertube-plugin-livechat-container')
if (!container) {
logger.error('Cant found the livechat container.')
return false
}
if (container.querySelector('iframe')) { try {
logger.error('Seems that there is already an iframe in the container.') if (!container) {
return false logger.error('Cant found the livechat container.')
} return false
}
// Creating the iframe... if (container.querySelector('converse-root')) {
const iframe = document.createElement('iframe') logger.error('Seems that there is already a ConverseJS in the container.')
iframe.setAttribute('src', iframeUri) return false
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')
// Hacking styles... // Loading converseJS...
hackStyles(true) 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 { function closeChat (): void {
@ -223,8 +224,12 @@ function register (registerOptions: RegisterClientOptions): void {
logger.error('Cant close livechat, container not found.') logger.error('Cant close livechat, container not found.')
return 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') container.setAttribute('peertube-plugin-livechat-state', 'closed')
@ -232,7 +237,7 @@ function register (registerOptions: RegisterClientOptions): void {
hackStyles(false) hackStyles(false)
} }
function initChat (video: Video): void { async function initChat (video: Video): Promise<void> {
if (!video) { if (!video) {
logger.error('No video provided') logger.error('No video provided')
return return
@ -254,15 +259,15 @@ function register (registerOptions: RegisterClientOptions): void {
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href) container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
placeholder.append(container) placeholder.append(container)
peertubeHelpers.getSettings().then((s: any) => { try {
settings = s settings = await peertubeHelpers.getSettings()
logger.log('Checking if this video should have a chat...') logger.log('Checking if this video should have a chat...')
if (settings['chat-no-anonymous'] === true && isAnonymousUser(registerOptions)) { if (settings['chat-no-anonymous'] === true && isAnonymousUser(registerOptions)) {
logger.log('No chat for anonymous users') logger.log('No chat for anonymous users')
return return
} }
if (!videoHasWebchat(s, video) && !videoHasRemoteWebchat(s, video)) { if (!videoHasWebchat(settings, video) && !videoHasRemoteWebchat(settings, video)) {
logger.log('This video has no webchat') logger.log('This video has no webchat')
return return
} }
@ -279,18 +284,16 @@ function register (registerOptions: RegisterClientOptions): void {
} }
} }
insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton).then(() => { await insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton)
if (settings['chat-auto-display']) { if (settings['chat-auto-display']) {
openChat(video) await openChat(video)
} else if (container) { } else if (container) {
container.setAttribute('peertube-plugin-livechat-state', 'closed') container.setAttribute('peertube-plugin-livechat-state', 'closed')
} }
}, () => { } catch (err) {
logger.error('insertChatDom has failed') logger.error('initChat has failed')
}) logger.error(err as string)
}, () => { }
logger.error('Cant get settings')
})
} }
let savedMyPluginFlexGrow: string | undefined 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') logger.info('We are in a playlist, we will not use the webchat')
return return
} }
initChat(video) initChat(video).then(() => {}, () => {})
} }
}) })
} }

View File

@ -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 { inIframe } from './lib/utils'
import { initDom } from './lib/dom' import { initDom } from './lib/dom'
import { import {
@ -54,14 +54,6 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
} }
window.initConversePlugins = initConversePlugins 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 * Init ConverseJS
* @param initConverseParams ConverseJS init Params * @param initConverseParams ConverseJS init Params

View File

@ -2,9 +2,12 @@
@import "shared/styles/index"; @import "shared/styles/index";
@import "./peertubetheme"; @import "./peertubetheme";
body.livechat-iframe #conversejs .chat-head { peertube-plugin-livechat-container,
// Hidding the chat-head when the plugin is displayed in an iframe. body.livechat-iframe {
display: none; #conversejs .chat-head {
// Hidding the chat-head when the plugin is displayed in an iframe or besides a video.
display: none;
}
} }
#conversejs-bg { #conversejs-bg {

View File

@ -4,7 +4,7 @@ function initDom ({ forceReadonly, transparent }: InitConverseJSParams, isInIfra
const body = document.querySelector('body') const body = document.querySelector('body')
if (isInIframe) { if (isInIframe) {
if (body) { 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) // prevent horizontal scrollbar when in iframe. (don't know why, but does not work if done by CSS)
body.style.overflowX = 'hidden' body.style.overflowX = 'hidden'
} }

View File

@ -90,6 +90,16 @@ interface ChannelConfiguration {
configuration: ChannelConfigurationOptions 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 { export type {
ConverseJSTheme, ConverseJSTheme,
InitConverseJSParams, InitConverseJSParams,
@ -98,5 +108,7 @@ export type {
ProsodyListRoomsResultRoom, ProsodyListRoomsResultRoom,
ChannelInfos, ChannelInfos,
ChannelConfigurationOptions, ChannelConfigurationOptions,
ChannelConfiguration ChannelConfiguration,
ChatIncludeMode,
ChatPeertubeIncludeMode
} }