Rewriting Share modal WIP:

Using lit to entirely rewrite the share chat modal.
This commit is contained in:
John Livingston 2024-06-14 13:03:14 +02:00
parent 4851e35fba
commit 75dd2e4d59
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
12 changed files with 559 additions and 436 deletions

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* 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;
}
}

View File

@ -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

View File

@ -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 {

View File

@ -0,0 +1 @@
import './share-chat'

View File

@ -0,0 +1,225 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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)}
<div class="livechat-shareurl-block">
${tplShareChatTips(this)}
${tplShareChatOptions(this)}
</div>
`
}
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<void> {
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
}
}

View File

@ -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`<div class="livechat-shareurl-copy">
<input
type="text"
readonly autocomplete="off" placeholder=""
class="form-control readonly"
value=${computedUrl.shareString}
@click=${(ev: Event) => {
const input = ev.target as HTMLInputElement
// Select the whole value when entering the input.
input.select()
input.setSelectionRange(0, 99999) /* For mobile devices */
}}
/>
<button type="button" class="btn btn-outline-secondary text-uppercase" @click=${el.copyUrl}>
${ptTr(LOC_COPY)}
</button>
<button
type="button" class="btn btn-outline-secondary text-uppercase"
@click=${el.openUrl}
?disabled=${computedUrl.openUrl === undefined}
>
${ptTr(LOC_OPEN)}
</button>
<livechat-help-button .page=${'documentation/user/streamers'}></livechat-help-button>
</div>`
}
function _tplShareChatTab (
el: ShareChatElement,
tabName: ShareChatElement['currentTab'],
label: string
): TemplateResult {
return html`
<a
class=${classMap({
'sub-menu-entry': true,
active: el.currentTab === tabName
})}
@click=${(ev: Event) => {
ev.preventDefault()
el.switchTab(tabName)
}}
>
${ptTr(label)}
</a>`
}
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`<div class="livechat-shareurl-tips">${ptTr(label)}</div>`
}
function _tplShareChatPeertubeOptions (_el: ShareChatElement): TemplateResult {
return html``
}
function _tplShareChatEmbedOptions (el: ShareChatElement): TemplateResult {
return html`
<label>
<input
type="checkbox"
?checked=${el.embedIFrame}
@click=${() => { el.embedIFrame = !el.embedIFrame }}
/>
${ptTr(LOC_GENERATE_IFRAME)}
</label>
<label>
<input
type="checkbox"
?checked=${el.embedReadOnly}
@click=${() => { el.embedReadOnly = !el.embedReadOnly }}
/>
${ptTr(LOC_READ_ONLY)}
</label>
<div
class=${classMap({
'livechat-shareurl-suboptions': true,
'livechat-shareurl-suboptions-disabled': !el.embedReadOnly
})}
>
<label>
<input
type="checkbox"
?checked=${el.embedReadOnlyScrollbar}
?disabled=${!el.embedReadOnly}
@click=${() => { el.embedReadOnlyScrollbar = !el.embedReadOnlyScrollbar }}
/>
${ptTr(LOC_SHOW_SCROLLBARR)}
</label>
<label>
<input
type="checkbox"
?checked=${el.embedReadOnlyTransparentBackground}
?disabled=${!el.embedReadOnly}
@click=${() => { el.embedReadOnlyTransparentBackground = !el.embedReadOnlyTransparentBackground }}
/>
${ptTr(LOC_TRANSPARENT_BACKGROUND)}
</label>
</div>
${
el.autocolorsAvailable
? html`
<label>
<input
type="checkbox"
?checked=${el.embedAutocolors}
@click=${() => { el.embedAutocolors = !el.embedAutocolors }}
/>
${ptTr(LOC_USE_CURRENT_THEME_COLOR)}
</label>`
: ''
}
`
}
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`<div class="livechat-shareurl-options">${tpl}</div>`
}

View File

@ -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

View File

@ -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<void> {
async function shareChatUrl (
registerOptions: RegisterClientOptions,
settings: LiveChatSettings,
video: Video
): Promise<void> {
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 <livechat-share-chat> 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`
<livechat-share-chat
._settings=${settings}
._video=${video}
></livechat-share-chat>
`, modalBodyElem)
}
})
}
@ -411,7 +101,7 @@ async function shareChatUrl (registerOptions: RegisterClientOptions, settings: a
})
peertubeHelpers.showModal({
title: labelShare,
content: `<p>${defaultUri ?? ''}</p>`, // incase the observer is broken...
content: '',
close: true
})
// just in case, remove the observer after a timeout, if not already done...

View File

@ -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']) {

View File

@ -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.