diff --git a/shared/css/load-css.tsx b/shared/css/load-css.tsx index c520ea68..ed63717b 100644 --- a/shared/css/load-css.tsx +++ b/shared/css/load-css.tsx @@ -21,7 +21,6 @@ import "./static/modal-query.scss" import "./static/modal-server.scss" import "./static/modal-musicmanage.scss" import "./static/modal-serverinfobandwidth.scss" -import "./static/modal-serverinfo.scss" import "./static/modal-settings.scss" import "./static/overlay-image-preview.scss" import "./static/color-variables.scss" diff --git a/shared/css/static/color-variables.scss b/shared/css/static/color-variables.scss index a64a0d9a..6ead398c 100644 --- a/shared/css/static/color-variables.scss +++ b/shared/css/static/color-variables.scss @@ -42,4 +42,14 @@ html:root { /* The host banner */ --hostbanner-background: #2e2e2e; + + /* Server Info */ + --serverinfo-background: #2f2f35; + --serverinfo-hostbanner-background: #26222a; + + --serverinfo-group-border: #1f2122; + --serverinfo-group-background: #28292b; + + --serverinfo-key: #557edc; + --serverinfo-value: #d6d6d7; } \ No newline at end of file diff --git a/shared/css/static/modal-serverinfo.scss b/shared/css/static/modal-serverinfo.scss deleted file mode 100644 index da83e98b..00000000 --- a/shared/css/static/modal-serverinfo.scss +++ /dev/null @@ -1,267 +0,0 @@ -@import "mixin"; - -html:root { - --serverinfo-background: #2f2f35; - --serverinfo-hostbanner-background: #26222a; - - --serverinfo-group-border: #1f2122; - --serverinfo-group-background: #28292b; - - --serverinfo-key: #557edc; - --serverinfo-value: #d6d6d7; -} - -:global { - .modal-body.modal-server-info { - padding: 0!important; - width: 55em; - - display: flex!important; - flex-direction: column!important; - justify-content: flex-start!important; - - background-color: var(--serverinfo-background); - - .container-tooltip { - flex-shrink: 0; - flex-grow: 0; - - position: relative; - width: 1.6em; - margin-left: .5em; - margin-right: .5em; - font-size: .9em; - - display: flex; - flex-direction: column; - justify-content: center; - - img { - height: 1em; - width: 1em; - - align-self: center; - font-size: 1.2em; - } - - .tooltip { - display: none; - } - } - - .container-top { - flex-grow: 0; - flex-shrink: 0; - - max-height: 9em; - //width: 30em; /* set a default width where we have to grow/shrink */ - - display: flex; - flex-direction: column; - justify-content: stretch; - - .container-hostbanner { - border: none; - border-radius: 0; - background-color: var(--serverinfo-hostbanner-background); - } - - &.hidden { - display: none; - } - } - - .container-body { - flex-shrink: 1; - min-height: 12em; /* 10em + 2 * 1em margin */ - - overflow-y: auto; - - @include chat-scrollbar-vertical(); - } - - .group { - flex-grow: 0; - flex-shrink: 0; - - margin: 1em; - padding: .5em; - - border-radius: .2em; - border: 1px solid var(--serverinfo-group-border); - - background-color: var(--serverinfo-group-background); - - display: flex; - flex-direction: row; - justify-content: stretch; - - height: 10em; - max-height: 10em; - - .container-image { - flex-grow: 0; - flex-shrink: 0; - - max-width: 15em; - max-height: 9em; /* minus one padding */ - - display: flex; - flex-direction: column; - justify-content: center; - - img { - object-fit: contain; - max-height: 100%; - max-width: 100%; - } - - margin-right: 2em; - @include transition(.25s ease-in-out); - } - - .container-properties { - flex-shrink: 1; - flex-grow: 1; - - min-width: 20em; - - display: flex; - flex-direction: column; - justify-content: flex-start; - - height: inherit; - - .row { - flex-grow: 0; - flex-shrink: 0; - - height: 1.8em; - - display: flex; - flex-direction: row; - justify-content: flex-start; - - .key { - flex-shrink: 0; - flex-grow: 0; - - color: var(--serverinfo-key); - text-transform: uppercase; - align-self: center; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - width: 15em; - } - - .value { - color: var(--serverinfo-value); - align-self: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - .country { - display: inline-block; - margin-right: .25em; - } - - &.server-version { - display: flex; - flex-direction: row; - justify-content: flex-start; - - a { - flex-shrink: 1; - min-width: 0; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - } - - .container-network { - display: flex; - flex-direction: row; - justify-content: center; - - .container-button { - margin-right: 1em; - - flex-shrink: 1e8; - min-width: 5em; - - display: flex; - flex-direction: column; - justify-content: flex-end; - - button { - height: 2.5em; - width: 12em; - - max-width: 100%; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .right { - flex-grow: 1; - flex-shrink: 1; - min-width: 10em; - } - } - } - - &.reverse { - flex-direction: row-reverse; - text-align: right; - - .container-image { - margin-right: 0; - margin-left: 2em; - } - - .container-properties { - .row { - flex-direction: row-reverse; - } - } - } - } - - .container-buttons { - margin: 1em; - - flex-grow: 0; - flex-shrink: 0; - - display: flex; - flex-direction: row; - justify-content: space-between; - - button { - min-width: 8em; - } - } - } -} - -@media all and (max-width: 50em) { - :global { - .modal-body.modal-server-info { - .container-image { - margin: 0!important; - max-width: 0!important; - } - } - } -} \ No newline at end of file diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index 95ffa989..913631f6 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -2,118 +2,28 @@ import {ChannelTree} from "./ChannelTree"; import {Settings, settings} from "../settings"; import * as contextmenu from "../ui/elements/ContextMenu"; import * as log from "../log"; -import {LogCategory, logInfo, LogType} from "../log"; +import {LogCategory, logError, logInfo, LogType} from "../log"; import {Sound} from "../audio/Sounds"; -import {openServerInfo} from "../ui/modal/ModalServerInfo"; import {createServerModal} from "../ui/modal/ModalServerEdit"; import {spawnIconSelect} from "../ui/modal/ModalIconSelect"; import {spawnAvatarList} from "../ui/modal/ModalAvatarList"; import {Registry} from "../events"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; -import { tr } from "tc-shared/i18n/localize"; +import {tr} from "tc-shared/i18n/localize"; import {spawnInviteGenerator} from "tc-shared/ui/modal/invite/Controller"; +import {HostBannerInfo, HostBannerInfoMode} from "tc-shared/ui/frames/HostBannerDefinitions"; +import {spawnServerInfoNew} from "tc-shared/ui/modal/server-info/Controller"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; +import {ErrorCode} from "tc-shared/connection/ErrorCode"; +import { + kServerConnectionInfoFields, + ServerConnectionInfo, + ServerConnectionInfoResult, + ServerProperties +} from "tc-shared/tree/ServerDefinitions"; -export class ServerProperties { - virtualserver_host: string = ""; - virtualserver_port: number = 0; - - virtualserver_name: string = ""; - virtualserver_name_phonetic: string = ""; - virtualserver_icon_id: number = 0; - virtualserver_version: string = "unknown"; - virtualserver_platform: string = "unknown"; - virtualserver_unique_identifier: string = ""; - - virtualserver_clientsonline: number = 0; - virtualserver_queryclientsonline: number = 0; - virtualserver_channelsonline: number = 0; - virtualserver_uptime: number = 0; - virtualserver_created: number = 0; - virtualserver_maxclients: number = 0; - virtualserver_reserved_slots: number = 0; - - virtualserver_password: string = ""; - virtualserver_flag_password: boolean = false; - - virtualserver_ask_for_privilegekey: boolean = false; - - virtualserver_welcomemessage: string = ""; - - virtualserver_hostmessage: string = ""; - virtualserver_hostmessage_mode: number = 0; - - virtualserver_hostbanner_url: string = ""; - virtualserver_hostbanner_gfx_url: string = ""; - virtualserver_hostbanner_gfx_interval: number = 0; - virtualserver_hostbanner_mode: number = 0; - - virtualserver_hostbutton_tooltip: string = ""; - virtualserver_hostbutton_url: string = ""; - virtualserver_hostbutton_gfx_url: string = ""; - - virtualserver_codec_encryption_mode: number = 0; - - virtualserver_default_music_group: number = 0; - virtualserver_default_server_group: number = 0; - virtualserver_default_channel_group: number = 0; - virtualserver_default_channel_admin_group: number = 0; - - //Special requested properties - virtualserver_default_client_description: string = ""; - virtualserver_default_channel_description: string = ""; - virtualserver_default_channel_topic: string = ""; - - virtualserver_antiflood_points_tick_reduce: number = 0; - virtualserver_antiflood_points_needed_command_block: number = 0; - virtualserver_antiflood_points_needed_ip_block: number = 0; - - virtualserver_country_code: string = "XX"; - - virtualserver_complain_autoban_count: number = 0; - virtualserver_complain_autoban_time: number = 0; - virtualserver_complain_remove_time: number = 0; - - virtualserver_needed_identity_security_level: number = 8; - virtualserver_weblist_enabled: boolean = false; - virtualserver_min_clients_in_channel_before_forced_silence: number = 0; - virtualserver_channel_temp_delete_delay_default: number = 60; - virtualserver_priority_speaker_dimm_modificator: number = -18; - - virtualserver_max_upload_total_bandwidth: number = 0; - virtualserver_upload_quota: number = 0; - virtualserver_max_download_total_bandwidth: number = 0; - virtualserver_download_quota: number = 0; - - virtualserver_month_bytes_downloaded: number = 0; - virtualserver_month_bytes_uploaded: number = 0; - virtualserver_total_bytes_downloaded: number = 0; - virtualserver_total_bytes_uploaded: number = 0; -} - -export interface ServerConnectionInfo { - connection_filetransfer_bandwidth_sent: number; - connection_filetransfer_bandwidth_received: number; - - connection_filetransfer_bytes_sent_total: number; - connection_filetransfer_bytes_received_total: number; - - connection_filetransfer_bytes_sent_month: number; - connection_filetransfer_bytes_received_month: number; - - connection_packets_sent_total: number; - connection_bytes_sent_total: number; - connection_packets_received_total: number; - connection_bytes_received_total: number; - - connection_bandwidth_sent_last_second_total: number; - connection_bandwidth_sent_last_minute_total: number; - connection_bandwidth_received_last_second_total: number; - connection_bandwidth_received_last_minute_total: number; - - connection_connected_time: number; - connection_packetloss_total: number; - connection_ping: number; -} +/* TODO: Rework all imports */ +export * from "./ServerDefinitions"; export interface ServerAddress { host: string; @@ -163,7 +73,8 @@ export interface ServerEvents extends ChannelTreeEntryEvents { notify_properties_updated: { updated_properties: Partial; server_properties: ServerProperties - } + }, + notify_host_banner_updated: {}, } export class ServerEntry extends ChannelTreeEntry { @@ -177,6 +88,10 @@ export class ServerEntry extends ChannelTreeEntry { private info_request_promise_resolve: any = undefined; private info_request_promise_reject: any = undefined; + private requestInfoPromise: Promise; + private requestInfoPromiseTimestamp: number; + + /* TODO: Remove this? */ private _info_connection_promise: Promise; private _info_connection_promise_timestamp: number; private _info_connection_promise_resolve: any; @@ -195,6 +110,17 @@ export class ServerEntry extends ChannelTreeEntry { this.channelTree = tree; this.remote_address = Object.assign({}, address); /* copy the address because it might get changed due to the DNS resolve */ this.properties.virtualserver_name = name; + + this.events.on("notify_properties_updated", event => { + if( + "virtualserver_hostbanner_url" in event.updated_properties || + "virtualserver_hostbanner_mode" in event.updated_properties || + "virtualserver_hostbanner_gfx_url" in event.updated_properties || + "virtualserver_hostbanner_gfx_interval" in event.updated_properties + ) { + this.events.fire("notify_host_banner_updated"); + } + }); } destroy() { @@ -212,9 +138,7 @@ export class ServerEntry extends ChannelTreeEntry { { type: contextmenu.MenuEntryType.ENTRY, name: tr("Show server info"), - callback: () => { - openServerInfo(this); - }, + callback: () => spawnServerInfoNew(this.channelTree.client), icon_class: "client-about" }, { type: contextmenu.MenuEntryType.ENTRY, @@ -350,11 +274,13 @@ export class ServerEntry extends ChannelTreeEntry { /* max 1s ago, so we could update every second */ request_connection_info() : Promise { - if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise) + if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise) { return this._info_connection_promise; + } - if(this._info_connection_promise_reject) + if(this._info_connection_promise_reject) { this._info_connection_promise_resolve("timeout"); + } let _local_reject; /* to ensure we're using the right resolve! */ this._info_connection_promise = new Promise((resolve, reject) => { @@ -364,10 +290,61 @@ export class ServerEntry extends ChannelTreeEntry { }); this._info_connection_promise_timestamp = Date.now(); - this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo", {}, {process_result: false}).catch(error => _local_reject(error)); + this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo", {}, { process_result: false }).catch(error => _local_reject(error)); return this._info_connection_promise; } + requestConnectionInfo() : Promise { + if(this.requestInfoPromise && Date.now() - 1000 < this.requestInfoPromiseTimestamp) { + return this.requestInfoPromise; + } + + this.requestInfoPromiseTimestamp = Date.now(); + return this.requestInfoPromise = this.doRequestConnectionInfo(); + } + + private async doRequestConnectionInfo() : Promise { + const connection = this.channelTree.client.serverConnection; + + let result: ServerConnectionInfoResult = { status: "error", message: "missing notify" }; + const handlerUnregister = connection.command_handler_boss().register_explicit_handler("notifyserverconnectioninfo", command => { + const payload = command.arguments[0]; + + const info = {} as any; + for(const key of Object.keys(kServerConnectionInfoFields)) { + if(!(key in payload)) { + result = { status: "error", message: "missing key " + key }; + return; + } + + info[key] = parseFloat(payload[key]); + } + + result = { status: "success", resultCached: false, result: info }; + return false; + }); + + try { + await connection.send_command("serverrequestconnectioninfo", {}, { process_result: false }); + } catch (error) { + if(error instanceof CommandResult) { + if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) { + result = { status: "no-permission", failedPermission: this.channelTree.client.permissions.getFailedPermission(error) }; + } else { + result = { status: "error", message: error.formattedMessage() }; + } + } else if(typeof error === "string") { + result = { status: "error", message: error }; + } else { + logError(LogCategory.NETWORKING, tr("Failed to request the server connection info: %o"), error); + result = { status: "error", message: tr("lookup the console") }; + } + } finally { + handlerUnregister(); + } + return result; + } + set_connection_info(info: ServerConnectionInfo) { if(!this._info_connection_promise_resolve) return; @@ -392,4 +369,36 @@ export class ServerEntry extends ChannelTreeEntry { this._info_connection_promise_resolve = undefined; this._info_connection_promise_timestamp = undefined; } + + generateHostBannerInfo() : HostBannerInfo { + if(!this.properties.virtualserver_hostbanner_gfx_url) { + return { status: "none" }; + } + + let mode: HostBannerInfoMode; + switch (this.properties.virtualserver_hostbanner_mode) { + case 0: + mode = "original"; + break; + + case 1: + mode = "resize"; + break; + + case 2: + default: + mode = "resize-ratio"; + break; + } + + return { + status: "set", + + linkUrl: this.properties.virtualserver_hostbanner_url, + mode: mode, + + imageUrl: this.properties.virtualserver_hostbanner_gfx_url, + updateInterval: this.properties.virtualserver_hostbanner_gfx_interval, + }; + } } \ No newline at end of file diff --git a/shared/js/tree/ServerDefinitions.ts b/shared/js/tree/ServerDefinitions.ts new file mode 100644 index 00000000..c56053db --- /dev/null +++ b/shared/js/tree/ServerDefinitions.ts @@ -0,0 +1,138 @@ +export class ServerProperties { + virtualserver_host: string = ""; + virtualserver_port: number = 0; + + virtualserver_name: string = ""; + virtualserver_name_phonetic: string = ""; + virtualserver_icon_id: number = 0; + virtualserver_version: string = "unknown"; + virtualserver_platform: string = "unknown"; + virtualserver_unique_identifier: string = ""; + + virtualserver_clientsonline: number = 0; + virtualserver_queryclientsonline: number = 0; + virtualserver_channelsonline: number = 0; + virtualserver_uptime: number = 0; + virtualserver_created: number = 0; + virtualserver_maxclients: number = 0; + virtualserver_reserved_slots: number = 0; + + virtualserver_password: string = ""; + virtualserver_flag_password: boolean = false; + + virtualserver_ask_for_privilegekey: boolean = false; + + virtualserver_welcomemessage: string = ""; + + virtualserver_hostmessage: string = ""; + virtualserver_hostmessage_mode: number = 0; + + virtualserver_hostbanner_url: string = ""; + virtualserver_hostbanner_gfx_url: string = ""; + virtualserver_hostbanner_gfx_interval: number = 0; + virtualserver_hostbanner_mode: number = 0; + + virtualserver_hostbutton_tooltip: string = ""; + virtualserver_hostbutton_url: string = ""; + virtualserver_hostbutton_gfx_url: string = ""; + + virtualserver_codec_encryption_mode: number = 0; + + virtualserver_default_music_group: number = 0; + virtualserver_default_server_group: number = 0; + virtualserver_default_channel_group: number = 0; + virtualserver_default_channel_admin_group: number = 0; + + //Special requested properties + virtualserver_default_client_description: string = ""; + virtualserver_default_channel_description: string = ""; + virtualserver_default_channel_topic: string = ""; + + virtualserver_antiflood_points_tick_reduce: number = 0; + virtualserver_antiflood_points_needed_command_block: number = 0; + virtualserver_antiflood_points_needed_ip_block: number = 0; + + virtualserver_country_code: string = "XX"; + + virtualserver_complain_autoban_count: number = 0; + virtualserver_complain_autoban_time: number = 0; + virtualserver_complain_remove_time: number = 0; + + virtualserver_needed_identity_security_level: number = 8; + virtualserver_weblist_enabled: boolean = false; + virtualserver_min_clients_in_channel_before_forced_silence: number = 0; + virtualserver_channel_temp_delete_delay_default: number = 60; + virtualserver_priority_speaker_dimm_modificator: number = -18; + + virtualserver_max_upload_total_bandwidth: number = 0; + virtualserver_upload_quota: number = 0; + virtualserver_max_download_total_bandwidth: number = 0; + virtualserver_download_quota: number = 0; + + virtualserver_month_bytes_downloaded: number = 0; + virtualserver_month_bytes_uploaded: number = 0; + virtualserver_total_bytes_downloaded: number = 0; + virtualserver_total_bytes_uploaded: number = 0; +} + +export const kServerConnectionInfoFields = { + "connection_filetransfer_bandwidth_sent": "number", + "connection_filetransfer_bandwidth_received": "number", + + "connection_filetransfer_bytes_sent_total": "number", + "connection_filetransfer_bytes_received_total": "number", + + "connection_filetransfer_bytes_sent_month": "number", + "connection_filetransfer_bytes_received_month": "number", + + "connection_packets_sent_total": "number", + "connection_bytes_sent_total": "number", + "connection_packets_received_total": "number", + "connection_bytes_received_total": "number", + + "connection_bandwidth_sent_last_second_total": "number", + "connection_bandwidth_sent_last_minute_total": "number", + "connection_bandwidth_received_last_second_total": "number", + "connection_bandwidth_received_last_minute_total": "number", + + "connection_connected_time": "number", + "connection_packetloss_total": "number", + "connection_ping": "number", +}; + +export interface ServerConnectionInfo { + connection_filetransfer_bandwidth_sent: number; + connection_filetransfer_bandwidth_received: number; + + connection_filetransfer_bytes_sent_total: number; + connection_filetransfer_bytes_received_total: number; + + connection_filetransfer_bytes_sent_month: number; + connection_filetransfer_bytes_received_month: number; + + connection_packets_sent_total: number; + connection_bytes_sent_total: number; + connection_packets_received_total: number; + connection_bytes_received_total: number; + + connection_bandwidth_sent_last_second_total: number; + connection_bandwidth_sent_last_minute_total: number; + connection_bandwidth_received_last_second_total: number; + connection_bandwidth_received_last_minute_total: number; + + connection_connected_time: number; + connection_packetloss_total: number; + connection_ping: number; +} + +export type ServerConnectionInfoResult = { + status: "success", + result: ServerConnectionInfo, + resultCached: boolean +} | { + status: "no-permission", + failedPermission: string +} | { + status: "error", + message: string +}; \ No newline at end of file diff --git a/shared/js/ui/frames/HostBannerController.ts b/shared/js/ui/frames/HostBannerController.ts index 77104892..44a2da5a 100644 --- a/shared/js/ui/frames/HostBannerController.ts +++ b/shared/js/ui/frames/HostBannerController.ts @@ -1,6 +1,6 @@ import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; +import {HostBannerUiEvents} from "tc-shared/ui/frames/HostBannerDefinitions"; import {Registry} from "tc-shared/events"; -import {HostBannerInfoMode, HostBannerUiEvents} from "tc-shared/ui/frames/HostBannerDefinitions"; export class HostBannerController { readonly uiEvents: Registry; @@ -38,15 +38,8 @@ export class HostBannerController { } protected initializeConnectionHandler(handler: ConnectionHandler) { - this.listenerConnection.push(handler.channelTree.server.events.on("notify_properties_updated", event => { - if( - "virtualserver_hostbanner_url" in event.updated_properties || - "virtualserver_hostbanner_mode" in event.updated_properties || - "virtualserver_hostbanner_gfx_url" in event.updated_properties || - "virtualserver_hostbanner_gfx_interval" in event.updated_properties - ) { - this.notifyHostBanner(); - } + this.listenerConnection.push(handler.channelTree.server.events.on("notify_host_banner_updated", () => { + this.notifyHostBanner(); })); this.listenerConnection.push(handler.events().on("notify_connection_state_changed", event => { @@ -58,39 +51,11 @@ export class HostBannerController { private notifyHostBanner() { if(this.currentConnection?.connected) { - const properties = this.currentConnection.channelTree.server.properties; - if(properties.virtualserver_hostbanner_gfx_url) { - let mode: HostBannerInfoMode; - switch (properties.virtualserver_hostbanner_mode) { - case 0: - mode = "original"; - break; - - case 1: - mode = "resize"; - break; - - case 2: - default: - mode = "resize-ratio"; - break; - } - - this.uiEvents.fire_react("notify_host_banner", { - banner: { - status: "set", - - linkUrl: properties.virtualserver_hostbanner_url, - mode: mode, - - imageUrl: properties.virtualserver_hostbanner_gfx_url, - updateInterval: properties.virtualserver_hostbanner_gfx_interval, - } - }); - return; - } + this.uiEvents.fire_react("notify_host_banner", { + banner: this.currentConnection.channelTree.server.generateHostBannerInfo() + }); + } else { + this.uiEvents.fire_react("notify_host_banner", { banner: { status: "none" }}); } - - this.uiEvents.fire_react("notify_host_banner", { banner: { status: "none" }}); } } \ No newline at end of file diff --git a/shared/js/ui/frames/HostBannerRenderer.scss b/shared/js/ui/frames/HostBannerRenderer.scss index 36cc859e..f1f224d4 100644 --- a/shared/js/ui/frames/HostBannerRenderer.scss +++ b/shared/js/ui/frames/HostBannerRenderer.scss @@ -32,7 +32,10 @@ min-height: 0; text-align: center; - cursor: pointer; + + &.clickable { + cursor: pointer; + } /* We're disabling the transition since it only works on appearing not on disappearing */ /* @include transition(height 0.5s ease-in-out); */ @@ -60,8 +63,13 @@ width: 100%; > img { - width: 100%; - height: 100%; + object-fit: contain; + + max-height: 100%; + max-width: 100%; + + min-width: 100%; + min-height: 100%; } } diff --git a/shared/js/ui/frames/HostBannerRenderer.tsx b/shared/js/ui/frames/HostBannerRenderer.tsx index b7f05a7d..e8fcf111 100644 --- a/shared/js/ui/frames/HostBannerRenderer.tsx +++ b/shared/js/ui/frames/HostBannerRenderer.tsx @@ -8,7 +8,7 @@ import {Settings} from "tc-shared/settings"; const cssStyle = require("./HostBannerRenderer.scss"); -export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, className?: string }) => { +export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet, clickable: boolean, className?: string }) => { const [ revision, setRevision ] = useState(Date.now()); useEffect(() => { if(!props.banner.updateInterval) { @@ -35,10 +35,11 @@ export const HostBannerRenderer = React.memo((props: { banner: HostBannerInfoSet
{ - if(props.banner.linkUrl) { + if(props.banner.linkUrl && props.clickable) { window.open(props.banner.linkUrl, "_blank"); } }} @@ -67,7 +68,7 @@ export const HostBanner = React.memo((props: { events: Registry - {hostBanner.status === "set" ? : undefined} + {hostBanner.status === "set" ? : undefined}
); diff --git a/shared/js/ui/modal/ModalServerInfo.ts b/shared/js/ui/modal/ModalServerInfo.ts deleted file mode 100644 index 47d34bd8..00000000 --- a/shared/js/ui/modal/ModalServerInfo.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { - openServerInfoBandwidth, - RequestInfoStatus, - ServerBandwidthInfoUpdateCallback -} from "../../ui/modal/ModalServerInfoBandwidth"; -import {ServerEntry} from "../../tree/Server"; -import {CommandResult} from "../../connection/ServerConnectionDeclaration"; -import {createErrorModal, createModal, Modal} from "../../ui/elements/Modal"; -import {LogCategory, logWarn} from "../../log"; -import * as tooltip from "../../ui/elements/Tooltip"; -import * as i18nc from "../../i18n/country"; -import {format_time, formatMessage} from "../../ui/frames/chat"; -import moment from "moment"; -import {ErrorCode} from "../../connection/ErrorCode"; -import {tr} from "tc-shared/i18n/localize"; - -export function openServerInfo(server: ServerEntry) { - let modal: Modal; - let update_callbacks: ServerBandwidthInfoUpdateCallback[] = []; - - modal = createModal({ - header: tr("Server Information: ") + server.properties.virtualserver_name, - body: () => { - const template = $("#tmpl_server_info").renderTag(); - - const children = template.children(); - const top = template.find(".container-top"); - const update_values = () => { - apply_hostbanner(server, top); - apply_category_1(server, children, update_callbacks); - apply_category_2(server, children, update_callbacks); - apply_category_3(server, children, update_callbacks); - }; - - const button_update = template.find(".button-update"); - button_update.on('click', event => { - button_update.prop("disabled", true); - server.updateProperties().then(() => { - update_callbacks = []; - update_values(); - }).catch(error => { - logWarn(LogCategory.CLIENT, tr("Failed to refresh server properties: %o"), error); - if (error instanceof CommandResult) - error = error.extra_message || error.message; - createErrorModal(tr("Refresh failed"), formatMessage(tr("Failed to refresh server properties.{:br:}Error: {}"), error)).open(); - }).then(() => { - button_update.prop("disabled", false); - }); - }).trigger('click'); - - update_values(); - tooltip.initialize(template); - return template.children(); - }, - footer: null, - min_width: "25em" - }); - - const updater = setInterval(() => { - server.request_connection_info().then(info => update_callbacks.forEach(e => e(RequestInfoStatus.SUCCESS, info))).catch(error => { - if (error instanceof CommandResult && error.id == ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) { - update_callbacks.forEach(e => e(RequestInfoStatus.NO_PERMISSION)); - return; - } - update_callbacks.forEach(e => e(RequestInfoStatus.UNKNOWN)); - }); - }, 1000); - - - modal.htmlTag.find(".button-close").on('click', event => modal.close()); - modal.htmlTag.find(".button-show-bandwidth").on('click', event => { - const custom_callbacks = []; - const custom_callback_caller = (status, info) => { - custom_callbacks.forEach(e => e(status, info)); - }; - - update_callbacks.push(custom_callback_caller); - openServerInfoBandwidth(server, custom_callbacks).close_listener.push(() => { - update_callbacks.remove(custom_callback_caller); - }); - }); - - modal.htmlTag.find(".modal-body").addClass("modal-server-info"); - modal.open(); - modal.close_listener.push(() => clearInterval(updater)); -} - -function apply_hostbanner(server: ServerEntry, tag: JQuery) { - let container: JQuery; - tag.empty().append( - container = $.spawn("div").addClass("container-hostbanner") - ).addClass("hidden"); - - /* FIXME: .... */ - /* - const htag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode); - htag.then(t => { - if (!t) return; - - tag.removeClass("hidden"); - container.append(t); - }); - */ -} - -function apply_category_1(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { - /* server name */ - { - const container = tag.find(".server-name"); - container.text(server.properties.virtualserver_name); - } - - /* server region */ - { - const container = tag.find(".server-region").empty(); - container.append( - $.spawn("div").addClass("country flag-" + server.properties.virtualserver_country_code.toLowerCase()), - $.spawn("a").text(i18nc.getCountryName(server.properties.virtualserver_country_code, tr("Global"))) - ); - } - - /* slots */ - { - const container = tag.find(".server-slots"); - - let text = server.properties.virtualserver_clientsonline + "/" + server.properties.virtualserver_maxclients; - if (server.properties.virtualserver_queryclientsonline) - text += " +" + (server.properties.virtualserver_queryclientsonline > 1 ? - server.properties.virtualserver_queryclientsonline + " " + tr("Queries") : - server.properties.virtualserver_queryclientsonline + " " + tr("Query")); - if (server.properties.virtualserver_reserved_slots) - text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")"; - - container.text(text); - } - - /* first run */ - { - const container = tag.find(".server-first-run"); - - container.text( - server.properties.virtualserver_created > 0 ? - moment(server.properties.virtualserver_created * 1000).format('MMMM Do YYYY, h:mm:ss a') : - tr("Unknown") - ); - } - - /* uptime */ - { - const container = tag.find(".server-uptime"); - const update = () => container.text(format_time(server.calculateUptime() * 1000, tr("just started"))); - update_callbacks.push(update); - update(); - } -} - -function apply_category_2(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { - /* ip */ - { - const container = tag.find(".server-ip"); - container.text(server.remote_address.host + (server.remote_address.port == 9987 ? "" : (":" + server.remote_address.port))) - } - - /* version */ - { - const container = tag.find(".server-version"); - - let timestamp = -1; - const version = (server.properties.virtualserver_version || "unknwon").replace(/ ?\[build: ?([0-9]+)]/gmi, (group, ts) => { - timestamp = parseInt(ts); - return ""; - }); - - container.find("a").text(version); - container.find(".container-tooltip").toggle(timestamp > 0).find(".tooltip a").text( - moment(timestamp * 1000).format('[Build timestamp:] YYYY-MM-DD HH:mm Z') - ); - } - - /* platform */ - { - const container = tag.find(".server-platform"); - container.text(server.properties.virtualserver_platform); - } - - /* ping */ - { - const container = tag.find(".server-ping"); - container.text(tr("calculating...")); - update_callbacks.push((status, data) => { - if (status === RequestInfoStatus.SUCCESS) - container.text(data.connection_ping.toFixed(0) + " " + "ms"); - else if (status === RequestInfoStatus.NO_PERMISSION) - container.text(tr("No Permissions")); - else - container.text(tr("receiving...")); - }); - } - - /* packet loss */ - { - const container = tag.find(".server-packet-loss"); - container.text(tr("receiving...")); - update_callbacks.push((status, data) => { - if (status === RequestInfoStatus.SUCCESS) - container.text(data.connection_packetloss_total.toFixed(2) + "%"); - else if (status === RequestInfoStatus.NO_PERMISSION) - container.text(tr("No Permissions")); - else - container.text(tr("receiving...")); - }); - } -} - -function apply_category_3(server: ServerEntry, tag: JQuery, update_callbacks: ServerBandwidthInfoUpdateCallback[]) { - /* unique id */ - { - const container = tag.find(".server-unique-id"); - container.text(server.properties.virtualserver_unique_identifier || tr("Unknown")); - } - - /* voice encryption */ - { - const container = tag.find(".server-voice-encryption"); - if (server.properties.virtualserver_codec_encryption_mode == 0) - container.text(tr("Globally off")); - else if (server.properties.virtualserver_codec_encryption_mode == 1) - container.text(tr("Individually configured per channel")); - else - container.text(tr("Globally on")); - } - - /* channel count */ - { - const container = tag.find(".server-channel-count"); - container.text(server.properties.virtualserver_channelsonline); - } - - /* minimal security level */ - { - const container = tag.find(".server-min-security-level"); - container.text(server.properties.virtualserver_needed_identity_security_level); - } - - /* complains */ - { - const container = tag.find(".server-complains"); - container.text(server.properties.virtualserver_complain_autoban_count); - } -} \ No newline at end of file diff --git a/shared/js/ui/modal/bookmarks/Renderer.tsx b/shared/js/ui/modal/bookmarks/Renderer.tsx index d92f0d92..d77d3bb2 100644 --- a/shared/js/ui/modal/bookmarks/Renderer.tsx +++ b/shared/js/ui/modal/bookmarks/Renderer.tsx @@ -276,6 +276,7 @@ const SelectedBookmarkBanner = React.memo(() => { updateInterval: 0 }} className={cssStyle.renderer} + clickable={false} /> ); diff --git a/shared/js/ui/modal/server-info/Controller.ts b/shared/js/ui/modal/server-info/Controller.ts new file mode 100644 index 00000000..bfa1364e --- /dev/null +++ b/shared/js/ui/modal/server-info/Controller.ts @@ -0,0 +1,190 @@ +import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; +import {Registry} from "tc-events"; +import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions"; +import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable"; +import {CallOnce, ignorePromise} from "tc-shared/proto"; +import {spawnModal} from "tc-shared/ui/react-elements/modal"; +import {ServerConnectionInfoResult, ServerProperties} from "tc-shared/tree/Server"; +import {LogCategory, logWarn} from "tc-shared/log"; +import {openServerInfoBandwidth} from "tc-shared/ui/modal/ModalServerInfoBandwidth"; + +const kPropertyUpdateMatrix: {[T in keyof ServerProperties]?: [keyof ModalServerInfoVariables]} = { + "virtualserver_name": [ "name" ], + "virtualserver_country_code": [ "region" ], + "virtualserver_reserved_slots": [ "slots" ], + "virtualserver_maxclients": [ "slots" ], + "virtualserver_clientsonline": [ "slots" ], + "virtualserver_queryclientsonline": [ "slots" ], + "virtualserver_created": [ "firstRun" ], + "virtualserver_uptime": [ "uptime" ], + + "virtualserver_version": [ "version" ], + "virtualserver_platform": [ "platform" ], + "virtualserver_unique_identifier": [ "uniqueId" ], + "virtualserver_channelsonline": [ "channelCount" ], + "virtualserver_codec_encryption_mode": [ "voiceDataEncryption" ], + "virtualserver_needed_identity_security_level": [ "securityLevel" ], + "virtualserver_complain_autoban_count": [ "complainsUntilBan" ], +}; + +class Controller { + readonly handler: ConnectionHandler; + readonly events: Registry; + readonly variables: IpcUiVariableProvider; + + private serverListeners: (() => void)[]; + + private connectionInfoInterval: number; + private connectionInfo: ServerConnectionInfoResult; + + private propertyUpdateInterval: number; + private nextRefreshAllowed: number; + + constructor(handler: ConnectionHandler) { + this.handler = handler; + + this.events = new Registry(); + this.variables = new IpcUiVariableProvider(); + } + + private getServerProperties() : ServerProperties { + return this.handler.channelTree.server.properties; + } + + @CallOnce + initialize() { + this.variables.setVariableProvider("name", () => this.getServerProperties().virtualserver_name); + this.variables.setVariableProvider("region", () => this.getServerProperties().virtualserver_country_code); + this.variables.setVariableProvider("slots", () => ({ + max: this.getServerProperties().virtualserver_maxclients, + reserved: this.getServerProperties().virtualserver_reserved_slots, + + used: this.getServerProperties().virtualserver_clientsonline, + queries: this.getServerProperties().virtualserver_queryclientsonline + })); + this.variables.setVariableProvider("firstRun", () => this.getServerProperties().virtualserver_created); + this.variables.setVariableProvider("uptime", () => this.getServerProperties().virtualserver_uptime); + + this.variables.setVariableProvider("ipAddress", () => { + const address = this.handler.channelTree.server.remote_address; + return address.host + (address.port === 9987 ? "" : ":" + address.port); + }); + this.variables.setVariableProvider("version", () => this.getServerProperties().virtualserver_version); + this.variables.setVariableProvider("platform", () => this.getServerProperties().virtualserver_platform); + /* TODO: Ping & Packet loss */ + + this.variables.setVariableProvider("uniqueId", () => this.getServerProperties().virtualserver_unique_identifier); + this.variables.setVariableProvider("channelCount", () => this.getServerProperties().virtualserver_channelsonline); + this.variables.setVariableProvider("voiceDataEncryption", () => { + switch(this.getServerProperties().virtualserver_codec_encryption_mode) { + case 0: + return "global-off"; + case 1: + return "channel-individual"; + case 2: + return "global-on"; + default: + return "unknown"; + } + }); + + this.variables.setVariableProvider("securityLevel", () => this.getServerProperties().virtualserver_needed_identity_security_level); + this.variables.setVariableProvider("complainsUntilBan", () => this.getServerProperties().virtualserver_complain_autoban_count); + + this.variables.setVariableProvider("hostBanner", () => this.handler.channelTree.server.generateHostBannerInfo()); + this.variables.setVariableProvider("connectionInfo", () => { + if(this.connectionInfo) { + return this.connectionInfo; + } else { + return { status: "loading" }; + } + }); + this.variables.setVariableProvider("refreshAllowed", () => this.nextRefreshAllowed); + + this.serverListeners = []; + this.serverListeners.push(this.handler.channelTree.server.events.on("notify_properties_updated", event => { + const updatedVariables = new Set(); + for(const key of Object.keys(event.updated_properties)) { + kPropertyUpdateMatrix[key]?.forEach(update => updatedVariables.add(update)); + } + + updatedVariables.forEach(entry => this.variables.sendVariable(entry)); + })); + this.serverListeners.push(this.handler.channelTree.server.events.on("notify_host_banner_updated", + () => this.variables.sendVariable("hostBanner") + )); + + this.events.on("action_refresh", () => this.refreshProperties()); + + this.refreshConnectionInfo(); + this.connectionInfoInterval = setInterval(() => this.refreshConnectionInfo(), 1000); + + this.refreshProperties(); + this.propertyUpdateInterval = setInterval(() => this.refreshProperties(), 30 * 1000); + } + + @CallOnce + destroy() { + clearInterval(this.connectionInfoInterval); + this.connectionInfoInterval = 0; + + clearInterval(this.propertyUpdateInterval); + this.propertyUpdateInterval = 0; + + this.serverListeners?.forEach(callback => callback()); + this.serverListeners = undefined; + + this.events.destroy(); + this.variables.destroy(); + } + + refreshProperties() { + if(Date.now() < this.nextRefreshAllowed) { + return; + } + + this.nextRefreshAllowed = Date.now() + 10 * 1000; + this.variables.sendVariable("refreshAllowed"); + + /* + * Updates itself will be triggered via the notify_properties_updated event + */ + const server = this.handler.channelTree.server; + server.updateProperties().catch(error => { + logWarn(LogCategory.GENERAL, tr("Failed to update server properties: %o"), error); + }); + } + + private refreshConnectionInfo() { + const server = this.handler.channelTree.server; + server.requestConnectionInfo().then(info => { + this.connectionInfo = info; + this.variables.sendVariable("connectionInfo"); + }); + } +} + +export function spawnServerInfoNew(handler: ConnectionHandler) { + const controller = new Controller(handler); + controller.initialize(); + + const modal = spawnModal("modal-server-info", [ + controller.events.generateIpcDescription(), + controller.variables.generateConsumerDescription() + ], { + popoutable: true + }); + + controller.events.on("action_close", () => modal.destroy()); + controller.events.on("action_show_bandwidth", () => { + openServerInfoBandwidth(handler.channelTree.server); + }); + + modal.getEvents().on("destroy", () => controller.destroy()); + modal.getEvents().on("destroy", handler.events().on("notify_connection_state_changed", event => { + if(event.newState !== ConnectionState.CONNECTED) { + modal.destroy(); + } + })); + ignorePromise(modal.show()); +} \ No newline at end of file diff --git a/shared/js/ui/modal/server-info/Definitions.ts b/shared/js/ui/modal/server-info/Definitions.ts new file mode 100644 index 00000000..561f74ca --- /dev/null +++ b/shared/js/ui/modal/server-info/Definitions.ts @@ -0,0 +1,31 @@ +import {HostBannerInfo} from "tc-shared/ui/frames/HostBannerDefinitions"; +import {ServerConnectionInfoResult} from "tc-shared/tree/ServerDefinitions"; + +export interface ModalServerInfoVariables { + readonly name: string, + readonly region: string, + readonly slots: { max: number, used: number, reserved: number, queries: number }, + readonly firstRun: number, + readonly uptime: number, + + readonly ipAddress: string, + readonly version: string, + readonly platform: string, + readonly connectionInfo: ServerConnectionInfoResult | { status: "loading" }, + + readonly uniqueId: string, + readonly channelCount: number, + readonly voiceDataEncryption: "global-on" | "global-off" | "channel-individual" | "unknown", + readonly securityLevel: number, + readonly complainsUntilBan: number, + + readonly hostBanner: HostBannerInfo, + + readonly refreshAllowed: number, +} + +export interface ModalServerInfoEvents { + action_show_bandwidth: {}, + action_refresh: {}, + action_close: {}, +} \ No newline at end of file diff --git a/shared/js/ui/modal/server-info/Renderer.scss b/shared/js/ui/modal/server-info/Renderer.scss new file mode 100644 index 00000000..022970b5 --- /dev/null +++ b/shared/js/ui/modal/server-info/Renderer.scss @@ -0,0 +1,250 @@ +@import "../../../../css/static/mixin"; +@import "../../../../css/static/properties"; + +.container { + padding: 0; + width: 55em; + + display: flex; + flex-direction: column; + justify-content: stretch; + + background-color: var(--serverinfo-background); + user-select: none; + + &.windowed { + height: 100%; + width: 100%; + } + + .containerHostBanner { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: column; + justify-content: stretch; + + max-height: 10em; + + background-color: var(--serverinfo-hostbanner-background); + + .hostBanner { + border: none; + border-radius: 0; + } + } + + .containerProperties { + flex-shrink: 1; + min-height: 12em; /* 10em + 2 * 1em margin */ + + overflow-y: auto; + + @include chat-scrollbar-vertical(); + } + + .buttons { + margin: 1em; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: space-between; + + button { + min-width: 8em; + } + } +} + +.group { + flex-grow: 0; + flex-shrink: 0; + + margin: 1em; + padding: .5em; + + border-radius: .2em; + border: 1px solid var(--serverinfo-group-border); + + background-color: var(--serverinfo-group-background); + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 10em; + max-height: 10em; + + .image { + flex-grow: 0; + flex-shrink: 0; + + max-width: 15em; + max-height: 9em; /* minus one padding */ + + display: flex; + flex-direction: column; + justify-content: center; + + img { + object-fit: contain; + max-height: 100%; + max-width: 100%; + } + + margin-right: 2em; + @include transition(.25s ease-in-out); + } + + &.reverse { + flex-direction: row-reverse; + text-align: right; + + .image { + margin-right: 0; + margin-left: 2em; + } + + .properties { + .row { + flex-direction: row-reverse; + } + } + } +} + +.properties { + flex-shrink: 1; + flex-grow: 1; + + min-width: 20em; + min-height: 10em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + height: inherit; + + overflow-y: auto; + @include chat-scrollbar(); + + .row { + flex-grow: 0; + flex-shrink: 0; + + height: 1.8em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + .key { + flex-shrink: 0; + flex-grow: 0; + + color: var(--serverinfo-key); + text-transform: uppercase; + align-self: center; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + width: 15em; + } + + .value { + color: var(--serverinfo-value); + align-self: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; + + .country { + display: inline-block; + margin-right: .25em; + } + + &.server-version { + display: flex; + flex-direction: row; + justify-content: flex-start; + + a { + flex-shrink: 1; + min-width: 0; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + .network { + display: flex; + flex-direction: row; + justify-content: center; + + .button { + margin-right: 1em; + + flex-shrink: 1e8; + min-width: 5em; + + display: flex; + flex-direction: column; + justify-content: flex-end; + + button { + height: 2.5em; + width: 12em; + + max-width: 100%; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .right { + flex-grow: 1; + flex-shrink: 1; + min-width: 10em; + } + } +} + +.version { + display: flex; + flex-direction: row; + + span { + display: flex; + flex-direction: column; + justify-content: center; + } + + .tooltip { + height: 1em; + width: 1em; + + align-self: center; + margin-right: .5em; + } +} + +@media all and (max-width: 50em) { + .group .image { + margin: 0!important; + max-width: 0!important; + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/server-info/Renderer.tsx b/shared/js/ui/modal/server-info/Renderer.tsx new file mode 100644 index 00000000..14b196b0 --- /dev/null +++ b/shared/js/ui/modal/server-info/Renderer.tsx @@ -0,0 +1,342 @@ +import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions"; +import React, {useContext, useEffect, useRef, useState} from "react"; +import {IpcRegistryDescription, Registry} from "tc-events"; +import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions"; +import {UiVariableConsumer} from "tc-shared/ui/utils/Variable"; +import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable"; +import {HostBannerRenderer} from "tc-shared/ui/frames/HostBannerRenderer"; + +import ImageServerEdit1 from "./serveredit_1.png"; +import ImageServerEdit2 from "./serveredit_2.png"; +import ImageServerEdit3 from "./serveredit_3.png"; +import {Translatable} from "tc-shared/ui/react-elements/i18n"; +import {joinClassList} from "tc-shared/ui/react-elements/Helper"; +import {CountryCode} from "tc-shared/ui/react-elements/CountryCode"; +import moment from "moment"; +import {tr} from "tc-shared/i18n/localize"; +import {format_online_time} from "tc-shared/utils/TimeUtils"; +import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip"; +import {Button} from "tc-shared/ui/react-elements/Button"; +import {ServerConnectionInfo} from "tc-shared/tree/ServerDefinitions"; + +const cssStyle = require("./Renderer.scss"); + +const EventContext = React.createContext>(undefined); +const VariablesContext = React.createContext>(undefined); + +const Group = React.memo((props: { + children: React.ReactElement[], + reverse: boolean +}) => ( +
+
+ {props.children[0]} +
+
+ {...props.children.slice(1)} +
+
+)); + +const HostBanner = React.memo(() => { + const variables = useContext(VariablesContext); + const hostBanner = variables.useReadOnly("hostBanner", undefined, { status: "none" }); + + if(hostBanner.status === "none") { + return null; + } else { + return ( +
+ +
+ ); + } +}); + +const TitleRenderer = React.memo(() => { + return <>Server Info; +}); + +const VariablePropertyName: {[T in keyof ModalServerInfoVariables]?: () => React.ReactElement } = { + name: () => Server name, + region: () => Server region, + slots: () => Slots, + firstRun: () => First run, + uptime: () => Uptime, + ipAddress: () => Ip Address, + version: () => Version, + platform: () => Platform, + uniqueId: () => Global unique id, + channelCount: () => Current channels, + voiceDataEncryption: () => Voice data encryption, + securityLevel: () => Minimal security level, + complainsUntilBan: () => Complains until ban +}; + +const VariableProperty = (props: { + property: T, + children?: (value: ModalServerInfoVariables[T]) => React.ReactNode +}) => { + const variables = useContext(VariablesContext); + const value = variables.useReadOnly(props.property, undefined, undefined); + + return ( +
+
+ {(VariablePropertyName[props.property] || (() => props.property))()} +
+
+ {(props.children || ((value) => value))(value)} +
+
+ ) +}; + +const ConnectionProperty = React.memo((props: { children: [ + React.ReactNode, + (value: ServerConnectionInfo ) => React.ReactNode +] }) => { + const variables = useContext(VariablesContext); + const value = variables.useReadOnly("connectionInfo", undefined, { status: "loading" }); + + let body; + switch (value.status) { + case "loading": + body = loading; + break; + + case "error": + body = error: {value.message}; + break; + + case "no-permission": + body = No Permission; + break; + + case "success": + body = {props.children[1](value.result)}; + break; + + default: + break; + } + + return ( +
+
+ {props.children[0]} +
+
+ {body} +
+
+ ) +}); + +const ServerFirstRun = React.memo(() => ( + + {value => ( + value > 0 ? moment(value * 1000).format('MMMM Do YYYY, h:mm:ss a') : + tr("Unknown") + )} + +)); + +const ServerSlots = React.memo(() => ( + + {value => { + if(!value) { + return "--"; + } + + let text = value.used + "/" + value.max; + if(value.reserved > 0) { + text += " (" + value.reserved + " " + tr("Reserved") + ")"; + } + if(value.queries > 1) { + text += " +" + value.queries + " " + tr("Queries"); + } else if(value.queries === 1) { + text += " " + tr("+1 Query"); + } + return text; + }} + +)); + +const OnlineTimestampRenderer = React.memo((props: { timestamp: number }) => { + const initialRenderTimestamp = useRef(Date.now()); + const [ , setRenderedTimestamp ] = useState(0); + + const difference = props.timestamp + Math.ceil((Date.now() - initialRenderTimestamp.current) / 1000); + useEffect(() => { + const interval = setInterval(() => { + setRenderedTimestamp(Date.now()); + }, 900); + return () => clearInterval(interval); + }, []); + + return <>{format_online_time(difference)}; +}); + +const kVersionsRegex = /(.*)\[Build: ([0-9]+)]/; +const VersionsTimestamp = (props: { timestamp: string }) => { + if(!props.timestamp) { + return null; + } + + const match = props.timestamp.match(kVersionsRegex); + if(!match || !match[2]) { + return {props.timestamp}; + } + + return ( +
+ + {"Build timestamp: " + moment(parseInt(match[2]) * 1000).format("YYYY-MM-DD HH:mm Z")} + + {match[1].trim()} +
+ ); +}; + +const ButtonRefresh = React.memo(() => { + const variables = useContext(VariablesContext); + const events = useContext(EventContext); + + const nextRefresh = variables.useReadOnly("refreshAllowed"); + const [ renderTimestamp, setRenderTimestamp ] = useState(Date.now()); + + const allowed = nextRefresh.status === "loaded" && renderTimestamp >= nextRefresh.value; + + useEffect(() => { + if(nextRefresh.status !== "loaded" || allowed) { + return; + } + + const time = nextRefresh.value - Date.now(); + const timeout = setTimeout(() => setRenderTimestamp(Date.now()), time); + return () => clearTimeout(timeout); + }, [ nextRefresh.value ]); + + return ( + + ); +}); + +const Buttons = React.memo(() => { + const events = useContext(EventContext); + return ( +
+ + + +
+ ) +}) + +class Modal extends AbstractModal { + private readonly events: Registry; + private readonly variables: UiVariableConsumer; + + constructor(events: IpcRegistryDescription, variables: IpcVariableDescriptor) { + super(); + + this.events = Registry.fromIpcDescription(events); + this.variables = createIpcUiVariableConsumer(variables); + } + + protected onDestroy() { + super.onDestroy(); + + this.events.destroy(); + this.variables.destroy(); + } + + renderBody(): React.ReactElement { + return ( + + +
+ +
+ + {""} + + + {value => } + + + + + + {value => } + + + + {""} + + + {value => } + + +
+
+ +
+
+ + Average ping + {value => value.connection_ping.toFixed(2) + " ms"} + + + Average packet loss + {value => value.connection_packetloss_total.toFixed(2) + " %"} + +
+
+
+ + {""} + + + + {value => { + switch(value) { + case "global-off": + return Globally off; + + case "global-on": + return Globally on; + + case "channel-individual": + return Individually configured per channel; + + case "unknown": + default: + return Unknown; + } + }} + + + + +
+ +
+
+
+ ); + } + + renderTitle(): string | React.ReactElement { + return ; + } +} + +export default Modal; \ No newline at end of file diff --git a/shared/img/serveredit_1.png b/shared/js/ui/modal/server-info/serveredit_1.png similarity index 100% rename from shared/img/serveredit_1.png rename to shared/js/ui/modal/server-info/serveredit_1.png diff --git a/shared/img/serveredit_2.png b/shared/js/ui/modal/server-info/serveredit_2.png similarity index 100% rename from shared/img/serveredit_2.png rename to shared/js/ui/modal/server-info/serveredit_2.png diff --git a/shared/img/serveredit_3.png b/shared/js/ui/modal/server-info/serveredit_3.png similarity index 100% rename from shared/img/serveredit_3.png rename to shared/js/ui/modal/server-info/serveredit_3.png diff --git a/shared/js/ui/react-elements/Tooltip.tsx b/shared/js/ui/react-elements/Tooltip.tsx index 70a97278..b0026a99 100644 --- a/shared/js/ui/react-elements/Tooltip.tsx +++ b/shared/js/ui/react-elements/Tooltip.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; -import {ReactElement} from "react"; +import {ReactElement, ReactNode} from "react"; import {guid} from "tc-shared/crypto/uid"; const cssStyle = require("./Tooltip.scss"); @@ -73,7 +73,7 @@ export interface TooltipState { } export interface TooltipProperties { - tooltip: () => ReactElement | ReactElement[] | string; + tooltip: () => ReactNode | ReactNode[] | string; className?: string; } @@ -153,7 +153,7 @@ export class Tooltip extends React.Component { } } -export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string, outerClassName?: string }) => ( +export const IconTooltip = (props: { children?: React.ReactNode | React.ReactNode[], className?: string, outerClassName?: string }) => ( props.children} className={props.outerClassName}>
{""} diff --git a/shared/js/ui/react-elements/modal/Definitions.ts b/shared/js/ui/react-elements/modal/Definitions.ts index b71057fd..737335fb 100644 --- a/shared/js/ui/react-elements/modal/Definitions.ts +++ b/shared/js/ui/react-elements/modal/Definitions.ts @@ -21,6 +21,7 @@ import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefini import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer"; import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions"; import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios"; +import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions"; import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions"; export type ModalType = "error" | "warning" | "info" | "none"; @@ -220,5 +221,9 @@ export interface ModalConstructorArguments { "modal-about": [ /* events */ IpcRegistryDescription, /* variables */ IpcVariableDescriptor + ], + "modal-server-info": [ + /* events */ IpcRegistryDescription, + /* variables */ IpcVariableDescriptor ] } \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Registry.ts b/shared/js/ui/react-elements/modal/Registry.ts index 9aa865e8..b01f9569 100644 --- a/shared/js/ui/react-elements/modal/Registry.ts +++ b/shared/js/ui/react-elements/modal/Registry.ts @@ -116,7 +116,7 @@ registerModal({ }); registerModal({ - modalId: "modal-about", - classLoader: async () => await import("tc-shared/ui/modal/about/Renderer"), + modalId: "modal-server-info", + classLoader: async () => await import("tc-shared/ui/modal/server-info/Renderer"), popoutSupported: true }); \ No newline at end of file diff --git a/shared/js/utils/TimeUtils.ts b/shared/js/utils/TimeUtils.ts index f6d0d7e7..7116b7c2 100644 --- a/shared/js/utils/TimeUtils.ts +++ b/shared/js/utils/TimeUtils.ts @@ -8,18 +8,27 @@ export function format_online_time(secs: number) : string { let seconds = Math.floor(secs % 60); let result = ""; - if(years > 0) + if(years > 0) { result += years + " " + tr("years") + " "; - if(years > 0 || days > 0) + } + + if(years > 0 || days > 0) { result += days + " " + tr("days") + " "; - if(years > 0 || days > 0 || hours > 0) + } + + if(years > 0 || days > 0 || hours > 0) { result += hours + " " + tr("hours") + " "; - if(years > 0 || days > 0 || hours > 0 || minutes > 0) + } + + if(years > 0 || days > 0 || hours > 0 || minutes > 0) { result += minutes + " " + tr("minutes") + " "; - if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) + } + + if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) { result += seconds + " " + tr("seconds") + " "; - else + } else { result = tr("now") + " "; + } return result.substr(0, result.length - 1); } \ No newline at end of file