diff --git a/CHANGELOG.md b/CHANGELOG.md index 90cc318c..b8514dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ TODO: tag conversejs livechat branch, and replace commit ID in build-converse.js * Fix cleanup on channel deletion. * #416: Deregister prosodyctl interval callback when spawn.stdin disappears. +* #423: Merging video-watch scope into common scope. ## 10.0.2 diff --git a/build-client.js b/build-client.js index 0ea4745d..2748d406 100644 --- a/build-client.js +++ b/build-client.js @@ -12,7 +12,6 @@ const sourcemap = process.env.NODE_ENV === 'dev' ? 'inline' : false const clientFiles = [ // Client files list, without the file extension: 'common-client-plugin', - 'videowatch-client-plugin', 'admin-plugin-client-plugin' ] diff --git a/client/common-client-plugin.ts b/client/common-client-plugin.ts index 88956777..e1c23f2b 100644 --- a/client/common-client-plugin.ts +++ b/client/common-client-plugin.ts @@ -5,6 +5,7 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientFormFieldOptions } from '@peertube/peertube-types' import { registerConfiguration } from './common/configuration/register' +import { registerVideoWatch } from './common/videowatch/register' import { registerRoom } from './common/room/register' import { initPtContext } from './common/lib/contexts/peertube' import './common/lib/elements' // Import shared elements. @@ -66,6 +67,7 @@ async function register (clientOptions: RegisterClientOptions): Promise { registerVideoField(webchatFieldOptions, { type: 'go-live' }) await Promise.all([ + registerVideoWatch(), registerRoom(clientOptions), registerConfiguration(clientOptions) ]) diff --git a/client/common/lib/contexts/peertube.ts b/client/common/lib/contexts/peertube.ts index b7b6bb59..3cbe0a06 100644 --- a/client/common/lib/contexts/peertube.ts +++ b/client/common/lib/contexts/peertube.ts @@ -2,10 +2,43 @@ // // SPDX-License-Identifier: AGPL-3.0-only +import type { SettingEntries } from '@peertube/peertube-types' import type { RegisterClientOptions } from '@peertube/peertube-types/client/types' +import { logger as loggerFunction } from '../../../utils/logger' -export interface PtContext { - ptOptions: RegisterClientOptions +// We precise some of the settings in SettingsEntries, to faciliate some type checking. +export type LiveChatSettings = SettingEntries & { + 'chat-per-live-video': boolean + 'chat-all-lives': boolean + 'chat-all-non-lives': boolean + 'chat-videos-list': string + 'federation-no-remote-chat': boolean + 'chat-style': string | undefined +} + +export class PtContext { + public readonly ptOptions: RegisterClientOptions + public readonly logger: typeof loggerFunction + private _settings: LiveChatSettings | undefined + + constructor ( + ptOptions: RegisterClientOptions, + logger: typeof loggerFunction + ) { + this.ptOptions = ptOptions + this.logger = logger + } + + /** + * Returns the livechat settings. + * Keep them in cache after first request. + */ + public async getSettings (): Promise { + if (!this._settings) { + this._settings = await this.ptOptions.peertubeHelpers.getSettings() as LiveChatSettings + } + return this._settings + } } let context: PtContext @@ -18,7 +51,8 @@ export function getPtContext (): PtContext { } export function initPtContext (ptOptions: RegisterClientOptions): void { - context = { - ptOptions - } + context = new PtContext( + ptOptions, + loggerFunction + ) } diff --git a/client/common/lib/elements/help-button.ts b/client/common/lib/elements/help-button.ts index 2177ad7d..98e06d94 100644 --- a/client/common/lib/elements/help-button.ts +++ b/client/common/lib/elements/help-button.ts @@ -5,7 +5,7 @@ import { html } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { unsafeHTML } from 'lit/directives/unsafe-html.js' -import { helpButtonSVG } from '../../../videowatch/buttons' +import { helpButtonSVG } from '../../videowatch/buttons' import { Task } from '@lit/task' import { localizedHelpUrl } from '../../../utils/help' import { ptTr } from '../directives/translation' diff --git a/client/videowatch/button.ts b/client/common/videowatch/button.ts similarity index 97% rename from client/videowatch/button.ts rename to client/common/videowatch/button.ts index 85db1955..045d0275 100644 --- a/client/videowatch/button.ts +++ b/client/common/videowatch/button.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { SVGButton } from './buttons' -import { logger } from '../utils/logger' +import { logger } from '../../utils/logger' interface displayButtonOptionsBase { buttonContainer: HTMLElement diff --git a/client/videowatch/buttons.ts b/client/common/videowatch/buttons.ts similarity index 100% rename from client/videowatch/buttons.ts rename to client/common/videowatch/buttons.ts diff --git a/client/common/videowatch/chat.ts b/client/common/videowatch/chat.ts new file mode 100644 index 00000000..2250e489 --- /dev/null +++ b/client/common/videowatch/chat.ts @@ -0,0 +1,376 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Video } from '@peertube/peertube-types' +import type { InitConverseJSParams } from 'shared/lib/types' +import { getPtContext, LiveChatSettings } from '../lib/contexts/peertube' +import { videoHasWebchat, videoHasRemoteWebchat } from 'shared/lib/video' +import { displayButton, displayButtonOptions } from './button' +import { + closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG, promoteSVG +} from './buttons' +import { shareChatUrl } from './share' +import { isAnonymousUser, guessIsMine, guessIamIModerator } from '../../utils/user' +import { localizedHelpUrl } from '../../utils/help' +import { getBaseRoute } from '../../utils/uri' +import { displayConverseJS } from '../../utils/conversejs' + +let savedMyPluginFlexGrow: string | undefined + +/** + * Initialize the chat for the current video + * @param video the video + */ +async function initChat (video: Video): Promise { + const ptContext = getPtContext() + const logger = ptContext.logger + savedMyPluginFlexGrow = undefined + + if (!video) { + logger.error('No video provided') + return + } + const placeholder = document.getElementById('plugin-placeholder-player-next') + if (!placeholder) { + logger.error('The required placeholder div is not present in the DOM.') + return + } + + let container = placeholder.querySelector('#peertube-plugin-livechat-container') + if (container) { + logger.log('The chat seems already initialized...') + return + } + container = document.createElement('div') + container.setAttribute('id', 'peertube-plugin-livechat-container') + container.setAttribute('peertube-plugin-livechat-state', 'initializing') + container.setAttribute('peertube-plugin-livechat-current-url', window.location.href) + placeholder.append(container) + + try { + const settings = await ptContext.getSettings() + + logger.log('Checking if this video should have a chat...') + if (settings['chat-no-anonymous'] === true && isAnonymousUser(ptContext.ptOptions)) { + logger.log('No chat for anonymous users') + return + } + if (!videoHasWebchat(settings, video) && !videoHasRemoteWebchat(settings, video)) { + logger.log('This video has no webchat') + return + } + + let showShareUrlButton: boolean = false + let showPromote: boolean = false + if (video.isLocal) { // No need for shareButton on remote chats. + const chatShareUrl = settings['chat-share-url'] ?? '' + if (chatShareUrl === 'everyone') { + showShareUrlButton = true + } else if (chatShareUrl === 'owner') { + showShareUrlButton = guessIsMine(ptContext.ptOptions, video) + } else if (chatShareUrl === 'owner+moderators') { + showShareUrlButton = guessIsMine(ptContext.ptOptions, video) || guessIamIModerator(ptContext.ptOptions) + } + + if (guessIamIModerator(ptContext.ptOptions)) { + showPromote = true + } + } + + await _insertChatDom( + container as HTMLElement, + video, + settings, + !!settings['chat-open-blank'], + showShareUrlButton, + showPromote + ) + 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) + } +} + +/** + * Insert the chat in the DOM. + * @param container + * @param video + * @param settings + * @param showOpenBlank + * @param showShareUrlButton + * @param showPromote + */ +async function _insertChatDom ( + container: HTMLElement, + video: Video, + settings: LiveChatSettings, + showOpenBlank: boolean, + showShareUrlButton: boolean, + showPromote: boolean +): Promise { + const ptContext = getPtContext() + const peertubeHelpers = ptContext.ptOptions.peertubeHelpers + const logger = ptContext.logger + + logger.log('Adding livechat in the DOM...') + const viewersDocumentationHelpUrl = await localizedHelpUrl(ptContext.ptOptions, { + page: 'documentation/user/viewers' + }) + const p = new Promise((resolve) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.all([ + peertubeHelpers.translate(LOC_OPEN_CHAT), + peertubeHelpers.translate(LOC_OPEN_CHAT_NEW_WINDOW), + peertubeHelpers.translate(LOC_CLOSE_CHAT), + peertubeHelpers.translate(LOC_SHARE_CHAT_LINK), + peertubeHelpers.translate(LOC_ONLINE_HELP), + peertubeHelpers.translate(LOC_PROMOTE) + ]).then(labels => { + const labelOpen = labels[0] + const labelOpenBlank = labels[1] + const labelClose = labels[2] + const labelShareUrl = labels[3] + const labelHelp = labels[4] + const labelPromote = labels[5] + + const buttonContainer = document.createElement('div') + 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).then(() => {}, () => {}) + }, + icon: openChatSVG, + additionalClasses: [] + }) + if (showOpenBlank) { + groupButtons.push({ + buttonContainer, + name: 'openblank', + label: labelOpenBlank, + callback: () => { + _closeChat() + window.open('/p/livechat/room?room=' + encodeURIComponent(video.uuid)) + }, + icon: openBlankChatSVG, + additionalClasses: [] + }) + } + if (showShareUrlButton) { + groupButtons.push({ + buttonContainer, + name: 'shareurl', + label: labelShareUrl, + callback: () => { + shareChatUrl(ptContext.ptOptions, settings, video).then(() => {}, () => {}) + }, + icon: shareChatUrlSVG, + additionalClasses: [] + }) + } + if (showPromote) { + groupButtons.push({ + buttonContainer, + name: 'promote', + label: labelPromote, + callback: async () => { + try { + // First we must get the room JID (can be video.uuid@ or channel.id@) + const response = await fetch( + getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' + + encodeURIComponent(video.uuid), + { + method: 'GET', + headers: peertubeHelpers.getAuthHeader() + } + ) + + const converseJSParams: InitConverseJSParams = await (response).json() + if (converseJSParams.isRemoteChat) { + throw new Error('Cant promote on remote chat.') + } + + const roomJIDLocalPart = converseJSParams.room.replace(/@.*$/, '') + + await fetch( + getBaseRoute(ptContext.ptOptions) + '/api/promote/' + encodeURIComponent(roomJIDLocalPart), + { + method: 'PUT', + headers: peertubeHelpers.getAuthHeader() + } + ) + } catch (err) { + console.error(err) + } + }, + icon: promoteSVG, + additionalClasses: [] + }) + } + groupButtons.push({ + buttonContainer, + name: 'help', + label: labelHelp, + href: viewersDocumentationHelpUrl, + targetBlank: true, + icon: helpButtonSVG, + additionalClasses: [] + }) + + // 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() + }) + }) + return p +} + +async function _openChat (video: Video): Promise { + const ptContext = getPtContext() + const peertubeHelpers = ptContext.ptOptions.peertubeHelpers + const logger = ptContext.logger + const settings = await ptContext.getSettings() + + if (!video) { + logger.log('No video.') + return false + } + + logger.info(`Trying to load the chat for video ${video.uuid}.`) + // 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') + + try { + if (!container) { + logger.error('Cant found the livechat container.') + return false + } + + if (container.getElementsByTagName('converse-root').length) { + logger.error('Seems that there is already a ConverseJS in the container.') + return false + } + + if (additionalStyles) { + container.setAttribute('style', additionalStyles) + } + container.setAttribute('peertube-plugin-livechat-state', 'open') + + // Hacking styles... + _hackStyles(true) + + // Loading converseJS... + await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false) + } catch (err) { + // Displaying an error page. + if (container) { + const message = document.createElement('div') + message.classList.add('peertube-plugin-livechat-error-message') + message.innerText = await peertubeHelpers.translate(LOC_CHATROOM_NOT_ACCESSIBLE) + container.append(message) + + container.querySelectorAll( + '.livechat-spinner, converse-root' + ).forEach(dom => dom.remove()) + } + + _hackStyles(false) + } +} + +function _closeChat (): void { + const logger = getPtContext().logger + + const container = document.getElementById('peertube-plugin-livechat-container') + if (!container) { + logger.error('Can\'t close livechat, container not found.') + return + } + + // Disconnecting ConverseJS + if (window.converse?.livechatDisconnect) { window.converse.livechatDisconnect() } + + // Removing from the DOM + container.querySelectorAll( + 'converse-root, .livechat-spinner, .peertube-plugin-livechat-error-message' + ).forEach(dom => dom.remove()) + + container.setAttribute('peertube-plugin-livechat-state', 'closed') + + // Un-Hacking styles... + _hackStyles(false) +} + +function _hackStyles (on: boolean): void { + try { + document.querySelectorAll('.peertube-plugin-livechat-buttons').forEach(buttons => { + if (on) { + buttons.classList.add('peertube-plugin-livechat-buttons-open') + } else { + buttons.classList.remove('peertube-plugin-livechat-buttons-open') + } + }) + const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder') + if (on) { + // Saving current style attributes and maximazing space for the chat + if (myPluginPlaceholder) { + savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else. + myPluginPlaceholder.style.flexGrow = '1' + } + } else { + // restoring values... + if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) { + myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow + } + } + } catch (err) { + getPtContext().logger.error(`Failed hacking styles: '${err as string}'`) + } +} + +export { + initChat +} diff --git a/client/common/videowatch/register.ts b/client/common/videowatch/register.ts new file mode 100644 index 00000000..e8018d6a --- /dev/null +++ b/client/common/videowatch/register.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Video } from '@peertube/peertube-types' +import { getPtContext } from '../lib/contexts/peertube' +import { initChat } from './chat' + +interface VideoWatchLoadedHookOptions { + videojs: any + video: Video + playlist?: any +} + +export async function registerVideoWatch (): Promise { + const ptContext = getPtContext() + ptContext.ptOptions.registerHook({ + target: 'action:video-watch.video.loaded', + handler: ({ + video, + playlist + }: VideoWatchLoadedHookOptions) => { + if (!video) { + ptContext.logger.error('No video argument in hook action:video-watch.video.loaded') + return + } + if (playlist) { + ptContext.logger.info('We are in a playlist, we will not use the webchat') + return + } + initChat(video).then(() => {}, () => {}) + } + }) +} diff --git a/client/videowatch/share.ts b/client/common/videowatch/share.ts similarity index 99% rename from client/videowatch/share.ts rename to client/common/videowatch/share.ts index 7acce62c..c6404a47 100644 --- a/client/videowatch/share.ts +++ b/client/common/videowatch/share.ts @@ -5,10 +5,10 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { Video } from '@peertube/peertube-types' import { helpButtonSVG } from './buttons' -import { logger } from '../utils/logger' +import { logger } from '../../utils/logger' import { getIframeUri, getXMPPAddr, UriOptions } from './uri' import { isAutoColorsAvailable } from 'shared/lib/autocolors' -import { localizedHelpUrl } from '../utils/help' +import { localizedHelpUrl } from '../../utils/help' interface ShareForm { shareString: HTMLInputElement diff --git a/client/videowatch/uri.ts b/client/common/videowatch/uri.ts similarity index 94% rename from client/videowatch/uri.ts rename to client/common/videowatch/uri.ts index eea5f7ed..2cc9bb2b 100644 --- a/client/videowatch/uri.ts +++ b/client/common/videowatch/uri.ts @@ -5,9 +5,9 @@ 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 '../utils/logger' -import { computeAutoColors } from '../utils/colors' +import { getBaseRoute } from '../../utils/uri' +import { logger } from '../../utils/logger' +import { computeAutoColors } from '../../utils/colors' interface UriOptions { readonly?: boolean | 'noscroll' diff --git a/client/utils/user.ts b/client/utils/user.ts new file mode 100644 index 00000000..d36bdde3 --- /dev/null +++ b/client/utils/user.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import type { RegisterClientOptions } from '@peertube/peertube-types/client' +import type { Video } from '@peertube/peertube-types' +import { logger } from './logger' + +export function isAnonymousUser (registerOptions: RegisterClientOptions): boolean { + return !registerOptions.peertubeHelpers.isLoggedIn() +} + +export function guessIsMine (registerOptions: RegisterClientOptions, video: Video): boolean { + // Note: this is not safe, but it is not a problem: + // this function is used for non critical functions + try { + if (!video) { + return false + } + if (!video.isLocal) { + return false + } + if (!window.localStorage) { + return false + } + const username = window.localStorage.getItem('username') ?? '' + if (!username) { + return false + } + if (username !== video.account?.name) { + return false + } + return true + } catch (err) { + logger.error(err as string) + return false + } +} + +export function guessIamIModerator (_registerOptions: RegisterClientOptions): boolean { + // Note: this is not safe, but it is not a problem: + // this function is used for non critical functions + try { + if (!window.localStorage) { + return false + } + const role = window.localStorage.getItem('role') ?? '' + if (!role) { + return false + } + if (role !== '0' && role !== '1') { + return false + } + return true + } catch (err) { + logger.error(err as string) + return false + } +} diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts deleted file mode 100644 index dc5c79ce..00000000 --- a/client/videowatch-client-plugin.ts +++ /dev/null @@ -1,426 +0,0 @@ -// SPDX-FileCopyrightText: 2024 John Livingston -// -// SPDX-License-Identifier: AGPL-3.0-only - -import type { Video } from '@peertube/peertube-types' -import type { RegisterClientOptions } from '@peertube/peertube-types/client' -import type { InitConverseJSParams } from 'shared/lib/types' -import { videoHasWebchat, videoHasRemoteWebchat } from 'shared/lib/video' -import { localizedHelpUrl } from './utils/help' -import { logger } from './utils/logger' -import { - closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG, promoteSVG -} from './videowatch/buttons' -import { displayButton, displayButtonOptions } from './videowatch/button' -import { shareChatUrl } from './videowatch/share' -import { displayConverseJS } from './utils/conversejs' -import { getBaseRoute } from './utils/uri' - -interface VideoWatchLoadedHookOptions { - videojs: any - video: Video - playlist?: any -} - -function isAnonymousUser (registerOptions: RegisterClientOptions): boolean { - return !registerOptions.peertubeHelpers.isLoggedIn() -} - -function guessIsMine (registerOptions: RegisterClientOptions, video: Video): boolean { - // Note: this is not safe, but it is not a problem: - // this function is used for non critical functions - try { - if (!video) { - return false - } - if (!video.isLocal) { - return false - } - if (!window.localStorage) { - return false - } - const username = window.localStorage.getItem('username') ?? '' - if (!username) { - return false - } - if (username !== video.account?.name) { - return false - } - return true - } catch (err) { - logger.error(err as string) - return false - } -} - -function guessIamIModerator (_registerOptions: RegisterClientOptions): boolean { - // Note: this is not safe, but it is not a problem: - // this function is used for non critical functions - try { - if (!window.localStorage) { - return false - } - const role = window.localStorage.getItem('role') ?? '' - if (!role) { - return false - } - if (role !== '0' && role !== '1') { - return false - } - return true - } catch (err) { - logger.error(err as string) - return false - } -} - -function register (registerOptions: RegisterClientOptions): void { - const { registerHook, peertubeHelpers } = registerOptions - let settings: any = {} // will be loaded later - - async function insertChatDom ( - container: HTMLElement, - video: Video, - showOpenBlank: boolean, - showShareUrlButton: boolean, - showPromote: boolean - ): Promise { - logger.log('Adding livechat in the DOM...') - const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, { - page: 'documentation/user/viewers' - }) - const p = new Promise((resolve) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.all([ - peertubeHelpers.translate(LOC_OPEN_CHAT), - peertubeHelpers.translate(LOC_OPEN_CHAT_NEW_WINDOW), - peertubeHelpers.translate(LOC_CLOSE_CHAT), - peertubeHelpers.translate(LOC_SHARE_CHAT_LINK), - peertubeHelpers.translate(LOC_ONLINE_HELP), - peertubeHelpers.translate(LOC_PROMOTE) - ]).then(labels => { - const labelOpen = labels[0] - const labelOpenBlank = labels[1] - const labelClose = labels[2] - const labelShareUrl = labels[3] - const labelHelp = labels[4] - const labelPromote = labels[5] - - const buttonContainer = document.createElement('div') - 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).then(() => {}, () => {}) - }, - icon: openChatSVG, - additionalClasses: [] - }) - if (showOpenBlank) { - groupButtons.push({ - buttonContainer, - name: 'openblank', - label: labelOpenBlank, - callback: () => { - closeChat() - window.open('/p/livechat/room?room=' + encodeURIComponent(video.uuid)) - }, - icon: openBlankChatSVG, - additionalClasses: [] - }) - } - if (showShareUrlButton) { - groupButtons.push({ - buttonContainer, - name: 'shareurl', - label: labelShareUrl, - callback: () => { - shareChatUrl(registerOptions, settings, video).then(() => {}, () => {}) - }, - icon: shareChatUrlSVG, - additionalClasses: [] - }) - } - if (showPromote) { - groupButtons.push({ - buttonContainer, - name: 'promote', - label: labelPromote, - callback: async () => { - try { - // First we must get the room JID (can be video.uuid@ or channel.id@) - const response = await fetch( - getBaseRoute(registerOptions) + '/api/configuration/room/' + - encodeURIComponent(video.uuid), - { - method: 'GET', - headers: peertubeHelpers.getAuthHeader() - } - ) - - const converseJSParams: InitConverseJSParams = await (response).json() - if (converseJSParams.isRemoteChat) { - throw new Error('Cant promote on remote chat.') - } - - const roomJIDLocalPart = converseJSParams.room.replace(/@.*$/, '') - - await fetch( - getBaseRoute(registerOptions) + '/api/promote/' + encodeURIComponent(roomJIDLocalPart), - { - method: 'PUT', - headers: peertubeHelpers.getAuthHeader() - } - ) - } catch (err) { - console.error(err) - } - }, - icon: promoteSVG, - additionalClasses: [] - }) - } - groupButtons.push({ - buttonContainer, - name: 'help', - label: labelHelp, - href: viewersDocumentationHelpUrl, - targetBlank: true, - icon: helpButtonSVG, - additionalClasses: [] - }) - - // 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() - }) - }) - return p - } - - 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}.`) - // 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') - - try { - if (!container) { - logger.error('Cant found the livechat container.') - return false - } - - if (container.getElementsByTagName('converse-root').length) { - logger.error('Seems that there is already a ConverseJS in the container.') - return false - } - - if (additionalStyles) { - container.setAttribute('style', additionalStyles) - } - container.setAttribute('peertube-plugin-livechat-state', 'open') - - // Hacking styles... - hackStyles(true) - - // Loading converseJS... - await displayConverseJS(registerOptions, container, roomkey, 'peertube-video', false) - } catch (err) { - // Displaying an error page. - if (container) { - const message = document.createElement('div') - message.classList.add('peertube-plugin-livechat-error-message') - message.innerText = await peertubeHelpers.translate(LOC_CHATROOM_NOT_ACCESSIBLE) - container.append(message) - - container.querySelectorAll( - '.livechat-spinner, converse-root' - ).forEach(dom => dom.remove()) - } - - hackStyles(false) - } - } - - function closeChat (): void { - const container = document.getElementById('peertube-plugin-livechat-container') - if (!container) { - logger.error('Can\'t close livechat, container not found.') - return - } - - // Disconnecting ConverseJS - if (window.converse?.livechatDisconnect) { window.converse.livechatDisconnect() } - - // Removing from the DOM - container.querySelectorAll( - 'converse-root, .livechat-spinner, .peertube-plugin-livechat-error-message' - ).forEach(dom => dom.remove()) - - container.setAttribute('peertube-plugin-livechat-state', 'closed') - - // Un-Hacking styles... - hackStyles(false) - } - - async function initChat (video: Video): Promise { - if (!video) { - logger.error('No video provided') - return - } - const placeholder = document.getElementById('plugin-placeholder-player-next') - if (!placeholder) { - logger.error('The required placeholder div is not present in the DOM.') - return - } - - let container = placeholder.querySelector('#peertube-plugin-livechat-container') - if (container) { - logger.log('The chat seems already initialized...') - return - } - container = document.createElement('div') - container.setAttribute('id', 'peertube-plugin-livechat-container') - container.setAttribute('peertube-plugin-livechat-state', 'initializing') - container.setAttribute('peertube-plugin-livechat-current-url', window.location.href) - placeholder.append(container) - - 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(settings, video) && !videoHasRemoteWebchat(settings, video)) { - logger.log('This video has no webchat') - return - } - - let showShareUrlButton: boolean = false - let showPromote: boolean = false - if (video.isLocal) { // No need for shareButton on remote chats. - const chatShareUrl = settings['chat-share-url'] ?? '' - if (chatShareUrl === 'everyone') { - showShareUrlButton = true - } else if (chatShareUrl === 'owner') { - showShareUrlButton = guessIsMine(registerOptions, video) - } else if (chatShareUrl === 'owner+moderators') { - showShareUrlButton = guessIsMine(registerOptions, video) || guessIamIModerator(registerOptions) - } - - if (guessIamIModerator(registerOptions)) { - showPromote = true - } - } - - await insertChatDom( - container as HTMLElement, - video, - !!settings['chat-open-blank'], - showShareUrlButton, - showPromote - ) - 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 - function hackStyles (on: boolean): void { - try { - document.querySelectorAll('.peertube-plugin-livechat-buttons').forEach(buttons => { - if (on) { - buttons.classList.add('peertube-plugin-livechat-buttons-open') - } else { - buttons.classList.remove('peertube-plugin-livechat-buttons-open') - } - }) - const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder') - if (on) { - // Saving current style attributes and maximazing space for the chat - if (myPluginPlaceholder) { - savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else. - myPluginPlaceholder.style.flexGrow = '1' - } - } else { - // restoring values... - if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) { - myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow - } - } - } catch (err) { - logger.error(`Failed hacking styles: '${err as string}'`) - } - } - - registerHook({ - target: 'action:video-watch.video.loaded', - handler: ({ - video, - playlist - }: VideoWatchLoadedHookOptions) => { - if (!video) { - logger.error('No video argument in hook action:video-watch.video.loaded') - return - } - if (playlist) { - logger.info('We are in a playlist, we will not use the webchat') - return - } - initChat(video).then(() => {}, () => {}) - } - }) -} - -export { - register -} diff --git a/package.json b/package.json index b192eef2..809f6818 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,6 @@ }, "bugs": "https://github.com/JohnXLivingston/peertube-plugin-livechat/issues", "clientScripts": [ - { - "script": "dist/client/videowatch-client-plugin.js", - "scopes": [ - "video-watch" - ] - }, { "script": "dist/client/common-client-plugin.js", "scopes": [ diff --git a/support/documentation/content/en/technical/sourcecode/_index.md b/support/documentation/content/en/technical/sourcecode/_index.md index bb236073..8a0b74e9 100644 --- a/support/documentation/content/en/technical/sourcecode/_index.md +++ b/support/documentation/content/en/technical/sourcecode/_index.md @@ -43,7 +43,7 @@ For example, `build-conversejs.js` use the folder `build/conversejs` to build a The `client` folder contains the front-end source code. -Files like `client/common-client-plugin.ts`, `client/videowatch-client-plugin.ts`, ... are the base files that +Files like `client/common-client-plugin.ts`, `admin-plugin-client-plugin.ts`, ... are the base files that are loaded by Peertube for different "scopes" (`common`, `videowatch`, ...). Please refer to the [Peertube plugin documentation](https://docs.joinpeertube.org/contribute/plugins) for more information.