refactor: Remove hardcoded values
All checks were successful
Queue Release Build / prepare (push) Successful in 2m28s
Deploy Web Apps / deploy (push) Successful in 7m58s
Queue Release Build / build-linux (push) Successful in 46m59s
Queue Release Build / build-windows (push) Successful in 26m2s
Queue Release Build / finalize (push) Successful in 23s
All checks were successful
Queue Release Build / prepare (push) Successful in 2m28s
Deploy Web Apps / deploy (push) Successful in 7m58s
Queue Release Build / build-linux (push) Successful in 46m59s
Queue Release Build / build-windows (push) Successful in 26m2s
Queue Release Build / finalize (push) Successful in 23s
This commit is contained in:
@@ -5,9 +5,10 @@ import {
|
|||||||
input
|
input
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { environment } from '../../../../../../../../environments/environment';
|
||||||
import { extractYoutubeVideoId } from '../../../../../domain/rules/link-embed.rules';
|
import { extractYoutubeVideoId } from '../../../../../domain/rules/link-embed.rules';
|
||||||
|
|
||||||
const YOUTUBE_EMBED_FALLBACK_ORIGIN = 'https://toju.app';
|
const YOUTUBE_EMBED_FALLBACK_ORIGIN = environment.publicOrigin;
|
||||||
|
|
||||||
function resolveYoutubeClientOrigin(): string {
|
function resolveYoutubeClientOrigin(): string {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
|
|
||||||
export interface ExperimentalVlcPlayerOptions {
|
export interface ExperimentalVlcPlayerOptions {
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
@@ -23,7 +24,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VLC_RUNTIME_SCRIPT_URL = '/vlcjs/metoyou-vlc-player.js';
|
const VLC_RUNTIME_SCRIPT_URL = environment.experimentalMedia.vlcRuntimeScriptUrl;
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ExperimentalVlcRuntimeService {
|
export class ExperimentalVlcRuntimeService {
|
||||||
@@ -60,6 +61,7 @@ export class ExperimentalVlcRuntimeService {
|
|||||||
|
|
||||||
script.src = VLC_RUNTIME_SCRIPT_URL;
|
script.src = VLC_RUNTIME_SCRIPT_URL;
|
||||||
script.async = true;
|
script.async = true;
|
||||||
|
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
const runtime = this.document.defaultView?.MetoYouVlcJs;
|
const runtime = this.document.defaultView?.MetoYouVlcJs;
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ export class ExperimentalVlcRuntimeService {
|
|||||||
|
|
||||||
resolve(runtime);
|
resolve(runtime);
|
||||||
};
|
};
|
||||||
|
|
||||||
script.onerror = () => reject(new Error(`The experimental VLC.js runtime was not found at ${VLC_RUNTIME_SCRIPT_URL}.`));
|
script.onerror = () => reject(new Error(`The experimental VLC.js runtime was not found at ${VLC_RUNTIME_SCRIPT_URL}.`));
|
||||||
|
|
||||||
this.document.head.appendChild(script);
|
this.document.head.appendChild(script);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ The standalone plugin store is available from the title bar Plugins button, the
|
|||||||
|
|
||||||
The plugin manager UI is split between Settings -> Client plugins for global client plugins and Settings -> Server -> Server plugins for chat-server plugins. The two pages filter by manifest `scope` and include installed plugins, capability grant toggles, per-plugin activate/reload/unload actions, runtime logs, extension-point counts, server requirements, generated settings, and docs.
|
The plugin manager UI is split between Settings -> Client plugins for global client plugins and Settings -> Server -> Server plugins for chat-server plugins. The two pages filter by manifest `scope` and include installed plugins, capability grant toggles, per-plugin activate/reload/unload actions, runtime logs, extension-point counts, server requirements, generated settings, and docs.
|
||||||
|
|
||||||
The Store tab consumes user-managed HTTP(S), `file://`, or absolute local-path source manifests. Local-path sources and entrypoints are read through the Electron desktop file bridge. A source manifest can expose a `plugins` array whose entries include `id`, `title`, `description`, `version`, `scope`, `author`/`authors`, `image`/`imageUrl`, `github`/`githubUrl`, `install`/`installUrl`/`manifestUrl`, `bundle`/`bundleUrl`, and `readme`/`readmeUrl`. Installing a `scope: "server"` plugin fetches the linked plugin manifest, validates it, registers it with the client registry, and persists the basic install metadata as a server plugin requirement. When a different user joins that server, required plugins block the join until the user accepts the download; optional and recommended plugins are offered as selectable downloads and can be skipped. Once a server has local server-scoped plugins installed, the title bar shows a compact Server plugins button for that server. Installing a `scope: "client"` plugin persists it locally for the current desktop/browser client.
|
The Store tab consumes user-managed HTTP(S), `file://`, or absolute local-path source manifests. New users and legacy source lists are seeded with the official Toju plugin repository at `https://raw.githubusercontent.com/Myxelium/official-toju-plugin-repository/refs/heads/master/plugin-source.json`, while source removal remains persisted after migration. Local-path sources and entrypoints are read through the Electron desktop file bridge. A source manifest can expose a `plugins` array whose entries include `id`, `title`, `description`, `version`, `scope`, `author`/`authors`, `image`/`imageUrl`, `github`/`githubUrl`, `install`/`installUrl`/`manifestUrl`, `bundle`/`bundleUrl`, and `readme`/`readmeUrl`. Installing a `scope: "server"` plugin fetches the linked plugin manifest, validates it, registers it with the client registry, and persists the basic install metadata as a server plugin requirement. When a different user joins that server, required plugins block the join until the user accepts the download; optional and recommended plugins are offered as selectable downloads and can be skipped. Once a server has local server-scoped plugins installed, the title bar shows a compact Server plugins button for that server. Installing a `scope: "client"` plugin persists it locally for the current desktop/browser client.
|
||||||
|
|
||||||
Store plugins can be published as cached browser bundles by adding `bundle` or `bundleUrl` to the source manifest entry. The bundle is a browser-safe ESM JavaScript file. During install, Electron downloads the bundle into app data under `plugin-bundles/<plugin-id>/<version>/main.js`, writes a cached manifest next to it, and registers the plugin from that local cached manifest path. If no bundle URL is provided and the manifest entrypoint is a relative browser module, Electron caches that entrypoint path instead. Browser-only clients still load directly from the source URL; the renderer CSP allows HTTP(S), `file://` via local cache, and blob-backed plugin entrypoints. Saved store sources refresh during app bootstrap; when a source advertises a higher version for an installed plugin, the store attempts to update the local cached bundle and persisted install metadata automatically.
|
Store plugins can be published as cached browser bundles by adding `bundle` or `bundleUrl` to the source manifest entry. The bundle is a browser-safe ESM JavaScript file. During install, Electron downloads the bundle into app data under `plugin-bundles/<plugin-id>/<version>/main.js`, writes a cached manifest next to it, and registers the plugin from that local cached manifest path. If no bundle URL is provided and the manifest entrypoint is a relative browser module, Electron caches that entrypoint path instead. Browser-only clients still load directly from the source URL; the renderer CSP allows HTTP(S), `file://` via local cache, and blob-backed plugin entrypoints. Saved store sources refresh during app bootstrap; when a source advertises a higher version for an installed plugin, the store attempts to update the local cached bundle and persisted install metadata automatically.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injector } from '@angular/core';
|
import { Injector } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import type { TojuPluginManifest } from '../../../../shared-kernel';
|
import type { TojuPluginManifest } from '../../../../shared-kernel';
|
||||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||||
import { PluginStoreService } from './plugin-store.service';
|
import { PluginStoreService } from './plugin-store.service';
|
||||||
@@ -6,8 +7,11 @@ import { PluginHostService } from './plugin-host.service';
|
|||||||
import { PluginDesktopStateService } from './plugin-desktop-state.service';
|
import { PluginDesktopStateService } from './plugin-desktop-state.service';
|
||||||
import { PluginRequirementService } from './plugin-requirement.service';
|
import { PluginRequirementService } from './plugin-requirement.service';
|
||||||
import { PluginRegistryService } from './plugin-registry.service';
|
import { PluginRegistryService } from './plugin-registry.service';
|
||||||
|
import { PluginCapabilityService } from './plugin-capability.service';
|
||||||
import type { PluginStoreEntry } from '../../domain/models/plugin-store.models';
|
import type { PluginStoreEntry } from '../../domain/models/plugin-store.models';
|
||||||
|
|
||||||
|
const OFFICIAL_PLUGIN_SOURCE_URL = environment.pluginStore.defaultSourceUrls[0];
|
||||||
|
|
||||||
describe('PluginStoreService', () => {
|
describe('PluginStoreService', () => {
|
||||||
let fetchMock: ReturnType<typeof vi.fn>;
|
let fetchMock: ReturnType<typeof vi.fn>;
|
||||||
let registerLocalManifest: ReturnType<typeof vi.fn>;
|
let registerLocalManifest: ReturnType<typeof vi.fn>;
|
||||||
@@ -36,7 +40,8 @@ describe('PluginStoreService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads plugin entries from source manifests and resolves relative links', async () => {
|
it('loads plugin entries from source manifests and resolves relative links', async () => {
|
||||||
fetchMock.mockResolvedValueOnce(jsonResponse({
|
mockFetchResponses(fetchMock, {
|
||||||
|
'https://plugins.example.test/index.json': jsonResponse({
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
author: 'Ada Example',
|
author: 'Ada Example',
|
||||||
@@ -51,16 +56,17 @@ describe('PluginStoreService', () => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
title: 'Example Plugins'
|
title: 'Example Plugins'
|
||||||
}));
|
})
|
||||||
|
});
|
||||||
|
|
||||||
const service = createService(registerLocalManifest, unregister);
|
const service = createService(registerLocalManifest, unregister);
|
||||||
|
|
||||||
await service.addSourceUrl('https://plugins.example.test/index.json#latest');
|
await service.addSourceUrl('https://plugins.example.test/index.json#latest');
|
||||||
|
|
||||||
expect(service.sourceUrls()).toEqual(['https://plugins.example.test/index.json']);
|
expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL, 'https://plugins.example.test/index.json']);
|
||||||
expect(service.sources()[0]?.title).toBe('Example Plugins');
|
|
||||||
expect(service.availablePlugins()).toEqual([
|
expect(service.sources().some((source) => source.title === 'Example Plugins')).toBe(true);
|
||||||
expect.objectContaining({
|
expect(service.availablePlugins()).toContainEqual(expect.objectContaining({
|
||||||
author: 'Ada Example',
|
author: 'Ada Example',
|
||||||
githubUrl: 'https://github.com/example/better-channels',
|
githubUrl: 'https://github.com/example/better-channels',
|
||||||
id: 'example.better-channels',
|
id: 'example.better-channels',
|
||||||
@@ -70,8 +76,43 @@ describe('PluginStoreService', () => {
|
|||||||
sourceTitle: 'Example Plugins',
|
sourceTitle: 'Example Plugins',
|
||||||
title: 'Better Channels',
|
title: 'Better Channels',
|
||||||
version: '1.2.0'
|
version: '1.2.0'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('seeds the official plugin repository for new users', () => {
|
||||||
|
const service = createService(registerLocalManifest, unregister);
|
||||||
|
|
||||||
|
expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL]);
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
OFFICIAL_PLUGIN_SOURCE_URL,
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
})
|
})
|
||||||
]);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the official plugin repository when loading legacy source lists', () => {
|
||||||
|
storage.setItem('metoyou_plugin_store', JSON.stringify({
|
||||||
|
installedPlugins: [],
|
||||||
|
schemaVersion: 1,
|
||||||
|
sourceUrls: ['https://plugins.example.test/index.json']
|
||||||
|
}));
|
||||||
|
|
||||||
|
const service = createService(registerLocalManifest, unregister);
|
||||||
|
|
||||||
|
expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL, 'https://plugins.example.test/index.json']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps user-removed default sources removed after schema migration', () => {
|
||||||
|
storage.setItem('metoyou_plugin_store', JSON.stringify({
|
||||||
|
installedPlugins: [],
|
||||||
|
schemaVersion: 2,
|
||||||
|
sourceUrls: ['https://plugins.example.test/index.json']
|
||||||
|
}));
|
||||||
|
|
||||||
|
const service = createService(registerLocalManifest, unregister);
|
||||||
|
|
||||||
|
expect(service.sourceUrls()).toEqual(['https://plugins.example.test/index.json']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts local source manifest paths and resolves relative file links', async () => {
|
it('accepts local source manifest paths and resolves relative file links', async () => {
|
||||||
@@ -94,9 +135,10 @@ describe('PluginStoreService', () => {
|
|||||||
|
|
||||||
await service.addSourceUrl('/home/ludde/Desktop/TestPlugin/plugin-source.json');
|
await service.addSourceUrl('/home/ludde/Desktop/TestPlugin/plugin-source.json');
|
||||||
|
|
||||||
expect(fetchMock).not.toHaveBeenCalled();
|
expect(fetchMock).not.toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin/plugin-source.json', expect.anything());
|
||||||
expect(readFile).toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin/plugin-source.json');
|
expect(readFile).toHaveBeenCalledWith('/home/ludde/Desktop/TestPlugin/plugin-source.json');
|
||||||
expect(service.sourceUrls()).toEqual(['file:///home/ludde/Desktop/TestPlugin/plugin-source.json']);
|
expect(service.sourceUrls()).toEqual([OFFICIAL_PLUGIN_SOURCE_URL, 'file:///home/ludde/Desktop/TestPlugin/plugin-source.json']);
|
||||||
|
|
||||||
expect(service.availablePlugins()).toEqual([
|
expect(service.availablePlugins()).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'example.local-plugin',
|
id: 'example.local-plugin',
|
||||||
@@ -112,7 +154,9 @@ describe('PluginStoreService', () => {
|
|||||||
const manifest = createManifest({ version: '1.0.0' });
|
const manifest = createManifest({ version: '1.0.0' });
|
||||||
const plugin = createStoreEntry({ version: '1.0.0' });
|
const plugin = createStoreEntry({ version: '1.0.0' });
|
||||||
|
|
||||||
fetchMock.mockResolvedValueOnce(jsonResponse(manifest));
|
mockFetchResponses(fetchMock, {
|
||||||
|
[plugin.installUrl ?? '']: jsonResponse(manifest)
|
||||||
|
});
|
||||||
|
|
||||||
const service = createService(registerLocalManifest, unregister);
|
const service = createService(registerLocalManifest, unregister);
|
||||||
|
|
||||||
@@ -141,9 +185,10 @@ describe('PluginStoreService', () => {
|
|||||||
writeFile: vi.fn(async () => true)
|
writeFile: vi.fn(async () => true)
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock
|
mockFetchResponses(fetchMock, {
|
||||||
.mockResolvedValueOnce(jsonResponse(manifest))
|
[plugin.bundleUrl ?? '']: textResponse('export function activate() {}'),
|
||||||
.mockResolvedValueOnce(textResponse('export function activate() {}'));
|
[plugin.installUrl ?? '']: jsonResponse(manifest)
|
||||||
|
});
|
||||||
|
|
||||||
const service = createService(registerLocalManifest, unregister, electronApi);
|
const service = createService(registerLocalManifest, unregister, electronApi);
|
||||||
|
|
||||||
@@ -171,7 +216,9 @@ describe('PluginStoreService', () => {
|
|||||||
it('loads plugin readmes as markdown text', async () => {
|
it('loads plugin readmes as markdown text', async () => {
|
||||||
const plugin = createStoreEntry({ readmeUrl: 'https://plugins.example.test/better/README.md' });
|
const plugin = createStoreEntry({ readmeUrl: 'https://plugins.example.test/better/README.md' });
|
||||||
|
|
||||||
fetchMock.mockResolvedValueOnce(textResponse('# Better Channels'));
|
mockFetchResponses(fetchMock, {
|
||||||
|
[plugin.readmeUrl ?? '']: textResponse('# Better Channels')
|
||||||
|
});
|
||||||
|
|
||||||
const service = createService(registerLocalManifest, unregister);
|
const service = createService(registerLocalManifest, unregister);
|
||||||
const readme = await service.loadReadme(plugin);
|
const readme = await service.loadReadme(plugin);
|
||||||
@@ -185,6 +232,21 @@ describe('PluginStoreService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function mockFetchResponses(fetchMock: ReturnType<typeof vi.fn>, responses: Record<string, Response>): void {
|
||||||
|
fetchMock.mockImplementation(async (url: RequestInfo | URL) => {
|
||||||
|
const requestUrl = typeof url === 'string'
|
||||||
|
? url
|
||||||
|
: url instanceof URL
|
||||||
|
? url.toString()
|
||||||
|
: url.url;
|
||||||
|
|
||||||
|
return responses[requestUrl]
|
||||||
|
?? (requestUrl === OFFICIAL_PLUGIN_SOURCE_URL
|
||||||
|
? jsonResponse({ plugins: [], title: 'Official Toju Plugins' })
|
||||||
|
: textResponse(''));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createService(
|
function createService(
|
||||||
registerLocalManifest: ReturnType<typeof vi.fn>,
|
registerLocalManifest: ReturnType<typeof vi.fn>,
|
||||||
unregister: ReturnType<typeof vi.fn>,
|
unregister: ReturnType<typeof vi.fn>,
|
||||||
@@ -220,6 +282,12 @@ function createService(
|
|||||||
writeJson: vi.fn(async () => undefined)
|
writeJson: vi.fn(async () => undefined)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PluginCapabilityService,
|
||||||
|
useValue: {
|
||||||
|
grantAll: vi.fn()
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: PluginRegistryService,
|
provide: PluginRegistryService,
|
||||||
useValue: { unregister }
|
useValue: { unregister }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||||
import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage';
|
import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage';
|
||||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||||
@@ -44,14 +45,11 @@ import { PluginDesktopStateService } from './plugin-desktop-state.service';
|
|||||||
import { PluginRequirementService } from './plugin-requirement.service';
|
import { PluginRequirementService } from './plugin-requirement.service';
|
||||||
import { PluginRegistryService } from './plugin-registry.service';
|
import { PluginRegistryService } from './plugin-registry.service';
|
||||||
|
|
||||||
const STORE_SCHEMA_VERSION = 1;
|
const STORE_SCHEMA_VERSION = 2;
|
||||||
const STORAGE_KEY_PLUGIN_STORE = 'metoyou_plugin_store';
|
const STORAGE_KEY_PLUGIN_STORE = 'metoyou_plugin_store';
|
||||||
const STORAGE_KEY_SERVER_PLUGIN_INSTALLS = 'metoyou_server_plugin_installs';
|
const STORAGE_KEY_SERVER_PLUGIN_INSTALLS = 'metoyou_server_plugin_installs';
|
||||||
const PLUGIN_CACHE_DIR = 'plugin-bundles';
|
const PLUGIN_CACHE_DIR = 'plugin-bundles';
|
||||||
const DEFAULT_STORE_STATE: PersistedPluginStoreState = {
|
const DEFAULT_PLUGIN_SOURCE_URLS = [...environment.pluginStore.defaultSourceUrls];
|
||||||
installedPlugins: [],
|
|
||||||
sourceUrls: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PluginStoreInstallOptions {
|
export interface PluginStoreInstallOptions {
|
||||||
activate?: boolean;
|
activate?: boolean;
|
||||||
@@ -953,12 +951,12 @@ export class PluginStoreService {
|
|||||||
const raw = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE));
|
const raw = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_STORE));
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return { ...DEFAULT_STORE_STATE };
|
return createDefaultStoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizePersistedState(JSON.parse(raw) as unknown);
|
return normalizePersistedState(JSON.parse(raw) as unknown);
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULT_STORE_STATE };
|
return createDefaultStoreState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1132,22 +1130,63 @@ function readPluginInstallScope(record: Record<string, unknown>): TojuPluginInst
|
|||||||
|
|
||||||
function normalizePersistedState(value: unknown): PersistedPluginStoreState {
|
function normalizePersistedState(value: unknown): PersistedPluginStoreState {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return { ...DEFAULT_STORE_STATE };
|
return createDefaultStoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const schemaVersion = typeof value['schemaVersion'] === 'number' ? value['schemaVersion'] : 0;
|
||||||
|
const sourceUrls = Array.isArray(value['sourceUrls'])
|
||||||
|
? normalizePluginSourceUrls(value['sourceUrls'])
|
||||||
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
installedPlugins: Array.isArray(value['installedPlugins'])
|
installedPlugins: Array.isArray(value['installedPlugins'])
|
||||||
? value['installedPlugins'].filter(isInstalledStorePlugin)
|
? value['installedPlugins'].filter(isInstalledStorePlugin)
|
||||||
: [],
|
: [],
|
||||||
sourceUrls: Array.isArray(value['sourceUrls'])
|
schemaVersion: STORE_SCHEMA_VERSION,
|
||||||
? value['sourceUrls']
|
sourceUrls: schemaVersion < STORE_SCHEMA_VERSION
|
||||||
.filter((entry): entry is string => typeof entry === 'string')
|
? mergePluginSourceUrls(DEFAULT_PLUGIN_SOURCE_URLS, sourceUrls)
|
||||||
.map((entry) => normalizeOptionalSourceUrl(entry))
|
: sourceUrls
|
||||||
.filter((entry): entry is string => !!entry)
|
|
||||||
: []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDefaultStoreState(): PersistedPluginStoreState {
|
||||||
|
return {
|
||||||
|
installedPlugins: [],
|
||||||
|
schemaVersion: STORE_SCHEMA_VERSION,
|
||||||
|
sourceUrls: [...DEFAULT_PLUGIN_SOURCE_URLS]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePluginSourceUrls(sourceUrls: unknown[]): string[] {
|
||||||
|
const normalizedSourceUrls: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of sourceUrls) {
|
||||||
|
if (typeof entry !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceUrl = normalizeOptionalSourceUrl(entry);
|
||||||
|
|
||||||
|
if (sourceUrl && !normalizedSourceUrls.includes(sourceUrl)) {
|
||||||
|
normalizedSourceUrls.push(sourceUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedSourceUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePluginSourceUrls(defaultSourceUrls: string[], sourceUrls: string[]): string[] {
|
||||||
|
const mergedSourceUrls: string[] = [];
|
||||||
|
|
||||||
|
for (const sourceUrl of defaultSourceUrls.concat(sourceUrls)) {
|
||||||
|
if (!mergedSourceUrls.includes(sourceUrl)) {
|
||||||
|
mergedSourceUrls.push(sourceUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedSourceUrls;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePersistedServerPluginInstallState(value: unknown): { servers: Record<string, InstalledStorePlugin[]> } {
|
function normalizePersistedServerPluginInstallState(value: unknown): { servers: Record<string, InstalledStorePlugin[]> } {
|
||||||
if (!isRecord(value) || !isRecord(value['servers'])) {
|
if (!isRecord(value) || !isRecord(value['servers'])) {
|
||||||
return { servers: {} };
|
return { servers: {} };
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface PluginStoreReadme {
|
|||||||
|
|
||||||
export interface PersistedPluginStoreState {
|
export interface PersistedPluginStoreState {
|
||||||
installedPlugins: InstalledStorePlugin[];
|
installedPlugins: InstalledStorePlugin[];
|
||||||
|
schemaVersion?: number;
|
||||||
sourceUrls: string[];
|
sourceUrls: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
type Signal
|
type Signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
import { STORAGE_KEY_ICE_SERVERS } from '../../core/constants';
|
import { STORAGE_KEY_ICE_SERVERS } from '../../core/constants';
|
||||||
import { ICE_SERVERS } from './realtime.constants';
|
|
||||||
|
|
||||||
export interface IceServerEntry {
|
export interface IceServerEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -15,7 +15,7 @@ export interface IceServerEntry {
|
|||||||
credential?: string;
|
credential?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_ENTRIES: IceServerEntry[] = ICE_SERVERS.map((server, index) => ({
|
const DEFAULT_ENTRIES: IceServerEntry[] = environment.realtime.defaultIceServers.map((server, index) => ({
|
||||||
id: `default-stun-${index}`,
|
id: `default-stun-${index}`,
|
||||||
type: 'stun' as const,
|
type: 'stun' as const,
|
||||||
urls: Array.isArray(server.urls) ? server.urls[0] : server.urls
|
urls: Array.isArray(server.urls) ? server.urls[0] : server.urls
|
||||||
|
|||||||
@@ -5,14 +5,6 @@ import type { LatencyProfile } from '../../shared-kernel';
|
|||||||
* Centralised here so nothing is hard-coded inline.
|
* Centralised here so nothing is hard-coded inline.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ICE_SERVERS: RTCIceServer[] = [
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
|
||||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
||||||
{ urls: 'stun:stun2.l.google.com:19302' },
|
|
||||||
{ urls: 'stun:stun3.l.google.com:19302' },
|
|
||||||
{ urls: 'stun:stun4.l.google.com:19302' }
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Base delay (ms) for exponential backoff on signaling reconnect */
|
/** Base delay (ms) for exponential backoff on signaling reconnect */
|
||||||
export const SIGNALING_RECONNECT_BASE_DELAY_MS = 1_000;
|
export const SIGNALING_RECONNECT_BASE_DELAY_MS = 1_000;
|
||||||
/** Maximum delay (ms) between signaling reconnect attempts */
|
/** Maximum delay (ms) between signaling reconnect attempts */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
|
publicOrigin: 'https://toju.app',
|
||||||
defaultServers: [
|
defaultServers: [
|
||||||
{
|
{
|
||||||
key: 'toju-primary',
|
key: 'toju-primary',
|
||||||
@@ -12,5 +13,20 @@ export const environment = {
|
|||||||
url: 'https://signal-sweden.toju.app'
|
url: 'https://signal-sweden.toju.app'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultServerUrl: 'https://signal.toju.app'
|
defaultServerUrl: 'https://signal.toju.app',
|
||||||
|
experimentalMedia: {
|
||||||
|
vlcRuntimeScriptUrl: '/vlcjs/metoyou-vlc-player.js'
|
||||||
|
},
|
||||||
|
pluginStore: {
|
||||||
|
defaultSourceUrls: ['https://raw.githubusercontent.com/Myxelium/official-toju-plugin-repository/refs/heads/master/plugin-source.json']
|
||||||
|
},
|
||||||
|
realtime: {
|
||||||
|
defaultIceServers: [
|
||||||
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun3.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun4.l.google.com:19302' }
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
|
publicOrigin: 'https://toju.app',
|
||||||
defaultServers: [
|
defaultServers: [
|
||||||
{
|
{
|
||||||
key: 'default',
|
key: 'default',
|
||||||
@@ -17,5 +18,20 @@ export const environment = {
|
|||||||
url: 'https://signal-sweden.toju.app'
|
url: 'https://signal-sweden.toju.app'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultServerUrl: 'https://46.59.68.77:3001'
|
defaultServerUrl: 'https://46.59.68.77:3001',
|
||||||
|
experimentalMedia: {
|
||||||
|
vlcRuntimeScriptUrl: '/vlcjs/metoyou-vlc-player.js'
|
||||||
|
},
|
||||||
|
pluginStore: {
|
||||||
|
defaultSourceUrls: ['https://raw.githubusercontent.com/Myxelium/official-toju-plugin-repository/refs/heads/master/plugin-source.json']
|
||||||
|
},
|
||||||
|
realtime: {
|
||||||
|
defaultIceServers: [
|
||||||
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun3.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun4.l.google.com:19302' }
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user