Better structure + channel home in LitElement format

This commit is contained in:
Mehdi Benadel 2024-05-23 14:41:11 +02:00
parent 687c4742f7
commit de974eae22
14 changed files with 191 additions and 217 deletions

View File

@ -0,0 +1,8 @@
import { createContext } from "@lit/context";
import type { RegisterClientOptions } from "@peertube/peertube-types/client/types";
import type { ChannelConfiguration } from "shared/lib/types";
import { ChannelDetailsService } from "../services/channel-details";
export const registerClientOptionsContext = createContext<RegisterClientOptions | undefined>(Symbol('register-client-options'));
export const channelConfigurationContext = createContext<ChannelConfiguration | undefined>(Symbol('channel-configuration'));
export const channelDetailsServiceContext = createContext<ChannelDetailsService | undefined>(Symbol('channel-configuration-service'));

View File

@ -1,9 +1,8 @@
import { PartInfo, PartType, directive } from 'lit/directive.js'
import { PartInfo, directive } from 'lit/directive.js'
import { AsyncDirective } from 'lit/async-directive.js'
import { RegisterClientHelpers } from '@peertube/peertube-types/client';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { html } from 'lit';
import { unsafeStatic } from 'lit/static-html.js';
export class TranslationDirective extends AsyncDirective {
@ -34,7 +33,7 @@ export class TranslationDirective extends AsyncDirective {
this._translatedValue = locId
}
this._asyncUpdateTranslation()
this._asyncUpdateTranslation().then(() => {}, () => {})
return this._internalRender()
}

View File

@ -1,22 +1,16 @@
import { RegisterClientOptions } from '@peertube/peertube-types/client'
import { css, html, LitElement } from 'lit'
import { repeat } from 'lit-html/directives/repeat.js'
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { ptTr } from './TranslationDirective'
import { localizedHelpUrl } from '../../../utils/help'
import './DynamicTableFormElement'
import './PluginConfigurationRow'
import './HelpButtonElement'
import { until } from 'async'
import { ptTr } from '../directives/translation'
import './dynamic-table-form'
import './plugin-configuration-row'
import './help-button'
import { Task } from '@lit/task';
import { ChannelConfiguration } from 'shared/lib/types'
import { ChannelConfigurationService } from './ChannelConfigurationService'
import { createContext, provide } from '@lit/context'
import type { ChannelConfiguration } from 'shared/lib/types'
import { ChannelDetailsService } from '../services/channel-details'
import { provide } from '@lit/context'
import { getGlobalStyleSheets } from '../../global-styles'
export const registerClientOptionsContext = createContext<RegisterClientOptions | undefined>(Symbol('register-client-options'));
export const channelConfigurationContext = createContext<ChannelConfiguration | undefined>(Symbol('channel-configuration'));
export const channelConfigurationServiceContext = createContext<ChannelConfigurationService | undefined>(Symbol('channel-configuration-service'));
import { channelConfigurationContext, channelDetailsServiceContext, registerClientOptionsContext } from '../contexts/channel'
@customElement('channel-configuration')
export class ChannelConfigurationElement extends LitElement {
@ -32,8 +26,8 @@ export class ChannelConfigurationElement extends LitElement {
@state()
public _channelConfiguration: ChannelConfiguration | undefined
@provide({ context: channelConfigurationServiceContext })
private _configurationService: ChannelConfigurationService | undefined
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService: ChannelDetailsService | undefined
static styles = [
...getGlobalStyleSheets()
@ -46,8 +40,8 @@ export class ChannelConfigurationElement extends LitElement {
task: async ([registerClientOptions], {signal}) => {
if (this.registerClientOptions) {
this._configurationService = new ChannelConfigurationService(this.registerClientOptions)
this._channelConfiguration = await this._configurationService.fetchConfiguration(this.channelId ?? 0)
this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions)
this._channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0)
}
},
@ -56,8 +50,8 @@ export class ChannelConfigurationElement extends LitElement {
});
private _saveConfig = () => {
if(this._configurationService && this._channelConfiguration) {
this._configurationService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration)
if(this._channelDetailsService && this._channelConfiguration) {
this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration)
.then((value) => {
this._formStatus = { success: true }
console.log(`Configuration has been updated`)
@ -326,7 +320,7 @@ export class ChannelConfigurationElement extends LitElement {
: ''
}
</form>
</div>${JSON.stringify(this._channelConfiguration)}`
</div>`
})
}
}

