From 75dd2e4d59355a4e23b4003324275f6824ec985e Mon Sep 17 00:00:00 2001 From: John Livingston Date: Fri, 14 Jun 2024 13:03:14 +0200 Subject: [PATCH] Rewriting Share modal WIP: Using lit to entirely rewrite the share chat modal. --- CHANGELOG.md | 1 + assets/styles/style.scss | 62 +-- assets/styles/style/elements/share-chat.scss | 77 ++++ client/@types/global.d.ts | 3 + client/common/lib/contexts/peertube.ts | 4 + client/common/videowatch/elements/index.ts | 1 + .../common/videowatch/elements/share-chat.ts | 225 +++++++++ .../elements/templates/share-chat.ts | 171 +++++++ client/common/videowatch/register.ts | 1 + client/common/videowatch/share.ts | 436 +++--------------- client/common/videowatch/uri.ts | 7 +- languages/en.yml | 7 +- 12 files changed, 559 insertions(+), 436 deletions(-) create mode 100644 assets/styles/style/elements/share-chat.scss create mode 100644 client/common/videowatch/elements/index.ts create mode 100644 client/common/videowatch/elements/share-chat.ts create mode 100644 client/common/videowatch/elements/templates/share-chat.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b8514dbd..0ce2626c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,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. +* Rewriting the share chat dialog with more modern code. ## 10.0.2 diff --git a/assets/styles/style.scss b/assets/styles/style.scss index 3dc28bd6..088f6c9f 100644 --- a/assets/styles/style.scss +++ b/assets/styles/style.scss @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +@use "style/elements/share-chat.scss"; + #peertube-plugin-livechat-container { display: flex; flex-direction: column; @@ -143,66 +145,6 @@ table.peertube-plugin-livechat-prosody-list-rooms td { padding: 4px 5px; } -.peertube-plugin-livechat-shareurl-modal { - & > * { - margin-top: 10px; - } - - .livechat-shareurl-copy { - display: flex; - flex-wrap: wrap; - - button { - white-space: nowrap; - } - - input { - flex-grow: 2; - width: auto !important; // must cancel the width: 100% of form-control - } - } - - .livechat-shareurl-web-options, - .livechat-shareurl-xmpp-options { - input[type="checkbox"], - input[type="radio"] { - margin-right: 20px; - } - - label { - display: block; - } - - .livechat-shareurl-web-options-readonly { - margin-left: 40px; - - & > * { - display: block; - } - - &.livechat-shareurl-web-options-readonly-disabled { - label { - color: var(--gray-dark); - } - } - } - } - - .livechat-shareurl-protocol { - display: flex; - flex-flow: row wrap; - column-gap: 30px; - - input[type="radio"] { - margin-right: 10px; - } - } - - .livechat-shareurl-tips { - min-height: 60px; - } -} - livechat-spinner, .livechat-spinner { display: flex; diff --git a/assets/styles/style/elements/share-chat.scss b/assets/styles/style/elements/share-chat.scss new file mode 100644 index 00000000..e75e5576 --- /dev/null +++ b/assets/styles/style/elements/share-chat.scss @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +livechat-share-chat { + display: block; + + & > * { + margin-top: 10px; + } + + .sub-menu-entry { + cursor: pointer; + } + + .livechat-shareurl-copy { + display: flex; + flex-wrap: wrap; + + button { + white-space: nowrap; + } + + input { + flex-grow: 2; + width: auto !important; // must cancel the width: 100% of form-control + } + } + + .livechat-shareurl-block { + // To avoid the height to change when switching tabs, we set a height and overflow. + height: 300px; + overflow-y: scroll; + } + + .livechat-shareurl-options { + input[type="checkbox"], + input[type="radio"] { + margin-right: 20px; + } + + label { + display: block; + } + + .livechat-shareurl-suboptions { + margin-left: 40px; + + & > * { + display: block; + } + + &.livechat-shareurl-suboptions-disabled { + label { + /* stylelint-disable-next-line custom-property-pattern */ + color: var(--greyForegroundColor); + } + } + } + } + + .livechat-shareurl-protocol { + display: flex; + flex-flow: row wrap; + column-gap: 30px; + + input[type="radio"] { + margin-right: 10px; + } + } + + .livechat-shareurl-tips { + min-height: 60px; + } +} diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index 02f40a25..b44834c9 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -110,3 +110,6 @@ declare const LOC_ACTION_REMOVE_ENTRY: string declare const LOC_ACTION_REMOVE_ENTRY_CONFIRM: string declare const LOC_LOADING_ERROR: string + +declare const LOC_SHARE_CHAT_EMBED: string +declare const LOC_SHARE_CHAT_PEERTUBE_TIPS: string diff --git a/client/common/lib/contexts/peertube.ts b/client/common/lib/contexts/peertube.ts index 3cbe0a06..0dac9914 100644 --- a/client/common/lib/contexts/peertube.ts +++ b/client/common/lib/contexts/peertube.ts @@ -4,6 +4,7 @@ import type { SettingEntries } from '@peertube/peertube-types' import type { RegisterClientOptions } from '@peertube/peertube-types/client/types' +import type { ConverseJSTheme } from 'shared/lib/types' import { logger as loggerFunction } from '../../../utils/logger' // We precise some of the settings in SettingsEntries, to faciliate some type checking. @@ -14,6 +15,9 @@ export type LiveChatSettings = SettingEntries & { 'chat-videos-list': string 'federation-no-remote-chat': boolean 'chat-style': string | undefined + 'prosody-room-allow-s2s': boolean + 'converse-theme': ConverseJSTheme + 'prosody-room-type': string } export class PtContext { diff --git a/client/common/videowatch/elements/index.ts b/client/common/videowatch/elements/index.ts new file mode 100644 index 00000000..5142fd40 --- /dev/null +++ b/client/common/videowatch/elements/index.ts @@ -0,0 +1 @@ +import './share-chat' diff --git a/client/common/videowatch/elements/share-chat.ts b/client/common/videowatch/elements/share-chat.ts new file mode 100644 index 00000000..49568a5c --- /dev/null +++ b/client/common/videowatch/elements/share-chat.ts @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Video } from '@peertube/peertube-types' +import type { LiveChatSettings } from '../../lib/contexts/peertube' +import { html, PropertyValues, TemplateResult } from 'lit' +import { customElement, property } from 'lit/decorators.js' +import { LivechatElement } from '../../lib/elements/livechat' +import { tplShareChatCopy, tplShareChatTips, tplShareChatTabs, tplShareChatOptions } from './templates/share-chat' +import { isAutoColorsAvailable } from 'shared/lib/autocolors' +import { getIframeUri, getXMPPAddr, UriOptions } from '../uri' + +/** + * Results of the ShareChatElement.computeUrl method. + */ +interface ComputedUrl { + /** + * The string to share. + */ + shareString: string + + /** + * The url to open in a browser window, if relevant (http:// or xmpp://). + * Undefined when not a standard uri (for iframes for exemple). + * This will be the url used by the "open" button. + */ + openUrl: string | undefined +} + +@customElement('livechat-share-chat') +export class ShareChatElement extends LivechatElement { + /** + * The associated video. + * Must be given when calling this custom element. + */ + @property({ attribute: false }) + protected _video!: Video + + /** + * The settings. + * Must be given when calling this custom element. + */ + @property({ attribute: false }) + protected _settings!: LiveChatSettings + + /** + * The current tab. + */ + @property({ attribute: false }) + public currentTab: 'peertube' | 'embed' | 'xmpp' = 'peertube' + + /** + * Should we render the XMPP tab? + */ + @property({ attribute: false }) + public xmppUriEnabled: boolean = false + + /** + * Can we use autocolors? + */ + @property({ attribute: false }) + public autocolorsAvailable: boolean = false + + /** + * In the Embed tab, should we generated an iframe link. + */ + @property({ attribute: false }) + public embedIFrame: boolean = false + + /** + * In the Embed tab, should we generated a read-only chat link. + */ + @property({ attribute: false }) + public embedReadOnly: boolean = false + + /** + * Read-only, with scrollbar? + */ + @property({ attribute: false }) + public embedReadOnlyScrollbar: boolean = false + + /** + * Read-only, transparent background? + */ + @property({ attribute: false }) + public embedReadOnlyTransparentBackground: boolean = false + + /** + * In the Embed tab, should we use current theme color? + */ + @property({ attribute: false }) + public embedAutocolors: boolean = false + + protected override firstUpdated (changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties) + const settings = this._settings + this.xmppUriEnabled = !!settings['prosody-room-allow-s2s'] + this.autocolorsAvailable = isAutoColorsAvailable(settings['converse-theme']) + + this._restorePreviousState() + } + + protected override render = (): TemplateResult => { + return html` + ${tplShareChatTabs(this)} + ${tplShareChatCopy(this)} +
+ ${tplShareChatTips(this)} + ${tplShareChatOptions(this)} +
+ ` + } + + protected _restorePreviousState (): void { + // TODO: restore previous state. + + // Some sanity checks, to not be in an impossible state. + if (!this.xmppUriEnabled && this.currentTab === 'xmpp') { + this.currentTab = 'peertube' + } + } + + public computeUrl (): ComputedUrl { + switch (this.currentTab) { + case 'peertube': return this._computeUrlPeertube() + case 'embed': return this._computeUrlEmbed() + case 'xmpp': return this._computeUrlXMPP() + default: + return { + shareString: '', + openUrl: undefined + } + } + } + + protected _computeUrlPeertube (): ComputedUrl { + const url = window.location.protocol + + '//' + + window.location.host + + '/p/livechat/room?room=' + + encodeURIComponent(this._video.uuid) + return { + shareString: url, + openUrl: url + } + } + + protected _computeUrlXMPP (): ComputedUrl { + const addr = getXMPPAddr(this.ptContext.ptOptions, this._settings, this._video) + return { + shareString: addr?.jid ?? '', + openUrl: addr?.uri + } + } + + protected _computeUrlEmbed (): ComputedUrl { + const uriOptions: UriOptions = { + ignoreAutoColors: this.autocolorsAvailable ? !this.embedAutocolors : true, + permanent: true + } + + if (this.embedReadOnly) { + uriOptions.readonly = this.embedReadOnlyScrollbar ? true : 'noscroll' + if (this.embedReadOnlyTransparentBackground) { + uriOptions.transparent = true + } + } + + // Note: for the "embed" case, the url is always the same as the iframe. + // So we use getIframeUri to compte, and just change the finale result if we really want the iframe. + const url = getIframeUri(this.ptContext.ptOptions, this._settings, this._video, uriOptions) + if (!url) { + return { + shareString: '', + openUrl: undefined + } + } + + if (!this.embedIFrame) { + return { + shareString: url, + openUrl: url + } + } + + // Actually building the iframe: + const iframe = document.createElement('iframe') + iframe.setAttribute('src', url) + iframe.setAttribute('title', this._video.name) + iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms') + iframe.setAttribute('width', '560') + iframe.setAttribute('height', '315') + iframe.setAttribute('frameborder', '0') + const iframeHTML = iframe.outerHTML + iframe.remove() + return { + shareString: iframeHTML, + openUrl: undefined + } + } + + /** + * Copy the current url in the clipboard. + */ + public async copyUrl (): Promise { + await navigator.clipboard.writeText(this.computeUrl().shareString) + this.ptNotifier.success(await this.ptTranslate(LOC_COPIED)) + } + + /** + * Opens the url. + */ + public openUrl (): void { + const url = this.computeUrl().openUrl + if (!url) { + return + } + window.open(url) + } + + public switchTab (tab: ShareChatElement['currentTab']): void { + this.currentTab = tab + } +} diff --git a/client/common/videowatch/elements/templates/share-chat.ts b/client/common/videowatch/elements/templates/share-chat.ts new file mode 100644 index 00000000..561deab7 --- /dev/null +++ b/client/common/videowatch/elements/templates/share-chat.ts @@ -0,0 +1,171 @@ +import type { ShareChatElement } from '../share-chat' +import { html, TemplateResult } from 'lit' +import { ptTr } from '../../../lib/directives/translation' +import { classMap } from 'lit/directives/class-map.js' + +export function tplShareChatCopy (el: ShareChatElement): TemplateResult { + const computedUrl = el.computeUrl() + return html`
+ { + const input = ev.target as HTMLInputElement + // Select the whole value when entering the input. + input.select() + input.setSelectionRange(0, 99999) /* For mobile devices */ + }} + /> + + + +
` +} + +function _tplShareChatTab ( + el: ShareChatElement, + tabName: ShareChatElement['currentTab'], + label: string +): TemplateResult { + return html` + { + ev.preventDefault() + el.switchTab(tabName) + }} + > + ${ptTr(label)} + ` +} + +export function tplShareChatTabs (el: ShareChatElement): TemplateResult { + return html` + ${_tplShareChatTab(el, 'peertube', LOC_WEB)} + ${_tplShareChatTab(el, 'embed', LOC_SHARE_CHAT_EMBED)} + ${ + el.xmppUriEnabled + ? _tplShareChatTab(el, 'xmpp', LOC_CONNECT_USING_XMPP) + : '' + } + ` +} + +export function tplShareChatTips (el: ShareChatElement): TemplateResult { + let label: string | undefined + switch (el.currentTab) { + case 'peertube': + label = LOC_SHARE_CHAT_PEERTUBE_TIPS + break + case 'embed': + label = LOC_TIPS_FOR_STREAMERS + break + case 'xmpp': + label = LOC_CONNECT_USING_XMPP_HELP + break + } + if (!label) { + return html`` + } + return html`
${ptTr(label)}
` +} + +function _tplShareChatPeertubeOptions (_el: ShareChatElement): TemplateResult { + return html`` +} + +function _tplShareChatEmbedOptions (el: ShareChatElement): TemplateResult { + return html` + + + +
+ + +
+ + ${ + el.autocolorsAvailable + ? html` + ` + : '' + } + ` +} + +function _tplShareChatXMPPOptions (_el: ShareChatElement): TemplateResult { + return html`` +} + +export function tplShareChatOptions (el: ShareChatElement): TemplateResult { + let tpl: TemplateResult + switch (el.currentTab) { + case 'peertube': + tpl = _tplShareChatPeertubeOptions(el) + break + case 'embed': + tpl = _tplShareChatEmbedOptions(el) + break + case 'xmpp': + tpl = _tplShareChatXMPPOptions(el) + break + default: + tpl = html`` + } + return html`
${tpl}
` +} diff --git a/client/common/videowatch/register.ts b/client/common/videowatch/register.ts index e8018d6a..4d431d6d 100644 --- a/client/common/videowatch/register.ts +++ b/client/common/videowatch/register.ts @@ -5,6 +5,7 @@ import type { Video } from '@peertube/peertube-types' import { getPtContext } from '../lib/contexts/peertube' import { initChat } from './chat' +import './elements' // Import all needed elements. interface VideoWatchLoadedHookOptions { videojs: any diff --git a/client/common/videowatch/share.ts b/client/common/videowatch/share.ts index c6404a47..c1a2c305 100644 --- a/client/common/videowatch/share.ts +++ b/client/common/videowatch/share.ts @@ -4,385 +4,68 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { Video } from '@peertube/peertube-types' -import { helpButtonSVG } from './buttons' +import type { LiveChatSettings } from '../lib/contexts/peertube' import { logger } from '../../utils/logger' -import { getIframeUri, getXMPPAddr, UriOptions } from './uri' -import { isAutoColorsAvailable } from 'shared/lib/autocolors' -import { localizedHelpUrl } from '../../utils/help' +import { html, render } from 'lit' -interface ShareForm { - shareString: HTMLInputElement - openButton: HTMLButtonElement - copyButton: HTMLButtonElement - readonly: HTMLInputElement - withscroll: HTMLInputElement - transparent: HTMLInputElement - readonlyOptions: HTMLElement - autoColors?: HTMLInputElement - generateIframe: HTMLInputElement - divTips: HTMLElement - radioProtocolWeb?: HTMLInputElement - radioProtocolXMPP?: HTMLInputElement - divWebOptions: HTMLDivElement -} - -async function shareChatUrl (registerOptions: RegisterClientOptions, settings: any, video: Video): Promise { +async function shareChatUrl ( + registerOptions: RegisterClientOptions, + settings: LiveChatSettings, + video: Video +): Promise { const peertubeHelpers = registerOptions.peertubeHelpers - const streamersHelpUrl = await localizedHelpUrl(registerOptions, { - page: 'documentation/user/streamers' - }) + const labelShare = await peertubeHelpers.translate(LOC_SHARE_CHAT_LINK) - const [ - labelShare, - labelWeb, - labelXMPP, - labelXMPPTips, - labelReadonly, - labelWithscroll, - labelTransparent, - labelOBSTips, - labelCopy, - labelCopied, - labelError, - labelOpen, - labelAutocolors, - labelGenerateIframe, - labelChatFor, - labelHelp - ] = await Promise.all([ - peertubeHelpers.translate(LOC_SHARE_CHAT_LINK), - peertubeHelpers.translate(LOC_WEB), - peertubeHelpers.translate(LOC_CONNECT_USING_XMPP), - peertubeHelpers.translate(LOC_CONNECT_USING_XMPP_HELP), - peertubeHelpers.translate(LOC_READ_ONLY), - peertubeHelpers.translate(LOC_SHOW_SCROLLBARR), - peertubeHelpers.translate(LOC_TRANSPARENT_BACKGROUND), - peertubeHelpers.translate(LOC_TIPS_FOR_STREAMERS), - peertubeHelpers.translate(LOC_COPY), - peertubeHelpers.translate(LOC_LINK_COPIED), - peertubeHelpers.translate(LOC_ERROR), - peertubeHelpers.translate(LOC_OPEN), - peertubeHelpers.translate(LOC_USE_CURRENT_THEME_COLOR), - peertubeHelpers.translate(LOC_GENERATE_IFRAME), - peertubeHelpers.translate(LOC_CHAT_FOR_LIVE_STREAM), - peertubeHelpers.translate(LOC_ONLINE_HELP) - ]) + // function save (form: ShareForm): void { + // if (!window.localStorage) { + // return + // } + // const v = { + // version: 1, // in case we add incompatible values in a near feature + // readonly: !!form.readonly.checked, + // withscroll: !!form.withscroll.checked, + // transparent: !!form.transparent.checked, + // autocolors: !!form.autoColors?.checked, + // generateIframe: !!form.generateIframe.checked, + // protocol: !form.radioProtocolWeb || form.radioProtocolWeb.checked ? 'web' : 'xmpp' + // } + // window.localStorage.setItem('peertube-plugin-livechat-shareurl', JSON.stringify(v)) + // } - const defaultUri = getIframeUri(registerOptions, settings, video) - if (!defaultUri) { - return - } - - let form: ShareForm | undefined - function renderContent (container: HTMLElement): void { - if (!form) { - container.childNodes.forEach(child => container.removeChild(child)) - - container.classList.add('peertube-plugin-livechat-shareurl-modal') - - const divShareString = document.createElement('div') - divShareString.classList.add('livechat-shareurl-copy') - const shareString = document.createElement('input') - shareString.setAttribute('type', 'text') - shareString.setAttribute('readonly', '') - shareString.setAttribute('autocomplete', 'off') - shareString.setAttribute('placeholder', '') - shareString.classList.add('form-control', 'readonly') - divShareString.append(shareString) - const copyButton = document.createElement('button') - copyButton.classList.add('btn', 'btn-outline-secondary', 'text-uppercase') - copyButton.textContent = labelCopy - divShareString.append(copyButton) - const openButton = document.createElement('button') - openButton.classList.add('btn', 'btn-outline-secondary', 'text-uppercase') - openButton.textContent = labelOpen - divShareString.append(openButton) - - const helpButton = document.createElement('a') - helpButton.href = streamersHelpUrl - helpButton.target = '_blank' - helpButton.innerHTML = helpButtonSVG() - helpButton.title = labelHelp - helpButton.classList.add('orange-button', 'peertube-button-link', 'peertube-plugin-livechat-button') - divShareString.append(helpButton) - - container.append(divShareString) - - let radioProtocolWeb - let radioProtocolXMPP - if (settings['prosody-room-allow-s2s']) { - const protocolContainer = document.createElement('div') - protocolContainer.classList.add('livechat-shareurl-protocol') - - radioProtocolWeb = document.createElement('input') - radioProtocolWeb.setAttribute('type', 'radio') - radioProtocolWeb.setAttribute('value', 'web') - radioProtocolWeb.setAttribute('name', 'protocol') - const radioProtocolWebLabel = document.createElement('label') - radioProtocolWebLabel.textContent = labelWeb - radioProtocolWebLabel.prepend(radioProtocolWeb) - protocolContainer.append(radioProtocolWebLabel) - - radioProtocolXMPP = document.createElement('input') - radioProtocolXMPP.setAttribute('type', 'radio') - radioProtocolXMPP.setAttribute('value', 'xmpp') - radioProtocolXMPP.setAttribute('name', 'protocol') - const radioProtocolXMPPLabel = document.createElement('label') - radioProtocolXMPPLabel.textContent = labelXMPP - radioProtocolXMPPLabel.prepend(radioProtocolXMPP) - protocolContainer.append(radioProtocolXMPPLabel) - - container.append(protocolContainer) - } - - const divTips = document.createElement('div') - divTips.textContent = '' - divTips.classList.add('livechat-shareurl-tips') - container.append(divTips) - - const divWebOptions = document.createElement('div') - divWebOptions.classList.add('livechat-shareurl-web-options') - container.append(divWebOptions) - - const readonly = document.createElement('input') - readonly.setAttribute('type', 'checkbox') - const readonlyLabelEl = document.createElement('label') - readonlyLabelEl.textContent = labelReadonly - readonlyLabelEl.prepend(readonly) - divWebOptions.append(readonlyLabelEl) - - const readonlyOptions = document.createElement('div') - readonlyOptions.classList.add('livechat-shareurl-web-options-readonly') - divWebOptions.append(readonlyOptions) - - const withscroll = document.createElement('input') - withscroll.setAttribute('type', 'checkbox') - const withscrollLabelEl = document.createElement('label') - withscrollLabelEl.textContent = labelWithscroll - withscrollLabelEl.prepend(withscroll) - readonlyOptions.append(withscrollLabelEl) - - const transparent = document.createElement('input') - transparent.setAttribute('type', 'checkbox') - const transparentLabelEl = document.createElement('label') - transparentLabelEl.textContent = labelTransparent - transparentLabelEl.prepend(transparent) - readonlyOptions.append(transparentLabelEl) - - let autoColors - if (isAutoColorsAvailable(settings['converse-theme'])) { - const label = document.createElement('label') - label.innerText = labelAutocolors - autoColors = document.createElement('input') - autoColors.setAttribute('type', 'checkbox') - label.prepend(autoColors) - divWebOptions.append(label) - } - - const generateIframe = document.createElement('input') - generateIframe.setAttribute('type', 'checkbox') - const generateIframeLabelEl = document.createElement('label') - generateIframeLabelEl.textContent = labelGenerateIframe - generateIframeLabelEl.prepend(generateIframe) - divWebOptions.append(generateIframeLabelEl) - - if (radioProtocolWeb) { - radioProtocolWeb.onclick = () => { - renderContent(container) - } - } - if (radioProtocolXMPP) { - radioProtocolXMPP.onclick = () => { - renderContent(container) - } - } - - readonly.onclick = () => { - renderContent(container) - } - withscroll.onclick = () => { - renderContent(container) - } - transparent.onclick = () => { - renderContent(container) - } - if (autoColors) { - autoColors.onclick = () => { - renderContent(container) - } - } - generateIframe.onclick = () => { - renderContent(container) - } - - shareString.onclick = () => { - shareString.select() - shareString.setSelectionRange(0, 99999) /* For mobile devices */ - } - - copyButton.onclick = () => { - shareString.select() - shareString.setSelectionRange(0, 99999) /* For mobile devices */ - navigator.clipboard.writeText(shareString.value).then(() => { - peertubeHelpers.notifier.success(labelCopied) - }, () => { - peertubeHelpers.notifier.error(labelError) - }) - } - - openButton.onclick = () => { - const uri = shareString.getAttribute('open-uri') ?? shareString.value - // Don't open the url if it is an iframe! - if (uri.startsWith('http') || uri.startsWith('xmpp')) { - window.open(uri) - } - } - - form = { - shareString, - copyButton, - openButton, - readonly, - withscroll, - transparent, - readonlyOptions, - autoColors, - generateIframe, - radioProtocolWeb, - radioProtocolXMPP, - divWebOptions, - divTips - } - restore(form) - } - - // Saving the form state, to restore each time the modal is opened. - save(form) - - const uriOptions: UriOptions = { - ignoreAutoColors: form.autoColors ? !form.autoColors.checked : true, - permanent: true - } - if (form.radioProtocolXMPP?.checked) { - // To minimize the height gap between the 2 modes, - // and prevent the dialog to resize and move too much, - // we use visibility instead of display - form.divTips.textContent = labelXMPPTips - form.divWebOptions.style.visibility = 'hidden' - } else { - form.divTips.textContent = labelOBSTips - form.divWebOptions.style.visibility = 'visible' - } - if (form.readonly.checked) { - if (form.withscroll.checked) { - uriOptions.readonly = true - } else { - uriOptions.readonly = 'noscroll' - } - if (form.transparent.checked) { - uriOptions.transparent = true - } - form.withscroll.disabled = false - form.transparent.disabled = false - form.readonlyOptions.classList.remove('livechat-shareurl-web-options-readonly-disabled') - } else { - form.withscroll.disabled = true - form.transparent.disabled = true - form.readonlyOptions.classList.add('livechat-shareurl-web-options-readonly-disabled') - } - let shareStringValue - let shareStringOpen: string | undefined - if (!form.radioProtocolXMPP?.checked) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (form.readonly?.checked || form.autoColors?.checked || form.generateIframe?.checked) { - shareStringValue = getIframeUri(registerOptions, settings, video, uriOptions) - if (form.generateIframe.checked) { - form.openButton.disabled = true - if (shareStringValue) { - // To properly escape all attributes, we are constructing an HTMLIframeElement - const iframe = document.createElement('iframe') - iframe.setAttribute('src', shareStringValue) - iframe.setAttribute('title', labelChatFor + ' ' + video.name) - iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms') - iframe.setAttribute('width', '560') - iframe.setAttribute('height', '315') - iframe.setAttribute('frameborder', '0') - shareStringValue = iframe.outerHTML - } - } else { - form.openButton.disabled = false - } - } else { - shareStringValue = window.location.protocol + - '//' + - window.location.host + - '/p/livechat/room?room=' + - encodeURIComponent(video.uuid) - } - } else { - // we must generate a XMPP room address - // form.openButton.disabled = true - const addr = getXMPPAddr(registerOptions, settings, video) - shareStringValue = addr?.jid - shareStringOpen = addr?.uri - } - form.shareString.setAttribute('value', shareStringValue ?? '') - if (shareStringOpen) { - form.shareString.setAttribute('open-uri', shareStringOpen) - } else { - form.shareString.removeAttribute('open-uri') - } - } - - function save (form: ShareForm): void { - if (!window.localStorage) { - return - } - const v = { - version: 1, // in case we add incompatible values in a near feature - readonly: !!form.readonly.checked, - withscroll: !!form.withscroll.checked, - transparent: !!form.transparent.checked, - autocolors: !!form.autoColors?.checked, - generateIframe: !!form.generateIframe.checked, - protocol: !form.radioProtocolWeb || form.radioProtocolWeb.checked ? 'web' : 'xmpp' - } - window.localStorage.setItem('peertube-plugin-livechat-shareurl', JSON.stringify(v)) - } - - function restore (form: ShareForm): void { - if (!window.localStorage) { - return - } - const s = window.localStorage.getItem('peertube-plugin-livechat-shareurl') - if (!s) { - return - } - let v: any - try { - v = JSON.parse(s) - if (!v || (typeof v !== 'object') || v.version !== 1) { - return - } - form.readonly.checked = !!v.readonly - form.withscroll.checked = !!v.withscroll - form.transparent.checked = !!v.transparent - if (form.autoColors) { - form.autoColors.checked = !!v.autocolors - } - form.generateIframe.checked = !!v.generateIframe - if (form.radioProtocolXMPP && v.protocol === 'xmpp') { - form.radioProtocolXMPP.checked = true - } else if (form.radioProtocolWeb) { - form.radioProtocolWeb.checked = true - } - } catch (err) { - logger.error(err as string) - } - } + // function restore (form: ShareForm): void { + // if (!window.localStorage) { + // return + // } + // const s = window.localStorage.getItem('peertube-plugin-livechat-shareurl') + // if (!s) { + // return + // } + // let v: any + // try { + // v = JSON.parse(s) + // if (!v || (typeof v !== 'object') || v.version !== 1) { + // return + // } + // form.readonly.checked = !!v.readonly + // form.withscroll.checked = !!v.withscroll + // form.transparent.checked = !!v.transparent + // if (form.autoColors) { + // form.autoColors.checked = !!v.autocolors + // } + // form.generateIframe.checked = !!v.generateIframe + // if (form.radioProtocolXMPP && v.protocol === 'xmpp') { + // form.radioProtocolXMPP.checked = true + // } else if (form.radioProtocolWeb) { + // form.radioProtocolWeb.checked = true + // } + // } catch (err) { + // logger.error(err as string) + // } + // } logger.info('Opening the share modal...') + // We can't just put in modalContent, Peertube sanitize it... const observer = new MutationObserver(mutations => { for (const { addedNodes } of mutations) { addedNodes.forEach(node => { @@ -401,7 +84,14 @@ async function shareChatUrl (registerOptions: RegisterClientOptions, settings: a logger.error('Modal has no body... Dont know how to fill it.') return } - renderContent(modalBodyElem) + modalBodyElem.childNodes.forEach(child => modalBodyElem.removeChild(child)) + + render(html` + + `, modalBodyElem) } }) } @@ -411,7 +101,7 @@ async function shareChatUrl (registerOptions: RegisterClientOptions, settings: a }) peertubeHelpers.showModal({ title: labelShare, - content: `

