diff --git a/CHANGELOG.md b/CHANGELOG.md index 13144054..6e071119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ TODO: https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/48 * #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames * #330: Chat does no more use an iframe to display the chat besides the videos. * #330: Fullscreen chat: now uses a custom page (in other words: when opening the chat in a new tab, you will have the Peertube menu). +* For anonymous users: new "log in using an external account" dialog, with following options: + * remote Peertube account ### Minor changes and fixes diff --git a/conversejs/build-conversejs-patch-i18n.js b/conversejs/build-conversejs-patch-i18n.js index eb19020c..ec2325d5 100644 --- a/conversejs/build-conversejs-patch-i18n.js +++ b/conversejs/build-conversejs-patch-i18n.js @@ -2,6 +2,7 @@ const fs = require('node:fs') const path = require('node:path') const YAML = require('yaml') +const locKeys = require('./loc.keys.js') /** * This script will patch ConverseJS .po files, to add custom strings. @@ -11,9 +12,7 @@ const livechatDir = path.resolve(__dirname, '..', 'languages') const converseDir = path.resolve(__dirname, '..', 'build', 'conversejs', 'src', 'i18n') // Labels to import: -const labels = loadLabels([ - 'slow_mode_info' -]) +const labels = loadLabels(locKeys) function loadLabels (keys) { const labels = {} diff --git a/conversejs/build-conversejs.sh b/conversejs/build-conversejs.sh index a1385470..74504f5a 100644 --- a/conversejs/build-conversejs.sh +++ b/conversejs/build-conversejs.sh @@ -79,6 +79,7 @@ rm -rf "$converse_build_dir/custom/" echo "Adding the custom files..." cp -r "$src_dir/custom/" "$converse_build_dir/custom/" mv "$converse_build_dir/custom/webpack.livechat.js" "$converse_build_dir/" +cp "$src_dir/loc.keys.js" "$converse_build_dir/" echo "Patching i18n files to add custom labels..." /bin/env node conversejs/build-conversejs-patch-i18n.js diff --git a/conversejs/custom/livechat-external-login-content.js b/conversejs/custom/livechat-external-login-content.js new file mode 100644 index 00000000..481dca66 --- /dev/null +++ b/conversejs/custom/livechat-external-login-content.js @@ -0,0 +1,117 @@ +import { api } from '@converse/headless/core.js' +import { CustomElement } from 'shared/components/element.js' +import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js' +import { __ } from 'i18n' + +export default class LivechatExternalLoginContentElement extends CustomElement { + static get properties () { + return { + remote_peertube_state: { type: String, attribute: false }, + remote_peertube_alert_message: { type: String, attribute: false }, + remote_peertube_try_anyway_url: { type: String, attribute: false } + } + } + + constructor () { + super() + this.remote_peertube_state = 'init' + } + + render () { + return tplExternalLoginModal(this, { + remote_peertube_state: this.remote_peertube_state, + remote_peertube_alert_message: this.remote_peertube_alert_message, + remote_peertube_try_anyway_url: this.remote_peertube_try_anyway_url + }) + } + + onKeyUp (_ev) { + if (this.remote_peertube_state !== 'init') { + this.remote_peertube_state = 'init' + this.remote_peertube_alert_message = '' + this.clearAlert() + } + } + + async openRemotePeertube (ev) { + ev.preventDefault() + this.clearAlert() + + const remotePeertubeUrl = ev.target.peertube_url.value.trim() + if (!remotePeertubeUrl) { return } + + this.remote_peertube_state = 'loading' + + try { + // Calling Peertube API to check if livechat plugin is available. + // In the meantime, this will also check that the URL exists, and is a Peertube instance + // (or something with similar API result... as the user typed the url, we assume there is no security risk here). + const configApiUrl = new URL('/api/v1/config', remotePeertubeUrl) + const config = await (await fetch(configApiUrl.toString())).json() + if (!config || typeof config !== 'object') { + throw new Error('Invalid config API result') + } + if (!('plugin' in config) || !('registered' in config.plugin) || !Array.isArray(config.plugin.registered)) { + throw new Error('No registered plugin in config API result') + } + if (!config.plugin.registered.find(p => p.npmName === 'peertube-plugin-livechat')) { + console.error('Plugin livechat not available on remote instance') + this.remote_peertube_state = 'error' + // eslint-disable-next-line no-undef + this.remote_peertube_alert_message = __(LOC_login_remote_peertube_no_livechat) + return + } + // Note: we do not check if the livechat plugin disables federation (neither on current or remote instance). + // We assume this is not a standard use case, and we don't want to add to much use cases. + + // Now we must search the current video on the remote instance, to be sure it federates, and to get the url. + // Note: url search can be disabled on remote instance for non logged in users... + // As we are not authenticated on remote here, there are chances that the search wont return anything. + // As a fallback, we will launch another search with the video UUID. + // And if no result neither, we will just propose to open using the lazy-load page. + const videoUrl = api.settings.get('livechat_peertube_video_original_url') + const videoUUID = api.settings.get('livechat_peertube_video_uuid') + for (const search of [videoUrl, videoUUID]) { + if (!search) { continue } + // searching first on federation network, then on vidiverse (this could be disabled) + for (const searchTarget of ['local', 'search-index']) { + const searchAPIUrl = new URL('/api/v1/search/videos', remotePeertubeUrl) + searchAPIUrl.searchParams.append('start', '0') + searchAPIUrl.searchParams.append('count', 1) + searchAPIUrl.searchParams.append('search', search) + searchAPIUrl.searchParams.append('searchTarget', searchTarget) + const videos = await (await fetch(searchAPIUrl.toString())).json() + if (videos && Array.isArray(videos.data) && videos.data.length > 0 && videos.data[0].uuid) { + console.log('Video found, opening on remote instance') + this.remote_peertube_state = 'ok' + window.location.href = new URL( + '/videos/watch/' + encodeURIComponent(videos.data[0].uuid), remotePeertubeUrl + ).toString() + return + } + } + } + + console.error('Video not found on remote instance') + this.remote_peertube_state = 'error' + // eslint-disable-next-line no-undef + this.remote_peertube_alert_message = __(LOC_login_remote_peertube_video_not_found) + this.remote_peertube_try_anyway_url = new URL( + '/search/lazy-load-video;url=' + encodeURIComponent(videoUrl), + remotePeertubeUrl + ).toString() + } catch (err) { + console.error(err) + this.remote_peertube_state = 'error' + // eslint-disable-next-line no-undef + this.remote_peertube_alert_message = __(LOC_login_remote_peertube_url_invalid) + } + } + + clearAlert () { + this.remote_peertube_alert_message = '' + this.remote_peertube_try_anyway_url = '' + } +} + +api.elements.define('converse-livechat-external-login-content', LivechatExternalLoginContentElement) diff --git a/conversejs/custom/shared/modals/livechat-external-login.js b/conversejs/custom/shared/modals/livechat-external-login.js new file mode 100644 index 00000000..a583e9b3 --- /dev/null +++ b/conversejs/custom/shared/modals/livechat-external-login.js @@ -0,0 +1,20 @@ +import { __ } from 'i18n' +import BaseModal from 'plugins/modal/modal.js' +import { api } from '@converse/headless/core' +import { html } from 'lit' +import 'livechat-external-login-content.js' + +class ExternalLoginModal extends BaseModal { + remotePeertubeError = '' + + renderModal () { + return html`` + } + + getModalTitle () { + // eslint-disable-next-line no-undef + return __(LOC_login_using_external_account) + } +} + +api.elements.define('converse-livechat-external-login', ExternalLoginModal) diff --git a/conversejs/custom/shared/styles/livechat.scss b/conversejs/custom/shared/styles/livechat.scss index 77759f96..f1157bf2 100644 --- a/conversejs/custom/shared/styles/livechat.scss +++ b/conversejs/custom/shared/styles/livechat.scss @@ -43,30 +43,39 @@ body.livechat-readonly.livechat-noscroll { } // Viewer mode -.livechat-viewer-mode-nick { +.livechat-viewer-mode-content { display: none; -} -body[livechat-viewer-mode="on"] { - .livechat-viewer-mode-nick { - display: initial; + form { + display: flex !important; + flex-flow: row wrap !important; + padding-bottom: 0.5em !important; + border-top: 1px solid var(--chatroom-head-bg-color) !important; + gap: 10px; + align-items: baseline; - form { - display: flex !important; - flex-flow: row wrap !important; - padding-bottom: 0.5em !important; - border-top: var(--chatroom-separator-border-bottom) !important; - gap: 10px; - align-items: baseline; - - label { - color: var(--text-color); // fix converseJs css that breaks this label color. - } + label { + color: var(--text-color); // fix converseJs css that breaks this label color. } } + hr { + margin: 0; + background-color: var(--chatroom-head-bg-color); + } + + .livechat-viewer-mode-external-login { + padding: 2em; + } +} + +body[livechat-viewer-mode="on"] { + .livechat-viewer-mode-content { + display: initial; + } + converse-muc-bottom-panel { - >:not(.livechat-viewer-mode-nick) { + >:not(.livechat-viewer-mode-content) { display: none; } } diff --git a/conversejs/custom/templates/livechat-external-login-modal.js b/conversejs/custom/templates/livechat-external-login-modal.js new file mode 100644 index 00000000..300fc7f8 --- /dev/null +++ b/conversejs/custom/templates/livechat-external-login-modal.js @@ -0,0 +1,56 @@ +import { __ } from 'i18n' +import { html } from 'lit' + +export const tplExternalLoginModal = (el, o) => { + // eslint-disable-next-line no-undef + const i18nRemotePeertube = __(LOC_login_remote_peertube) + // eslint-disable-next-line no-undef + const i18nRemotePeertubeUrl = __(LOC_login_remote_peertube_url) + const i18nRemotePeertubeOpen = __('OK') + return html`` +} diff --git a/conversejs/custom/templates/muc-bottom-panel.js b/conversejs/custom/templates/muc-bottom-panel.js index 37beeb84..e1963f50 100644 --- a/conversejs/custom/templates/muc-bottom-panel.js +++ b/conversejs/custom/templates/muc-bottom-panel.js @@ -3,6 +3,7 @@ import { _converse, api } from '@converse/headless/core' import { html } from 'lit' import tplMucBottomPanel from '../../src/plugins/muc-views/templates/muc-bottom-panel.js' import { CustomElement } from 'shared/components/element.js' +import 'shared/modals/livechat-external-login.js' async function setNickname (ev, model) { ev.preventDefault() @@ -54,7 +55,8 @@ class SlowMode extends CustomElement { return html`
${__( - 'Slow mode is enabled, users can send a message every %1$s seconds.', + // eslint-disable-next-line no-undef + LOC_slow_mode_info, this.model.config.get('slow_mode_duration') )} @@ -82,14 +84,15 @@ export default (o) => { const i18nNickname = __('Nickname') const i18nJoin = __('Enter groupchat') const i18nHeading = __('Choose a nickname to enter') + // eslint-disable-next-line no-undef + const i18nExternalLogin = __(LOC_login_using_external_account) return html` -
setNickname(ev, model)}> -
+
+ setNickname(ev, model)}>
{
+ ${ + // If we open a room with forcetype, there is no current video... So just disabling external login + // (in such case, we should be logged in as admin/moderator...) + !api.settings.get('livechat_peertube_video_original_url') + ? '' + : html` +
+ + ` + }
${tplSlowMode(o)} ${tplMucBottomPanel(o)}` diff --git a/conversejs/custom/webpack.livechat.js b/conversejs/custom/webpack.livechat.js index c8d58958..6893b01d 100644 --- a/conversejs/custom/webpack.livechat.js +++ b/conversejs/custom/webpack.livechat.js @@ -1,18 +1,54 @@ const prod = require('./webpack/webpack.build.js') const { merge } = require('webpack-merge') +const webpack = require('webpack') const path = require('path') +const fs = require('fs') +const locKeys = require('./loc.keys.js') + +function loadLocs () { + // Loading english strings, so we can inject them as constants. + const refFile = path.resolve(__dirname, '..', '..', 'dist', 'languages', 'en.reference.json') + if (!fs.existsSync(refFile)) { + throw new Error('Missing english reference file, please run "npm run build:languages" before building ConverseJS') + } + const english = require(refFile) + + const r = {} + for (const key of locKeys) { + if (!(key in english) || (typeof english[key] !== 'string')) { + throw new Error('Missing english string key=' + key) + } + r['LOC_' + key] = JSON.stringify(english[key]) + } + return r +} module.exports = merge(prod, { entry: path.resolve(__dirname, 'custom/entry.js'), output: { filename: 'converse.min.js' }, + plugins: [ + new webpack.DefinePlugin(loadLocs()) + ], resolve: { extensions: ['.js'], alias: { './templates/muc-bottom-panel.js': path.resolve('custom/templates/muc-bottom-panel.js'), '../../templates/background_logo.js$': path.resolve(__dirname, 'custom/templates/background_logo.js'), - 'shared/styles/index.scss$': path.resolve(__dirname, 'custom/shared/styles/livechat.scss') + 'shared/styles/index.scss$': path.resolve(__dirname, 'custom/shared/styles/livechat.scss'), + 'shared/modals/livechat-external-login.js': path.resolve( + __dirname, + 'custom/shared/modals/livechat-external-login.js' + ), + 'templates/livechat-external-login-modal.js': path.resolve( + __dirname, + 'custom/templates/livechat-external-login-modal.js' + ), + 'livechat-external-login-content.js': path.resolve( + __dirname, + 'custom/livechat-external-login-content.js' + ) } } }) diff --git a/conversejs/lib/converse-params.ts b/conversejs/lib/converse-params.ts index f94954ae..9042bdec 100644 --- a/conversejs/lib/converse-params.ts +++ b/conversejs/lib/converse-params.ts @@ -8,7 +8,10 @@ import type { AuthentInfos } from './auth' * @returns default parameters to provide to ConverseJS. */ function defaultConverseParams ( - { forceReadonly, theme, assetsPath, room, forceDefaultHideMucParticipants, autofocus }: InitConverseJSParams + { + forceReadonly, theme, assetsPath, room, forceDefaultHideMucParticipants, autofocus, + peertubeVideoOriginalUrl, peertubeVideoUUID + }: InitConverseJSParams ): any { const mucShowInfoMessages = forceReadonly ? [ @@ -87,7 +90,10 @@ function defaultConverseParams ( colorize_username: true, // This is a specific settings, that is used in ConverseJS customization, to force avatars loading in readonly mode. - livechat_load_all_vcards: !!forceReadonly + livechat_load_all_vcards: !!forceReadonly, + + livechat_peertube_video_original_url: peertubeVideoOriginalUrl, + livechat_peertube_video_uuid: peertubeVideoUUID } // TODO: params.clear_messages_on_reconnection = true when muc_mam will be available. diff --git a/conversejs/lib/plugins/livechat-viewer-mode.ts b/conversejs/lib/plugins/livechat-viewer-mode.ts index 94c86819..562ae384 100644 --- a/conversejs/lib/plugins/livechat-viewer-mode.ts +++ b/conversejs/lib/plugins/livechat-viewer-mode.ts @@ -6,7 +6,9 @@ export const livechatViewerModePlugin = { const _converse = this._converse _converse.api.settings.extend({ - livechat_enable_viewer_mode: false + livechat_enable_viewer_mode: false, + livechat_peertube_video_original_url: undefined, + livechat_peertube_video_uuid: undefined }) const originalGetDefaultMUCNickname = _converse.getDefaultMUCNickname diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js new file mode 100644 index 00000000..730fcb2b --- /dev/null +++ b/conversejs/loc.keys.js @@ -0,0 +1,19 @@ +/** Localization keys to inject in ConverseJS: + * these keys are used to: + * - inject needed localization strings in ConverseJS language files + * - defined global variable using Webpack, to retrieve the english key to pass to the ConverseJS localization function +*/ +const locKeys = [ + 'slow_mode_info', + 'login_using_external_account', + 'login_remote_peertube', + 'login_remote_peertube_searching', + 'login_remote_peertube_url', + 'login_remote_peertube_url_invalid', + 'login_remote_peertube_no_livechat', + 'login_remote_peertube_video_not_found', + 'login_remote_peertube_video_not_found_try_anyway', + 'login_remote_peertube_video_not_found_try_anyway_button' +] + +module.exports = locKeys diff --git a/languages/en.yml b/languages/en.yml index 9c4dc2ad..4bcbeaca 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -392,3 +392,13 @@ invalid_value: "Invalid value." slow_mode_info: "Slow mode is enabled, users can send a message every %1$s seconds." chatroom_not_accessible: "This chatroom does not exist, or is not accessible to you." + +login_using_external_account: "Log in using an external account" +login_remote_peertube: "Log in using an account on another Peertube instance:" +login_remote_peertube_url: "Your Peertube instance URL" +login_remote_peertube_searching: "Searching the video on the Peertube instance..." +login_remote_peertube_url_invalid: "Invalid Peertube URL." +login_remote_peertube_no_livechat: "The livechat plugin is not installed on this Peertube instance." +login_remote_peertube_video_not_found: "This video is not available on this Peertube instance." +login_remote_peertube_video_not_found_try_anyway: "In some cases, the video can still be retrieved if you connect to the remote instance." +login_remote_peertube_video_not_found_try_anyway_button: "Try anyway to open the video on the Peertube instance" diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts index 16cb850c..213a991e 100644 --- a/server/lib/conversejs/params.ts +++ b/server/lib/conversejs/params.ts @@ -77,6 +77,8 @@ async function getConverseJSParams ( } = connectionInfos return { + peertubeVideoOriginalUrl: roomInfos.video?.url, + peertubeVideoUUID: roomInfos.video?.uuid, staticBaseUrl, assetsPath: staticBaseUrl + 'conversejs/', isRemoteChat: !!(roomInfos.video?.remote), diff --git a/shared/lib/types.ts b/shared/lib/types.ts index 1b94be23..6180450c 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -1,6 +1,8 @@ type ConverseJSTheme = 'peertube' | 'default' | 'concord' interface InitConverseJSParams { + peertubeVideoOriginalUrl?: string + peertubeVideoUUID?: string isRemoteChat: boolean localAnonymousJID: string | null remoteAnonymousJID: string | null