View File

@ -0,0 +1,86 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { ptTr } from '../directives/translation'
import './help-button'
import { Task } from '@lit/task';
import type { ChannelLiveChatInfos } from 'shared/lib/types'
import { ChannelDetailsService } from '../services/channel-details'
import { provide } from '@lit/context'
import { getGlobalStyleSheets } from '../../global-styles'
import { channelDetailsServiceContext, registerClientOptionsContext } from '../contexts/channel'
@customElement('channel-home')
export class ChannelHomeElement extends LitElement {
@provide({ context: registerClientOptionsContext })
@property({ attribute: false })
public registerClientOptions: RegisterClientOptions | undefined
@state()
public _channels: ChannelLiveChatInfos[] | undefined
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService: ChannelDetailsService | undefined
static styles = [
...getGlobalStyleSheets()
];
@state()
public _formStatus: boolean | any = undefined
private _asyncTaskRender = new Task(this, {
task: async ([registerClientOptions], {signal}) => {
// Getting the current username in localStorage. Don't know any cleaner way to do.
const username = window.localStorage.getItem('username')
if (!username) {
throw new Error('Can\'t get the current username.')
}
if (this.registerClientOptions) {
this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions)
this._channels = await this._channelDetailsService.fetchUserChannels(username)
}
},
args: () => [this.registerClientOptions]
});
render = () => {
return this._asyncTaskRender.render({
complete: () => html`
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-home">
<h1>
${ptTr(LOC_LIVECHAT_CONFIGURATION_TITLE)}
<help-button .page="documentation/user/streamers/channel">
</help-button>
</h1>
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_DESC)}</p>
<p>${ptTr(LOC_LIVECHAT_CONFIGURATION_PLEASE_SELECT)}</p>
<ul class="peertube-plugin-livechat-configuration-home-channels">
${this._channels?.map((channel) => html`
<li>
<a href="${channel.livechatConfigurationUri}">
${channel.avatar ?
html`<img class="avatar channel" src="${channel.avatar.path}">`
:
html`<div class="avatar channel initial gray"></div>`
}
</a>
<div class="peertube-plugin-livechat-configuration-home-info">
<a href="${channel.livechatConfigurationUri}">
<div>${channel.displayName}</div>
<div>${channel.name}</div>
</a>
</div>
</li>
`)}
</ul>
</div>
`
})
}
}

View File

