Reworked the channel info modal with React
parent
feeb086cb8
commit
eebe7aa6b9
|
@ -8,7 +8,6 @@ import "./static/modal.scss"
|
||||||
import "./static/modals.scss"
|
import "./static/modals.scss"
|
||||||
import "./static/modal-banclient.scss"
|
import "./static/modal-banclient.scss"
|
||||||
import "./static/modal-banlist.scss"
|
import "./static/modal-banlist.scss"
|
||||||
import "./static/modal-channelinfo.scss"
|
|
||||||
import "./static/modal-clientinfo.scss"
|
import "./static/modal-clientinfo.scss"
|
||||||
import "./static/modal-group-assignment.scss"
|
import "./static/modal-group-assignment.scss"
|
||||||
import "./static/modal-icons.scss"
|
import "./static/modal-icons.scss"
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
@import "mixin";
|
|
||||||
@import "properties";
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.modal-body.modal-channel-info {
|
|
||||||
display: flex!important;
|
|
||||||
flex-direction: column!important;
|
|
||||||
justify-content: stretch!important;
|
|
||||||
|
|
||||||
min-width: 30em!important;
|
|
||||||
max-height: calc(100vh - 10em)!important;
|
|
||||||
padding: 0 !important;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
padding-top: 1em;
|
|
||||||
padding-left: .5em;
|
|
||||||
padding-right: .5em;
|
|
||||||
|
|
||||||
.column {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
min-width: 6em;
|
|
||||||
width: 10em;
|
|
||||||
|
|
||||||
margin-right: .5em;
|
|
||||||
margin-left: .5em;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #557edc;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.audio-encrypted {
|
|
||||||
/* looks better */
|
|
||||||
.value {
|
|
||||||
height: 1.6em;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-description {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
min-height: 8em; /* description plus title */
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
padding-top: 1em;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #557edc;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
.button-copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
margin-top: .1em; /* looks a bit better */
|
|
||||||
margin-left: .5em;
|
|
||||||
border-radius: .2em;
|
|
||||||
|
|
||||||
width: 1.3em;
|
|
||||||
height: 1.3em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
div {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #313135;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include transition($button_hover_animation_time ease-in-out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
display: block;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
border-radius: 0.2em;
|
|
||||||
border: 1px solid #212324;
|
|
||||||
background-color: #3a3b3f;
|
|
||||||
|
|
||||||
padding: .5em;
|
|
||||||
|
|
||||||
height: max-content;
|
|
||||||
min-height: 6em;
|
|
||||||
max-height: 40em;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
@include chat-scrollbar-vertical();
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-value {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
font-size: 1.25em;
|
|
||||||
height: (6em / 1.25); /* min value height and a bit more */
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-buttons {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {rendererHTML, rendererReact, rendererText, BBCodeHandlerContext} from "tc-shared/text/bbcode/renderer";
|
import {rendererHTML, rendererReact, rendererText, BBCodeHandlerContext} from "tc-shared/text/bbcode/renderer";
|
||||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
import {parseBBCode as parseBBCode} from "vendor/xbbcode/parser";
|
||||||
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
|
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
|
||||||
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
|
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
|
||||||
import "./bbcode.scss";
|
import "./bbcode.scss";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {BBCodeHandlerContext, rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
import {BBCodeHandlerContext, rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
import {TagElement} from "vendor/xbbcode/elements";
|
import {BBCodeTagElement} from "vendor/xbbcode/elements";
|
||||||
import {BBCodeRenderer} from "tc-shared/text/bbcode";
|
import {BBCodeRenderer} from "tc-shared/text/bbcode";
|
||||||
import {spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
import {spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||||
import {copyToClipboard} from "tc-shared/utils/helpers";
|
import {copyToClipboard} from "tc-shared/utils/helpers";
|
||||||
|
@ -78,8 +78,8 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "XBBCode code tag init",
|
name: "XBBCode code tag init",
|
||||||
function: async () => {
|
function: async () => {
|
||||||
ipcChannel = getIpcInstance().createCoreControlChannel("bbcode-youtube");
|
ipcChannel = getIpcInstance().createCoreControlChannel("bbcode-youtube");
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<BBCodeTagElement, React.ReactNode> {
|
||||||
render(element: TagElement): React.ReactNode {
|
render(element: BBCodeTagElement): React.ReactNode {
|
||||||
const text = rendererText.render(element);
|
const text = rendererText.render(element);
|
||||||
return <YoutubeRenderer url={text} />;
|
return <YoutubeRenderer url={text} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as loader from "tc-loader";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { rendererReact } from "tc-shared/text/bbcode/renderer";
|
import { rendererReact } from "tc-shared/text/bbcode/renderer";
|
||||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
import {TextElement} from "vendor/xbbcode/elements";
|
import {BBCodeTextElement} from "vendor/xbbcode/elements";
|
||||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
function: async () => {
|
function: async () => {
|
||||||
let reactId = 0;
|
let reactId = 0;
|
||||||
|
|
||||||
rendererReact.setTextRenderer(new class extends ElementRenderer<TextElement, React.ReactNode> {
|
rendererReact.setTextRenderer(new class extends ElementRenderer<BBCodeTextElement, React.ReactNode> {
|
||||||
render(element: TextElement, renderer: ReactRenderer): React.ReactNode {
|
render(element: BBCodeTextElement, renderer: ReactRenderer): React.ReactNode {
|
||||||
if(!settings.getValue(Settings.KEY_CHAT_COLORED_EMOJIES)) {
|
if(!settings.getValue(Settings.KEY_CHAT_COLORED_EMOJIES)) {
|
||||||
return element.text();
|
return element.text();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as hljs from "highlight.js/lib/core";
|
||||||
|
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
import {TagElement} from "vendor/xbbcode/elements";
|
import {BBCodeTagElement} from "vendor/xbbcode/elements";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
import * as DOMPurify from "dompurify";
|
import * as DOMPurify from "dompurify";
|
||||||
|
@ -92,12 +92,12 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* override default parser */
|
/* override default parser */
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<BBCodeTagElement, React.ReactNode> {
|
||||||
tags(): string | string[] {
|
tags(): string | string[] {
|
||||||
return ["code", "icode", "i-code"];
|
return ["code", "icode", "i-code"];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(element: TagElement): React.ReactNode {
|
render(element: BBCodeTagElement): React.ReactNode {
|
||||||
const klass = element.tagNormalized != 'code' ? cssStyle.inlineCode : cssStyle.code;
|
const klass = element.tagNormalized != 'code' ? cssStyle.inlineCode : cssStyle.code;
|
||||||
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
import {TagElement} from "vendor/xbbcode/elements";
|
import {BBCodeTagElement} from "vendor/xbbcode/elements";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||||
|
@ -57,12 +57,12 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
function: async () => {
|
function: async () => {
|
||||||
let reactId = 0;
|
let reactId = 0;
|
||||||
|
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<BBCodeTagElement, React.ReactNode> {
|
||||||
tags(): string | string[] {
|
tags(): string | string[] {
|
||||||
return ["img", "image"];
|
return ["img", "image"];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(element: TagElement): React.ReactNode {
|
render(element: BBCodeTagElement): React.ReactNode {
|
||||||
let target;
|
let target;
|
||||||
let content = rendererText.render(element);
|
let content = rendererText.render(element);
|
||||||
if (!element.options) {
|
if (!element.options) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
||||||
|
|
||||||
import "./emoji";
|
import "./emoji";
|
||||||
import "./highlight";
|
import "./highlight";
|
||||||
import "./YoutubeController";
|
import "./YoutubeRenderer";
|
||||||
import "./url";
|
import "./url";
|
||||||
import "./image";
|
import "./image";
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||||
import {copyToClipboard} from "tc-shared/utils/helpers";
|
import {copyToClipboard} from "tc-shared/utils/helpers";
|
||||||
import * as loader from "tc-loader";
|
import * as loader from "tc-loader";
|
||||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||||
import {TagElement} from "vendor/xbbcode/elements";
|
import {BBCodeTagElement} from "vendor/xbbcode/elements";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
import {rendererReact, rendererText, BBCodeHandlerContext} from "tc-shared/text/bbcode/renderer";
|
import {rendererReact, rendererText, BBCodeHandlerContext} from "tc-shared/text/bbcode/renderer";
|
||||||
|
@ -53,8 +53,8 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
let reactId = 0;
|
let reactId = 0;
|
||||||
|
|
||||||
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
rendererReact.registerCustomRenderer(new class extends ElementRenderer<BBCodeTagElement, React.ReactNode> {
|
||||||
render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
|
render(element: BBCodeTagElement, renderer: ReactRenderer): React.ReactNode {
|
||||||
let target: string;
|
let target: string;
|
||||||
if (!element.options) {
|
if (!element.options) {
|
||||||
target = rendererText.render(element);
|
target = rendererText.render(element);
|
||||||
|
|
|
@ -2,8 +2,8 @@ import UrlKnife from 'url-knife';
|
||||||
import {Settings, settings} from "../settings";
|
import {Settings, settings} from "../settings";
|
||||||
import {renderMarkdownAsBBCode} from "../text/markdown";
|
import {renderMarkdownAsBBCode} from "../text/markdown";
|
||||||
import {escapeBBCode} from "../text/bbcode";
|
import {escapeBBCode} from "../text/bbcode";
|
||||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
import {parseBBCode as parseBBCode} from "vendor/xbbcode/parser";
|
||||||
import {TagElement} from "vendor/xbbcode/elements";
|
import {BBCodeTagElement} from "vendor/xbbcode/elements";
|
||||||
import {regexImage} from "tc-shared/text/bbcode/image";
|
import {regexImage} from "tc-shared/text/bbcode/image";
|
||||||
|
|
||||||
interface UrlKnifeUrl {
|
interface UrlKnifeUrl {
|
||||||
|
@ -88,7 +88,7 @@ export function preprocessChatMessageForSend(message: string) : string {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!(element instanceof TagElement)) {
|
if(!(element instanceof BBCodeTagElement)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {ChannelTree} from "./ChannelTree";
|
import {ChannelTree} from "./ChannelTree";
|
||||||
import {ClientEntry, ClientEvents} from "./Client";
|
import {ClientEntry, ClientEvents} from "./Client";
|
||||||
import * as log from "../log";
|
import * as log from "../log";
|
||||||
import {LogCategory, logInfo, LogType, logWarn} from "../log";
|
import {LogCategory, logError, logInfo, LogType, logWarn} from "../log";
|
||||||
import {PermissionType} from "../permission/PermissionType";
|
import {PermissionType} from "../permission/PermissionType";
|
||||||
import {settings, Settings} from "../settings";
|
import {settings, Settings} from "../settings";
|
||||||
import * as contextmenu from "../ui/elements/ContextMenu";
|
import * as contextmenu from "../ui/elements/ContextMenu";
|
||||||
|
@ -10,7 +10,6 @@ import {Sound} from "../audio/Sounds";
|
||||||
import {createErrorModal, createInfoModal, createInputModal} from "../ui/elements/Modal";
|
import {createErrorModal, createInfoModal, createInputModal} from "../ui/elements/Modal";
|
||||||
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
||||||
import {hashPassword} from "../utils/helpers";
|
import {hashPassword} from "../utils/helpers";
|
||||||
import {openChannelInfo} from "../ui/modal/ModalChannelInfo";
|
|
||||||
import {formatMessage} from "../ui/frames/chat";
|
import {formatMessage} from "../ui/frames/chat";
|
||||||
|
|
||||||
import {Registry} from "../events";
|
import {Registry} from "../events";
|
||||||
|
@ -18,10 +17,13 @@ import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
|
||||||
import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer";
|
import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer";
|
||||||
import {ErrorCode} from "../connection/ErrorCode";
|
import {ErrorCode} from "../connection/ErrorCode";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
import {EventChannelData} from "tc-shared/connectionlog/Definitions";
|
import {EventChannelData} from "tc-shared/connectionlog/Definitions";
|
||||||
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
|
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
|
||||||
import {spawnInviteGenerator} from "tc-shared/ui/modal/invite/Controller";
|
import {spawnInviteGenerator} from "tc-shared/ui/modal/invite/Controller";
|
||||||
|
import {NoThrow} from "tc-shared/proto";
|
||||||
|
import {ChannelDescriptionResult} from "tc-shared/tree/ChannelDefinitions";
|
||||||
|
import {spawnChannelInfo} from "tc-shared/ui/modal/channel-info/Controller";
|
||||||
|
|
||||||
export enum ChannelType {
|
export enum ChannelType {
|
||||||
PERMANENT,
|
PERMANENT,
|
||||||
|
@ -203,9 +205,9 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
private _destroyed = false;
|
private _destroyed = false;
|
||||||
|
|
||||||
private cachedPasswordHash: string;
|
private cachedPasswordHash: string;
|
||||||
private channelDescriptionCached: boolean;
|
private channelDescriptionCacheTimestamp: number;
|
||||||
private channelDescriptionCallback: ((success: boolean) => void)[];
|
private channelDescriptionCallback: ((success: boolean) => void)[];
|
||||||
private channelDescriptionPromise: Promise<string>;
|
private channelDescriptionPromise: Promise<ChannelDescriptionResult>;
|
||||||
|
|
||||||
private collapsed: boolean;
|
private collapsed: boolean;
|
||||||
private subscribed: boolean;
|
private subscribed: boolean;
|
||||||
|
@ -243,7 +245,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
this.collapsed = this.channelTree.client.settings.getValue(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId));
|
this.collapsed = this.channelTree.client.settings.getValue(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId));
|
||||||
this.subscriptionMode = this.channelTree.client.settings.getValue(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId), ChannelSubscribeMode.INHERITED);
|
this.subscriptionMode = this.channelTree.client.settings.getValue(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId), ChannelSubscribeMode.INHERITED);
|
||||||
|
|
||||||
this.channelDescriptionCached = false;
|
this.channelDescriptionCacheTimestamp = 0;
|
||||||
this.channelDescriptionCallback = [];
|
this.channelDescriptionCallback = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,45 +282,78 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
return this.parsed_channel_name.text;
|
return this.parsed_channel_name.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChannelDescription() : Promise<string> {
|
clearDescriptionCache() {
|
||||||
if(this.channelDescriptionPromise) {
|
this.channelDescriptionPromise = undefined;
|
||||||
return this.channelDescriptionPromise;
|
this.channelDescriptionCacheTimestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NoThrow
|
||||||
|
async getChannelDescription(ignoreCache: boolean) : Promise<ChannelDescriptionResult> {
|
||||||
|
if(ignoreCache || Date.now() - 120 * 1000 > this.channelDescriptionCacheTimestamp) {
|
||||||
|
this.channelDescriptionPromise = this.doGetChannelDescriptionNew();
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = this.doGetChannelDescription();
|
return await this.channelDescriptionPromise;
|
||||||
this.channelDescriptionPromise = promise;
|
|
||||||
promise
|
|
||||||
.then(() => this.channelDescriptionPromise = undefined)
|
|
||||||
.catch(() => this.channelDescriptionPromise = undefined);
|
|
||||||
return promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isDescriptionCached() {
|
@NoThrow
|
||||||
return this.channelDescriptionCached;
|
private async doGetChannelDescriptionNew() : Promise<ChannelDescriptionResult> {
|
||||||
}
|
try {
|
||||||
|
|
||||||
private async doGetChannelDescription() {
|
|
||||||
if(!this.channelDescriptionCached) {
|
|
||||||
await this.channelTree.client.serverConnection.send_command("channelgetdescription", {
|
await this.channelTree.client.serverConnection.send_command("channelgetdescription", {
|
||||||
cid: this.channelId
|
cid: this.channelId
|
||||||
}, {
|
}, {
|
||||||
process_result: false
|
process_result: false
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
if(!this.channelDescriptionCached) {
|
if(error instanceof CommandResult) {
|
||||||
/* since the channel description is a low command it will not be processed in sync */
|
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||||
await new Promise((resolve, reject) => {
|
return {
|
||||||
this.channelDescriptionCallback.push(succeeded => {
|
status: "no-permissions",
|
||||||
if(succeeded) {
|
failedPermission: this.channelTree.client.permissions.getFailedPermission(error)
|
||||||
resolve();
|
}
|
||||||
} else {
|
} else {
|
||||||
reject(tr("failed to receive description"));
|
return {
|
||||||
}
|
status: "error",
|
||||||
})
|
message: error.formattedMessage()
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
} else if(typeof error === "string") {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logError(LogCategory.CHANNEL, tr("Failed to query channel description for channel %d: %o"), this.channelId, error);
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: tr("lookup the console")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.properties.channel_description;
|
|
||||||
|
if(!this.channelDescriptionCacheTimestamp) {
|
||||||
|
/* since the channel description is a low command it will not be processed later */
|
||||||
|
const result = await new Promise(resolve => {
|
||||||
|
this.channelDescriptionCallback.push(resolve);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.channelDescriptionCallback.remove(resolve);
|
||||||
|
resolve(false);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!result) {
|
||||||
|
return { status: "error", message: tr("description query failed") };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
description: this.properties.channel_description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDescriptionCached() {
|
||||||
|
return this.channelDescriptionCacheTimestamp > Date.now() - 120 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerClient(client: ClientEntry) {
|
registerClient(client: ClientEntry) {
|
||||||
|
@ -481,7 +516,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
name: tr("Show channel info"),
|
name: tr("Show channel info"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
trigger_close = false;
|
trigger_close = false;
|
||||||
openChannelInfo(this);
|
spawnChannelInfo(this);
|
||||||
},
|
},
|
||||||
icon_class: "client-about"
|
icon_class: "client-about"
|
||||||
}, {
|
}, {
|
||||||
|
@ -646,7 +681,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
const hasUpdate = JSON.map_field_to(this.properties, value, variable.key);
|
const hasUpdate = JSON.map_field_to(this.properties, value, variable.key);
|
||||||
|
|
||||||
if(key == "channel_description") {
|
if(key == "channel_description") {
|
||||||
this.channelDescriptionCached = true;
|
this.channelDescriptionCacheTimestamp = Date.now();
|
||||||
this.channelDescriptionCallback.forEach(callback => callback(true));
|
this.channelDescriptionCallback.forEach(callback => callback(true));
|
||||||
this.channelDescriptionCallback = [];
|
this.channelDescriptionCallback = [];
|
||||||
}
|
}
|
||||||
|
@ -681,10 +716,14 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
|
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
|
||||||
}
|
}
|
||||||
|
|
||||||
channelType() : ChannelType {
|
getChannelType() : ChannelType {
|
||||||
if(this.properties.channel_flag_permanent == true) return ChannelType.PERMANENT;
|
if(this.properties.channel_flag_permanent == true) {
|
||||||
if(this.properties.channel_flag_semi_permanent == true) return ChannelType.SEMI_PERMANENT;
|
return ChannelType.PERMANENT;
|
||||||
return ChannelType.TEMPORARY;
|
} else if(this.properties.channel_flag_semi_permanent == true) {
|
||||||
|
return ChannelType.SEMI_PERMANENT;
|
||||||
|
} else {
|
||||||
|
return ChannelType.TEMPORARY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinChannel(ignorePasswordFlag?: boolean) : Promise<boolean> {
|
async joinChannel(ignorePasswordFlag?: boolean) : Promise<boolean> {
|
||||||
|
@ -870,11 +909,11 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDescriptionChanged() {
|
handleDescriptionChanged() {
|
||||||
if(!this.channelDescriptionCached) {
|
if(!this.channelDescriptionCacheTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.channelDescriptionCached = false;
|
this.channelDescriptionCacheTimestamp = 0;
|
||||||
this.properties.channel_description = undefined;
|
this.properties.channel_description = undefined;
|
||||||
this.events.fire("notify_description_changed");
|
this.events.fire("notify_description_changed");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export type ChannelDescriptionResult = {
|
||||||
|
status: "success",
|
||||||
|
description: string,
|
||||||
|
handlerId: string
|
||||||
|
} | {
|
||||||
|
status: "empty"
|
||||||
|
} | {
|
||||||
|
status: "no-permissions",
|
||||||
|
failedPermission: string
|
||||||
|
} | {
|
||||||
|
status: "error",
|
||||||
|
message: string
|
||||||
|
};
|
|
@ -16,7 +16,7 @@ import {spawnServerInfoNew} from "tc-shared/ui/modal/server-info/Controller";
|
||||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||||
import {
|
import {
|
||||||
kServerConnectionInfoFields,
|
kServerConnectionInfoFields, ServerAudioEncryptionMode,
|
||||||
ServerConnectionInfo,
|
ServerConnectionInfo,
|
||||||
ServerConnectionInfoResult,
|
ServerConnectionInfoResult,
|
||||||
ServerProperties
|
ServerProperties
|
||||||
|
@ -401,4 +401,18 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
updateInterval: this.properties.virtualserver_hostbanner_gfx_interval,
|
updateInterval: this.properties.virtualserver_hostbanner_gfx_interval,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudioEncryptionMode() : ServerAudioEncryptionMode {
|
||||||
|
switch (this.properties.virtualserver_codec_encryption_mode) {
|
||||||
|
case 0:
|
||||||
|
return "globally-off";
|
||||||
|
|
||||||
|
default:
|
||||||
|
case 1:
|
||||||
|
return "individual";
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return "globally-on";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
export type ServerAudioEncryptionMode = "globally-on" | "globally-off" | "individual";
|
||||||
|
|
||||||
export class ServerProperties {
|
export class ServerProperties {
|
||||||
virtualserver_host: string = "";
|
virtualserver_host: string = "";
|
||||||
virtualserver_port: number = 0;
|
virtualserver_port: number = 0;
|
||||||
|
|
|
@ -80,40 +80,39 @@ export class ChannelDescriptionController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateCachedDescriptionStatus() {
|
private async updateCachedDescriptionStatus() {
|
||||||
try {
|
if(this.currentChannel) {
|
||||||
let description;
|
const handlerId = this.currentChannel.channelTree.client.handlerId;
|
||||||
if(this.currentChannel) {
|
const result = await this.currentChannel.getChannelDescription(false);
|
||||||
description = await new Promise<any>((resolve, reject) => {
|
switch (result.status) {
|
||||||
this.currentChannel.getChannelDescription().then(resolve).catch(reject);
|
case "success":
|
||||||
setTimeout(() => reject(tr("timeout")), 5000);
|
case "empty":
|
||||||
});
|
this.cachedDescriptionStatus = {
|
||||||
}
|
status: "success",
|
||||||
|
description: result.status === "success" ? result.description : undefined,
|
||||||
|
handlerId: handlerId
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
this.cachedDescriptionStatus = {
|
case "no-permissions":
|
||||||
status: "success",
|
|
||||||
description: description,
|
|
||||||
handlerId: this.currentChannel?.channelTree.client.handlerId || "unknown"
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if(error instanceof CommandResult) {
|
|
||||||
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
|
||||||
const permission = this.currentChannel?.channelTree.client.permissions.resolveInfo(parseInt(error.json["failed_permid"]));
|
|
||||||
this.cachedDescriptionStatus = {
|
this.cachedDescriptionStatus = {
|
||||||
status: "no-permissions",
|
status: "no-permissions",
|
||||||
failedPermission: permission ? permission.name : "unknown"
|
failedPermission: result.failedPermission
|
||||||
};
|
};
|
||||||
return;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
error = error.formattedMessage();
|
case "error":
|
||||||
} else if(typeof error !== "string") {
|
default:
|
||||||
logError(LogCategory.GENERAL, tr("Failed to get channel descriptions: %o"), error);
|
this.cachedDescriptionStatus = {
|
||||||
error = tr("lookup the console");
|
status: "error",
|
||||||
|
reason: result.message || tr("unknown query result"),
|
||||||
|
};
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
this.cachedDescriptionStatus = {
|
this.cachedDescriptionStatus = {
|
||||||
status: "error",
|
status: "success",
|
||||||
reason: error
|
description: undefined,
|
||||||
|
handlerId: "unknown"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.cachedDescriptionAge = Date.now();
|
this.cachedDescriptionAge = Date.now();
|
||||||
|
|
|
@ -1,161 +0,0 @@
|
||||||
import {createInfoModal, createModal, Modal} from "../../ui/elements/Modal";
|
|
||||||
import {ChannelEntry} from "../../tree/Channel";
|
|
||||||
import {copyToClipboard} from "../../utils/helpers";
|
|
||||||
import * as tooltip from "../../ui/elements/Tooltip";
|
|
||||||
import {formatMessage} from "../../ui/frames/chat";
|
|
||||||
import {renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
|
||||||
import {tr} from "tc-shared/i18n/localize";
|
|
||||||
|
|
||||||
export function openChannelInfo(channel: ChannelEntry) {
|
|
||||||
let modal: Modal;
|
|
||||||
|
|
||||||
modal = createModal({
|
|
||||||
header: tr("Channel information: ") + channel.channelName(),
|
|
||||||
body: () => {
|
|
||||||
const template = $("#tmpl_channel_info").renderTag();
|
|
||||||
|
|
||||||
const update_values = (container) => {
|
|
||||||
|
|
||||||
apply_channel_description(container.find(".container-description"), channel);
|
|
||||||
apply_general(container, channel);
|
|
||||||
};
|
|
||||||
|
|
||||||
template.find(".button-copy").on('click', event => {
|
|
||||||
copyToClipboard(channel.properties.channel_description);
|
|
||||||
createInfoModal(tr("Description copied"), tr("The channel description has been copied to your clipboard!")).open();
|
|
||||||
});
|
|
||||||
|
|
||||||
const button_update = template.find(".button-update");
|
|
||||||
button_update.on('click', event => update_values(modal.htmlTag));
|
|
||||||
|
|
||||||
update_values(template);
|
|
||||||
tooltip.initialize(template);
|
|
||||||
return template.children();
|
|
||||||
},
|
|
||||||
footer: null,
|
|
||||||
width: "65em"
|
|
||||||
});
|
|
||||||
modal.htmlTag.find(".button-close").on('click', event => modal.close());
|
|
||||||
modal.htmlTag.find(".modal-body").addClass("modal-channel-info");
|
|
||||||
modal.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
function apply_channel_description(container: JQuery, channel: ChannelEntry) {
|
|
||||||
const container_value = container.find(".value");
|
|
||||||
const container_no_value = container.find(".no-value");
|
|
||||||
|
|
||||||
channel.getChannelDescription().then(description => {
|
|
||||||
container_value.empty();
|
|
||||||
if (description) {
|
|
||||||
renderBBCodeAsJQuery(description, { convertSingleUrls: true }).forEach(element => container_value.append(element));
|
|
||||||
container_no_value.hide();
|
|
||||||
container_value.show();
|
|
||||||
} else {
|
|
||||||
container_no_value.text(tr("Channel has no description"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
container_value.hide();
|
|
||||||
container_no_value.text(tr("loading...")).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
const codec_names = [
|
|
||||||
tr("Speex Narrowband"),
|
|
||||||
tr("Speex Wideband"),
|
|
||||||
tr("Speex Ultra-Wideband"),
|
|
||||||
tr("CELT Mono"),
|
|
||||||
tr("Opus Voice"),
|
|
||||||
tr("Opus Music")
|
|
||||||
];
|
|
||||||
|
|
||||||
function apply_general(container: JQuery, channel: ChannelEntry) {
|
|
||||||
/* channel type */
|
|
||||||
{
|
|
||||||
const tag = container.find(".channel-type .value").empty();
|
|
||||||
if (channel.properties.channel_flag_permanent)
|
|
||||||
tag.text(tr("Permanent"));
|
|
||||||
else if (channel.properties.channel_flag_semi_permanent)
|
|
||||||
tag.text(tr("Semi permanent"));
|
|
||||||
else
|
|
||||||
//TODO: Channel delete delay!
|
|
||||||
tag.text(tr("Temporary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* chat mode */
|
|
||||||
{
|
|
||||||
const tag = container.find(".chat-mode .value").empty();
|
|
||||||
if(channel.properties.channel_conversation_mode === 0 || channel.properties.channel_flag_password) {
|
|
||||||
tag.text(tr("Private"));
|
|
||||||
} else if(channel.properties.channel_conversation_mode === 1) {
|
|
||||||
if (channel.properties.channel_conversation_history_length == -1) {
|
|
||||||
tag.text(tr("Public; Semi permanent message saving"));
|
|
||||||
} else if (channel.properties.channel_conversation_history_length == 0) {
|
|
||||||
tag.text(tr("Public; Permanent message saving"));
|
|
||||||
} else {
|
|
||||||
tag.append(formatMessage(tr("Public; Saving last {} messages"), channel.properties.channel_conversation_history_length));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tag.text(tr("No chatting"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* current clients */
|
|
||||||
{
|
|
||||||
const tag = container.find(".current-clients .value").empty();
|
|
||||||
|
|
||||||
if (channel.isSubscribed()) {
|
|
||||||
const current = channel.clients().length;
|
|
||||||
let channel_limit = tr("Unlimited");
|
|
||||||
if (!channel.properties.channel_flag_maxclients_unlimited)
|
|
||||||
channel_limit = "" + channel.properties.channel_maxclients;
|
|
||||||
else if (!channel.properties.channel_flag_maxfamilyclients_unlimited) {
|
|
||||||
if (channel.properties.channel_maxfamilyclients >= 0)
|
|
||||||
channel_limit = "" + channel.properties.channel_maxfamilyclients;
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.text(current + " / " + channel_limit);
|
|
||||||
} else {
|
|
||||||
tag.text(tr("Channel not subscribed"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* audio codec */
|
|
||||||
{
|
|
||||||
const tag = container.find(".audio-codec .value").empty();
|
|
||||||
tag.text((codec_names[channel.properties.channel_codec] || tr("Unknown")) + " (" + channel.properties.channel_codec_quality + ")")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* audio encrypted */
|
|
||||||
{
|
|
||||||
const tag = container.find(".audio-encrypted .value").empty();
|
|
||||||
const mode = channel.channelTree.server.properties.virtualserver_codec_encryption_mode;
|
|
||||||
let appendix;
|
|
||||||
if (mode == 1)
|
|
||||||
appendix = tr("Overridden by the server with Unencrypted!");
|
|
||||||
else if (mode == 2)
|
|
||||||
appendix = tr("Overridden by the server with Encrypted!");
|
|
||||||
|
|
||||||
tag.html((channel.properties.channel_codec_is_unencrypted ? tr("Unencrypted") : tr("Encrypted")) + (appendix ? "<br>" + appendix : ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
/* flag password */
|
|
||||||
{
|
|
||||||
const tag = container.find(".flag-password .value").empty();
|
|
||||||
if (channel.properties.channel_flag_password)
|
|
||||||
tag.text(tr("Yes"));
|
|
||||||
else
|
|
||||||
tag.text(tr("No"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* topic */
|
|
||||||
{
|
|
||||||
const container_tag = container.find(".topic");
|
|
||||||
const tag = container_tag.find(".value").empty();
|
|
||||||
if (channel.properties.channel_topic) {
|
|
||||||
container_tag.show();
|
|
||||||
tag.text(channel.properties.channel_topic);
|
|
||||||
} else {
|
|
||||||
container_tag.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import {BodyCreator, createModal, Modal, ModalFunctions} from "../../ui/elements/Modal";
|
|
||||||
|
|
||||||
export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: {
|
|
||||||
text_yes?: string,
|
|
||||||
text_no?: string,
|
|
||||||
|
|
||||||
closeable?: boolean;
|
|
||||||
}) : Modal {
|
|
||||||
properties = properties || {};
|
|
||||||
|
|
||||||
const props = ModalFunctions.warpProperties({});
|
|
||||||
props.template_properties || (props.template_properties = {});
|
|
||||||
props.template_properties.text_yes = properties.text_yes || tr("Yes");
|
|
||||||
props.template_properties.text_no = properties.text_no || tr("No");
|
|
||||||
props.template = "#tmpl_modal_yesno";
|
|
||||||
|
|
||||||
props.header = header;
|
|
||||||
props.template_properties.question = ModalFunctions.jqueriefy(body);
|
|
||||||
|
|
||||||
props.closeable = typeof (properties.closeable) !== "boolean" || properties.closeable;
|
|
||||||
const modal = createModal(props);
|
|
||||||
let submited = false;
|
|
||||||
const button_yes = modal.htmlTag.find(".button-yes");
|
|
||||||
const button_no = modal.htmlTag.find(".button-no");
|
|
||||||
|
|
||||||
button_yes.on('click', event => {
|
|
||||||
if (!submited) {
|
|
||||||
submited = true;
|
|
||||||
callback(true);
|
|
||||||
}
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
button_no.on('click', event => {
|
|
||||||
if (!submited) {
|
|
||||||
submited = true;
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.close_listener.push(() => button_no.trigger('click'));
|
|
||||||
modal.open();
|
|
||||||
return modal;
|
|
||||||
}
|
|
|
@ -176,10 +176,9 @@ ChannelPropertyProviders["sortingOrder"] = {
|
||||||
ChannelPropertyProviders["topic"] = SimplePropertyProvider("channel_topic", "");
|
ChannelPropertyProviders["topic"] = SimplePropertyProvider("channel_topic", "");
|
||||||
ChannelPropertyProviders["description"] = {
|
ChannelPropertyProviders["description"] = {
|
||||||
provider: async (properties, channel) => {
|
provider: async (properties, channel) => {
|
||||||
if(typeof properties.channel_description !== "undefined" && (properties.channel_description.length !== 0 || channel?.isDescriptionCached())) {
|
const description = await channel.getChannelDescription(false);
|
||||||
return properties.channel_description;
|
if(description.status === "success") {
|
||||||
} else if(channel) {
|
return description.description;
|
||||||
return await channel.getChannelDescription();
|
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
import {Registry} from "tc-events";
|
||||||
|
import {ModalChannelInfoEvents, ModalChannelInfoVariables} from "tc-shared/ui/modal/channel-info/Definitions";
|
||||||
|
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {ChannelEntry, ChannelProperties, ChannelType} from "tc-shared/tree/Channel";
|
||||||
|
import {CallOnce, ignorePromise} from "tc-shared/proto";
|
||||||
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const kChannelUpdateMapping: {[key in keyof ChannelProperties]?: (keyof ModalChannelInfoVariables)[] } = {
|
||||||
|
channel_name: [ "name" ],
|
||||||
|
channel_description: [ "description" ],
|
||||||
|
channel_topic: [ "topic" ],
|
||||||
|
channel_conversation_history_length: [ "chatMode" ],
|
||||||
|
channel_conversation_mode: [ "chatMode" ],
|
||||||
|
channel_maxclients: [ "currentClients" ],
|
||||||
|
channel_maxfamilyclients: [ "currentClients" ],
|
||||||
|
channel_flag_maxclients_unlimited: [ "currentClients" ],
|
||||||
|
channel_flag_maxfamilyclients_unlimited: [ "currentClients" ],
|
||||||
|
channel_flag_maxfamilyclients_inherited: [ "currentClients" ],
|
||||||
|
channel_codec_quality: [ "audioCodec" ],
|
||||||
|
channel_codec: [ "audioCodec" ],
|
||||||
|
channel_codec_is_unencrypted: [ "audioEncrypted" ],
|
||||||
|
channel_flag_password: [ "password" ],
|
||||||
|
channel_flag_semi_permanent: [ "type" ],
|
||||||
|
channel_flag_permanent: [ "type" ],
|
||||||
|
channel_delete_delay: [ "type" ],
|
||||||
|
channel_flag_default: [ "type" ],
|
||||||
|
}
|
||||||
|
|
||||||
|
class Controller {
|
||||||
|
readonly channel: ChannelEntry;
|
||||||
|
readonly events: Registry<ModalChannelInfoEvents>;
|
||||||
|
readonly variables: IpcUiVariableProvider<ModalChannelInfoVariables>;
|
||||||
|
|
||||||
|
private channelEvents: (() => void)[];
|
||||||
|
|
||||||
|
constructor(channel: ChannelEntry) {
|
||||||
|
this.channel = channel;
|
||||||
|
this.events = new Registry<ModalChannelInfoEvents>();
|
||||||
|
this.variables = new IpcUiVariableProvider<ModalChannelInfoVariables>();
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
|
destroy() {
|
||||||
|
this.channelEvents?.forEach(callback => callback());
|
||||||
|
this.channelEvents = undefined;
|
||||||
|
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallOnce
|
||||||
|
initialize() {
|
||||||
|
this.variables.setVariableProvider("name", () => this.channel.properties.channel_name);
|
||||||
|
this.variables.setVariableProvider("type", () => {
|
||||||
|
switch (this.channel.getChannelType()) {
|
||||||
|
case ChannelType.PERMANENT:
|
||||||
|
return "permanent";
|
||||||
|
|
||||||
|
case ChannelType.SEMI_PERMANENT:
|
||||||
|
return "semi-permanent";
|
||||||
|
|
||||||
|
case ChannelType.TEMPORARY:
|
||||||
|
return "temporary";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.variables.setVariableProvider("chatMode", () => {
|
||||||
|
if(this.channel.properties.channel_conversation_mode === 0 || this.channel.properties.channel_flag_password) {
|
||||||
|
return { mode: "private" };
|
||||||
|
} else if(this.channel.properties.channel_conversation_mode === 1) {
|
||||||
|
return { mode: "public", history: this.channel.properties.channel_conversation_history_length };
|
||||||
|
} else {
|
||||||
|
return { mode: "none" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.variables.setVariableProvider("currentClients", () => {
|
||||||
|
if(!this.channel.isSubscribed()) {
|
||||||
|
return { status: "unsubscribed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let limit;
|
||||||
|
if(!this.channel.properties.channel_flag_maxclients_unlimited) {
|
||||||
|
limit = this.channel.properties.channel_maxclients;
|
||||||
|
} else if(!this.channel.properties.channel_flag_maxfamilyclients_unlimited) {
|
||||||
|
if(this.channel.properties.channel_flag_maxfamilyclients_inherited) {
|
||||||
|
limit = "inherited";
|
||||||
|
} else {
|
||||||
|
limit = this.channel.properties.channel_maxfamilyclients;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
limit = "unlimited";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "subscribed",
|
||||||
|
online: this.channel.clients().length,
|
||||||
|
limit: limit
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.variables.setVariableProvider("audioCodec", () => ({
|
||||||
|
codec: this.channel.properties.channel_codec,
|
||||||
|
quality: this.channel.properties.channel_codec_quality
|
||||||
|
}));
|
||||||
|
this.variables.setVariableProvider("audioEncrypted", () => ({
|
||||||
|
channel: this.channel.properties.channel_codec_is_unencrypted,
|
||||||
|
server: this.channel.channelTree.server.getAudioEncryptionMode()
|
||||||
|
}));
|
||||||
|
this.variables.setVariableProvider("password", () => this.channel.properties.channel_flag_password);
|
||||||
|
this.variables.setVariableProvider("topic", () => this.channel.properties.channel_topic);
|
||||||
|
this.variables.setVariableProvider("description", async () => {
|
||||||
|
const result = _.cloneDeep(await this.channel.getChannelDescription(false));
|
||||||
|
if(result.status === "success") {
|
||||||
|
result.handlerId = this.channel.channelTree.client.handlerId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.on("action_reload_description", () => {
|
||||||
|
this.channel.clearDescriptionCache();
|
||||||
|
this.variables.sendVariable("description");
|
||||||
|
})
|
||||||
|
|
||||||
|
this.channelEvents = [];
|
||||||
|
this.channelEvents.push(this.channel.events.on("notify_properties_updated", event => {
|
||||||
|
const updatedVariables = new Set<keyof ModalChannelInfoVariables>();
|
||||||
|
for(const key of Object.keys(event.updated_properties)) {
|
||||||
|
kChannelUpdateMapping[key]?.forEach(update => updatedVariables.add(update));
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedVariables.forEach(entry => this.variables.sendVariable(entry));
|
||||||
|
}));
|
||||||
|
this.channelEvents.push(this.channel.events.on("notify_subscribe_state_changed", () => {
|
||||||
|
this.variables.sendVariable("currentClients");
|
||||||
|
}));
|
||||||
|
this.channelEvents.push(this.channel.channelTree.events.on("notify_client_enter_view", event => {
|
||||||
|
if(event.targetChannel === this.channel) {
|
||||||
|
this.variables.sendVariable("currentClients");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.channelEvents.push(this.channel.channelTree.events.on("notify_client_moved", event => {
|
||||||
|
if(event.oldChannel === this.channel || event.newChannel === this.channel) {
|
||||||
|
this.variables.sendVariable("currentClients");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.channelEvents.push(this.channel.channelTree.events.on("notify_client_leave_view", event => {
|
||||||
|
if(event.sourceChannel === this.channel) {
|
||||||
|
this.variables.sendVariable("currentClients");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnChannelInfo(channel: ChannelEntry) {
|
||||||
|
const controller = new Controller(channel);
|
||||||
|
controller.initialize();
|
||||||
|
|
||||||
|
const modal = spawnModal("channel-info", [
|
||||||
|
controller.events.generateIpcDescription(),
|
||||||
|
controller.variables.generateConsumerDescription(),
|
||||||
|
], {
|
||||||
|
popoutable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.getEvents().on("destroy", () => controller.destroy());
|
||||||
|
ignorePromise(modal.show());
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* TODO: If channel is temporary: delete delay! */
|
||||||
|
import {ServerAudioEncryptionMode} from "tc-shared/tree/ServerDefinitions";
|
||||||
|
import {ChannelDescriptionResult} from "tc-shared/tree/ChannelDefinitions";
|
||||||
|
|
||||||
|
export interface ModalChannelInfoVariables {
|
||||||
|
readonly name: string,
|
||||||
|
readonly type: "default" | "permanent" | "semi-permanent" | "temporary" | "unknown";
|
||||||
|
readonly chatMode: { mode: "private" | "none" } | { mode: "public", history: number | 0 | -1 }
|
||||||
|
readonly currentClients: { status: "subscribed", online: number, limit: number | "unlimited" | "inherited" } | { status: "unsubscribed" },
|
||||||
|
readonly audioCodec: { codec: number, quality: number },
|
||||||
|
readonly audioEncrypted: { channel: boolean, server: ServerAudioEncryptionMode },
|
||||||
|
readonly password: boolean,
|
||||||
|
readonly topic: string,
|
||||||
|
readonly description: ChannelDescriptionResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalChannelInfoEvents {
|
||||||
|
action_reload_description: {}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.windowed {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.windowed) {
|
||||||
|
min-width: 30em;
|
||||||
|
max-height: calc(100vh - 10em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 6em;
|
||||||
|
width: 10em;
|
||||||
|
|
||||||
|
margin-right: .5em;
|
||||||
|
margin-left: .5em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #557edc;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.audioEncrypted {
|
||||||
|
/* looks better */
|
||||||
|
.value {
|
||||||
|
height: 1.6em;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-height: 8em; /* description plus title */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #557edc;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.buttonCopy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-top: .1em; /* looks a bit better */
|
||||||
|
margin-left: .5em;
|
||||||
|
border-radius: .2em;
|
||||||
|
|
||||||
|
width: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
div {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #313135;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
border-radius: 0.2em;
|
||||||
|
border: 1px solid #212324;
|
||||||
|
background-color: #3a3b3f;
|
||||||
|
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
height: max-content;
|
||||||
|
min-height: 6em;
|
||||||
|
max-height: 40em;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
height: (6em / 1.25); /* min value height and a bit more */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
color: #666666;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
}
|
|
@ -0,0 +1,305 @@
|
||||||
|
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||||
|
import React, {useContext} from "react";
|
||||||
|
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||||
|
import {ModalChannelInfoEvents, ModalChannelInfoVariables} from "tc-shared/ui/modal/channel-info/Definitions";
|
||||||
|
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||||
|
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||||
|
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {tr} from "tc-shared/i18n/localize";
|
||||||
|
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
import {BBCodeRenderer} from "tc-shared/text/bbcode";
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
const VariablesContext = React.createContext<UiVariableConsumer<ModalChannelInfoVariables>>(undefined);
|
||||||
|
const EventContext = React.createContext<Registry<ModalChannelInfoEvents>>(undefined);
|
||||||
|
|
||||||
|
const kCodecNames = [
|
||||||
|
() => useTr("Speex Narrowband"),
|
||||||
|
() => useTr("Speex Wideband"),
|
||||||
|
() => useTr("Speex Ultra-Wideband"),
|
||||||
|
() => useTr("CELT Mono"),
|
||||||
|
() => useTr("Opus Voice"),
|
||||||
|
() => useTr("Opus Music")
|
||||||
|
];
|
||||||
|
|
||||||
|
const TitleRenderer = React.memo(() => {
|
||||||
|
const title = useContext(VariablesContext).useReadOnly("name");
|
||||||
|
if(title.status === "loaded") {
|
||||||
|
return (
|
||||||
|
<VariadicTranslatable text={"Channel information: {}"} key={"channel"}>
|
||||||
|
{title.value}
|
||||||
|
</VariadicTranslatable>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Translatable key={"general"}>Channel information</Translatable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const TopicRenderer = React.memo(() => {
|
||||||
|
const topic = useContext(VariablesContext).useReadOnly("topic");
|
||||||
|
if(topic.status !== "loaded" || !topic.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Topic</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
{topic.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const DescriptionRenderer = React.memo(() => {
|
||||||
|
const description = useContext(VariablesContext).useReadOnly("description");
|
||||||
|
|
||||||
|
let overlay;
|
||||||
|
let descriptionBody;
|
||||||
|
if(description.status === "loaded") {
|
||||||
|
switch(description.value.status) {
|
||||||
|
case "success":
|
||||||
|
descriptionBody = (
|
||||||
|
<BBCodeRenderer
|
||||||
|
message={description.value.description}
|
||||||
|
settings={{ convertSingleUrls: true }}
|
||||||
|
handlerId={description.value.handlerId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "empty":
|
||||||
|
overlay = <Translatable key={"no-description"}>Channel has no description</Translatable>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "no-permissions":
|
||||||
|
overlay = (
|
||||||
|
<VariadicTranslatable key={"no-permissions"} text={"No permissions to view the channel description:\n{}"}>
|
||||||
|
{description.value.failedPermission}
|
||||||
|
</VariadicTranslatable>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
default:
|
||||||
|
overlay = (
|
||||||
|
<VariadicTranslatable key={"error"} text={"Failed to query channel description:\n{}"}>
|
||||||
|
{description.value.status === "error" ? description.value.message : tr("Unknown error")}
|
||||||
|
</VariadicTranslatable>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
overlay = <React.Fragment><Translatable>Loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.description}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
<Translatable>Description</Translatable>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value} style={{ display: descriptionBody ? undefined : "none" }}>
|
||||||
|
{descriptionBody}
|
||||||
|
</div>
|
||||||
|
<div className={joinClassList(cssStyle.overlay, descriptionBody && cssStyle.hidden)}>
|
||||||
|
{overlay}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const kVariablePropertyName: {[T in keyof ModalChannelInfoVariables]?: () => React.ReactElement } = {
|
||||||
|
name: () => <Translatable>Channel Name</Translatable>,
|
||||||
|
type: () => <Translatable>Channel Type</Translatable>,
|
||||||
|
chatMode: () => <Translatable>Chat Mode</Translatable>,
|
||||||
|
currentClients: () => <Translatable>Current Clients</Translatable>,
|
||||||
|
audioCodec: () => <Translatable>Audio Codec</Translatable>,
|
||||||
|
audioEncrypted: () => <Translatable>Audio Encrypted</Translatable>,
|
||||||
|
password: () => <Translatable>Password protected</Translatable>,
|
||||||
|
topic: () => <Translatable>Topic</Translatable>,
|
||||||
|
description: () => <Translatable>Description</Translatable>
|
||||||
|
};
|
||||||
|
|
||||||
|
const PropertyRenderer = <T extends keyof ModalChannelInfoVariables>(props: {
|
||||||
|
property: T,
|
||||||
|
children: (value: ModalChannelInfoVariables[T]) => React.ReactNode | React.ReactNode[],
|
||||||
|
className?: string,
|
||||||
|
}) => {
|
||||||
|
const value = useContext(VariablesContext).useReadOnly(props.property);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.column + " " + props.className}>
|
||||||
|
<div className={cssStyle.title}>
|
||||||
|
{kVariablePropertyName[props.property]()}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.value}>
|
||||||
|
{value.status === "loaded" ? props.children(value.value) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Modal extends AbstractModal {
|
||||||
|
private readonly events: Registry<ModalChannelInfoEvents>;
|
||||||
|
private readonly variables: UiVariableConsumer<ModalChannelInfoVariables>;
|
||||||
|
|
||||||
|
constructor(events: IpcRegistryDescription<ModalChannelInfoEvents>, variables: IpcVariableDescriptor<ModalChannelInfoVariables>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = Registry.fromIpcDescription(events);
|
||||||
|
this.variables = createIpcUiVariableConsumer(variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
this.events.destroy();
|
||||||
|
this.variables.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<EventContext.Provider value={this.events}>
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
|
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<PropertyRenderer property={"type"}>
|
||||||
|
{value => {
|
||||||
|
switch (value) {
|
||||||
|
case "default":
|
||||||
|
return <Translatable key={value}>Default Channel</Translatable>;
|
||||||
|
|
||||||
|
case "permanent":
|
||||||
|
return <Translatable key={value}>Permanent</Translatable>;
|
||||||
|
|
||||||
|
case "semi-permanent":
|
||||||
|
return <Translatable key={value}>Semi-Permanent</Translatable>;
|
||||||
|
|
||||||
|
case "temporary":
|
||||||
|
return <Translatable key={value}>Temporary</Translatable>;
|
||||||
|
|
||||||
|
case "unknown":
|
||||||
|
default:
|
||||||
|
return <Translatable key={value}>Unknown</Translatable>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</PropertyRenderer>
|
||||||
|
<PropertyRenderer property={"chatMode"}>
|
||||||
|
{value => {
|
||||||
|
switch (value.mode) {
|
||||||
|
case "private":
|
||||||
|
return <Translatable key={"private"}>Private</Translatable>;
|
||||||
|
|
||||||
|
case "none":
|
||||||
|
return <Translatable key={"disabled"}>disabled</Translatable>;
|
||||||
|
|
||||||
|
case "public":
|
||||||
|
if(value.history === -1) {
|
||||||
|
return <Translatable key={"semi-permanent"}>Semi-Permanent</Translatable>;
|
||||||
|
} else if(value.history === 0) {
|
||||||
|
return <Translatable key={"permanent"}>Permanent</Translatable>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<VariadicTranslatable key={"public"} text={"Public; Saving last {} messages"}>
|
||||||
|
{value.history}
|
||||||
|
</VariadicTranslatable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return <Translatable key={"unknown"}>Unknown</Translatable>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</PropertyRenderer>
|
||||||
|
<PropertyRenderer property={"currentClients"}>
|
||||||
|
{value => {
|
||||||
|
if(value.status === "subscribed") {
|
||||||
|
let limit;
|
||||||
|
if(value.limit === "unlimited") {
|
||||||
|
limit = <Translatable key={"unlimited"}>Unlimited</Translatable>;
|
||||||
|
} else if(value.limit === "inherited") {
|
||||||
|
limit = <Translatable key={"inherited"}>Inherited</Translatable>;
|
||||||
|
} else {
|
||||||
|
limit = value.limit.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{value.online} / {limit}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else if(value.status === "unsubscribed") {
|
||||||
|
return <Translatable key={"unsubscribed"}>Not subscribed</Translatable>;
|
||||||
|
} else {
|
||||||
|
return <Translatable key={"unknown"}>Unknown</Translatable>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</PropertyRenderer>
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<PropertyRenderer property={"audioCodec"}>
|
||||||
|
{value => (
|
||||||
|
<React.Fragment>
|
||||||
|
{kCodecNames[value.codec]() || tr("Unknown")} ({value.quality})
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</PropertyRenderer>
|
||||||
|
<PropertyRenderer property={"audioEncrypted"} className={cssStyle.audioEncrypted}>
|
||||||
|
{value => {
|
||||||
|
let textValue = value.channel ? "Encrypted" : "Unencrypted";
|
||||||
|
switch(value.server) {
|
||||||
|
case "globally-on":
|
||||||
|
return (
|
||||||
|
<VariadicTranslatable text={"{}\nOverridden by the server with encrypted!"} key={"global-on"}>
|
||||||
|
{textValue}
|
||||||
|
</VariadicTranslatable>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "globally-off":
|
||||||
|
return (
|
||||||
|
<VariadicTranslatable text={"{}\nOverridden by the server with unencrypted!"} key={"global-off"}>
|
||||||
|
{textValue}
|
||||||
|
</VariadicTranslatable>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return textValue;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</PropertyRenderer>
|
||||||
|
<PropertyRenderer property={"password"}>
|
||||||
|
{value => value ? (
|
||||||
|
<Translatable key={"enabled"}>Yes</Translatable>
|
||||||
|
) : (
|
||||||
|
<Translatable key={"disabled"}>No</Translatable>
|
||||||
|
)}
|
||||||
|
</PropertyRenderer>
|
||||||
|
</div>
|
||||||
|
<TopicRenderer />
|
||||||
|
<DescriptionRenderer />
|
||||||
|
<div className={cssStyle.buttons}>
|
||||||
|
<Button color={"green"} onClick={() => this.events.fire("action_reload_description")}>
|
||||||
|
<Translatable>Refresh</Translatable>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VariablesContext.Provider>
|
||||||
|
</EventContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitle(): string | React.ReactElement {
|
||||||
|
return (
|
||||||
|
<VariablesContext.Provider value={this.variables}>
|
||||||
|
<TitleRenderer />
|
||||||
|
</VariablesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
|
@ -25,6 +25,7 @@ import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/moda
|
||||||
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||||
import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/Definitions";
|
import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/Definitions";
|
||||||
import {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
|
import {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
|
||||||
|
import {ModalChannelInfoEvents, ModalChannelInfoVariables} from "tc-shared/ui/modal/channel-info/Definitions";
|
||||||
|
|
||||||
export type ModalType = "error" | "warning" | "info" | "none";
|
export type ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -179,6 +180,10 @@ export interface ModalConstructorArguments {
|
||||||
/* events */ IpcRegistryDescription<ChannelEditEvents>,
|
/* events */ IpcRegistryDescription<ChannelEditEvents>,
|
||||||
/* isChannelCreate */ boolean
|
/* isChannelCreate */ boolean
|
||||||
],
|
],
|
||||||
|
"channel-info": [
|
||||||
|
/* events */ IpcRegistryDescription<ModalChannelInfoEvents>,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalChannelInfoVariables>
|
||||||
|
],
|
||||||
"echo-test": [
|
"echo-test": [
|
||||||
/* events */ IpcRegistryDescription<EchoTestEvents>
|
/* events */ IpcRegistryDescription<EchoTestEvents>
|
||||||
],
|
],
|
||||||
|
|
|
@ -37,6 +37,12 @@ registerModal({
|
||||||
popoutSupported: false /* TODO: Needs style fixing */
|
popoutSupported: false /* TODO: Needs style fixing */
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerModal({
|
||||||
|
modalId: "channel-info",
|
||||||
|
classLoader: async () => await import("tc-shared/ui/modal/channel-info/Renderer"),
|
||||||
|
popoutSupported: true
|
||||||
|
});
|
||||||
|
|
||||||
registerModal({
|
registerModal({
|
||||||
modalId: "echo-test",
|
modalId: "echo-test",
|
||||||
classLoader: async () => await import("tc-shared/ui/modal/echo-test/Renderer"),
|
classLoader: async () => await import("tc-shared/ui/modal/echo-test/Renderer"),
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit d75b0292e8726646867967c8b35cbb52c1580769
|
Subproject commit e61434dc048c5de71c31ca3a11f7db97217b0231
|
Loading…
Reference in New Issue