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

This commit is contained in:
2026-05-17 18:18:14 +02:00
parent a173299ad3
commit ecb1a4b3a0
10 changed files with 201 additions and 65 deletions

View File

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

View File

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

View File

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

View File

@@ -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,42 +40,79 @@ 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, {
plugins: [ 'https://plugins.example.test/index.json': jsonResponse({
{ plugins: [
author: 'Ada Example', {
description: 'Adds better channel tools.', author: 'Ada Example',
github: 'https://github.com/example/better-channels', description: 'Adds better channel tools.',
id: 'example.better-channels', github: 'https://github.com/example/better-channels',
image: './images/better.png', id: 'example.better-channels',
install: './better/toju-plugin.json', image: './images/better.png',
readme: './better/README.md', install: './better/toju-plugin.json',
title: 'Better Channels', readme: './better/README.md',
version: '1.2.0' title: 'Better Channels',
} version: '1.2.0'
], }
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(service.availablePlugins()).toContainEqual(expect.objectContaining({
author: 'Ada Example',
githubUrl: 'https://github.com/example/better-channels',
id: 'example.better-channels',
imageUrl: 'https://plugins.example.test/images/better.png',
installUrl: 'https://plugins.example.test/better/toju-plugin.json',
readmeUrl: 'https://plugins.example.test/better/README.md',
sourceTitle: 'Example Plugins',
title: 'Better Channels',
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({ expect.objectContaining({
author: 'Ada Example', headers: { Accept: 'application/json' }
githubUrl: 'https://github.com/example/better-channels',
id: 'example.better-channels',
imageUrl: 'https://plugins.example.test/images/better.png',
installUrl: 'https://plugins.example.test/better/toju-plugin.json',
readmeUrl: 'https://plugins.example.test/better/README.md',
sourceTitle: 'Example Plugins',
title: 'Better Channels',
version: '1.2.0'
}) })
]); );
});
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 }

View File

@@ -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: {} };

View File

@@ -48,6 +48,7 @@ export interface PluginStoreReadme {
export interface PersistedPluginStoreState { export interface PersistedPluginStoreState {
installedPlugins: InstalledStorePlugin[]; installedPlugins: InstalledStorePlugin[];
schemaVersion?: number;
sourceUrls: string[]; sourceUrls: string[];
} }

View File

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

View File

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

View File

@@ -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' }
]
}
}; };

View File

@@ -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' }
]
}
}; };