@ -1,13 +1,13 @@
import { css, html, LitElement } from 'lit'
import { html, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { helpButtonSVG } from '../../../videowatch/buttons'
import { consume } from '@lit/context'
import { registerClientOptionsContext } from './ChannelConfigurationElement'
import { RegisterClientOptions } from '@peertube/peertube-types/client'
import { registerClientOptionsContext } from '../contexts/channel'
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { Task } from '@lit/task'
import { localizedHelpUrl } from '../../../utils/help'
import { ptTr } from './TranslationDirective'
import { ptTr } from '../directives/translation'
import { DirectiveResult } from 'lit/directive'
import { getGlobalStyleSheets } from '../../global-styles'

View File

@ -1,10 +1,10 @@
import { css, html, LitElement } from 'lit'
import { html, LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import './HelpButtonElement'
import './help-button'
import { getGlobalStyleSheets } from '../../global-styles'
@customElement('plugin-configuration-row')
export class PluginConfigurationRow extends LitElement {
export class PluginConfigurationRowElement extends LitElement {
@property({ attribute: false })
public title: string = `title`

View File

@ -3,8 +3,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { renderConfigurationHome } from './templates/home'
import './templates/ChannelConfigurationElement'
import './elements/channel-home'
import './elements/channel-configuration'
import { html, render } from 'lit'
/**
@ -20,7 +20,7 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
registerClientRoute({
route: 'livechat/configuration',
onMount: async ({ rootEl }) => {
render(await renderConfigurationHome(clientOptions), rootEl)
render(html`<channel-home .registerClientOptions=${clientOptions}></channel-home>`, rootEl)
}
})

View File

@ -1,9 +1,9 @@
import { RegisterClientOptions } from "@peertube/peertube-types/client"
import { ChannelConfiguration, ChannelConfigurationOptions } from "shared/lib/types"
import type { RegisterClientOptions } from "@peertube/peertube-types/client"
import { ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions } from "shared/lib/types"
import { getBaseRoute } from "../../../utils/uri"
export class ChannelConfigurationService {
export class ChannelDetailsService {
public _registerClientOptions: RegisterClientOptions
@ -42,6 +42,37 @@ export class ChannelConfigurationService {
return await response.json()
}
fetchUserChannels = async (username: string): Promise<ChannelLiveChatInfos[]> => {
// FIXME: if more than 100 channels, loop (or add a pagination)
const channels = await (await fetch(
'/api/v1/accounts/' + encodeURIComponent(username) + '/video-channels?start=0&count=100&sort=name',
{
method: 'GET',
headers: this._headers
}
)).json()
if (!channels || !('data' in channels) || !Array.isArray(channels.data)) {
throw new Error('Can\'t get the channel list.')
}
for (const channel of channels.data) {
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
if (!channel.avatar && channel.avatars && Array.isArray(channel.avatars)) {
for (const avatar of channel.avatars) {
if (avatar.width === 120) {
channel.avatar = avatar
break
}
}
}
}
return channels.data
}
fetchConfiguration = async (channelId: number): Promise<ChannelConfiguration> => {
const response = await fetch(
getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId),

View File

@ -1,132 +0,0 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { localizedHelpUrl } from '../../../utils/help'
import { helpButtonSVG } from '../../../videowatch/buttons'
import { TemplateResult, html } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
interface HomeViewData {
title: string
description: string
please_select: string
channels: any[]
helpButton: TemplateResult
}
/**
* Renders the livechat configuration setup home page.
* @param registerClientOptions Peertube client options
* @returns The page content
*/
async function renderConfigurationHome (registerClientOptions: RegisterClientOptions): Promise<TemplateResult> {
const { peertubeHelpers } = registerClientOptions
try {
// Getting the current username in localStorage. Don't know any cleaner way to do.
const username = window.localStorage.getItem('username')
if (!username) {
throw new Error('Can\'t get the current username.')
}
// FIXME: if more than 100 channels, loop (or add a pagination)
const channels = await (await fetch(
'/api/v1/accounts/' + encodeURIComponent(username) + '/video-channels?start=0&count=100&sort=name',
{
method: 'GET',
headers: peertubeHelpers.getAuthHeader()
}
)).json()
if (!channels || !('data' in channels) || !Array.isArray(channels.data)) {
throw new Error('Can\'t get the channel list.')
}
for (const channel of channels.data) {
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
if (!channel.avatar && channel.avatars && Array.isArray(channel.avatars)) {
for (const avatar of channel.avatars) {
if (avatar.width === 120) {
channel.avatar = avatar
break
}
}
}
}
const view : HomeViewData = {
title: await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_TITLE),
description: await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_DESC),
please_select: await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_PLEASE_SELECT),
channels: channels.data,
helpButton: await _fillViewHelpButtons(registerClientOptions)
}
return renderConfigurationHomeFromTemplate(view)
} catch (err: any) {
peertubeHelpers.notifier.error(err.toString())
return html``
}
}
async function _fillViewHelpButtons ( // TODO: refactor with the similar function in channel.ts
registerClientOptions: RegisterClientOptions
): Promise<TemplateResult> {
const title = await registerClientOptions.peertubeHelpers.translate(LOC_ONLINE_HELP)
const button = async (page: string): Promise<TemplateResult> => {
const helpUrl = await localizedHelpUrl(registerClientOptions, {
page
})
const helpIcon = helpButtonSVG()
return html`<a
href="${helpUrl}"
target=_blank
title="${title}"
class="orange-button peertube-button-link"
>${unsafeHTML(helpIcon)}</a>`
}
return button('documentation/user/streamers/channel')
}
function renderConfigurationHomeFromTemplate(view: HomeViewData) {
return html`
<div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-home">
<h1>
${view.title}
${view.helpButton}
</h1>
<p>${view.description}</p>
<p>${view.please_select}</p>
<ul class="peertube-plugin-livechat-configuration-home-channels">
${view.channels.map((channel) => html`
<li>
<a href="${channel.livechatConfigurationUri}">
${channel.avatar ?
html`<img class="avatar channel" src="${channel.avatar.path}">`
:
html`<div class="avatar channel initial gray"></div>`
}
</a>
<div class="peertube-plugin-livechat-configuration-home-info">
<a href="${channel.livechatConfigurationUri}">
<div>${channel.displayName}</div>
<div>${channel.name}</div>
</a>
</div>
</li>
`)}
</ul>
</div>
`
}
export {
renderConfigurationHome
}

View File

@ -2,25 +2,25 @@
"compilerOptions": {
"experimentalDecorators": true,
"module": "es2022",
"moduleResolution": "node",
"moduleResolution": "node",
"target": "es2022",
"allowJs": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"alwaysStrict": true, // should already be true because of strict:true
"noImplicitAny": true, // should already be true because of strict:true
"noImplicitThis": true, // should already be true because of strict:true
"noImplicitReturns": true,
"strictBindCallApply": true, // should already be true because of strict:true
"noUnusedLocals": false,
"allowSyntheticDefaultImports": true, // Seems necessary for peertube types to work
"isolatedModules": true, // Needed by esbuild https://esbuild.github.io/content-types/#isolated-modules
"esModuleInterop": true, // Needed by esbuild https://esbuild.github.io/content-types/#es-module-interop
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"alwaysStrict": true, // should already be true because of strict:true
"noImplicitAny": true, // should already be true because of strict:true
"noImplicitThis": true, // should already be true because of strict:true
"noImplicitReturns": true,
"strictBindCallApply": true, // should already be true because of strict:true
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true, // Seems necessary for peertube types to work
"isolatedModules": true, // Needed by esbuild https://esbuild.github.io/content-types/#isolated-modules
"esModuleInterop": true, // Needed by esbuild https://esbuild.github.io/content-types/#es-module-interop
"outDir": "../dist/client",
"paths": {
"shared/*": ["../shared/*"]
}
"paths": {
"shared/*": ["../shared/*"]
}
},
"include": [
"./**/*",

29
package-lock.json generated
View File

@ -20,7 +20,6 @@
"lit": "^3.1.3",
"log-rotate": "^0.2.8",
"openid-client": "^5.6.5",
"rxjs": "^7.8.1",
"validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.3.0"
},
@ -10535,19 +10534,6 @@
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
"dev": true
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/rxjs/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/safe-array-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
@ -20337,21 +20323,6 @@
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
"dev": true
},
"rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}
}
},
"safe-array-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",

View File

@ -44,7 +44,6 @@
"lit": "^3.1.3",
"log-rotate": "^0.2.8",
"openid-client": "^5.6.5",
"rxjs": "^7.8.1",
"validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.3.0"
},

View File

@ -2,6 +2,17 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
// Not working for some reason
// import type { ActorImage } from '@peertube/peertube-types'
interface ActorImage {
width: number
path: string
url?: string
createdAt: Date | string
updatedAt: Date | string
}
type ConverseJSTheme = 'peertube' | 'default' | 'concord'
interface InitConverseJSParams {
@ -71,6 +82,12 @@ interface ChannelInfos {
displayName: string
}
interface ChannelLiveChatInfos extends ChannelInfos {
avatar: ActorImage
avatars: ActorImage[]
livechatConfigurationUri: string
}
interface ChannelConfigurationOptions {
bot: {
enabled: boolean
@ -134,6 +151,7 @@ export type {
ProsodyListRoomsResult,
ProsodyListRoomsResultRoom,
ChannelInfos,
ChannelLiveChatInfos,
ChannelConfigurationOptions,
ChannelConfiguration,
ChatIncludeMode,