${defaultUri ?? ''}

`, // incase the observer is broken... + content: '', close: true }) // just in case, remove the observer after a timeout, if not already done... diff --git a/client/common/videowatch/uri.ts b/client/common/videowatch/uri.ts index 2cc9bb2b..b130df25 100644 --- a/client/common/videowatch/uri.ts +++ b/client/common/videowatch/uri.ts @@ -58,12 +58,17 @@ function getIframeUri ( return iframeUriStr } +interface GetXMPPAddrSettings { + 'prosody-room-allow-s2s': boolean + 'prosody-room-type': string +} + interface XMPPAddr { uri: string jid: string } function getXMPPAddr ( - registerOptions: RegisterClientOptions, settings: any, video: Video + registerOptions: RegisterClientOptions, settings: GetXMPPAddrSettings, video: Video ): XMPPAddr | null { // returns something like xmpp:256896ac-199a-4dab-bb3a-4fd916140272@room.instance.tdl?join if (!settings['prosody-room-allow-s2s']) { diff --git a/languages/en.yml b/languages/en.yml index 10f51613..801b1554 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -9,8 +9,9 @@ read_only: "Read-only" show_scrollbarr: "Show the scrollbar" transparent_background: "Transparent background (for stream integration, with OBS for example)" -tips_for_streamers: "Tips for streamers: To add the chat to your OBS, generate a read-only - link and use it as a browser source." +tips_for_streamers: | + Tips for streamers: To embed the chat in your video stream using OBS for example, + generate a read-only link and use it as a browser source. copy: "Copy" copied: "Copied" link_copied: "Link copied" @@ -518,3 +519,5 @@ action_remove_entry: Remove this entry action_remove_entry_confirm: Are you sure you want to remove this entry? loading_error: An error occured while loading data. +share_chat_embed: Embed +share_chat_peertube_tips: This link will open the chat within the Peertube interface.