From 9f9643ac891bc4bc53227cf596d38143eca9601f Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 14 Dec 2021 17:46:07 +0100 Subject: [PATCH] Share chat modal WIP. --- client/videowatch-client-plugin.ts | 2 +- client/videowatch/share.ts | 111 ++++++++++++++++++++++++++++- client/videowatch/uri.ts | 48 +++++++++---- 3 files changed, 142 insertions(+), 19 deletions(-) diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts index 77362ff8..9958c5c0 100644 --- a/client/videowatch-client-plugin.ts +++ b/client/videowatch-client-plugin.ts @@ -70,7 +70,7 @@ function register (registerOptions: RegisterOptions): void { name: 'shareurl', label: labelShareUrl, callback: () => { - shareChatUrl(registerOptions).then(() => {}, () => {}) + shareChatUrl(registerOptions, settings, video).then(() => {}, () => {}) }, icon: shareChatUrlSVG, additionalClasses: [] diff --git a/client/videowatch/share.ts b/client/videowatch/share.ts index dea504b6..5fb4fbf1 100644 --- a/client/videowatch/share.ts +++ b/client/videowatch/share.ts @@ -1,9 +1,114 @@ -async function shareChatUrl ({ peertubeHelpers }: RegisterOptions): Promise { +import { logger } from './logger' +import { getIframeUri, UriOptions } from './uri' + +async function shareChatUrl (registerOptions: RegisterOptions, settings: any, video: Video): Promise { + const peertubeHelpers = registerOptions.peertubeHelpers + + const [ + labelShare, + labelReadonly, + tipsOBS + ] = await Promise.all([ + peertubeHelpers.translate('Share chat link'), + peertubeHelpers.translate('Read-only'), + // eslint-disable-next-line max-len + peertubeHelpers.translate('Tips for streamers: To add the chat to your OBS, generate a read-only link and use it as a browser source.') + ]) + + const defaultUri = getIframeUri(registerOptions, settings, video) + if (!defaultUri) { + return + } + + let form: { + readonly: HTMLInputElement + url: HTMLInputElement + } | undefined + function renderContent (container: HTMLElement): void { + if (!form) { + container.childNodes.forEach(child => container.removeChild(child)) + const pTips = document.createElement('p') + pTips.textContent = tipsOBS + container.append(pTips) + + const pReadonly = document.createElement('p') + container.append(pReadonly) + const readonly = document.createElement('input') + readonly.setAttribute('type', 'checkbox') + const readonlyLabelEl = document.createElement('label') + readonlyLabelEl.textContent = labelReadonly + readonlyLabelEl.prepend(readonly) + pReadonly.append(readonlyLabelEl) + + const pUrl = document.createElement('p') + container.append(pUrl) + const url = document.createElement('input') + url.setAttribute('type', 'text') + url.setAttribute('readonly', '') + url.setAttribute('autocomplete', 'off') + url.setAttribute('placeholder', '') + url.classList.add('form-control', 'readonly') + pUrl.append(url) + + readonly.onclick = () => { + renderContent(container) + } + + form = { + readonly, + url + } + } + + // TODO: save last form state, to restore each time the modal is opened. + + const uriOptions: UriOptions = { + ignoreAutoColors: true, + permanent: true + } + if (form.readonly.checked) { + uriOptions.readonly = true + } + const iframeUri = getIframeUri(registerOptions, settings, video, uriOptions) + form.url.setAttribute('value', iframeUri ?? '') + } + + logger.info('Opening the share modal...') + const observer = new MutationObserver(mutations => { + for (const { addedNodes } of mutations) { + addedNodes.forEach(node => { + if ((node as HTMLElement).localName === 'ngb-modal-window') { + logger.info('Detecting a new modal, checking if this is the good one...') + if (!(node as HTMLElement).querySelector) { return } + const title = (node as HTMLElement).querySelector('.modal-title') + if (!(title?.textContent === labelShare)) { + return + } + logger.info('Yes, it is the good modal!') + observer.disconnect() + + const modalBodyElem: HTMLElement | null = (node as HTMLElement).querySelector('.modal-body') + if (!modalBodyElem) { + logger.error('Modal has no body... Dont know how to fill it.') + return + } + renderContent(modalBodyElem) + } + }) + } + }) + observer.observe(document.body, { + childList: true + }) peertubeHelpers.showModal({ - title: 'TODO', - content: '

TODO

', + title: labelShare, + content: `

${defaultUri ?? ''}

`, close: true }) + // just in case, remove the observer after a timeout, if not already done... + setTimeout(() => { + observer.disconnect() + }, 1000) } export { diff --git a/client/videowatch/uri.ts b/client/videowatch/uri.ts index 5fae7280..199d8e24 100644 --- a/client/videowatch/uri.ts +++ b/client/videowatch/uri.ts @@ -3,38 +3,49 @@ import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors' import { logger } from './logger' import { computeAutoColors } from './colors' -function getBaseRoute ({ peertubeHelpers }: RegisterOptions): string { +interface UriOptions { + readonly?: boolean + ignoreAutoColors?: boolean + permanent?: boolean +} + +function getBaseRoute ({ peertubeHelpers }: RegisterOptions, 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() - // we can't use '/plugins/livechat/router', because the loaded html page needs correct relative paths. return staticBase.replace(/\/static.*$/, '/router') } -function getIframeUri (registerOptions: RegisterOptions, settings: any, video: Video): string | null { +function getIframeUri ( + registerOptions: RegisterOptions, settings: any, video: Video, uriOptions: UriOptions = {} +): string | null { if (!settings) { logger.error('Settings are not initialized, too soon to compute the iframeUri') return null } - let iframeUri = '' + let iframeUriStr = '' const chatType: ChatType = (settings['chat-type'] ?? 'disabled') as ChatType if (chatType === 'builtin-prosody' || chatType === 'builtin-converse') { // Using the builtin converseJS - iframeUri = getBaseRoute(registerOptions) + '/webchat/room/' + encodeURIComponent(video.uuid) + iframeUriStr = getBaseRoute(registerOptions, uriOptions.permanent) + iframeUriStr += '/webchat/room/' + encodeURIComponent(video.uuid) } else if (chatType === 'external-uri') { - iframeUri = settings['chat-uri'] || '' - iframeUri = iframeUri.replace(/{{VIDEO_UUID}}/g, encodeURIComponent(video.uuid)) - if (iframeUri.includes('{{CHANNEL_ID}}')) { + iframeUriStr = settings['chat-uri'] || '' + iframeUriStr = iframeUriStr.replace(/{{VIDEO_UUID}}/g, encodeURIComponent(video.uuid)) + if (iframeUriStr.includes('{{CHANNEL_ID}}')) { if (!video.channel || !video.channel.id) { logger.error('Missing channel info in video object.') return null } - iframeUri = iframeUri.replace(/{{CHANNEL_ID}}/g, encodeURIComponent(video.channel.id)) + iframeUriStr = iframeUriStr.replace(/{{CHANNEL_ID}}/g, encodeURIComponent(video.channel.id)) } - if (!/^https?:\/\//.test(iframeUri)) { + if (!/^https?:\/\//.test(iframeUriStr)) { logger.error('The webchaturi must begin with https://') return null } @@ -42,12 +53,15 @@ function getIframeUri (registerOptions: RegisterOptions, settings: any, video: V logger.error('Chat disabled.') return null } - if (iframeUri === '') { + if (iframeUriStr === '') { logger.error('No iframe uri') return null } + const iFrameUri = new URL(iframeUriStr, window.location.origin) + if ( + !uriOptions.ignoreAutoColors && settings['converse-autocolors'] && isAutoColorsAvailable(settings['chat-type'] as ChatType, settings['converse-theme']) ) { @@ -55,21 +69,25 @@ function getIframeUri (registerOptions: RegisterOptions, settings: any, video: V try { const autocolors = computeAutoColors() if (autocolors) { - const url = new URL(iframeUri, window.location.origin) for (const p in autocolors) { - url.searchParams.set('_ac_' + p, autocolors[p as keyof AutoColors]) + iFrameUri.searchParams.set('_ac_' + p, autocolors[p as keyof AutoColors]) } - iframeUri = url.href } } catch (err) { logger.error(`Failed computing autocolors: '${err as string}'`) } } - return iframeUri + if (uriOptions.readonly) { + iFrameUri.searchParams.set('_readonly', 'true') + } + + iframeUriStr = iFrameUri.href + return iframeUriStr } export { + UriOptions, getBaseRoute, getIframeUri }