diff --git a/CHANGELOG.md b/CHANGELOG.md index 829b6fcd..149ced51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ TODO: tag conversejs livechat branch, and replace commit ID in build-converse.js * Overhauled configuration page, with more broadly customizable lists of parameters by @Murazaki ([See pull request #352](https://github.com/JohnXLivingston/peertube-plugin-livechat/pull/352)). * #377: new setting to listen C2S connection on non-localhost interfaces. * #130: channel custom emojis. +* #98: OBS Dock. You can now generate links to join chatrooms with your current user. This can be used to create Docks in OBS for example. This could also be used to generate authentication token to join the chat from 3rd party tools. ### Minor changes and fixes diff --git a/assets/styles/elements/_dynamic-table-form.scss b/assets/styles/elements/_dynamic-table-form.scss index 4ac017e4..bf34f911 100644 --- a/assets/styles/elements/_dynamic-table-form.scss +++ b/assets/styles/elements/_dynamic-table-form.scss @@ -10,42 +10,15 @@ @use "sass:color"; @use "../variables"; +@use "../mixins/buttons"; +@use "../mixins/tables"; livechat-dynamic-table-form { - // We need this variable to be known at that time - $bs-green: #39cc0b; - display: block; margin-bottom: 3rem; table { - table-layout: fixed; - text-align: center; - - tr { - border: 1px var(--greyBackgroundColor) solid; - } - - td, - th { - word-wrap: break-word; - vertical-align: top; - padding: 5px 7px; - } - - td:last-child { - vertical-align: middle; - min-width: 28px; - - > input:not([type="checkbox"]), - textarea { - min-width: 150px; - } - } - - tbody tr:nth-child(odd) { - background-color: var(--greySecondaryBackgroundColor); - } + @include tables.data-table; .livechat-dynamic-table-form-description-header { font-size: small; @@ -57,74 +30,11 @@ livechat-dynamic-table-form { text-align: left; } - .dynamic-table-add-row, - .dynamic-table-remove-row { - // Peertube rounded-line-height-1-5 mixins - line-height: variables.$button-calc-line-height; - - // Peertube peertube-button mixin (but with less horizontal padding) - padding: 4px; - border: 0; - font-weight: variables.$font-semibold; - border-radius: 3px !important; - text-align: center; - cursor: pointer; - font-size: variables.$button-font-size; - } - .dynamic-table-add-row { - background-color: var(--bs-green); - - &, - &:active, - &:focus { - color: #fff; - background-color: color.adjust($bs-green, $lightness: 5%); - } - - &:focus, - &.focus-visible { - box-shadow: 0 0 0 0.2rem color.adjust($bs-green, $lightness: 20%); - } - - &:hover { - color: #fff; - background-color: color.adjust($bs-green, $lightness: 10%); - } - - &[disabled], - &.disabled { - cursor: default; - color: #fff; - background-color: var(--inputBorderColor); - } + @include buttons.button-row-add; } .dynamic-table-remove-row { - background-color: var(--bs-orange); - - &, - &:active, - &:focus { - color: #fff; - background-color: var(--mainColor); - } - - &:focus, - &.focus-visible { - box-shadow: 0 0 0 0.2rem var(--mainHoverColor); - } - - &:hover { - color: #fff; - background-color: var(--mainHoverColor); - } - - &[disabled], - &.disabled { - cursor: default; - color: #fff; - background-color: var(--inputBorderColor); - } + @include buttons.button-row-remove; } } diff --git a/assets/styles/elements/_index.scss b/assets/styles/elements/_index.scss index 18b200a6..9565fe6f 100644 --- a/assets/styles/elements/_index.scss +++ b/assets/styles/elements/_index.scss @@ -11,3 +11,4 @@ @use "share-chat"; @use "spinner"; @use "tags-input"; +@use "token-list"; diff --git a/assets/styles/elements/_token-list.scss b/assets/styles/elements/_token-list.scss new file mode 100644 index 00000000..cd6281bc --- /dev/null +++ b/assets/styles/elements/_token-list.scss @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +@use "../mixins/buttons"; +@use "../mixins/tables"; + +livechat-token-list { + table { + @include tables.data-table; + } + + .livechat-create-token { + @include buttons.button-row-add; + } + + .livechat-revoke-token { + @include buttons.button-row-remove; + } +} diff --git a/assets/styles/mixins/_buttons.scss b/assets/styles/mixins/_buttons.scss new file mode 100644 index 00000000..c9d70f13 --- /dev/null +++ b/assets/styles/mixins/_buttons.scss @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2024 Mehdi Benadel + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +@use "sass:color"; +@use "../variables"; + +/* We are disabling stylelint-disable custom-property-pattern so we can use Peertube var without warnings. */ +/* stylelint-disable custom-property-pattern */ + +// We need this variable to be known at that time +$bs-green: #39cc0b; + +@mixin button-row { + // Peertube rounded-line-height-1-5 mixins + line-height: variables.$button-calc-line-height; + + // Peertube peertube-button mixin (but with less horizontal padding) + padding: 4px; + border: 0; + font-weight: variables.$font-semibold; + border-radius: 3px !important; + text-align: center; + cursor: pointer; + font-size: variables.$button-font-size; +} + +@mixin button-row-add { + @include button-row; + + background-color: var(--bs-green); + + &, + &:active, + &:focus { + color: #fff; + background-color: color.adjust($bs-green, $lightness: 5%); + } + + &:focus, + &.focus-visible { + box-shadow: 0 0 0 0.2rem color.adjust($bs-green, $lightness: 20%); + } + + &:hover { + color: #fff; + background-color: color.adjust($bs-green, $lightness: 10%); + } + + &[disabled], + &.disabled { + cursor: default; + color: #fff; + background-color: var(--inputBorderColor); + } +} + +@mixin button-row-remove { + @include button-row; + + background-color: var(--bs-orange); + + &, + &:active, + &:focus { + color: #fff; + background-color: var(--mainColor); + } + + &:focus, + &.focus-visible { + box-shadow: 0 0 0 0.2rem var(--mainHoverColor); + } + + &:hover { + color: #fff; + background-color: var(--mainHoverColor); + } + + &[disabled], + &.disabled { + cursor: default; + color: #fff; + background-color: var(--inputBorderColor); + } +} diff --git a/assets/styles/mixins/tables.scss b/assets/styles/mixins/tables.scss new file mode 100644 index 00000000..dd7ce961 --- /dev/null +++ b/assets/styles/mixins/tables.scss @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2024 Mehdi Benadel + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* We are disabling stylelint-disable custom-property-pattern so we can use Peertube var without warnings. */ +/* stylelint-disable custom-property-pattern */ + +@mixin data-table { + table-layout: fixed; + text-align: center; + + tr { + border: 1px var(--greyBackgroundColor) solid; + } + + td, + th { + word-wrap: break-word; + vertical-align: top; + padding: 5px 7px; + } + + td:last-child { + vertical-align: middle; + min-width: 28px; + + > input:not([type="checkbox"]), + textarea { + min-width: 150px; + } + } + + tbody tr:nth-child(odd) { + background-color: var(--greySecondaryBackgroundColor); + } +} diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index b44834c9..ff3d961a 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -113,3 +113,11 @@ declare const LOC_LOADING_ERROR: string declare const LOC_SHARE_CHAT_EMBED: string declare const LOC_SHARE_CHAT_PEERTUBE_TIPS: string +declare const LOC_SHARE_CHAT_DOCK: string +declare const LOC_SHARE_CHAT_DOCK_TIPS: string +declare const LOC_TOKEN_LABEL: string +declare const LOC_TOKEN_JID: string +declare const LOC_TOKEN_PASSWORD: string +declare const LOC_TOKEN_ACTION_CREATE: string +declare const LOC_TOKEN_ACTION_REVOKE: string +declare const LOC_TOKEN_DEFAULT_LABEL: string diff --git a/client/common/lib/buttons.ts b/client/common/lib/buttons.ts new file mode 100644 index 00000000..69fea1c4 --- /dev/null +++ b/client/common/lib/buttons.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Mehdi Benadel +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/ +export const AddSVG: string = + ` + + + ` + +// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/ +export const RemoveSVG: string = + ` + + + ` diff --git a/client/common/lib/elements/dynamic-table-form.ts b/client/common/lib/elements/dynamic-table-form.ts index 38ddd396..5170b79d 100644 --- a/client/common/lib/elements/dynamic-table-form.ts +++ b/client/common/lib/elements/dynamic-table-form.ts @@ -15,24 +15,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js' import { classMap } from 'lit/directives/class-map.js' import { LivechatElement } from './livechat' import { ptTr } from '../directives/translation' - -// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/ -const AddSVG: string = - ` - - - ` - -// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/ -const RemoveSVG: string = - ` - - - ` +import { AddSVG, RemoveSVG } from '../buttons' type DynamicTableAcceptedTypes = number | string | boolean | Date | Array diff --git a/client/common/lib/elements/index.js b/client/common/lib/elements/index.js index 1a147d5a..cca16edd 100644 --- a/client/common/lib/elements/index.js +++ b/client/common/lib/elements/index.js @@ -10,4 +10,5 @@ import './configuration-section-header' import './tags-input' import './image-file-input' import './spinner' +import './token-list' import './error' diff --git a/client/common/lib/elements/templates/token-list.ts b/client/common/lib/elements/templates/token-list.ts new file mode 100644 index 00000000..7ce9e3b6 --- /dev/null +++ b/client/common/lib/elements/templates/token-list.ts @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import type { LivechatTokenListElement } from '../token-list' +import { html, TemplateResult } from 'lit' +import { unsafeHTML } from 'lit/directives/unsafe-html.js' +import { repeat } from 'lit/directives/repeat.js' +import { ptTr } from '../../directives/translation' +import { AddSVG, RemoveSVG } from '../../buttons' + +export function tplTokenList (el: LivechatTokenListElement): TemplateResult { + return html`
+ + + + + + + + + + + + ${ + repeat(el.tokenList ?? [], (token) => token.id, (token) => { + html` + + + + + + ` + }) + } + + + + + + +
${ptTr(LOC_TOKEN_LABEL)}${ptTr(LOC_TOKEN_JID)}${ptTr(LOC_TOKEN_PASSWORD)}
${ + el.mode === 'select' + ? html`` + : '' + }${token.label}${token.jid}${token.password} + +
+ +
+
` +} diff --git a/client/common/lib/elements/token-list.ts b/client/common/lib/elements/token-list.ts new file mode 100644 index 00000000..fecd4cf8 --- /dev/null +++ b/client/common/lib/elements/token-list.ts @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { html } from 'lit' +import { customElement, property } from 'lit/decorators.js' +import { LivechatElement } from './livechat' +import { TokenListService } from '../services/token-list' +import { tplTokenList } from './templates/token-list' +import { Task } from '@lit/task' +import { LivechatToken } from 'shared/lib/types' + +@customElement('livechat-token-list') +export class LivechatTokenListElement extends LivechatElement { + /** + * Indicate the mode to use: + * * list: just display tokens + * * select: select one token + */ + @property({ attribute: true }) + public mode: 'select' | 'list' = 'list' + + @property({ attribute: false }) + public tokenList?: LivechatToken[] + + @property({ attribute: false }) + public currentSelectedToken?: LivechatToken + + @property({ attribute: false }) + public actionDisabled: boolean = false + + private readonly _tokenListService: TokenListService + private readonly _asyncTaskRender: Task + + constructor () { + super() + this._tokenListService = new TokenListService() + this._asyncTaskRender = this._initTask() + } + + protected _initTask (): Task { + return new Task(this, { + task: async () => { + this.tokenList = await this._tokenListService.fetchTokenList() + this.actionDisabled = false + }, + args: () => [] + }) + } + + protected override render = (): unknown => { + return this._asyncTaskRender.render({ + pending: () => html``, + error: () => html``, + complete: () => tplTokenList(this) + }) + } + + public selectToken (ev: Event, token: LivechatToken): void { + ev.preventDefault() + if (!this.tokenList?.includes(token)) { return } + this.currentSelectedToken = token + this.dispatchEvent(new CustomEvent('update', {})) + } + + public async revokeToken (token: LivechatToken): Promise { + this.actionDisabled = true + try { + await this._tokenListService.revokeToken(token) + this.tokenList = this.tokenList?.filter(t => t !== token) ?? [] + if (this.currentSelectedToken === token) { + this.currentSelectedToken = undefined + } + this.requestUpdate('tokenList') + this.dispatchEvent(new CustomEvent('update', {})) + } catch (err: any) { + this.logger.error(err) + this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR)) + } finally { + this.actionDisabled = false + } + } + + public async createToken (): Promise { + this.actionDisabled = true + try { + const token = await this._tokenListService.createToken(await this.ptTranslate(LOC_TOKEN_DEFAULT_LABEL)) + this.tokenList ??= [] + this.tokenList.push(token) + if (this.mode === 'select') { + this.currentSelectedToken = token + } + this.requestUpdate('tokenList') + this.dispatchEvent(new CustomEvent('update', {})) + } catch (err: any) { + this.logger.error(err) + this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR)) + } finally { + this.actionDisabled = false + } + } +} diff --git a/client/common/lib/services/token-list.ts b/client/common/lib/services/token-list.ts new file mode 100644 index 00000000..95e7ac00 --- /dev/null +++ b/client/common/lib/services/token-list.ts @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { getPtContext } from '../contexts/peertube' +import { getBaseRoute } from '../../../utils/uri' +import { LivechatToken } from 'shared/lib/types' + +export class TokenListService { + private readonly _headers: any = {} + private readonly _apiUrl: string + + constructor () { + this._headers = getPtContext().ptOptions.peertubeHelpers.getAuthHeader() ?? {} + this._headers['content-type'] = 'application/json;charset=UTF-8' + this._apiUrl = getBaseRoute(getPtContext().ptOptions) + '/api/auth/tokens' + } + + public async fetchTokenList (): Promise { + const response = await fetch( + this._apiUrl, + { + method: 'GET', + headers: this._headers + } + ) + + if (!response.ok) { + throw new Error('Can\'t get livechat token list.') + } + + return response.json() + } + + public async createToken (label: string): Promise { + const response = await fetch( + this._apiUrl, + { + method: 'POST', + headers: this._headers, + body: JSON.stringify({ + label + }) + } + ) + + if (!response.ok) { + throw new Error('Can\'t create livechat token.') + } + + return response.json() + } + + public async revokeToken (token: LivechatToken): Promise { + const response = await fetch( + this._apiUrl + '/' + encodeURIComponent(token.id), + { + method: 'DELETE', + headers: this._headers + } + ) + + if (!response.ok) { + throw new Error('Can\'t delete livechat token.') + } + + return response.json() + } +} diff --git a/client/common/videowatch/elements/share-chat.ts b/client/common/videowatch/elements/share-chat.ts index 3141d93c..0f54f681 100644 --- a/client/common/videowatch/elements/share-chat.ts +++ b/client/common/videowatch/elements/share-chat.ts @@ -10,8 +10,9 @@ 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' +import { isAnonymousUser } from '../../../utils/user' -const validTabNames = ['peertube', 'embed', 'xmpp'] as const +const validTabNames = ['peertube', 'embed', 'dock', 'xmpp'] as const type ValidTabNames = typeof validTabNames[number] @@ -60,6 +61,12 @@ export class ShareChatElement extends LivechatElement { @property({ attribute: false }) public xmppUriEnabled: boolean = false + /** + * Should we render the Dock tab? + */ + @property({ attribute: false }) + public dockEnabled: boolean = false + /** * Can we use autocolors? */ @@ -100,6 +107,10 @@ export class ShareChatElement extends LivechatElement { super.firstUpdated(changedProperties) const settings = this._settings this.xmppUriEnabled = !!settings['prosody-room-allow-s2s'] + // Note: for dockEnabled, we check: + // * that the user is logged in + // * that the video is local (for remote video, tests case are too complicated, and it's not the main use case, so…) + this.dockEnabled = !isAnonymousUser(this.ptContext.ptOptions) && this._video.isLocal this.autocolorsAvailable = isAutoColorsAvailable(settings['converse-theme']) this._restorePreviousState() @@ -154,6 +165,9 @@ export class ShareChatElement extends LivechatElement { if (!this.xmppUriEnabled && this.currentTab === 'xmpp') { this.currentTab = 'peertube' } + if (!this.dockEnabled && this.currentTab === 'dock') { + this.currentTab = 'peertube' + } } protected _saveCurrentState (): void { @@ -177,6 +191,7 @@ export class ShareChatElement extends LivechatElement { switch (this.currentTab) { case 'peertube': return this._computeUrlPeertube() case 'embed': return this._computeUrlEmbed() + case 'dock': return this._computeUrlDock() case 'xmpp': return this._computeUrlXMPP() default: return { @@ -206,6 +221,32 @@ export class ShareChatElement extends LivechatElement { } } + protected _computeUrlDock (): ComputedUrl { + return { + shareString: '', + openUrl: undefined + } + // const uriOptions: UriOptions = { + // ignoreAutoColors: true, + // permanent: 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 + // } + // } + + // return { + // shareString: url, + // openUrl: url + // } + } + protected _computeUrlEmbed (): ComputedUrl { const uriOptions: UriOptions = { ignoreAutoColors: this.autocolorsAvailable ? !this.embedAutocolors : true, diff --git a/client/common/videowatch/elements/templates/share-chat.ts b/client/common/videowatch/elements/templates/share-chat.ts index 94cb6d99..c7c6db76 100644 --- a/client/common/videowatch/elements/templates/share-chat.ts +++ b/client/common/videowatch/elements/templates/share-chat.ts @@ -22,7 +22,11 @@ export function tplShareChatCopy (el: ShareChatElement): TemplateResult { input.setSelectionRange(0, 99999) /* For mobile devices */ }} /> -