diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2d8998..6fd6abf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ### Features -* Builtin Prosody: Readonly mode. You can open the chat in readonly mode. Could be used to integrate in OBS for example. +* Builtin Prosody: + * Readonly mode. You can open the chat in readonly mode. Could be used to integrate in OBS for example. + * Share chat url: modal for video owner (and instance's moderators) that allows to generate a link to the chat. So you can - for example - obtain the url to use for OBS integration. * Builtin Prosody: you can now allow «external XMPP components» to connect. This can be used for exemple to connect bots or bridges. For now, only connections from localhost will be allowed. ### Minor changes and fixes diff --git a/assets/style.scss b/assets/style.scss index 075a223b..f111025b 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -53,6 +53,13 @@ } .peertube-plugin-livechat-multi-button-secondary { + border-radius: 0 !important; + margin-right: 0 !important; + margin-left: 0 !important; + border-left: 1px solid currentColor; + } + + .peertube-plugin-livechat-multi-button-last-secondary { border-top-left-radius: 0 !important; border-bottom-left-radius: 0 !important; margin-left: 0 !important; diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts index db017173..ad00acb9 100644 --- a/client/videowatch-client-plugin.ts +++ b/client/videowatch-client-plugin.ts @@ -1,7 +1,10 @@ import type { ChatType } from 'shared/lib/types' import { videoHasWebchat } from 'shared/lib/video' import { AutoColors, isAutoColorsAvailable, areAutoColorsValid } from 'shared/lib/autocolors' -import { closeSVG, openBlankChatSVG, openChatSVG, SVGButton } from './videowatch/buttons' +import { logger } from './videowatch/logger' +import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG } from './videowatch/buttons' +import { displayButton, displayButtonOptions } from './videowatch/button' +import { shareChatUrl } from './videowatch/share' interface VideoWatchLoadedHookOptions { videojs: any @@ -9,14 +12,8 @@ interface VideoWatchLoadedHookOptions { playlist?: any } -function register ({ registerHook, peertubeHelpers }: RegisterOptions): void { - const logger = { - log: (s: string) => console.log('[peertube-plugin-livechat] ' + s), - info: (s: string) => console.info('[peertube-plugin-livechat] ' + s), - error: (s: string) => console.error('[peertube-plugin-livechat] ' + s), - warn: (s: string) => console.warn('[peertube-plugin-livechat] ' + s) - } - +function register (registerOptions: RegisterOptions): void { + const { registerHook, peertubeHelpers } = registerOptions let settings: any = {} function getBaseRoute (): string { @@ -125,58 +122,22 @@ function register ({ registerHook, peertubeHelpers }: RegisterOptions): void { return autocolors } - function displayButton ( - buttonContainer: HTMLElement, - name: string, - label: string, - callback: () => void | boolean, - icon?: SVGButton, - additionalClasses?: string[] - ): void { - const button = document.createElement('a') - button.classList.add( - 'orange-button', 'peertube-button-link', - 'peertube-plugin-livechat-button', - 'peertube-plugin-livechat-button-' + name - ) - if (additionalClasses) { - for (let i = 0; i < additionalClasses.length; i++) { - button.classList.add(additionalClasses[i]) - } - } - button.onclick = callback - if (icon) { - try { - const svg = icon() - const tmp = document.createElement('span') - tmp.innerHTML = svg.trim() - const svgDom = tmp.firstChild - if (svgDom) { - button.prepend(svgDom) - } - } catch (err) { - logger.error('Failed to generate the ' + name + ' button: ' + (err as string)) - } - - button.setAttribute('title', label) - } else { - button.textContent = label - } - buttonContainer.append(button) - } - - async function insertChatDom (container: HTMLElement, video: Video, showOpenBlank: boolean): Promise { + async function insertChatDom ( + container: HTMLElement, video: Video, showOpenBlank: boolean, showShareUrlButton: boolean + ): Promise { logger.log('Adding livechat in the DOM...') const p = new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all([ peertubeHelpers.translate('Open chat'), peertubeHelpers.translate('Open chat in a new window'), - peertubeHelpers.translate('Close chat') + peertubeHelpers.translate('Close chat'), + peertubeHelpers.translate('Share link') ]).then(labels => { const labelOpen = labels[0] const labelOpenBlank = labels[1] const labelClose = labels[2] + const labelShareUrl = labels[3] const iframeUri = getIframeUri(video) if (!iframeUri) { @@ -187,27 +148,66 @@ function register ({ registerHook, peertubeHelpers }: RegisterOptions): void { buttonContainer.classList.add('peertube-plugin-livechat-buttons') container.append(buttonContainer) + // Here are buttons that are magically merged + const groupButtons: displayButtonOptions[] = [] + groupButtons.push({ + buttonContainer, + name: 'open', + label: labelOpen, + callback: () => openChat(video), + icon: openChatSVG, + additionalClasses: [] + }) if (showOpenBlank) { - displayButton( - buttonContainer, 'open', - labelOpen, () => openChat(video), - openChatSVG, - ['peertube-plugin-livechat-multi-button-main'] - ) - displayButton( - buttonContainer, 'openblank', - labelOpenBlank, () => { + groupButtons.push({ + buttonContainer, + name: 'openblank', + label: labelOpenBlank, + callback: () => { closeChat() window.open(iframeUri) }, - openBlankChatSVG, - ['peertube-plugin-livechat-multi-button-secondary'] - ) - } else { - displayButton(buttonContainer, 'open', labelOpen, () => openChat(video), openChatSVG) + icon: openBlankChatSVG, + additionalClasses: [] + }) + } + if (showShareUrlButton) { + groupButtons.push({ + buttonContainer, + name: 'shareurl', + label: labelShareUrl, + callback: () => { + shareChatUrl(registerOptions) + }, + icon: shareChatUrlSVG, + additionalClasses: [] + }) } - displayButton(buttonContainer, 'close', labelClose, () => closeChat(), closeSVG) + // If more than one groupButtons: + // - the first must have class 'peertube-plugin-livechat-multi-button-main' + // - middle ones must have 'peertube-plugin-livechat-multi-button-secondary' + // - the last must have 'peertube-plugin-livechat-multi-button-last-secondary' + if (groupButtons.length > 1) { + groupButtons[0].additionalClasses?.push('peertube-plugin-livechat-multi-button-main') + for (let i = 1; i < groupButtons.length - 1; i++) { // middle + groupButtons[i].additionalClasses?.push('peertube-plugin-livechat-multi-button-secondary') + } + groupButtons[groupButtons.length - 1] + .additionalClasses?.push('peertube-plugin-livechat-multi-button-last-secondary') + } + + for (const button of groupButtons) { + displayButton(button) + } + + displayButton({ + buttonContainer, + name: 'close', + label: labelClose, + callback: () => closeChat(), + icon: closeSVG + }) resolve() }) }) @@ -301,7 +301,12 @@ function register ({ registerHook, peertubeHelpers }: RegisterOptions): void { return } - insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank']).then(() => { + let showShareUrlButton: boolean = false + if (settings['chat-type'] === 'builtin-prosody') { + // FIXME: showShareUrlButton should only be true for video owner and instance moderators. + showShareUrlButton = true + } + insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton).then(() => { if (settings['chat-auto-display']) { openChat(video) } else if (container) { diff --git a/client/videowatch/button.ts b/client/videowatch/button.ts new file mode 100644 index 00000000..e63ddef1 --- /dev/null +++ b/client/videowatch/button.ts @@ -0,0 +1,56 @@ +import type { SVGButton } from './buttons' +import { logger } from './logger' + +interface displayButtonOptions { + buttonContainer: HTMLElement + name: string + label: string + callback: () => void | boolean + icon?: SVGButton + additionalClasses?: string[] +} + +function displayButton ({ + name, + label, + callback, + buttonContainer, + additionalClasses, + icon +}: displayButtonOptions): void { + const button = document.createElement('a') + button.classList.add( + 'orange-button', 'peertube-button-link', + 'peertube-plugin-livechat-button', + 'peertube-plugin-livechat-button-' + name + ) + if (additionalClasses) { + for (let i = 0; i < additionalClasses.length; i++) { + button.classList.add(additionalClasses[i]) + } + } + button.onclick = callback + if (icon) { + try { + const svg = icon() + const tmp = document.createElement('span') + tmp.innerHTML = svg.trim() + const svgDom = tmp.firstChild + if (svgDom) { + button.prepend(svgDom) + } + } catch (err) { + logger.error('Failed to generate the ' + name + ' button: ' + (err as string)) + } + + button.setAttribute('title', label) + } else { + button.textContent = label + } + buttonContainer.append(button) +} + +export { + displayButtonOptions, + displayButton +} diff --git a/client/videowatch/buttons.ts b/client/videowatch/buttons.ts index 2dcdbc35..e92cc1a8 100644 --- a/client/videowatch/buttons.ts +++ b/client/videowatch/buttons.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ type SVGButton = () => string const closeSVG: SVGButton = () => { @@ -84,9 +85,24 @@ const openBlankChatSVG: SVGButton = () => { ` } +const shareChatUrlSVG: SVGButton = () => { + // This content comes from the file public/image/url.svg, after svgo cleaning. + // To get the formated content, you can do: + // xmllint dist/client/images/url.svg --format + // Then replace the color by `currentColor` + return ` + + + + + +` +} + export { closeSVG, openChatSVG, openBlankChatSVG, + shareChatUrlSVG, SVGButton } diff --git a/client/videowatch/logger.ts b/client/videowatch/logger.ts new file mode 100644 index 00000000..c489326d --- /dev/null +++ b/client/videowatch/logger.ts @@ -0,0 +1,10 @@ +const logger = { + log: (s: string) => console.log('[peertube-plugin-livechat] ' + s), + info: (s: string) => console.info('[peertube-plugin-livechat] ' + s), + error: (s: string) => console.error('[peertube-plugin-livechat] ' + s), + warn: (s: string) => console.warn('[peertube-plugin-livechat] ' + s) +} + +export { + logger +} diff --git a/client/videowatch/share.ts b/client/videowatch/share.ts new file mode 100644 index 00000000..dea504b6 --- /dev/null +++ b/client/videowatch/share.ts @@ -0,0 +1,11 @@ +async function shareChatUrl ({ peertubeHelpers }: RegisterOptions): Promise { + peertubeHelpers.showModal({ + title: 'TODO', + content: '

TODO

', + close: true + }) +} + +export { + shareChatUrl +} diff --git a/public/images/url.svg b/public/images/url.svg new file mode 100644 index 00000000..8cff86c1 --- /dev/null +++ b/public/images/url.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + +