Introducing the new UI variable system
parent
7918af1b31
commit
c8b6998e35
|
@ -22,7 +22,7 @@ export class SingletonEvent implements Event<SingletonEvents, "singletone-instan
|
||||||
as<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
|
as<T extends keyof SingletonEvents>() : SingletonEvents[T] { return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventReceiver<Events extends { [key: string]: any } = { [key: string]: any }> {
|
export interface EventSender<Events extends { [key: string]: any } = { [key: string]: any }> {
|
||||||
fire<T extends keyof Events>(event_type: T, data?: Events[T], overrideTypeKey?: boolean);
|
fire<T extends keyof Events>(event_type: T, data?: Events[T], overrideTypeKey?: boolean);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,11 +44,11 @@ export interface EventReceiver<Events extends { [key: string]: any } = { [key: s
|
||||||
}
|
}
|
||||||
|
|
||||||
const event_annotation_key = guid();
|
const event_annotation_key = guid();
|
||||||
export class Registry<Events extends { [key: string]: any } = { [key: string]: any }> implements EventReceiver<Events> {
|
export class Registry<Events extends { [key: string]: any } = { [key: string]: any }> implements EventSender<Events> {
|
||||||
private readonly registryUuid;
|
private readonly registryUuid;
|
||||||
|
|
||||||
private handler: {[key: string]: ((event) => void)[]} = {};
|
private handler: {[key: string]: ((event) => void)[]} = {};
|
||||||
private connections: {[key: string]: EventReceiver<Events>[]} = {};
|
private connections: {[key: string]: EventSender<Events>[]} = {};
|
||||||
private eventHandlerObjects: {
|
private eventHandlerObjects: {
|
||||||
object: any,
|
object: any,
|
||||||
handlers: {[key: string]: ((event) => void)[]}
|
handlers: {[key: string]: ((event) => void)[]}
|
||||||
|
@ -66,12 +66,11 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
|
||||||
this.registryUuid = "evreg_data_" + guid();
|
this.registryUuid = "evreg_data_" + guid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enableDebug(prefix: string) { this.debugPrefix = prefix || "---"; }
|
enableDebug(prefix: string) { this.debugPrefix = prefix || "---"; }
|
||||||
disableDebug() { this.debugPrefix = undefined; }
|
disableDebug() { this.debugPrefix = undefined; }
|
||||||
|
|
||||||
enable_warn_unhandled_events() { this.warnUnhandledEvents = true; }
|
enableWarnUnhandledEvents() { this.warnUnhandledEvents = true; }
|
||||||
disable_warn_unhandled_events() { this.warnUnhandledEvents = false; }
|
disableWarnUnhandledEvents() { this.warnUnhandledEvents = false; }
|
||||||
|
|
||||||
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
||||||
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
||||||
|
@ -158,21 +157,21 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
|
||||||
}, reactEffectDependencies);
|
}, reactEffectDependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectAll<EOther, T extends keyof Events & keyof EOther>(target: EventReceiver<Events>) {
|
connectAll<EOther, T extends keyof Events & keyof EOther>(target: EventSender<Events>) {
|
||||||
(this.connections[null as any] || (this.connections[null as any] = [])).push(target as any);
|
(this.connections[null as any] || (this.connections[null as any] = [])).push(target as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect<EOther, T extends (keyof Events & keyof EOther)>(events: T | T[], target: EventReceiver<EOther>) {
|
connect<EOther, T extends (keyof Events & keyof EOther)>(events: T | T[], target: EventSender<EOther>) {
|
||||||
for(const event of Array.isArray(events) ? events : [events])
|
for(const event of Array.isArray(events) ? events : [events])
|
||||||
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
(this.connections[event as string] || (this.connections[event as string] = [])).push(target as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: EventReceiver<Events>) {
|
disconnect<EOther, T extends keyof Events & keyof EOther>(events: T | T[], target: EventSender<Events>) {
|
||||||
for(const event of Array.isArray(events) ? events : [events])
|
for(const event of Array.isArray(events) ? events : [events])
|
||||||
(this.connections[event as string] || []).remove(target as any);
|
(this.connections[event as string] || []).remove(target as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectAll<EOther>(target: EventReceiver<EOther>) {
|
disconnectAll<EOther>(target: EventSender<EOther>) {
|
||||||
this.connections[null as any]?.remove(target as any);
|
this.connections[null as any]?.remove(target as any);
|
||||||
for(const event of Object.keys(this.connections))
|
for(const event of Object.keys(this.connections))
|
||||||
this.connections[event].remove(target as any);
|
this.connections[event].remove(target as any);
|
||||||
|
@ -266,7 +265,7 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
|
||||||
this.pendingReactCallbacksFrame = 0;
|
this.pendingReactCallbacksFrame = 0;
|
||||||
this.pendingReactCallbacks = undefined;
|
this.pendingReactCallbacks = undefined;
|
||||||
|
|
||||||
/* run this after the requestAnimationFrame has been finished */
|
/* run this after the requestAnimationFrame has been finished since else it might be fired instantly */
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
/* batch all react updates */
|
/* batch all react updates */
|
||||||
unstable_batchedUpdates(() => {
|
unstable_batchedUpdates(() => {
|
||||||
|
@ -344,10 +343,11 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
|
||||||
this.eventHandlerObjects.remove(data);
|
this.eventHandlerObjects.remove(data);
|
||||||
|
|
||||||
for(const key of Object.keys(data.handlers)) {
|
for(const key of Object.keys(data.handlers)) {
|
||||||
for(const evhandler of data.handlers[key])
|
for(const evhandler of data.handlers[key]) {
|
||||||
this.off(evhandler);
|
this.off(evhandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RegistryMap = {[key: string]: any /* can't use Registry here since the template parameter is missing */ };
|
export type RegistryMap = {[key: string]: any /* can't use Registry here since the template parameter is missing */ };
|
||||||
|
@ -376,8 +376,9 @@ export function ReactEventHandler<ObjectClass = React.Component<any, any>, Event
|
||||||
if(!registry) throw "Event registry returned for an event object is invalid";
|
if(!registry) throw "Event registry returned for an event object is invalid";
|
||||||
registry.register_handler(this);
|
registry.register_handler(this);
|
||||||
|
|
||||||
if(typeof didMount === "function")
|
if(typeof didMount === "function") {
|
||||||
didMount.call(this, arguments);
|
didMount.call(this, arguments);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const willUnmount = constructor.prototype.componentWillUnmount;
|
const willUnmount = constructor.prototype.componentWillUnmount;
|
||||||
|
@ -390,165 +391,14 @@ export function ReactEventHandler<ObjectClass = React.Component<any, any>, Event
|
||||||
console.warn("Failed to unregister event handler: %o", error);
|
console.warn("Failed to unregister event handler: %o", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof willUnmount === "function")
|
if(typeof willUnmount === "function") {
|
||||||
willUnmount.call(this, arguments);
|
willUnmount.call(this, arguments);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace modal {
|
export namespace modal {
|
||||||
export type BotStatusType = "name" | "description" | "volume" | "country_code" | "channel_commander" | "priority_speaker";
|
|
||||||
export type PlaylistStatusType = "replay_mode" | "finished" | "delete_played" | "max_size" | "notify_song_change";
|
|
||||||
export interface music_manage {
|
|
||||||
show_container: { container: "settings" | "permissions"; };
|
|
||||||
|
|
||||||
/* setting relevant */
|
|
||||||
query_bot_status: {},
|
|
||||||
bot_status: {
|
|
||||||
status: "success" | "error";
|
|
||||||
error_msg?: string;
|
|
||||||
data?: {
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
volume: number,
|
|
||||||
|
|
||||||
country_code: string,
|
|
||||||
default_country_code: string,
|
|
||||||
|
|
||||||
channel_commander: boolean,
|
|
||||||
priority_speaker: boolean,
|
|
||||||
|
|
||||||
client_version: string,
|
|
||||||
client_platform: string,
|
|
||||||
|
|
||||||
uptime_mode: number,
|
|
||||||
bot_type: number
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set_bot_status: {
|
|
||||||
key: BotStatusType,
|
|
||||||
value: any
|
|
||||||
},
|
|
||||||
set_bot_status_result: {
|
|
||||||
key: BotStatusType,
|
|
||||||
status: "success" | "error" | "timeout",
|
|
||||||
error_msg?: string,
|
|
||||||
value?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
query_playlist_status: {},
|
|
||||||
playlist_status: {
|
|
||||||
status: "success" | "error",
|
|
||||||
error_msg?: string,
|
|
||||||
data?: {
|
|
||||||
replay_mode: number,
|
|
||||||
finished: boolean,
|
|
||||||
delete_played: boolean,
|
|
||||||
max_size: number,
|
|
||||||
notify_song_change: boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set_playlist_status: {
|
|
||||||
key: PlaylistStatusType,
|
|
||||||
value: any
|
|
||||||
},
|
|
||||||
set_playlist_status_result: {
|
|
||||||
key: PlaylistStatusType,
|
|
||||||
status: "success" | "error" | "timeout",
|
|
||||||
error_msg?: string,
|
|
||||||
value?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
/* permission relevant */
|
|
||||||
show_client_list: {},
|
|
||||||
hide_client_list: {},
|
|
||||||
|
|
||||||
filter_client_list: { filter: string | undefined },
|
|
||||||
|
|
||||||
"refresh_permissions": {},
|
|
||||||
|
|
||||||
query_special_clients: {},
|
|
||||||
special_client_list: {
|
|
||||||
status: "success" | "error" | "error-permission",
|
|
||||||
error_msg?: string,
|
|
||||||
clients?: {
|
|
||||||
name: string,
|
|
||||||
unique_id: string,
|
|
||||||
database_id: number
|
|
||||||
}[]
|
|
||||||
},
|
|
||||||
|
|
||||||
search_client: { text: string },
|
|
||||||
search_client_result: {
|
|
||||||
status: "error" | "timeout" | "empty" | "success",
|
|
||||||
error_msg?: string,
|
|
||||||
client?: {
|
|
||||||
name: string,
|
|
||||||
unique_id: string,
|
|
||||||
database_id: number
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* sets a client to set the permission for */
|
|
||||||
special_client_set: {
|
|
||||||
client?: {
|
|
||||||
name: string,
|
|
||||||
unique_id: string,
|
|
||||||
database_id: number
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"query_general_permissions": {},
|
|
||||||
"general_permissions": {
|
|
||||||
status: "error" | "timeout" | "success",
|
|
||||||
error_msg?: string,
|
|
||||||
permissions?: {[key: string]:number}
|
|
||||||
},
|
|
||||||
"set_general_permission_result": {
|
|
||||||
status: "error" | "success",
|
|
||||||
key: string,
|
|
||||||
value?: number,
|
|
||||||
error_msg?: string
|
|
||||||
},
|
|
||||||
"set_general_permission": { /* try to change a permission for the server */
|
|
||||||
key: string,
|
|
||||||
value: number
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
"query_client_permissions": { client_database_id: number },
|
|
||||||
"client_permissions": {
|
|
||||||
status: "error" | "timeout" | "success",
|
|
||||||
client_database_id: number,
|
|
||||||
error_msg?: string,
|
|
||||||
permissions?: {[key: string]:number}
|
|
||||||
},
|
|
||||||
"set_client_permission_result": {
|
|
||||||
status: "error" | "success",
|
|
||||||
client_database_id: number,
|
|
||||||
key: string,
|
|
||||||
value?: number,
|
|
||||||
error_msg?: string
|
|
||||||
},
|
|
||||||
"set_client_permission": { /* try to change a permission for the server */
|
|
||||||
client_database_id: number,
|
|
||||||
key: string,
|
|
||||||
value: number
|
|
||||||
},
|
|
||||||
|
|
||||||
"query_group_permissions": { permission_name: string },
|
|
||||||
"group_permissions": {
|
|
||||||
permission_name: string;
|
|
||||||
status: "error" | "timeout" | "success"
|
|
||||||
groups?: {
|
|
||||||
name: string,
|
|
||||||
value: number,
|
|
||||||
id: number
|
|
||||||
}[],
|
|
||||||
error_msg?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace settings {
|
export namespace settings {
|
||||||
export type ProfileInfo = {
|
export type ProfileInfo = {
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -707,74 +557,5 @@ export namespace modal {
|
||||||
|
|
||||||
"setup-forum-connection": {}
|
"setup-forum-connection": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MicrophoneSettings = "volume" | "vad-type" | "ppt-key" | "ppt-release-delay" | "ppt-release-delay-active" | "threshold-threshold";
|
|
||||||
export interface microphone {
|
|
||||||
"query-devices": { refresh_list: boolean },
|
|
||||||
"query-device-result": {
|
|
||||||
status: "success" | "error" | "timeout",
|
|
||||||
|
|
||||||
error?: string,
|
|
||||||
devices?: {
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
driver: string
|
|
||||||
}[]
|
|
||||||
active_device?: string;
|
|
||||||
},
|
|
||||||
|
|
||||||
"query-settings": {},
|
|
||||||
"query-settings-result": {
|
|
||||||
status: "success" | "error" | "timeout",
|
|
||||||
|
|
||||||
error?: string,
|
|
||||||
info?: {
|
|
||||||
volume: number,
|
|
||||||
vad_type: string,
|
|
||||||
|
|
||||||
vad_ppt: {
|
|
||||||
key: any, /* ppt.KeyDescriptor */
|
|
||||||
release_delay: number,
|
|
||||||
release_delay_active: boolean
|
|
||||||
},
|
|
||||||
vad_threshold: {
|
|
||||||
threshold: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"set-device": { device_id: string },
|
|
||||||
"set-device-result": {
|
|
||||||
device_id: string,
|
|
||||||
status: "success" | "error" | "timeout",
|
|
||||||
|
|
||||||
error?: string
|
|
||||||
},
|
|
||||||
|
|
||||||
"set-setting": {
|
|
||||||
setting: MicrophoneSettings;
|
|
||||||
value: any;
|
|
||||||
},
|
|
||||||
"set-setting-result": {
|
|
||||||
setting: MicrophoneSettings,
|
|
||||||
status: "success" | "error" | "timeout",
|
|
||||||
|
|
||||||
error?: string,
|
|
||||||
value?: any
|
|
||||||
},
|
|
||||||
|
|
||||||
"update-device-level": {
|
|
||||||
devices: {
|
|
||||||
device_id: string,
|
|
||||||
status: "success" | "error",
|
|
||||||
|
|
||||||
level?: number,
|
|
||||||
error?: string
|
|
||||||
}[]
|
|
||||||
},
|
|
||||||
|
|
||||||
"audio-initialized": {},
|
|
||||||
"deinitialize": {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -68,7 +68,7 @@ export let enabled_mapping = new Map<number, boolean>([
|
||||||
[LogCategory.VOICE, true],
|
[LogCategory.VOICE, true],
|
||||||
[LogCategory.AUDIO, true],
|
[LogCategory.AUDIO, true],
|
||||||
[LogCategory.CHAT, true],
|
[LogCategory.CHAT, true],
|
||||||
[LogCategory.I18N, true],
|
[LogCategory.I18N, false],
|
||||||
[LogCategory.IDENTITIES, true],
|
[LogCategory.IDENTITIES, true],
|
||||||
[LogCategory.IPC, true],
|
[LogCategory.IPC, true],
|
||||||
[LogCategory.STATISTICS, true],
|
[LogCategory.STATISTICS, true],
|
||||||
|
|
|
@ -23,7 +23,7 @@ class PopoutConversationRenderer extends AbstractModal {
|
||||||
noFirstMessageOverlay={this.userData.noFirstMessageOverlay} />;
|
noFirstMessageOverlay={this.userData.noFirstMessageOverlay} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return "Conversations";
|
return "Conversations";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,7 @@ class VolumeChange extends InternalModal {
|
||||||
return <VolumeChangeModal remote={false} clientName={this.client.clientNickName()} events={this.events} />;
|
return <VolumeChangeModal remote={false} clientName={this.client.clientNickName()} events={this.events} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return <Translatable>Change local volume</Translatable>;
|
return <Translatable>Change local volume</Translatable>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -284,7 +284,7 @@ class VolumeChangeBot extends InternalModal {
|
||||||
return <VolumeChangeModal remote={true} clientName={this.client.clientNickName()} maxVolume={this.maxValue} events={this.events} />;
|
return <VolumeChangeModal remote={true} clientName={this.client.clientNickName()} maxVolume={this.maxValue} events={this.events} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return <Translatable>Change remote volume</Translatable>;
|
return <Translatable>Change remote volume</Translatable>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,7 +272,7 @@ class ModalGroupCreate extends InternalModal {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return this.target === "server" ? <Translatable>Create a new server group</Translatable> : <Translatable>Create a new channel group</Translatable>;
|
return this.target === "server" ? <Translatable>Create a new server group</Translatable> : <Translatable>Create a new channel group</Translatable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,7 @@ class ModalGroupPermissionCopy extends InternalModal {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return <Translatable>Copy group permissions</Translatable>;
|
return <Translatable>Copy group permissions</Translatable>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1162,7 +1162,7 @@ export class ChannelEditModal extends InternalModal {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement {
|
renderTitle(): string | React.ReactElement {
|
||||||
if(this.isChannelCreate) {
|
if(this.isChannelCreate) {
|
||||||
return <Translatable key={"create"}>Create channel</Translatable>;
|
return <Translatable key={"create"}>Create channel</Translatable>;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {ConnectProperties, ConnectUiEvents, PropertyValidState} from "tc-shared/ui/modal/connect/Definitions";
|
import {
|
||||||
|
ConnectUiEvents,
|
||||||
|
ConnectUiVariables,
|
||||||
|
} from "tc-shared/ui/modal/connect/Definitions";
|
||||||
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
|
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
|
||||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||||
|
@ -17,7 +20,8 @@ import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import {parseServerAddress} from "tc-shared/tree/Server";
|
import {parseServerAddress} from "tc-shared/tree/Server";
|
||||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||||
import * as ipRegex from "ip-regex";
|
import * as ipRegex from "ip-regex";
|
||||||
import _ = require("lodash");
|
import {UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||||
|
import {createLocalUiVariables} from "tc-shared/ui/utils/LocalVariable";
|
||||||
|
|
||||||
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
|
const kRegexDomain = /^(localhost|((([a-zA-Z0-9_-]{0,63}\.){0,253})?[a-zA-Z0-9_-]{0,63}\.[a-zA-Z]{2,64}))$/i;
|
||||||
|
|
||||||
|
@ -37,19 +41,11 @@ export type ConnectParameters = {
|
||||||
defaultChannelPassword?: string,
|
defaultChannelPassword?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidityStates = {[T in keyof PropertyValidState]: boolean};
|
|
||||||
const kDefaultValidityStates: ValidityStates = {
|
|
||||||
address: false,
|
|
||||||
nickname: false,
|
|
||||||
password: false,
|
|
||||||
profile: false
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConnectController {
|
class ConnectController {
|
||||||
readonly uiEvents: Registry<ConnectUiEvents>;
|
readonly uiEvents: Registry<ConnectUiEvents>;
|
||||||
|
readonly uiVariables: UiVariableProvider<ConnectUiVariables>;
|
||||||
|
|
||||||
private readonly defaultAddress: string;
|
private readonly defaultAddress: string;
|
||||||
private readonly propertyProvider: {[K in keyof ConnectProperties]?: () => Promise<ConnectProperties[K]>} = {};
|
|
||||||
|
|
||||||
private historyShown: boolean;
|
private historyShown: boolean;
|
||||||
|
|
||||||
|
@ -59,32 +55,22 @@ class ConnectController {
|
||||||
private currentPasswordHashed: boolean;
|
private currentPasswordHashed: boolean;
|
||||||
private currentProfile: ConnectionProfile | undefined;
|
private currentProfile: ConnectionProfile | undefined;
|
||||||
|
|
||||||
private addressChanged: boolean;
|
|
||||||
private nicknameChanged: boolean;
|
|
||||||
|
|
||||||
private selectedHistoryId: number;
|
private selectedHistoryId: number;
|
||||||
private history: ConnectionHistoryEntry[];
|
private history: ConnectionHistoryEntry[];
|
||||||
|
|
||||||
private validStates: {[T in keyof PropertyValidState]: boolean} = {
|
private validateNickname: boolean;
|
||||||
address: false,
|
private validateAddress: boolean;
|
||||||
nickname: false,
|
|
||||||
password: false,
|
|
||||||
profile: false
|
|
||||||
};
|
|
||||||
|
|
||||||
private validateStates: {[T in keyof PropertyValidState]: boolean} = {
|
constructor(uiVariables: UiVariableProvider<ConnectUiVariables>) {
|
||||||
profile: false,
|
|
||||||
password: false,
|
|
||||||
nickname: false,
|
|
||||||
address: false
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.uiEvents = new Registry<ConnectUiEvents>();
|
this.uiEvents = new Registry<ConnectUiEvents>();
|
||||||
this.uiEvents.enableDebug("modal-connect");
|
this.uiEvents.enableDebug("modal-connect");
|
||||||
|
|
||||||
|
this.uiVariables = uiVariables;
|
||||||
this.history = undefined;
|
this.history = undefined;
|
||||||
|
|
||||||
|
this.validateNickname = false;
|
||||||
|
this.validateAddress = false;
|
||||||
|
|
||||||
this.defaultAddress = "ts.teaspeak.de";
|
this.defaultAddress = "ts.teaspeak.de";
|
||||||
this.historyShown = settings.getValue(Settings.KEY_CONNECT_SHOW_HISTORY);
|
this.historyShown = settings.getValue(Settings.KEY_CONNECT_SHOW_HISTORY);
|
||||||
|
|
||||||
|
@ -92,35 +78,117 @@ class ConnectController {
|
||||||
this.currentProfile = findConnectProfile(settings.getValue(Settings.KEY_CONNECT_PROFILE)) || defaultConnectProfile();
|
this.currentProfile = findConnectProfile(settings.getValue(Settings.KEY_CONNECT_PROFILE)) || defaultConnectProfile();
|
||||||
this.currentNickname = settings.getValue(Settings.KEY_CONNECT_USERNAME);
|
this.currentNickname = settings.getValue(Settings.KEY_CONNECT_USERNAME);
|
||||||
|
|
||||||
this.addressChanged = false;
|
this.uiEvents.on("action_delete_history", event => {
|
||||||
this.nicknameChanged = false;
|
connectionHistory.deleteConnectionAttempts(event.target, event.targetType).then(() => {
|
||||||
|
this.history = undefined;
|
||||||
|
this.uiVariables.sendVariable("history");
|
||||||
|
}).catch(error => {
|
||||||
|
logWarn(LogCategory.GENERAL, tr("Failed to delete connection attempts: %o"), error);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
this.propertyProvider["nickname"] = async () => ({
|
this.uiEvents.on("action_manage_profiles", () => {
|
||||||
|
/* TODO: This is more a hack. Proper solution is that the connection profiles fire events if they've been changed... */
|
||||||
|
const modal = spawnSettingsModal("identity-profiles");
|
||||||
|
modal.close_listener.push(() => {
|
||||||
|
this.uiVariables.sendVariable("profiles", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiEvents.on("action_select_history", event => this.setSelectedHistoryId(event.id));
|
||||||
|
|
||||||
|
this.uiEvents.on("action_connect", () => {
|
||||||
|
this.validateNickname = true;
|
||||||
|
this.validateAddress = true;
|
||||||
|
this.updateValidityStates();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiVariables.setVariableProvider("server_address", () => ({
|
||||||
|
currentAddress: this.currentAddress,
|
||||||
|
defaultAddress: this.defaultAddress
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.uiVariables.setVariableProvider("server_address_valid", () => {
|
||||||
|
if(this.validateAddress) {
|
||||||
|
const address = this.currentAddress || this.defaultAddress || "";
|
||||||
|
const parsedAddress = parseServerAddress(address);
|
||||||
|
|
||||||
|
if(parsedAddress) {
|
||||||
|
kRegexDomain.lastIndex = 0;
|
||||||
|
return kRegexDomain.test(parsedAddress.host) || ipRegex({ exact: true }).test(parsedAddress.host);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiVariables.setVariableEditor("server_address", newValue => {
|
||||||
|
if(this.currentAddress === newValue.currentAddress) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSelectedAddress(newValue.currentAddress, true, false);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiVariables.setVariableProvider("nickname", () => ({
|
||||||
defaultNickname: this.currentProfile?.connectUsername(),
|
defaultNickname: this.currentProfile?.connectUsername(),
|
||||||
currentNickname: this.currentNickname,
|
currentNickname: this.currentNickname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.uiVariables.setVariableProvider("nickname_valid", () => {
|
||||||
|
if(this.validateNickname) {
|
||||||
|
const nickname = this.currentNickname || this.currentProfile?.connectUsername() || "";
|
||||||
|
return nickname.length >= 3 && nickname.length <= 30;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.propertyProvider["address"] = async () => ({
|
this.uiVariables.setVariableEditor("nickname", newValue => {
|
||||||
currentAddress: this.currentAddress,
|
if(this.currentNickname === newValue.currentNickname) {
|
||||||
defaultAddress: this.defaultAddress,
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentNickname = newValue.currentNickname;
|
||||||
|
settings.setValue(Settings.KEY_CONNECT_USERNAME, this.currentNickname);
|
||||||
|
|
||||||
|
this.validateNickname = true;
|
||||||
|
this.uiVariables.sendVariable("nickname_valid");
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.propertyProvider["password"] = async () => this.currentPassword ? ({
|
this.uiVariables.setVariableProvider("password", () => ({
|
||||||
hashed: this.currentPasswordHashed,
|
password: this.currentPassword,
|
||||||
password: this.currentPassword
|
hashed: this.currentPasswordHashed
|
||||||
}) : undefined;
|
}));
|
||||||
|
|
||||||
this.propertyProvider["profiles"] = async () => ({
|
this.uiVariables.setVariableEditor("password", newValue => {
|
||||||
selected: this.currentProfile?.id,
|
if(this.currentPassword === newValue.password) {
|
||||||
profiles: availableConnectProfiles().map(profile => ({
|
return false;
|
||||||
id: profile.id,
|
}
|
||||||
valid: profile.valid(),
|
|
||||||
name: profile.profileName
|
this.currentPassword = newValue.password;
|
||||||
}))
|
this.currentPasswordHashed = newValue.hashed;
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.propertyProvider["historyShown"] = async () => this.historyShown;
|
this.uiVariables.setVariableProvider("profile_valid", () => !!this.currentProfile?.valid());
|
||||||
this.propertyProvider["history"] = async () => {
|
|
||||||
|
this.uiVariables.setVariableProvider("historyShown", () => this.historyShown);
|
||||||
|
this.uiVariables.setVariableEditor("historyShown", newValue => {
|
||||||
|
if(this.historyShown === newValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.historyShown = newValue;
|
||||||
|
settings.setValue(Settings.KEY_CONNECT_SHOW_HISTORY, newValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uiVariables.setVariableProvider("history",async () => {
|
||||||
if(!this.history) {
|
if(!this.history) {
|
||||||
this.history = await connectionHistory.lastConnectedServers(10);
|
this.history = await connectionHistory.lastConnectedServers(10);
|
||||||
}
|
}
|
||||||
|
@ -133,30 +201,14 @@ class ConnectController {
|
||||||
uniqueServerId: entry.serverUniqueId
|
uniqueServerId: entry.serverUniqueId
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
this.uiEvents.on("query_property", event => this.sendProperty(event.property));
|
this.uiVariables.setVariableProvider("history_entry", async customData => {
|
||||||
this.uiEvents.on("query_property_valid", event => this.uiEvents.fire_react("notify_property_valid", { property: event.property, value: this.validStates[event.property] }));
|
const info = await connectionHistory.queryServerInfo(customData.serverUniqueId);
|
||||||
this.uiEvents.on("query_history_connections", event => {
|
return {
|
||||||
connectionHistory.countConnectCount(event.target, event.targetType).catch(async error => {
|
|
||||||
logError(LogCategory.GENERAL, tr("Failed to query the connect count for %s (%s): %o"), event.target, event.targetType, error);
|
|
||||||
return -1;
|
|
||||||
}).then(count => {
|
|
||||||
this.uiEvents.fire_react("notify_history_connections", {
|
|
||||||
target: event.target,
|
|
||||||
targetType: event.targetType,
|
|
||||||
value: count
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.uiEvents.on("query_history_entry", event => {
|
|
||||||
connectionHistory.queryServerInfo(event.serverUniqueId).then(info => {
|
|
||||||
this.uiEvents.fire_react("notify_history_entry", {
|
|
||||||
serverUniqueId: event.serverUniqueId,
|
|
||||||
info: {
|
|
||||||
icon: {
|
icon: {
|
||||||
iconId: info.iconId,
|
iconId: info.iconId,
|
||||||
serverUniqueId: event.serverUniqueId,
|
serverUniqueId: customData.serverUniqueId,
|
||||||
handlerId: undefined
|
handlerId: undefined
|
||||||
},
|
},
|
||||||
name: info.name,
|
name: info.name,
|
||||||
|
@ -164,99 +216,52 @@ class ConnectController {
|
||||||
country: info.country,
|
country: info.country,
|
||||||
clients: info.clientsOnline,
|
clients: info.clientsOnline,
|
||||||
maxClients: info.clientsMax
|
maxClients: info.clientsMax
|
||||||
}
|
};
|
||||||
});
|
|
||||||
}).catch(async error => {
|
|
||||||
logError(LogCategory.GENERAL, tr("Failed to query the history server info for %s: %o"), event.serverUniqueId, error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uiEvents.on("action_toggle_history", event => {
|
this.uiVariables.setVariableProvider("history_connections", async customData => {
|
||||||
if(this.historyShown === event.enabled) {
|
return await connectionHistory.countConnectCount(customData.target, customData.targetType).catch(async error => {
|
||||||
return;
|
logError(LogCategory.GENERAL, tr("Failed to query the connect count for %s (%s): %o"), customData.target, customData.targetType, error);
|
||||||
}
|
return -1;
|
||||||
|
|
||||||
this.historyShown = event.enabled;
|
|
||||||
this.sendProperty("historyShown").then(undefined);
|
|
||||||
settings.setValue(Settings.KEY_CONNECT_SHOW_HISTORY, event.enabled);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.uiEvents.on("action_delete_history", event => {
|
|
||||||
connectionHistory.deleteConnectionAttempts(event.target, event.targetType).then(() => {
|
|
||||||
this.history = undefined;
|
|
||||||
this.sendProperty("history").then(undefined);
|
|
||||||
}).catch(error => {
|
|
||||||
logWarn(LogCategory.GENERAL, tr("Failed to delete connection attempts: %o"), error);
|
|
||||||
})
|
})
|
||||||
});
|
|
||||||
|
|
||||||
this.uiEvents.on("action_manage_profiles", () => {
|
this.uiVariables.setVariableProvider("profiles", () => ({
|
||||||
/* TODO: This is more a hack. Proper solution is that the connection profiles fire events if they've been changed... */
|
selected: this.currentProfile?.id,
|
||||||
const modal = spawnSettingsModal("identity-profiles");
|
profiles: availableConnectProfiles().map(profile => ({
|
||||||
modal.close_listener.push(() => {
|
id: profile.id,
|
||||||
this.sendProperty("profiles").then(undefined);
|
valid: profile.valid(),
|
||||||
});
|
name: profile.profileName
|
||||||
});
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
this.uiEvents.on("action_select_profile", event => {
|
this.uiVariables.setVariableEditor("profiles", newValue => {
|
||||||
const profile = findConnectProfile(event.id);
|
const profile = findConnectProfile(newValue.selected);
|
||||||
if(!profile) {
|
if(!profile) {
|
||||||
createErrorModal(tr("Invalid profile"), tr("Target connect profile is missing.")).open();
|
createErrorModal(tr("Invalid profile"), tr("Target connect profile is missing.")).open();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSelectedProfile(profile);
|
this.setSelectedProfile(profile);
|
||||||
|
return; /* No need to update anything. The ui should received the values needed already */
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uiEvents.on("action_set_address", event => this.setSelectedAddress(event.address, event.validate, event.updateUi));
|
|
||||||
|
|
||||||
this.uiEvents.on("action_set_nickname", event => {
|
|
||||||
if(this.currentNickname !== event.nickname) {
|
|
||||||
this.currentNickname = event.nickname;
|
|
||||||
settings.setValue(Settings.KEY_CONNECT_USERNAME, event.nickname);
|
|
||||||
|
|
||||||
if(event.updateUi) {
|
|
||||||
this.sendProperty("nickname").then(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validateStates["nickname"] = event.validate;
|
|
||||||
this.updateValidityStates();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.uiEvents.on("action_set_password", event => {
|
|
||||||
if(this.currentPassword === event.password) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPassword = event.password;
|
|
||||||
this.currentPasswordHashed = event.hashed;
|
|
||||||
if(event.updateUi) {
|
|
||||||
this.sendProperty("password").then(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validateStates["password"] = true;
|
|
||||||
this.updateValidityStates();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.uiEvents.on("action_select_history", event => this.setSelectedHistoryId(event.id));
|
|
||||||
|
|
||||||
this.uiEvents.on("action_connect", () => {
|
|
||||||
Object.keys(this.validateStates).forEach(key => this.validateStates[key] = true);
|
|
||||||
this.updateValidityStates();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateValidityStates();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
Object.keys(this.propertyProvider).forEach(key => delete this.propertyProvider[key]);
|
|
||||||
this.uiEvents.destroy();
|
this.uiEvents.destroy();
|
||||||
|
this.uiVariables.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
generateConnectParameters() : ConnectParameters | undefined {
|
generateConnectParameters() : ConnectParameters | undefined {
|
||||||
if(Object.keys(this.validStates).findIndex(key => this.validStates[key] === false) !== -1) {
|
if(!this.uiVariables.getVariableSync("nickname_valid", undefined, true)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.uiVariables.getVariableSync("server_address_valid", undefined, true)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.uiVariables.getVariableSync("profile_valid", undefined, true)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +284,7 @@ class ConnectController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedHistoryId = id;
|
this.selectedHistoryId = id;
|
||||||
this.sendProperty("history").then(undefined);
|
this.uiVariables.sendVariable("history");
|
||||||
|
|
||||||
const historyEntry = this.history?.find(entry => entry.id === id);
|
const historyEntry = this.history?.find(entry => entry.id === id);
|
||||||
if(!historyEntry) { return; }
|
if(!historyEntry) { return; }
|
||||||
|
@ -289,9 +294,9 @@ class ConnectController {
|
||||||
this.currentPassword = historyEntry.hashedPassword;
|
this.currentPassword = historyEntry.hashedPassword;
|
||||||
this.currentPasswordHashed = true;
|
this.currentPasswordHashed = true;
|
||||||
|
|
||||||
this.sendProperty("address").then(undefined);
|
this.uiVariables.sendVariable("server_address");
|
||||||
this.sendProperty("password").then(undefined);
|
this.uiVariables.sendVariable("password");
|
||||||
this.sendProperty("nickname").then(undefined);
|
this.uiVariables.sendVariable("nickname");
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedAddress(address: string | undefined, validate: boolean, updateUi: boolean) {
|
setSelectedAddress(address: string | undefined, validate: boolean, updateUi: boolean) {
|
||||||
|
@ -301,12 +306,12 @@ class ConnectController {
|
||||||
this.setSelectedHistoryId(-1);
|
this.setSelectedHistoryId(-1);
|
||||||
|
|
||||||
if(updateUi) {
|
if(updateUi) {
|
||||||
this.sendProperty("address").then(undefined);
|
this.uiVariables.sendVariable("server_address");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validateStates["address"] = validate;
|
this.validateAddress = true;
|
||||||
this.updateValidityStates();
|
this.uiVariables.sendVariable("server_address_valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedProfile(profile: ConnectionProfile | undefined) {
|
setSelectedProfile(profile: ConnectionProfile | undefined) {
|
||||||
|
@ -315,62 +320,19 @@ class ConnectController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentProfile = profile;
|
this.currentProfile = profile;
|
||||||
this.sendProperty("profiles").then(undefined);
|
this.uiVariables.sendVariable("profile_valid");
|
||||||
|
this.uiVariables.sendVariable("profiles");
|
||||||
settings.setValue(Settings.KEY_CONNECT_PROFILE, profile.id);
|
settings.setValue(Settings.KEY_CONNECT_PROFILE, profile.id);
|
||||||
|
|
||||||
/* Clear out the nickname on profile switch and use the default nickname */
|
/* Clear out the nickname on profile switch and use the default nickname */
|
||||||
this.uiEvents.fire("action_set_nickname", { nickname: undefined, validate: true, updateUi: true });
|
this.currentNickname = undefined;
|
||||||
|
this.uiVariables.sendVariable("nickname");
|
||||||
this.validateStates["profile"] = true;
|
|
||||||
this.updateValidityStates();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateValidityStates() {
|
private updateValidityStates() {
|
||||||
const newStates = Object.assign({}, kDefaultValidityStates);
|
this.uiVariables.sendVariable("server_address_valid");
|
||||||
if(this.validateStates["nickname"]) {
|
this.uiVariables.sendVariable("nickname_valid");
|
||||||
const nickname = this.currentNickname || this.currentProfile?.connectUsername() || "";
|
this.uiVariables.sendVariable("profile_valid");
|
||||||
newStates["nickname"] = nickname.length >= 3 && nickname.length <= 30;
|
|
||||||
} else {
|
|
||||||
newStates["nickname"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.validateStates["address"]) {
|
|
||||||
const address = this.currentAddress || this.defaultAddress || "";
|
|
||||||
const parsedAddress = parseServerAddress(address);
|
|
||||||
|
|
||||||
if(parsedAddress) {
|
|
||||||
kRegexDomain.lastIndex = 0;
|
|
||||||
newStates["address"] = kRegexDomain.test(parsedAddress.host) || ipRegex({ exact: true }).test(parsedAddress.host);
|
|
||||||
} else {
|
|
||||||
newStates["address"] = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newStates["address"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
newStates["profile"] = !!this.currentProfile?.valid();
|
|
||||||
newStates["password"] = true;
|
|
||||||
|
|
||||||
for(const key of Object.keys(newStates)) {
|
|
||||||
if(_.isEqual(this.validStates[key], newStates[key])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validStates[key] = newStates[key];
|
|
||||||
this.uiEvents.fire_react("notify_property_valid", { property: key as any, value: this.validStates[key] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendProperty(property: keyof ConnectProperties) {
|
|
||||||
if(!this.propertyProvider[property]) {
|
|
||||||
logWarn(LogCategory.GENERAL, tr("Tried to send a property where we don't have a provider for"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uiEvents.fire_react("notify_property", {
|
|
||||||
property: property,
|
|
||||||
value: await this.propertyProvider[property]()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +344,8 @@ export type ConnectModalOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnConnectModalNew(options: ConnectModalOptions) {
|
export function spawnConnectModalNew(options: ConnectModalOptions) {
|
||||||
const controller = new ConnectController();
|
const [ variableProvider, variableConsumer ] = createLocalUiVariables<ConnectUiVariables>();
|
||||||
|
const controller = new ConnectController(variableProvider);
|
||||||
|
|
||||||
if(typeof options.selectedAddress === "string") {
|
if(typeof options.selectedAddress === "string") {
|
||||||
controller.setSelectedAddress(options.selectedAddress, false, true);
|
controller.setSelectedAddress(options.selectedAddress, false, true);
|
||||||
|
@ -392,7 +355,7 @@ export function spawnConnectModalNew(options: ConnectModalOptions) {
|
||||||
controller.setSelectedProfile(options.selectedProfile);
|
controller.setSelectedProfile(options.selectedProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = spawnReactModal(ConnectModal, controller.uiEvents, options.connectInANewTab || false);
|
const modal = spawnReactModal(ConnectModal, controller.uiEvents, variableConsumer, options.connectInANewTab || false);
|
||||||
modal.show();
|
modal.show();
|
||||||
|
|
||||||
modal.events.one("destroy", () => {
|
modal.events.one("destroy", () => {
|
||||||
|
|
|
@ -23,81 +23,47 @@ export type ConnectHistoryServerInfo = {
|
||||||
maxClients: number | -1
|
maxClients: number | -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectProperties {
|
export interface ConnectUiVariables {
|
||||||
address: {
|
"server_address": {
|
||||||
currentAddress: string,
|
currentAddress: string,
|
||||||
defaultAddress: string,
|
defaultAddress?: string,
|
||||||
},
|
},
|
||||||
nickname: {
|
"server_address_valid": boolean,
|
||||||
|
|
||||||
|
"nickname": {
|
||||||
currentNickname: string | undefined,
|
currentNickname: string | undefined,
|
||||||
defaultNickname: string | undefined,
|
defaultNickname?: string,
|
||||||
},
|
},
|
||||||
password: {
|
"nickname_valid": boolean,
|
||||||
|
|
||||||
|
"password": {
|
||||||
password: string,
|
password: string,
|
||||||
hashed: boolean
|
hashed: boolean
|
||||||
} | undefined,
|
} | undefined,
|
||||||
profiles: {
|
|
||||||
profiles: ConnectProfileEntry[],
|
"profiles": {
|
||||||
|
profiles?: ConnectProfileEntry[],
|
||||||
selected: string
|
selected: string
|
||||||
},
|
},
|
||||||
historyShown: boolean,
|
"profile_valid": boolean,
|
||||||
history: {
|
|
||||||
|
"historyShown": boolean,
|
||||||
|
"history": {
|
||||||
|
__readonly?,
|
||||||
history: ConnectHistoryEntry[],
|
history: ConnectHistoryEntry[],
|
||||||
selected: number | -1,
|
selected: number | -1,
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
export interface PropertyValidState {
|
"history_entry": ConnectHistoryServerInfo,
|
||||||
address: boolean,
|
"history_connections": number
|
||||||
nickname: boolean,
|
|
||||||
password: boolean,
|
|
||||||
profile: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IAccess<I, T extends keyof I> = {
|
|
||||||
property: T,
|
|
||||||
value: I[T]
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ConnectUiEvents {
|
export interface ConnectUiEvents {
|
||||||
action_manage_profiles: {},
|
action_manage_profiles: {},
|
||||||
action_select_profile: { id: string },
|
|
||||||
action_select_history: { id: number },
|
action_select_history: { id: number },
|
||||||
action_connect: { newTab: boolean },
|
action_connect: { newTab: boolean },
|
||||||
action_toggle_history: { enabled: boolean }
|
|
||||||
action_delete_history: {
|
action_delete_history: {
|
||||||
target: string,
|
target: string,
|
||||||
targetType: "address" | "server-unique-id"
|
targetType: "address" | "server-unique-id"
|
||||||
},
|
},
|
||||||
action_set_nickname: { nickname: string, validate: boolean, updateUi: boolean },
|
|
||||||
action_set_address: { address: string, validate: boolean, updateUi: boolean },
|
|
||||||
action_set_password: { password: string, hashed: boolean, updateUi: boolean },
|
|
||||||
|
|
||||||
query_property: {
|
|
||||||
property: keyof ConnectProperties
|
|
||||||
},
|
|
||||||
query_property_valid: {
|
|
||||||
property: keyof PropertyValidState
|
|
||||||
},
|
|
||||||
|
|
||||||
notify_property: IAccess<ConnectProperties, keyof ConnectProperties>,
|
|
||||||
notify_property_valid: IAccess<PropertyValidState, keyof PropertyValidState>,
|
|
||||||
|
|
||||||
query_history_entry: {
|
|
||||||
serverUniqueId: string
|
|
||||||
},
|
|
||||||
query_history_connections: {
|
|
||||||
target: string,
|
|
||||||
targetType: "address" | "server-unique-id"
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_history_entry: {
|
|
||||||
serverUniqueId: string,
|
|
||||||
info: ConnectHistoryServerInfo
|
|
||||||
},
|
|
||||||
notify_history_connections: {
|
|
||||||
target: string,
|
|
||||||
targetType: "address" | "server-unique-id",
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,12 +1,9 @@
|
||||||
import {
|
import {
|
||||||
ConnectHistoryEntry,
|
ConnectHistoryEntry,
|
||||||
ConnectHistoryServerInfo,
|
ConnectUiEvents, ConnectUiVariables,
|
||||||
ConnectProperties,
|
|
||||||
ConnectUiEvents,
|
|
||||||
PropertyValidState
|
|
||||||
} from "tc-shared/ui/modal/connect/Definitions";
|
} from "tc-shared/ui/modal/connect/Definitions";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useContext, useState} from "react";
|
import {useContext} from "react";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
@ -19,123 +16,100 @@ import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import * as i18n from "../../../i18n/country";
|
import * as i18n from "../../../i18n/country";
|
||||||
import {getIconManager} from "tc-shared/file/Icons";
|
import {getIconManager} from "tc-shared/file/Icons";
|
||||||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
|
||||||
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
||||||
|
const VariablesContext = React.createContext<UiVariableConsumer<ConnectUiVariables>>(undefined);
|
||||||
|
|
||||||
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
const cssStyle = require("./Renderer.scss");
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue: V) : [ConnectProperties[T] | V, (value: ConnectProperties[T]) => void] {
|
const InputServerAddress = React.memo(() => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const [ value, setValue ] = useState<ConnectProperties[T] | V>(() => {
|
|
||||||
events.fire("query_property", { property: key });
|
|
||||||
return defaultValue;
|
|
||||||
});
|
|
||||||
events.reactUse("notify_property", event => event.property === key && setValue(event.value as any));
|
|
||||||
|
|
||||||
return [value, setValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
function usePropertyValid<T extends keyof PropertyValidState>(key: T, defaultValue: PropertyValidState[T]) : PropertyValidState[T] {
|
|
||||||
const events = useContext(EventContext);
|
|
||||||
const [ value, setValue ] = useState<PropertyValidState[T]>(() => {
|
|
||||||
events.fire("query_property_valid", { property: key });
|
|
||||||
return defaultValue;
|
|
||||||
});
|
|
||||||
events.reactUse("notify_property_valid", event => event.property === key && setValue(event.value as any));
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const InputServerAddress = () => {
|
|
||||||
const events = useContext(EventContext);
|
|
||||||
const [address, setAddress] = useProperty("address", undefined);
|
|
||||||
const valid = usePropertyValid("address", true);
|
|
||||||
const newTab = useContext(ConnectDefaultNewTabContext);
|
const newTab = useContext(ConnectDefaultNewTabContext);
|
||||||
|
|
||||||
|
const variables = useContext(VariablesContext);
|
||||||
|
const address = variables.useVariable("server_address");
|
||||||
|
const addressValid = variables.useVariable("server_address_valid");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledFlatInputField
|
<ControlledFlatInputField
|
||||||
value={address?.currentAddress || ""}
|
value={address.localValue?.currentAddress || ""}
|
||||||
placeholder={address?.defaultAddress || tr("Please enter a address")}
|
placeholder={address.remoteValue?.defaultAddress || tr("Please enter a address")}
|
||||||
|
|
||||||
className={cssStyle.inputAddress}
|
className={cssStyle.inputAddress}
|
||||||
|
|
||||||
label={<Translatable>Server address</Translatable>}
|
label={<Translatable>Server address</Translatable>}
|
||||||
labelType={"static"}
|
labelType={"static"}
|
||||||
|
|
||||||
invalid={valid ? undefined : <Translatable>Please enter a valid server address</Translatable>}
|
invalid={!!addressValid.localValue ? undefined : <Translatable>Please enter a valid server address</Translatable>}
|
||||||
|
editable={address.status === "loaded"}
|
||||||
|
|
||||||
onInput={value => {
|
onInput={value => {
|
||||||
setAddress({ currentAddress: value, defaultAddress: address.defaultAddress });
|
address.setValue({ currentAddress: value }, true);
|
||||||
events.fire("action_set_address", { address: value, validate: true, updateUi: false });
|
addressValid.setValue(true, true);
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
events.fire("action_set_address", { address: address?.currentAddress, validate: true, updateUi: true });
|
|
||||||
}}
|
}}
|
||||||
|
onBlur={() => address.setValue({ currentAddress: address.localValue?.currentAddress })}
|
||||||
onEnter={() => {
|
onEnter={() => {
|
||||||
/* Setting the address just to ensure */
|
/* Setting the address just to ensure */
|
||||||
events.fire("action_set_address", { address: address?.currentAddress, validate: true, updateUi: true });
|
address.setValue({ currentAddress: address.localValue?.currentAddress });
|
||||||
events.fire("action_connect", { newTab });
|
events.fire("action_connect", { newTab });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
});
|
||||||
|
|
||||||
const InputServerPassword = () => {
|
const InputServerPassword = () => {
|
||||||
const events = useContext(EventContext);
|
const variables = useContext(VariablesContext);
|
||||||
const [password, setPassword] = useProperty("password", undefined);
|
const password = variables.useVariable("password");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatInputField
|
<FlatInputField
|
||||||
className={cssStyle.inputPassword}
|
className={cssStyle.inputPassword}
|
||||||
value={!password?.hashed ? password?.password || "" : ""}
|
value={!password.localValue?.hashed ? password.localValue?.password || "" : ""}
|
||||||
placeholder={password?.hashed ? tr("Password Hidden") : null}
|
placeholder={password.localValue?.hashed ? tr("Password Hidden") : null}
|
||||||
type={"password"}
|
type={"password"}
|
||||||
label={<Translatable>Server password</Translatable>}
|
label={<Translatable>Server password</Translatable>}
|
||||||
labelType={password?.hashed ? "static" : "floating"}
|
labelType={password.localValue?.hashed ? "static" : "floating"}
|
||||||
onInput={value => {
|
onInput={value => password.setValue({ password: value, hashed: false }, true)}
|
||||||
setPassword({ password: value, hashed: false });
|
onBlur={() => password.setValue(password.localValue)}
|
||||||
events.fire("action_set_password", { password: value, hashed: false, updateUi: false });
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
if(password) {
|
|
||||||
events.fire("action_set_password", { password: password.password, hashed: password.hashed, updateUi: true });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputNickname = () => {
|
const InputNickname = () => {
|
||||||
const events = useContext(EventContext);
|
const variables = useContext(VariablesContext);
|
||||||
const [nickname, setNickname] = useProperty("nickname", undefined);
|
|
||||||
const valid = usePropertyValid("nickname", true);
|
const nickname = variables.useVariable("nickname");
|
||||||
|
const valid = variables.useVariable("nickname_valid");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledFlatInputField
|
<ControlledFlatInputField
|
||||||
className={cssStyle.inputNickname}
|
className={cssStyle.inputNickname}
|
||||||
value={nickname?.currentNickname || ""}
|
value={nickname.localValue?.currentNickname || ""}
|
||||||
placeholder={nickname ? nickname.defaultNickname ? nickname.defaultNickname : tr("Please enter a nickname") : tr("loading...")}
|
placeholder={nickname.remoteValue ? nickname.remoteValue.defaultNickname ? nickname.remoteValue.defaultNickname : tr("Please enter a nickname") : tr("loading...")}
|
||||||
label={<Translatable>Nickname</Translatable>}
|
label={<Translatable>Nickname</Translatable>}
|
||||||
labelType={"static"}
|
labelType={"static"}
|
||||||
invalid={valid ? undefined : <Translatable>Nickname too short or too long</Translatable>}
|
invalid={!!valid.localValue ? undefined : <Translatable>Nickname too short or too long</Translatable>}
|
||||||
onInput={value => {
|
onInput={value => {
|
||||||
setNickname({ currentNickname: value, defaultNickname: nickname.defaultNickname });
|
nickname.setValue({ currentNickname: value }, true);
|
||||||
events.fire("action_set_nickname", { nickname: value, validate: true, updateUi: false });
|
valid.setValue(true, true);
|
||||||
}}
|
}}
|
||||||
onBlur={() => events.fire("action_set_nickname", { nickname: nickname?.currentNickname, validate: true, updateUi: true })}
|
onBlur={() => nickname.setValue({ currentNickname: nickname.localValue?.currentNickname })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputProfile = () => {
|
const InputProfile = () => {
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const [profiles] = useProperty("profiles", undefined);
|
const variables = useContext(VariablesContext);
|
||||||
const selectedProfile = profiles?.profiles.find(profile => profile.id === profiles?.selected);
|
const profiles = variables.useVariable("profiles");
|
||||||
|
const selectedProfile = profiles.remoteValue?.profiles.find(profile => profile.id === profiles.remoteValue?.selected);
|
||||||
|
|
||||||
let invalidMarker;
|
let invalidMarker;
|
||||||
if(profiles) {
|
if(profiles) {
|
||||||
if(!profiles.selected) {
|
if(!profiles.remoteValue?.selected) {
|
||||||
/* We have to select a profile. */
|
/* We have to select a profile. */
|
||||||
/* TODO: Only show if we've tried to press connect */
|
/* TODO: Only show if we've tried to press connect */
|
||||||
//invalidMarker = <Translatable key={"no-profile"}>Please select a profile</Translatable>;
|
//invalidMarker = <Translatable key={"no-profile"}>Please select a profile</Translatable>;
|
||||||
|
@ -150,19 +124,23 @@ const InputProfile = () => {
|
||||||
<div className={cssStyle.inputProfile}>
|
<div className={cssStyle.inputProfile}>
|
||||||
<ControlledSelect
|
<ControlledSelect
|
||||||
className={cssStyle.input}
|
className={cssStyle.input}
|
||||||
value={selectedProfile ? selectedProfile.id : profiles?.selected ? "invalid" : profiles ? "no-selected" : "loading"}
|
value={selectedProfile ? selectedProfile.id : profiles.remoteValue?.selected ? "invalid" : profiles ? "no-selected" : "loading"}
|
||||||
type={"flat"}
|
type={"flat"}
|
||||||
label={<Translatable>Connect profile</Translatable>}
|
label={<Translatable>Connect profile</Translatable>}
|
||||||
invalid={invalidMarker}
|
invalid={invalidMarker}
|
||||||
invalidClassName={cssStyle.invalidFeedback}
|
invalidClassName={cssStyle.invalidFeedback}
|
||||||
onChange={event => events.fire("action_select_profile", { id: event.target.value })}
|
onChange={event => profiles.setValue({ selected: event.target.value })}
|
||||||
>
|
>
|
||||||
<option key={"no-selected"} value={"no-selected"} style={{ display: "none" }}>{useTr("please select")}</option>
|
<option value={"no-selected"} style={{ display: "none" }}>{useTr("please select")}</option>
|
||||||
<option key={"invalid"} value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
|
<option value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
|
||||||
<option key={"loading"} value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
|
<option value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
|
||||||
{profiles?.profiles.map(profile => (
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
profiles.remoteValue?.profiles.map(profile => (
|
||||||
<option key={"profile-" + profile.id} value={profile.id}>{profile.name}</option>
|
<option key={"profile-" + profile.id} value={profile.id}>{profile.name}</option>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
</ControlledSelect>
|
</ControlledSelect>
|
||||||
<Button className={cssStyle.button} type={"small"} color={"none"} onClick={() => events.fire("action_manage_profiles")}>
|
<Button className={cssStyle.button} type={"small"} color={"none"} onClick={() => events.fire("action_manage_profiles")}>
|
||||||
<Translatable>Profiles</Translatable>
|
<Translatable>Profiles</Translatable>
|
||||||
|
@ -174,6 +152,7 @@ const InputProfile = () => {
|
||||||
const ConnectContainer = () => (
|
const ConnectContainer = () => (
|
||||||
<div className={cssStyle.connectContainer}>
|
<div className={cssStyle.connectContainer}>
|
||||||
<div className={cssStyle.row}>
|
<div className={cssStyle.row}>
|
||||||
|
{/* <InputServerAddress /> */}
|
||||||
<InputServerAddress />
|
<InputServerAddress />
|
||||||
<InputServerPassword />
|
<InputServerPassword />
|
||||||
</div>
|
</div>
|
||||||
|
@ -185,11 +164,11 @@ const ConnectContainer = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const ButtonToggleHistory = () => {
|
const ButtonToggleHistory = () => {
|
||||||
const [state] = useProperty("historyShown", false);
|
const variables = useContext(VariablesContext);
|
||||||
const events = useContext(EventContext);
|
const historyShown = variables.useVariable("historyShown");
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
if(state) {
|
if(historyShown.localValue) {
|
||||||
body = (
|
body = (
|
||||||
<React.Fragment key={"hide"}>
|
<React.Fragment key={"hide"}>
|
||||||
<div className={cssStyle.containerText}><Translatable>Hide connect history</Translatable></div>
|
<div className={cssStyle.containerText}><Translatable>Hide connect history</Translatable></div>
|
||||||
|
@ -209,7 +188,7 @@ const ButtonToggleHistory = () => {
|
||||||
className={cssStyle.buttonShowHistory + " " + cssStyle.button}
|
className={cssStyle.buttonShowHistory + " " + cssStyle.button}
|
||||||
type={"small"}
|
type={"small"}
|
||||||
color={"none"}
|
color={"none"}
|
||||||
onClick={() => events.fire("action_toggle_history", { enabled: !state })}
|
onClick={() => historyShown.setValue(!historyShown.localValue)}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -281,34 +260,23 @@ const HistoryTableEntryConnectCount = React.memo((props: { entry: ConnectHistory
|
||||||
const targetType = props.entry.uniqueServerId === kUnknownHistoryServerUniqueId ? "address" : "server-unique-id";
|
const targetType = props.entry.uniqueServerId === kUnknownHistoryServerUniqueId ? "address" : "server-unique-id";
|
||||||
const target = targetType === "address" ? props.entry.targetAddress : props.entry.uniqueServerId;
|
const target = targetType === "address" ? props.entry.targetAddress : props.entry.uniqueServerId;
|
||||||
|
|
||||||
const events = useContext(EventContext);
|
const { value } = useContext(VariablesContext).useVariableReadOnly("history_connections", {
|
||||||
const [ amount, setAmount ] = useState(() => {
|
|
||||||
events.fire("query_history_connections", {
|
|
||||||
target,
|
target,
|
||||||
targetType
|
targetType
|
||||||
});
|
}, -1);
|
||||||
return -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
events.reactUse("notify_history_connections", event => event.targetType === targetType && event.target === target && setAmount(event.value));
|
if(value >= 0) {
|
||||||
|
return <React.Fragment key={"set"}>{value}</React.Fragment>;
|
||||||
if(amount >= 0) {
|
|
||||||
return <React.Fragment key={"set"}>{amount}</React.Fragment>;
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const HistoryTableEntry = React.memo((props: { entry: ConnectHistoryEntry, selected: boolean }) => {
|
const HistoryTableEntry = React.memo((props: { entry: ConnectHistoryEntry, selected: boolean }) => {
|
||||||
const connectNewTab = useContext(ConnectDefaultNewTabContext);
|
|
||||||
const events = useContext(EventContext);
|
const events = useContext(EventContext);
|
||||||
const [ info, setInfo ] = useState<ConnectHistoryServerInfo>(() => {
|
const connectNewTab = useContext(ConnectDefaultNewTabContext);
|
||||||
if(props.entry.uniqueServerId !== kUnknownHistoryServerUniqueId) {
|
const variables = useContext(VariablesContext);
|
||||||
events.fire("query_history_entry", { serverUniqueId: props.entry.uniqueServerId });
|
const { value: info } = variables.useVariableReadOnly("history_entry", { serverUniqueId: props.entry.uniqueServerId }, undefined);
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
events.reactUse("notify_history_entry", event => event.serverUniqueId === props.entry.uniqueServerId && setInfo(event.info));
|
|
||||||
|
|
||||||
const icon = getIconManager().resolveIcon(info ? info.icon.iconId : 0, info?.icon.serverUniqueId, info?.icon.handlerId);
|
const icon = getIconManager().resolveIcon(info ? info.icon.iconId : 0, info?.icon.serverUniqueId, info?.icon.handlerId);
|
||||||
|
|
||||||
|
@ -364,9 +332,9 @@ const HistoryTableEntry = React.memo((props: { entry: ConnectHistoryEntry, selec
|
||||||
});
|
});
|
||||||
|
|
||||||
const HistoryTable = () => {
|
const HistoryTable = () => {
|
||||||
const [history] = useProperty("history", undefined);
|
const { value: history } = useContext(VariablesContext).useVariableReadOnly("history", undefined, undefined);
|
||||||
let body;
|
|
||||||
|
|
||||||
|
let body;
|
||||||
if(history) {
|
if(history) {
|
||||||
if(history.history.length > 0) {
|
if(history.history.length > 0) {
|
||||||
body = history.history.map(entry => <HistoryTableEntry entry={entry} key={"entry-" + entry.id} selected={entry.id === history.selected} />);
|
body = history.history.map(entry => <HistoryTableEntry entry={entry} key={"entry-" + entry.id} selected={entry.id === history.selected} />);
|
||||||
|
@ -411,7 +379,8 @@ const HistoryTable = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const HistoryContainer = () => {
|
const HistoryContainer = () => {
|
||||||
const [historyShown] = useProperty("historyShown", false);
|
const variables = useContext(VariablesContext);
|
||||||
|
const { value: historyShown } = variables.useVariableReadOnly("historyShown", undefined, false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={joinClassList(cssStyle.historyContainer, historyShown && cssStyle.shown)}>
|
<div className={joinClassList(cssStyle.historyContainer, historyShown && cssStyle.shown)}>
|
||||||
|
@ -422,11 +391,13 @@ const HistoryContainer = () => {
|
||||||
|
|
||||||
export class ConnectModal extends InternalModal {
|
export class ConnectModal extends InternalModal {
|
||||||
private readonly events: Registry<ConnectUiEvents>;
|
private readonly events: Registry<ConnectUiEvents>;
|
||||||
|
private readonly variables: UiVariableConsumer<ConnectUiVariables>;
|
||||||
private readonly connectNewTabByDefault: boolean;
|
private readonly connectNewTabByDefault: boolean;
|
||||||
|
|
||||||
constructor(events: Registry<ConnectUiEvents>, connectNewTabByDefault: boolean) {
|
constructor(events: Registry<ConnectUiEvents>, variables: UiVariableConsumer<ConnectUiVariables>, connectNewTabByDefault: boolean) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.variables = variables;
|
||||||
this.events = events;
|
this.events = events;
|
||||||
this.connectNewTabByDefault = connectNewTabByDefault;
|
this.connectNewTabByDefault = connectNewTabByDefault;
|
||||||
}
|
}
|
||||||
|
@ -434,6 +405,7 @@ export class ConnectModal extends InternalModal {
|
||||||
renderBody(): React.ReactElement {
|
renderBody(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<EventContext.Provider value={this.events}>
|
<EventContext.Provider value={this.events}>
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
||||||
<div className={cssStyle.container}>
|
<div className={cssStyle.container}>
|
||||||
<ConnectContainer />
|
<ConnectContainer />
|
||||||
|
@ -441,11 +413,12 @@ export class ConnectModal extends InternalModal {
|
||||||
<HistoryContainer />
|
<HistoryContainer />
|
||||||
</div>
|
</div>
|
||||||
</ConnectDefaultNewTabContext.Provider>
|
</ConnectDefaultNewTabContext.Provider>
|
||||||
|
</VariablesContext.Provider>
|
||||||
</EventContext.Provider>
|
</EventContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement {
|
renderTitle(): string | React.ReactElement {
|
||||||
return <Translatable>Connect to a server</Translatable>;
|
return <Translatable>Connect to a server</Translatable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -420,7 +420,7 @@ class PopoutConversationUI extends AbstractModal {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return "CSS Variable editor";
|
return "CSS Variable editor";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export function spawnEchoTestModal(connection: ConnectionHandler) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
renderTitle(): string | React.ReactElement<Translatable> {
|
||||||
return <Translatable>Voice echo test</Translatable>;
|
return <Translatable>Voice echo test</Translatable>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -189,7 +189,7 @@ export class ModalGlobalSettingsEditor extends InternalModal {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
renderTitle(): string | React.ReactElement<Translatable> {
|
||||||
return <Translatable>Global settings registry</Translatable>;
|
return <Translatable>Global settings registry</Translatable>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -343,7 +343,7 @@ class PermissionEditorModal extends InternalModal {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): React.ReactElement<Translatable> {
|
renderTitle(): React.ReactElement<Translatable> {
|
||||||
return <Translatable>Server permissions</Translatable>;
|
return <Translatable>Server permissions</Translatable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ class FileTransferModal extends InternalModal {
|
||||||
this.transferInfoEvents.fire("notify_destroy");
|
this.transferInfoEvents.fire("notify_destroy");
|
||||||
}
|
}
|
||||||
|
|
||||||
title() {
|
renderTitle() {
|
||||||
return <Translatable>File Browser</Translatable>;
|
return <Translatable>File Browser</Translatable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -855,7 +855,7 @@ export class ModalVideoSource extends InternalModal {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
renderTitle(): string | React.ReactElement<Translatable> {
|
||||||
return <Translatable>Start video Broadcasting</Translatable>;
|
return <Translatable>Start video Broadcasting</Translatable>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function spawnUpdatedModal(changes: { changesUI?: ChangeLog, changesClien
|
||||||
return <WhatsNew changesUI={changes.changesUI} changesClient={changes.changesClient}/>;
|
return <WhatsNew changesUI={changes.changesUI} changesClient={changes.changesClient}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
renderTitle(): string | React.ReactElement<Translatable> {
|
||||||
return <Translatable>We've updated the client for you</Translatable>;
|
return <Translatable>We've updated the client for you</Translatable>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,7 +39,7 @@ export abstract class AbstractModal {
|
||||||
protected constructor() {}
|
protected constructor() {}
|
||||||
|
|
||||||
abstract renderBody() : ReactElement;
|
abstract renderBody() : ReactElement;
|
||||||
abstract title() : string | React.ReactElement;
|
abstract renderTitle() : string | React.ReactElement;
|
||||||
|
|
||||||
/* only valid for the "inline" modals */
|
/* only valid for the "inline" modals */
|
||||||
type() : ModalType { return "none"; }
|
type() : ModalType { return "none"; }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ChannelMessage, IPCChannel} from "../../../ipc/BrowserIPC";
|
import {ChannelMessage, IPCChannel} from "../../../ipc/BrowserIPC";
|
||||||
import {EventReceiver, RegistryMap} from "../../../events";
|
import {EventSender, RegistryMap} from "../../../events";
|
||||||
|
|
||||||
export interface PopoutIPCMessage {
|
export interface PopoutIPCMessage {
|
||||||
"hello-popout": { version: string },
|
"hello-popout": { version: string },
|
||||||
|
@ -41,7 +41,7 @@ export abstract class EventControllerBase<Type extends "controller" | "popout">
|
||||||
protected ipcRemoteId: string;
|
protected ipcRemoteId: string;
|
||||||
|
|
||||||
protected localRegistries: RegistryMap;
|
protected localRegistries: RegistryMap;
|
||||||
private localEventReceiver: {[key: string]: EventReceiver};
|
private localEventReceiver: {[key: string]: EventSender};
|
||||||
|
|
||||||
private omitEventType: string = undefined;
|
private omitEventType: string = undefined;
|
||||||
private omitEventData: any;
|
private omitEventData: any;
|
||||||
|
@ -61,7 +61,7 @@ export abstract class EventControllerBase<Type extends "controller" | "popout">
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createEventReceiver(key: string) : EventReceiver {
|
private createEventReceiver(key: string) : EventSender {
|
||||||
let refThis = this;
|
let refThis = this;
|
||||||
|
|
||||||
const fireEvent = (type: "react" | "later", eventType: any, data?: any[], callback?: () => void) => {
|
const fireEvent = (type: "react" | "later", eventType: any, data?: any[], callback?: () => void) => {
|
||||||
|
@ -80,7 +80,7 @@ export abstract class EventControllerBase<Type extends "controller" | "popout">
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return new class implements EventReceiver {
|
return new class implements EventSender {
|
||||||
fire<T extends keyof {}>(eventType: T, data?: any[T], overrideTypeKey?: boolean) {
|
fire<T extends keyof {}>(eventType: T, data?: any[T], overrideTypeKey?: boolean) {
|
||||||
if(refThis.omitEventType === eventType && refThis.omitEventData === data) {
|
if(refThis.omitEventType === eventType && refThis.omitEventData === data) {
|
||||||
refThis.omitEventType = undefined;
|
refThis.omitEventType = undefined;
|
||||||
|
|
|
@ -25,7 +25,7 @@ class TitleRenderer {
|
||||||
|
|
||||||
this.modalInstance = instance;
|
this.modalInstance = instance;
|
||||||
if(this.modalInstance) {
|
if(this.modalInstance) {
|
||||||
ReactDOM.render(<>{this.modalInstance.title()}</>, this.htmlContainer);
|
ReactDOM.render(<>{this.modalInstance.renderTitle()}</>, this.htmlContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class ChannelTreeModal extends AbstractModal {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): React.ReactElement {
|
renderTitle(): React.ReactElement {
|
||||||
return <TitleRenderer events={this.eventsUI} />;
|
return <TitleRenderer events={this.eventsUI} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {UiVariableConsumer, UiVariableMap, UiVariableProvider} from "tc-shared/ui/utils/Variable";
|
||||||
|
|
||||||
|
class LocalUiVariableProvider<Variables extends UiVariableMap> extends UiVariableProvider<Variables> {
|
||||||
|
private consumer: LocalUiVariableConsumer<Variables>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
|
this.consumer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConsumer(consumer: LocalUiVariableConsumer<Variables>) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected doSendVariable(variable: string, customData: any, value: any) {
|
||||||
|
this.consumer.notifyRemoteVariable(variable, customData, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public doEditVariable(variable: string, customData: any, newValue: any): Promise<void> | void {
|
||||||
|
return super.doEditVariable(variable, customData, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalUiVariableConsumer<Variables extends UiVariableMap> extends UiVariableConsumer<Variables> {
|
||||||
|
private provider: LocalUiVariableProvider<Variables>;
|
||||||
|
|
||||||
|
constructor(provider: LocalUiVariableProvider<Variables>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
super.destroy();
|
||||||
|
this.provider = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected doEditVariable(variable: string, customData: any, value: any): Promise<void> | void {
|
||||||
|
return this.provider.doEditVariable(variable, customData, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected doRequestVariable(variable: string, customData: any) {
|
||||||
|
return this.provider.sendVariable(variable, customData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public notifyRemoteVariable(variable: string, customData: any, value: any) {
|
||||||
|
super.notifyRemoteVariable(variable, customData, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLocalUiVariables<Variables extends UiVariableMap>() : [UiVariableProvider<Variables>, UiVariableConsumer<Variables>] {
|
||||||
|
const provider = new LocalUiVariableProvider();
|
||||||
|
const consumer = new LocalUiVariableConsumer(provider);
|
||||||
|
provider.setConsumer(consumer);
|
||||||
|
return [provider, consumer];
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import * as _ from "lodash";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* To deliver optimized performance, we only promisify the values we need.
|
||||||
|
* If done so, we have the change to instantaneously load local values without needing
|
||||||
|
* to rerender the ui.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type UiVariable = Transferable | undefined | null | number | string | object;
|
||||||
|
export type UiVariableMap = { [key: string]: any }; //UiVariable | Readonly<UiVariable>
|
||||||
|
|
||||||
|
type UiVariableEditor<Variables extends UiVariableMap, T extends keyof Variables> = Variables[T] extends { __readonly } ?
|
||||||
|
never :
|
||||||
|
(newValue: Variables[T], customData: any) => Variables[T] | void | boolean;
|
||||||
|
|
||||||
|
type UiVariableEditorPromise<Variables extends UiVariableMap, T extends keyof Variables> = Variables[T] extends { __readonly } ?
|
||||||
|
never :
|
||||||
|
(newValue: Variables[T], customData: any) => Promise<Variables[T] | void | boolean>;
|
||||||
|
|
||||||
|
export abstract class UiVariableProvider<Variables extends UiVariableMap> {
|
||||||
|
private variableProvider: {[key: string]: (customData: any) => any | Promise<any>} = {};
|
||||||
|
private variableEditor: {[key: string]: (newValue, customData: any) => any | Promise<any>} = {};
|
||||||
|
|
||||||
|
protected constructor() { }
|
||||||
|
|
||||||
|
destroy() { }
|
||||||
|
|
||||||
|
setVariableProvider<T extends keyof Variables>(variable: T, provider: (customData: any) => Variables[T] | Promise<Variables[T]>) {
|
||||||
|
this.variableProvider[variable as any] = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariableEditor<T extends keyof Variables>(variable: T, editor: UiVariableEditor<Variables, T>) {
|
||||||
|
this.variableEditor[variable as any] = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariableEditorAsync<T extends keyof Variables>(variable: T, editor: UiVariableEditorPromise<Variables, T>) {
|
||||||
|
this.variableEditor[variable as any] = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send/update a variable
|
||||||
|
* @param variable The target variable to send.
|
||||||
|
* @param customData
|
||||||
|
* @param forceSend If `true` the variable will be send event though it hasn't changed.
|
||||||
|
*/
|
||||||
|
sendVariable<T extends keyof Variables>(variable: T, customData?: any, forceSend?: boolean) : void | Promise<void> {
|
||||||
|
const providers = this.variableProvider[variable as any];
|
||||||
|
if(!providers) {
|
||||||
|
throw tr("missing provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = providers(customData);
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
return result
|
||||||
|
.then(result => this.doSendVariable(variable as any, customData, result))
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.doSendVariable(variable as any, customData, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVariable<T extends keyof Variables>(variable: T, customData?: any, ignoreCache?: boolean) : Promise<Variables[T]> {
|
||||||
|
const providers = this.variableProvider[variable as any];
|
||||||
|
if(!providers) {
|
||||||
|
throw tr("missing provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = providers(customData);
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
return await result;
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariableSync<T extends keyof Variables>(variable: T, customData?: any, ignoreCache?: boolean) : Variables[T] {
|
||||||
|
const providers = this.variableProvider[variable as any];
|
||||||
|
if(!providers) {
|
||||||
|
throw tr("missing provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = providers(customData);
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
throw tr("tried to get an async variable synchronous");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected resolveVariable(variable: string, customData: any): Promise<any> | any {
|
||||||
|
const providers = this.variableProvider[variable];
|
||||||
|
if(!providers) {
|
||||||
|
throw tr("missing provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return providers(customData);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected doEditVariable(variable: string, customData: any, newValue: any) : Promise<void> | void {
|
||||||
|
const editor = this.variableEditor[variable];
|
||||||
|
if(!editor) {
|
||||||
|
throw tr("variable is read only");
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditResult = result => {
|
||||||
|
if(typeof result === "undefined") {
|
||||||
|
/* change succeeded, no need to notify any variable since the consumer already has the newest value */
|
||||||
|
} else if(result === true || result === false) {
|
||||||
|
/* The new variable has been accepted/rejected and the variable should be updated on the remote side. */
|
||||||
|
/* TODO: Use cached value if the result is `false` */
|
||||||
|
this.sendVariable(variable, customData, true);
|
||||||
|
} else {
|
||||||
|
/* The new value hasn't been accepted. Instead a new value has been returned. */
|
||||||
|
this.doSendVariable(variable, customData, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditError = error => {
|
||||||
|
console.error("Failed to change variable %s: %o", variable, error);
|
||||||
|
this.sendVariable(variable, customData, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result = editor(newValue, customData);
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
return result.then(handleEditResult).catch(handleEditError);
|
||||||
|
} else {
|
||||||
|
handleEditResult(result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleEditError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract doSendVariable(variable: string, customData: any, value: any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UiVariableStatus<Variables extends UiVariableMap, T extends keyof Variables> = ({
|
||||||
|
status: "loading",
|
||||||
|
|
||||||
|
localValue: undefined,
|
||||||
|
remoteValue: undefined
|
||||||
|
} | {
|
||||||
|
status: "loaded" | "applying",
|
||||||
|
|
||||||
|
localValue: Omit<Variables[T], "__readonly">,
|
||||||
|
remoteValue: Omit<Variables[T], "__readonly">,
|
||||||
|
}) & (Variables[T] extends { __readonly } ? {} : { setValue: (newValue: Variables[T], localOnly?: boolean) => void });
|
||||||
|
|
||||||
|
export type UiReadOnlyVariableStatus<Variables extends UiVariableMap, T extends keyof Variables, DefaultValue = undefined> = {
|
||||||
|
status: "loading",
|
||||||
|
value: DefaultValue,
|
||||||
|
} | {
|
||||||
|
status: "loaded" | "applying",
|
||||||
|
value: Omit<Variables[T], "__readonly">,
|
||||||
|
};
|
||||||
|
|
||||||
|
type UiVariableCacheEntry = {
|
||||||
|
useCount: number,
|
||||||
|
customData: any | undefined,
|
||||||
|
currentValue: any | undefined,
|
||||||
|
status: "loading" | "loaded" | "applying",
|
||||||
|
updateListener: ((forceSetLocalVariable: boolean) => void)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let staticRevisionId = 0;
|
||||||
|
/**
|
||||||
|
* @returns An array containing variable information `[ value, setValue(newValue, localOnly), remoteValue ]`
|
||||||
|
*/
|
||||||
|
export abstract class UiVariableConsumer<Variables extends UiVariableMap> {
|
||||||
|
private variableCache: {[key: string]: UiVariableCacheEntry[]} = {};
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.variableCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
useVariable<T extends keyof Variables>(
|
||||||
|
variable: T,
|
||||||
|
customData?: any
|
||||||
|
) : UiVariableStatus<Variables, T> {
|
||||||
|
let cacheEntry = this.variableCache[variable as string]?.find(variable => _.isEqual(variable.customData, customData));
|
||||||
|
if(!cacheEntry) {
|
||||||
|
this.variableCache[variable as string] = this.variableCache[variable as string] || [];
|
||||||
|
this.variableCache[variable as string].push(cacheEntry = {
|
||||||
|
customData,
|
||||||
|
currentValue: undefined,
|
||||||
|
status: "loading",
|
||||||
|
useCount: 0,
|
||||||
|
updateListener: []
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Might already call notifyRemoteVariable */
|
||||||
|
this.doRequestVariable(variable as string, customData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ localValue, setLocalValue ] = useState(() => {
|
||||||
|
/* Variable constructor */
|
||||||
|
cacheEntry.useCount++;
|
||||||
|
return cacheEntry.currentValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [, setRemoteVersion ] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/* Initial rendered */
|
||||||
|
if(cacheEntry.status === "loaded" && !_.isEqual(localValue, cacheEntry.currentValue)) {
|
||||||
|
/* Update value to the, now, up 2 date value*/
|
||||||
|
setLocalValue(cacheEntry.currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener;
|
||||||
|
cacheEntry.updateListener.push(listener = forceUpdateLocalVariable => {
|
||||||
|
if(forceUpdateLocalVariable) {
|
||||||
|
setLocalValue(cacheEntry.currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We can't just increment the old one by one since this update listener may fires twice before rendering */
|
||||||
|
setRemoteVersion(++staticRevisionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cacheEntry.updateListener.remove(listener);
|
||||||
|
|
||||||
|
/* Variable destructor */
|
||||||
|
if(--cacheEntry.useCount === 0) {
|
||||||
|
const cache = this.variableCache[variable as string];
|
||||||
|
if(!cache) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.remove(cacheEntry);
|
||||||
|
if(cache.length === 0) {
|
||||||
|
delete this.variableCache[variable as string];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
if(cacheEntry.status === "loading") {
|
||||||
|
return {
|
||||||
|
status: "loading",
|
||||||
|
localValue: localValue,
|
||||||
|
remoteValue: undefined,
|
||||||
|
setValue: () => {}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: cacheEntry.status,
|
||||||
|
localValue: localValue,
|
||||||
|
remoteValue: cacheEntry.currentValue,
|
||||||
|
setValue: (newValue, localOnly) => {
|
||||||
|
if(!localOnly && !_.isEqual(cacheEntry.currentValue, newValue)) {
|
||||||
|
const editingFinished = (succeeded: boolean) => {
|
||||||
|
if(cacheEntry.status !== "applying") {
|
||||||
|
/* A new value has already been emitted */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = newValue;
|
||||||
|
if(!succeeded) {
|
||||||
|
value = cacheEntry.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheEntry.status = "loaded";
|
||||||
|
cacheEntry.currentValue = value;
|
||||||
|
cacheEntry.updateListener.forEach(callback => callback(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
cacheEntry.status = "applying";
|
||||||
|
const result = this.doEditVariable(variable as string, customData, newValue);
|
||||||
|
if(result instanceof Promise) {
|
||||||
|
result.then(() => editingFinished(true)).catch(async error => {
|
||||||
|
console.error("Failed to change variable %s: %o", variable, error);
|
||||||
|
editingFinished(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* State has changed, enforce a rerender */
|
||||||
|
cacheEntry.updateListener.forEach(callback => callback(false));
|
||||||
|
} else {
|
||||||
|
editingFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!_.isEqual(newValue, localValue)) {
|
||||||
|
setLocalValue(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useVariableReadOnly<T extends keyof Variables, DefaultValue>(
|
||||||
|
variable: T,
|
||||||
|
customData?: any,
|
||||||
|
defaultValue?: DefaultValue
|
||||||
|
) : UiReadOnlyVariableStatus<Variables, T, DefaultValue> {
|
||||||
|
/* TODO: Use a simplified logic here */
|
||||||
|
const v = this.useVariable(variable, customData);
|
||||||
|
if(v.status === "loading") {
|
||||||
|
return {
|
||||||
|
status: "loading",
|
||||||
|
value: defaultValue
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: v.status,
|
||||||
|
value: v.remoteValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected notifyRemoteVariable(variable: string, customData: any | undefined, value: any) {
|
||||||
|
let cacheEntry = this.variableCache[variable]?.find(variable => _.isEqual(variable.customData, customData));
|
||||||
|
if(!cacheEntry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheEntry.status = "loaded";
|
||||||
|
cacheEntry.currentValue = value;
|
||||||
|
cacheEntry.updateListener.forEach(callback => callback(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract doRequestVariable(variable: string, customData: any | undefined);
|
||||||
|
protected abstract doEditVariable(variable: string, customData: any | undefined, value: any) : Promise<void> | void;
|
||||||
|
}
|
|
@ -498,7 +498,7 @@ class ModalVideoPopout extends AbstractModal {
|
||||||
this.events = registryMap["default"] as any;
|
this.events = registryMap["default"] as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
renderTitle(): string | React.ReactElement<Translatable> {
|
||||||
return <TitleRenderer events={this.events} />;
|
return <TitleRenderer events={this.events} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue