Using React for the client channel and a lot of performance updates related to the channel tree
parent
58dc5da34f
commit
7b261d6f15
16
ChangeLog.md
16
ChangeLog.md
|
@ -1,12 +1,22 @@
|
|||
# Changelog:
|
||||
* **11.03.20**
|
||||
* **18.04.20**
|
||||
- Recoded the channel tree using React
|
||||
- Heavily improved channel tree performance on large servers (fluent scroll & updates)
|
||||
- Automatically scroll to channel tree selection
|
||||
- Fixed client speak indicator
|
||||
- Fixed the message unread indicator only shows up after the second message (as well increase visibility)
|
||||
- Fixed the invalid initialisation of codec workers
|
||||
- Improved context menu subcontainer selection
|
||||
- Fixed client channel permission tab within the permission editor (previously you've been kick from the server)
|
||||
|
||||
* **11.04.20**
|
||||
- Only show the host message when its not empty
|
||||
|
||||
* **10.03.20**
|
||||
* **10.04.20**
|
||||
- Improved key code displaying
|
||||
- Added a keymap system (Hotkeys)
|
||||
|
||||
* **09.03.20**
|
||||
* **09.04.20**
|
||||
- Using React for the client control bar
|
||||
- Saving last away state and message
|
||||
- Saving last query show state
|
||||
|
|
4
file.ts
4
file.ts
|
@ -676,7 +676,7 @@ namespace server {
|
|||
handle_api_request(request, response, url);
|
||||
return;
|
||||
} else if(url.pathname === "/") {
|
||||
url.pathname = "/index.php";
|
||||
url.pathname = "/index.html";
|
||||
}
|
||||
serve_file(url.pathname, url.query, response);
|
||||
}
|
||||
|
@ -688,7 +688,7 @@ namespace watcher {
|
|||
return cp.spawn(process.env.comspec, ["/C", cmd, ...args], {
|
||||
stdio: "pipe",
|
||||
cwd: __dirname,
|
||||
env: process.env
|
||||
env: Object.assign({ NODE_ENV: "development" }, process.env)
|
||||
});
|
||||
else
|
||||
return cp.spawn(cmd, args, {
|
||||
|
|
|
@ -5,3 +5,5 @@ window["loader"] = loader_base;
|
|||
setTimeout(loader.run, 0);
|
||||
|
||||
export {};
|
||||
|
||||
//window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
|
|
@ -242,6 +242,16 @@
|
|||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"@types/loader-utils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/loader-utils/-/loader-utils-1.1.3.tgz",
|
||||
"integrity": "sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/webpack": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.149",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
||||
|
@ -8421,6 +8431,11 @@
|
|||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
|
||||
|
|
|
@ -13,12 +13,10 @@
|
|||
"csso": "csso",
|
||||
"tsc": "tsc",
|
||||
"start": "npm run compile-project-base && node file.js ndevelop",
|
||||
|
||||
"build-web": "webpack --config webpack-web.config.js",
|
||||
"develop-web": "npm run compile-project-base && node file.js develop web",
|
||||
"build-client": "webpack --config webpack-client.config.js",
|
||||
"develop-client": "npm run compile-project-base && node file.js develop client",
|
||||
|
||||
"webpack-web": "webpack --config webpack-web.config.js",
|
||||
"webpack-client": "webpack --config webpack-client.config.js",
|
||||
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js"
|
||||
|
@ -33,6 +31,7 @@
|
|||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/html-minifier": "^3.5.3",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/loader-utils": "^1.1.3",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/node": "^12.7.2",
|
||||
|
@ -84,6 +83,7 @@
|
|||
"moment": "^2.24.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"webrtc-adapter": "^7.5.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,8 @@
|
|||
}
|
||||
|
||||
.sub-container {
|
||||
padding-right: 3px;
|
||||
margin-right: -3px;
|
||||
padding-right: 24px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
|
@ -85,7 +86,7 @@
|
|||
left: 100%;
|
||||
top: -4px;
|
||||
position: absolute;
|
||||
margin-left: 3px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1155,6 +1155,8 @@ $bot_thumbnail_height: 9em;
|
|||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
|
|
@ -807,7 +807,6 @@ export class ConnectionHandler {
|
|||
}
|
||||
|
||||
resize_elements() {
|
||||
this.channelTree.handle_resized();
|
||||
this.invoke_resized_on_activate = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import * as log from "tc-shared/log";
|
||||
import * as hex from "tc-shared/crypto/hex";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as hex from "tc-shared/crypto/hex";
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {format_time} from "tc-shared/ui/frames/chat";
|
||||
|
||||
export class FileEntry {
|
||||
name: string;
|
||||
|
@ -130,7 +132,7 @@ export class RequestFileUpload implements UploadTransfer {
|
|||
throw "invalid size";
|
||||
form_data.append("file", new Blob([data], { type: "application/octet-stream" }));
|
||||
} else {
|
||||
const buffer = <BufferSource>data;
|
||||
const buffer = data as BufferSource;
|
||||
if(buffer.byteLength != this.transfer_key.total_size)
|
||||
throw "invalid size";
|
||||
|
||||
|
@ -416,11 +418,6 @@ export class FileManager extends AbstractCommandHandler {
|
|||
}
|
||||
}
|
||||
|
||||
export class Icon {
|
||||
id: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export enum ImageType {
|
||||
UNKNOWN,
|
||||
BITMAP,
|
||||
|
@ -552,34 +549,228 @@ export class CacheManager {
|
|||
}
|
||||
}
|
||||
|
||||
export class IconManager {
|
||||
private static cache: CacheManager = new CacheManager("icons");
|
||||
const icon_cache: CacheManager = new CacheManager("icons");
|
||||
export interface IconManagerEvents {
|
||||
notify_icon_state_changed: {
|
||||
icon_id: number,
|
||||
server_unique_id: string,
|
||||
|
||||
icon: LocalIcon
|
||||
},
|
||||
}
|
||||
|
||||
//TODO: Invalidate icon after certain time if loading has failed and try to redownload (only if an icon loader has been set!)
|
||||
type IconLoader = (icon?: LocalIcon) => Promise<Response>;
|
||||
export class LocalIcon {
|
||||
readonly icon_id: number;
|
||||
readonly server_unique_id: string;
|
||||
readonly status_change_callbacks: ((icon?: LocalIcon) => void)[] = [];
|
||||
|
||||
status: "loading" | "loaded" | "empty" | "error" | "destroyed";
|
||||
|
||||
loaded_url?: string;
|
||||
error_message?: string;
|
||||
|
||||
private callback_icon_loader: IconLoader;
|
||||
|
||||
constructor(id: number, server: string, loader_or_response: Response | IconLoader | undefined) {
|
||||
this.icon_id = id;
|
||||
this.server_unique_id = server;
|
||||
|
||||
if(id >= 0 && id <= 1000) {
|
||||
/* Internal TeaSpeak icons. These must be handled differently! */
|
||||
this.status = "loaded";
|
||||
} else {
|
||||
this.status = "loading";
|
||||
if(loader_or_response instanceof Response) {
|
||||
this.set_image(loader_or_response).catch(error => {
|
||||
log.error(LogCategory.GENERAL, tr("Icon set image method threw an unexpected error: %o"), error);
|
||||
this.status = "error";
|
||||
this.error_message = "unexpected parse error";
|
||||
this.triggerStatusChange();
|
||||
});
|
||||
} else {
|
||||
this.callback_icon_loader = loader_or_response;
|
||||
this.load().catch(error => {
|
||||
log.error(LogCategory.GENERAL, tr("Icon load method threw an unexpected error: %o"), error);
|
||||
this.status = "error";
|
||||
this.error_message = "unexpected load error";
|
||||
this.triggerStatusChange();
|
||||
}).then(() => {
|
||||
this.callback_icon_loader = undefined; /* release resources captured by possible closures */
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private triggerStatusChange() {
|
||||
for(const lister of this.status_change_callbacks.slice(0))
|
||||
lister(this);
|
||||
}
|
||||
|
||||
/* called within the CachedIconManager */
|
||||
protected destroy() {
|
||||
if(typeof this.loaded_url === "string" && URL.revokeObjectURL)
|
||||
URL.revokeObjectURL(this.loaded_url);
|
||||
|
||||
this.status = "destroyed";
|
||||
this.loaded_url = undefined;
|
||||
this.error_message = undefined;
|
||||
|
||||
this.triggerStatusChange();
|
||||
this.status_change_callbacks.splice(0, this.status_change_callbacks.length);
|
||||
}
|
||||
|
||||
private async load() {
|
||||
if(!icon_cache.setupped())
|
||||
await icon_cache.setup();
|
||||
|
||||
let response = await icon_cache.resolve_cached("icon_" + this.server_unique_id + "_" + this.icon_id); //TODO age!
|
||||
if(!response) {
|
||||
if(typeof this.callback_icon_loader !== "function") {
|
||||
this.status = "empty";
|
||||
this.triggerStatusChange();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
response = await this.callback_icon_loader(this);
|
||||
} catch (error) {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to download icon %d: %o"), this.icon_id, error);
|
||||
await this.set_error(typeof error === "string" ? error : tr("Failed to load icon"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.set_image(response);
|
||||
} catch (error) {
|
||||
log.error(LogCategory.GENERAL, tr("Failed to update icon image for icon %d: %o"), this.icon_id, error);
|
||||
await this.set_error(typeof error === "string" ? error : tr("Failed to update icon from downloaded file"));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded_url = await response_to_url(response);
|
||||
this.status = "loaded";
|
||||
this.triggerStatusChange();
|
||||
}
|
||||
|
||||
async set_image(response: Response) {
|
||||
if(this.icon_id >= 0 && this.icon_id <= 1000) throw "Could not set image for internal icon";
|
||||
|
||||
const type = image_type(response.headers.get('X-media-bytes'));
|
||||
if(type === ImageType.UNKNOWN) throw "unknown image type";
|
||||
|
||||
const media = media_image_type(type);
|
||||
await icon_cache.put_cache("icon_" + this.server_unique_id + "_" + this.icon_id, response.clone(), "image/" + media);
|
||||
|
||||
this.loaded_url = await response_to_url(response);
|
||||
this.status = "loaded";
|
||||
this.triggerStatusChange();
|
||||
}
|
||||
|
||||
set_error(error: string) {
|
||||
if(this.status === "loaded" || this.status === "destroyed") return;
|
||||
if(this.status === "error" && this.error_message === error) return;
|
||||
this.status = "error";
|
||||
this.error_message = error;
|
||||
this.triggerStatusChange();
|
||||
}
|
||||
|
||||
async await_loading() {
|
||||
await new Promise(resolve => {
|
||||
if(this.status !== "loading") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const callback = () => {
|
||||
if(this.status === "loading") return;
|
||||
|
||||
this.status_change_callbacks.remove(callback);
|
||||
resolve();
|
||||
};
|
||||
this.status_change_callbacks.push(callback);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function response_to_url(response: Response) {
|
||||
if(!response.headers.has('X-media-bytes'))
|
||||
throw "missing media bytes";
|
||||
|
||||
const type = image_type(response.headers.get('X-media-bytes'));
|
||||
const media = media_image_type(type);
|
||||
|
||||
const blob = await response.blob();
|
||||
if(blob.type !== "image/" + media)
|
||||
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
|
||||
else
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
class CachedIconManager {
|
||||
private loaded_icons: {[id: string]:LocalIcon} = {};
|
||||
|
||||
async clear_cache() {
|
||||
await icon_cache.reset();
|
||||
this.clear_memory_cache();
|
||||
}
|
||||
|
||||
clear_memory_cache() {
|
||||
for(const icon_id of Object.keys(this.loaded_icons))
|
||||
this.loaded_icons[icon_id]["destroy"]();
|
||||
this.loaded_icons = {};
|
||||
}
|
||||
|
||||
load_icon(id: number, server_unique_id: string, fallback_load?: IconLoader) : LocalIcon {
|
||||
const cache_id = server_unique_id + "_" + (id >>> 0);
|
||||
if(this.loaded_icons[cache_id]) return this.loaded_icons[cache_id];
|
||||
|
||||
return (this.loaded_icons[cache_id] = new LocalIcon(id >>> 0, server_unique_id, fallback_load));
|
||||
}
|
||||
|
||||
async put_icon(id: number, server_unique_id: string, icon: Response) {
|
||||
const cache_id = server_unique_id + "_" + (id >>> 0);
|
||||
if(this.loaded_icons[cache_id])
|
||||
await this.loaded_icons[cache_id].set_image(icon);
|
||||
else {
|
||||
const licon = this.loaded_icons[cache_id] = new LocalIcon(id >>> 0, server_unique_id, icon);
|
||||
await new Promise((resolve, reject) => {
|
||||
const cb = () => {
|
||||
licon.status_change_callbacks.remove(cb);
|
||||
if(licon.status === "loaded")
|
||||
resolve();
|
||||
else
|
||||
reject(licon.status === "error" ? licon.error_message || tr("Unknown error") : tr("Invalid status"));
|
||||
};
|
||||
|
||||
licon.status_change_callbacks.push(cb);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
export const icon_cache_loader = new CachedIconManager();
|
||||
window.addEventListener("beforeunload", () => {
|
||||
icon_cache_loader.clear_memory_cache();
|
||||
});
|
||||
|
||||
type IconManagerLoadingData = {
|
||||
result: "success" | "error" | "unset";
|
||||
next_retry?: number;
|
||||
error?: string;
|
||||
}
|
||||
export class IconManager {
|
||||
handle: FileManager;
|
||||
private _id_urls: {[id:number]:string} = {};
|
||||
private _loading_promises: {[id:number]:Promise<Icon>} = {};
|
||||
readonly events: Registry<IconManagerEvents>;
|
||||
private loading_timestamps: {[key: number]: IconManagerLoadingData} = {};
|
||||
|
||||
constructor(handle: FileManager) {
|
||||
this.handle = handle;
|
||||
this.events = new Registry<IconManagerEvents>();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(URL.revokeObjectURL) {
|
||||
for(const id of Object.keys(this._id_urls))
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
}
|
||||
this._id_urls = undefined;
|
||||
this._loading_promises = undefined;
|
||||
}
|
||||
|
||||
async clear_cache() {
|
||||
await IconManager.cache.reset();
|
||||
if(URL.revokeObjectURL) {
|
||||
for(const id of Object.keys(this._id_urls))
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
}
|
||||
this._id_urls = {};
|
||||
this._loading_promises = {};
|
||||
this.loading_timestamps = {};
|
||||
}
|
||||
|
||||
async delete_icon(id: number) : Promise<void> {
|
||||
|
@ -599,83 +790,31 @@ export class IconManager {
|
|||
return this.handle.download_file("", "/icon_" + id);
|
||||
}
|
||||
|
||||
private static async _response_url(response: Response) {
|
||||
if(!response.headers.has('X-media-bytes'))
|
||||
throw "missing media bytes";
|
||||
|
||||
const type = image_type(response.headers.get('X-media-bytes'));
|
||||
const media = media_image_type(type);
|
||||
|
||||
const blob = await response.blob();
|
||||
if(blob.type !== "image/" + media)
|
||||
return URL.createObjectURL(blob.slice(0, blob.size, "image/" + media));
|
||||
else
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
async resolved_cached?(id: number) : Promise<Icon> {
|
||||
if(this._id_urls[id])
|
||||
return {
|
||||
id: id,
|
||||
url: this._id_urls[id]
|
||||
};
|
||||
|
||||
if(!IconManager.cache.setupped())
|
||||
await IconManager.cache.setup();
|
||||
|
||||
const response = await IconManager.cache.resolve_cached('icon_' + id); //TODO age!
|
||||
if(response) {
|
||||
const url = await IconManager._response_url(response);
|
||||
if(this._id_urls[id])
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
return {
|
||||
id: id,
|
||||
url: url
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static _static_id_url: {[icon: number]:string} = {};
|
||||
private static _static_cached_promise: {[icon: number]:Promise<Icon>} = {};
|
||||
static load_cached_icon(id: number, ignore_age?: boolean) : Promise<Icon> | Icon {
|
||||
if(this._static_id_url[id]) {
|
||||
return {
|
||||
id: id,
|
||||
url: this._static_id_url[id]
|
||||
};
|
||||
}
|
||||
|
||||
if(this._static_cached_promise[id])
|
||||
return this._static_cached_promise[id];
|
||||
|
||||
return (this._static_cached_promise[id] = (async () => {
|
||||
if(!this.cache.setupped())
|
||||
await this.cache.setup();
|
||||
|
||||
const response = await this.cache.resolve_cached('icon_' + id); //TODO age!
|
||||
if(response) {
|
||||
const url = await this._response_url(response);
|
||||
if(this._static_id_url[id])
|
||||
URL.revokeObjectURL(this._static_id_url[id]);
|
||||
this._static_id_url[id] = url;
|
||||
|
||||
return {
|
||||
id: id,
|
||||
url: url
|
||||
};
|
||||
private async server_icon_loader(icon: LocalIcon) : Promise<Response> {
|
||||
const loading_data: IconManagerLoadingData = this.loading_timestamps[icon.icon_id] || (this.loading_timestamps[icon.icon_id] = { result: "unset" });
|
||||
if(loading_data.result === "error") {
|
||||
if(!loading_data.next_retry || loading_data.next_retry > Date.now()) {
|
||||
log.debug(LogCategory.GENERAL, tr("Don't retry icon download from server. We'll try again in %s"),
|
||||
!loading_data.next_retry ? tr("never") : format_time(loading_data.next_retry - Date.now(), tr("1 second")));
|
||||
throw loading_data.error;
|
||||
}
|
||||
})());
|
||||
}
|
||||
}
|
||||
|
||||
private async _load_icon(id: number) : Promise<Icon> {
|
||||
try {
|
||||
let download_key: DownloadKey;
|
||||
try {
|
||||
download_key = await this.create_icon_download(id);
|
||||
download_key = await this.create_icon_download(icon.icon_id);
|
||||
} catch(error) {
|
||||
log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), id, error);
|
||||
throw "Failed to request icon";
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorID.FILE_NOT_FOUND)
|
||||
throw tr("Icon could not be found");
|
||||
else if(error.id === ErrorID.PERMISSION_ERROR)
|
||||
throw tr("No permissions to download icon");
|
||||
else
|
||||
throw error.extra_message || error.message;
|
||||
}
|
||||
log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), icon.icon_id, error);
|
||||
throw typeof error === "string" ? error : tr("Failed to initialize icon download");
|
||||
}
|
||||
|
||||
const downloader = spawn_download_transfer(download_key);
|
||||
|
@ -683,58 +822,21 @@ export class IconManager {
|
|||
try {
|
||||
response = await downloader.request_file();
|
||||
} catch(error) {
|
||||
log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), id, error);
|
||||
log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), icon.icon_id, error);
|
||||
throw "failed to download icon";
|
||||
}
|
||||
|
||||
const type = image_type(response.headers.get('X-media-bytes'));
|
||||
const media = media_image_type(type);
|
||||
|
||||
await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
|
||||
const url = await IconManager._response_url(response.clone());
|
||||
if(this._id_urls[id])
|
||||
URL.revokeObjectURL(this._id_urls[id]);
|
||||
this._id_urls[id] = url;
|
||||
|
||||
this._loading_promises[id] = undefined;
|
||||
return {
|
||||
id: id,
|
||||
url: url
|
||||
};
|
||||
} catch(error) {
|
||||
setTimeout(() => {
|
||||
this._loading_promises[id] = undefined;
|
||||
}, 1000 * 60); /* try again in 60 seconds */
|
||||
loading_data.result = "success";
|
||||
return response;
|
||||
} catch (error) {
|
||||
loading_data.result = "error";
|
||||
loading_data.error = error as string;
|
||||
loading_data.next_retry = Date.now() + 300 * 1000;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
download_icon(id: number) : Promise<Icon> {
|
||||
return this._loading_promises[id] || (this._loading_promises[id] = this._load_icon(id));
|
||||
}
|
||||
|
||||
async resolve_icon(id: number) : Promise<Icon> {
|
||||
id = id >>> 0;
|
||||
try {
|
||||
const result = await this.resolved_cached(id);
|
||||
if(result)
|
||||
return result;
|
||||
throw "";
|
||||
} catch(error) { }
|
||||
|
||||
try {
|
||||
const result = await this.download_icon(id);
|
||||
if(result)
|
||||
return result;
|
||||
throw "load result is empty";
|
||||
} catch(error) {
|
||||
log.error(LogCategory.CLIENT, tr("Icon download failed of icon %d: %o"), id, error);
|
||||
}
|
||||
|
||||
throw "icon not found";
|
||||
}
|
||||
|
||||
static generate_tag(icon: Promise<Icon> | Icon | undefined, options?: {
|
||||
static generate_tag(icon: LocalIcon | undefined, options?: {
|
||||
animate?: boolean
|
||||
}) : JQuery<HTMLDivElement> {
|
||||
options = options || {};
|
||||
|
@ -743,42 +845,50 @@ export class IconManager {
|
|||
let icon_load_image = $.spawn("div").addClass("icon_loading");
|
||||
|
||||
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
|
||||
const _apply = (icon) => {
|
||||
let id = icon ? (icon.id >>> 0) : 0;
|
||||
if (!icon || id == 0) {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
return;
|
||||
} else if (id < 1000) {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
|
||||
icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + id);
|
||||
return;
|
||||
}
|
||||
|
||||
icon_image.attr("src", icon.url);
|
||||
icon_container.append(icon_image).removeClass("icon_empty");
|
||||
|
||||
if (typeof (options.animate) !== "boolean" || options.animate) {
|
||||
icon_image.css("opacity", 0);
|
||||
|
||||
icon_load_image.animate({opacity: 0}, 50, function () {
|
||||
icon_load_image.remove();
|
||||
icon_image.animate({opacity: 1}, 150);
|
||||
});
|
||||
} else {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
}
|
||||
};
|
||||
if(icon instanceof Promise) {
|
||||
icon.then(_apply).catch(error => {
|
||||
log.error(LogCategory.CLIENT, tr("Could not load icon. Reason: %s"), error);
|
||||
icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon");
|
||||
});
|
||||
if (icon.icon_id == 0) {
|
||||
icon_load_image = undefined;
|
||||
} else if (icon.icon_id < 1000) {
|
||||
icon_load_image = undefined;
|
||||
icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + icon.icon_id);
|
||||
} else {
|
||||
_apply(icon as Icon);
|
||||
const loading_done = sync => {//TODO: Show error?
|
||||
if(icon.status === "empty") {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
} else if(icon.status === "error") {
|
||||
//TODO: Error icon?
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
} else {
|
||||
icon_image.attr("src", icon.loaded_url);
|
||||
icon_container.append(icon_image).removeClass("icon_empty");
|
||||
|
||||
if (!sync && (typeof (options.animate) !== "boolean" || options.animate)) {
|
||||
icon_image.css("opacity", 0);
|
||||
|
||||
icon_load_image.animate({opacity: 0}, 50, function () {
|
||||
icon_load_image.remove();
|
||||
icon_image.animate({opacity: 1}, 150);
|
||||
});
|
||||
} else {
|
||||
icon_load_image.remove();
|
||||
icon_load_image = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(icon.status !== "loading")
|
||||
loading_done(true);
|
||||
else {
|
||||
const cb = () => {
|
||||
if(icon.status === "loading") return;
|
||||
|
||||
icon.status_change_callbacks.remove(cb);
|
||||
loading_done(false);
|
||||
};
|
||||
icon.status_change_callbacks.push(cb);
|
||||
}
|
||||
}
|
||||
|
||||
if(icon_load_image)
|
||||
|
@ -790,19 +900,20 @@ export class IconManager {
|
|||
animate?: boolean
|
||||
}) : JQuery<HTMLDivElement> {
|
||||
options = options || {};
|
||||
return IconManager.generate_tag(this.load_icon(id), options);
|
||||
}
|
||||
|
||||
id = id >>> 0;
|
||||
if(id == 0 || !id)
|
||||
return IconManager.generate_tag({id: id, url: ""}, options);
|
||||
else if(id < 1000)
|
||||
return IconManager.generate_tag({id: id, url: ""}, options);
|
||||
|
||||
|
||||
if(this._id_urls[id]) {
|
||||
return IconManager.generate_tag({id: id, url: this._id_urls[id]}, options);
|
||||
} else {
|
||||
return IconManager.generate_tag(this.resolve_icon(id), options);
|
||||
load_icon(id: number) : LocalIcon {
|
||||
const server_uid = this.handle.handle.channelTree.server.properties.virtualserver_unique_identifier;
|
||||
let icon = icon_cache_loader.load_icon(id, server_uid, this.server_icon_loader.bind(this));
|
||||
if(icon.status !== "loading" && icon.status !== "loaded") {
|
||||
this.server_icon_loader(icon).then(response => {
|
||||
return icon.set_image(response);
|
||||
}).catch(error => {
|
||||
console.warn("Failed to update broken cached icon from server: %o", error);
|
||||
})
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -818,7 +929,7 @@ export class AvatarManager {
|
|||
|
||||
private static cache: CacheManager;
|
||||
private _cached_avatars: {[response_avatar_id:number]:Avatar} = {};
|
||||
private _loading_promises: {[response_avatar_id:number]:Promise<Icon>} = {};
|
||||
private _loading_promises: {[response_avatar_id:number]:Promise<any>} = {};
|
||||
|
||||
constructor(handle: FileManager) {
|
||||
this.handle = handle;
|
|
@ -68,6 +68,7 @@ export interface Bookmark {
|
|||
connect_profile: string;
|
||||
|
||||
last_icon_id?: number;
|
||||
last_icon_server_id?: string;
|
||||
}
|
||||
|
||||
export interface DirectoryBookmark {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {spawnPoke} from "tc-shared/ui/modal/ModalPoke";
|
|||
import {PrivateConversationState} from "tc-shared/ui/frames/side/private_conversations";
|
||||
import {Conversation} from "tc-shared/ui/frames/side/conversations";
|
||||
import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
||||
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
|
||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
constructor(connection: AbstractServerConnection) {
|
||||
|
@ -137,7 +138,13 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
handle_command(command: ServerCommand) : boolean {
|
||||
if(this[command.command]) {
|
||||
this[command.command](command.arguments);
|
||||
/* batch all updates the command applies to the channel tree */
|
||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
try {
|
||||
this[command.command](command.arguments);
|
||||
} finally {
|
||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -297,24 +304,25 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
private createChannelFromJson(json, ignoreOrder: boolean = false) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
|
||||
let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"], tree.findChannel(json["cpid"]));
|
||||
tree.insertChannel(channel);
|
||||
let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"]);
|
||||
let parent, previous;
|
||||
if(json["channel_order"] !== "0") {
|
||||
let prev = tree.findChannel(json["channel_order"]);
|
||||
if(!prev && json["channel_order"] != 0) {
|
||||
previous = tree.findChannel(json["channel_order"]);
|
||||
if(!previous && json["channel_order"] != 0) {
|
||||
if(!ignoreOrder) {
|
||||
log.error(LogCategory.NETWORKING, tr("Invalid channel order id!"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let parent = tree.findChannel(json["cpid"]);
|
||||
if(!parent && json["cpid"] != 0) {
|
||||
log.error(LogCategory.NETWORKING, tr("Invalid channel parent"));
|
||||
return;
|
||||
}
|
||||
tree.moveChannel(channel, prev, parent); //TODO test if channel exists!
|
||||
}
|
||||
|
||||
parent = tree.findChannel(json["cpid"]);
|
||||
if(!parent && json["cpid"] != 0) {
|
||||
log.error(LogCategory.NETWORKING, tr("Invalid channel parent"));
|
||||
return;
|
||||
}
|
||||
|
||||
tree.insertChannel(channel, previous, parent);
|
||||
if(ignoreOrder) {
|
||||
for(let ch of tree.channels) {
|
||||
if(ch.properties.channel_order == channel.channelId) {
|
||||
|
@ -340,16 +348,30 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
channel.updateVariables(...updates);
|
||||
}
|
||||
|
||||
private batch_update_finished_timeout;
|
||||
handleCommandChannelList(json) {
|
||||
this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */
|
||||
log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length);
|
||||
if(this.batch_update_finished_timeout) {
|
||||
clearTimeout(this.batch_update_finished_timeout);
|
||||
this.batch_update_finished_timeout = 0;
|
||||
/* batch update is still active */
|
||||
} else {
|
||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
}
|
||||
|
||||
for(let index = 0; index < json.length; index++)
|
||||
this.createChannelFromJson(json[index], true);
|
||||
|
||||
this.batch_update_finished_timeout = setTimeout(() => {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
handleCommandChannelListFinished(json) {
|
||||
this.connection.client.channelTree.show_channel_tree();
|
||||
if(this.batch_update_finished_timeout) {
|
||||
clearTimeout(this.batch_update_finished_timeout);
|
||||
this.batch_update_finished_timeout = 0;
|
||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
}
|
||||
}
|
||||
|
||||
handleCommandChannelCreate(json) {
|
||||
|
@ -795,7 +817,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
||||
const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||
if(client) /* the client itself might be invisible */
|
||||
client.flag_text_unread = conversation.is_unread();
|
||||
client.setUnread(conversation.is_unread());
|
||||
} else {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
||||
}
|
||||
|
@ -822,7 +844,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
message: json["msg"]
|
||||
});
|
||||
if(conversation.is_unread() && channel)
|
||||
channel.flag_text_unread = true;
|
||||
channel.setUnread(true);
|
||||
} else if(mode == 3) {
|
||||
this.connection_handler.log.log(server_log.Type.GLOBAL_MESSAGE, {
|
||||
message: json["msg"],
|
||||
|
@ -844,7 +866,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
|
||||
message: json["msg"]
|
||||
});
|
||||
this.connection_handler.channelTree.server.flag_text_unread = conversation.is_unread();
|
||||
this.connection_handler.channelTree.server.setUnread(conversation.is_unread());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1000,14 +1022,19 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}
|
||||
|
||||
handleNotifyChannelSubscribed(json) {
|
||||
for(const entry of json) {
|
||||
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
|
||||
if(!channel) {
|
||||
console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']);
|
||||
continue;
|
||||
}
|
||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
try {
|
||||
for(const entry of json) {
|
||||
const channel = this.connection.client.channelTree.findChannel(parseInt(entry["cid"]));
|
||||
if(!channel) {
|
||||
console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']);
|
||||
continue;
|
||||
}
|
||||
|
||||
channel.flag_subscribed = true;
|
||||
channel.flag_subscribed = true;
|
||||
}
|
||||
} finally {
|
||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export enum ErrorID {
|
|||
PLAYLIST_IS_IN_USE = 0x2103,
|
||||
|
||||
FILE_ALREADY_EXISTS = 2050,
|
||||
FILE_NOT_FOUND = 2051,
|
||||
|
||||
CLIENT_INVALID_ID = 0x0200,
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import * as React from "react";
|
||||
|
@ -74,8 +74,8 @@ export class Registry<Events> {
|
|||
}
|
||||
}
|
||||
|
||||
off<T extends keyof Events>(handler: (event?: Event<Events, T>) => void);
|
||||
off<T extends keyof Events>(event: T, handler: (event?: Event<Events, T>) => void);
|
||||
off<T extends keyof Events>(handler: (event?) => void);
|
||||
off<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
|
||||
off(event: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
|
||||
off(handler_or_events, handler?) {
|
||||
if(typeof handler_or_events === "function") {
|
||||
|
@ -107,36 +107,49 @@ export class Registry<Events> {
|
|||
this.connections[event].remove(target as any);
|
||||
}
|
||||
|
||||
fire<T extends keyof Events>(event_type: T, data?: Events[T]) {
|
||||
fire<T extends keyof Events>(event_type: T, data?: Events[T], overrideTypeKey?: boolean) {
|
||||
if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type);
|
||||
|
||||
if(typeof data === "object" && 'type' in data) throw tr("The keyword 'type' is reserved for the event type and should not be passed as argument");
|
||||
if(typeof data === "object" && 'type' in data && !overrideTypeKey) {
|
||||
if((data as any).type !== event_type) {
|
||||
debugger;
|
||||
throw tr("The keyword 'type' is reserved for the event type and should not be passed as argument");
|
||||
}
|
||||
}
|
||||
const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, {
|
||||
type: event_type,
|
||||
as: function () { return this; }
|
||||
});
|
||||
|
||||
this.fire_event(event_type as string, event);
|
||||
}
|
||||
|
||||
private fire_event(type: string, data: any) {
|
||||
let invoke_count = 0;
|
||||
for(const handler of (this.handler[event_type as string] || [])) {
|
||||
handler(event);
|
||||
for(const handler of (this.handler[type]?.slice(0) || [])) {
|
||||
handler(data);
|
||||
invoke_count++;
|
||||
|
||||
const reg_data = handler[this.registry_uuid];
|
||||
if(typeof reg_data === "object" && reg_data.singleshot)
|
||||
this.handler[event_type as string].remove(handler);
|
||||
this.handler[type].remove(handler);
|
||||
}
|
||||
|
||||
for(const evhandler of (this.connections[event_type as string] || [])) {
|
||||
evhandler.fire(event_type as any, event as any);
|
||||
for(const evhandler of (this.connections[type]?.slice(0) || [])) {
|
||||
evhandler.fire_event(type, data);
|
||||
invoke_count++;
|
||||
}
|
||||
if(invoke_count === 0) {
|
||||
console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debug_prefix, event_type);
|
||||
console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debug_prefix, type);
|
||||
}
|
||||
}
|
||||
|
||||
fire_async<T extends keyof Events>(event_type: T, data?: Events[T]) {
|
||||
setTimeout(() => this.fire(event_type, data));
|
||||
fire_async<T extends keyof Events>(event_type: T, data?: Events[T], callback?: () => void) {
|
||||
setTimeout(() => {
|
||||
this.fire(event_type, data);
|
||||
if(typeof callback === "function")
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -227,31 +240,6 @@ export function ReactEventHandler<ObjectClass = React.Component<any, any>, Event
|
|||
}
|
||||
}
|
||||
|
||||
export namespace channel_tree {
|
||||
export interface client {
|
||||
"enter_view": {},
|
||||
"left_view": {},
|
||||
|
||||
"property_update": {
|
||||
properties: string[]
|
||||
},
|
||||
|
||||
"music_status_update": {
|
||||
player_buffered_index: number,
|
||||
player_replay_index: number
|
||||
},
|
||||
"music_song_change": {
|
||||
"song": SongInfo
|
||||
},
|
||||
|
||||
/* TODO: Move this out of the music bots interface? */
|
||||
"playlist_song_add": { song: PlaylistSong },
|
||||
"playlist_song_remove": { song_id: number },
|
||||
"playlist_song_reorder": { song_id: number, previous_song_id: number },
|
||||
"playlist_song_loaded": { song_id: number, success: boolean, error_msg?: string, metadata?: string },
|
||||
}
|
||||
}
|
||||
|
||||
export namespace sidebar {
|
||||
export interface music {
|
||||
"open": {}, /* triggers when frame should be shown */
|
||||
|
@ -289,13 +277,13 @@ export namespace sidebar {
|
|||
"reorder_begin": { song_id: number; entry: JQuery },
|
||||
"reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number },
|
||||
|
||||
"player_time_update": channel_tree.client["music_status_update"],
|
||||
"player_song_change": channel_tree.client["music_song_change"],
|
||||
"player_time_update": ClientEvents["music_status_update"],
|
||||
"player_song_change": ClientEvents["music_song_change"],
|
||||
|
||||
"playlist_song_add": channel_tree.client["playlist_song_add"] & { insert_effect?: boolean },
|
||||
"playlist_song_remove": channel_tree.client["playlist_song_remove"],
|
||||
"playlist_song_reorder": channel_tree.client["playlist_song_reorder"],
|
||||
"playlist_song_loaded": channel_tree.client["playlist_song_loaded"] & { html_entry?: JQuery },
|
||||
"playlist_song_add": ClientEvents["playlist_song_add"] & { insert_effect?: boolean },
|
||||
"playlist_song_remove": ClientEvents["playlist_song_remove"],
|
||||
"playlist_song_reorder": ClientEvents["playlist_song_reorder"],
|
||||
"playlist_song_loaded": ClientEvents["playlist_song_loaded"] & { html_entry?: JQuery },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -700,9 +688,11 @@ export namespace modal {
|
|||
}
|
||||
|
||||
//Some test code
|
||||
const eclient = new Registry<channel_tree.client>();
|
||||
/*
|
||||
const eclient = new Registry<ClientEvents>();
|
||||
const emusic = new Registry<sidebar.music>();
|
||||
|
||||
eclient.on("property_update", event => { event.as<"playlist_song_loaded">(); });
|
||||
eclient.connect("playlist_song_loaded", emusic);
|
||||
eclient.connect("playlist_song_loaded", emusic);
|
||||
*/
|
|
@ -6,6 +6,7 @@ import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
|||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
|
||||
export enum GroupType {
|
||||
QUERY,
|
||||
|
@ -31,10 +32,21 @@ export class GroupPermissionRequest {
|
|||
promise: LaterPromise<PermissionValue[]>;
|
||||
}
|
||||
|
||||
export class Group {
|
||||
properties: GroupProperties = new GroupProperties();
|
||||
export interface GroupEvents {
|
||||
notify_deleted: {},
|
||||
|
||||
notify_properties_updated: {
|
||||
updated_properties: {[Key in keyof GroupProperties]: GroupProperties[Key]};
|
||||
group_properties: GroupProperties
|
||||
}
|
||||
}
|
||||
|
||||
export class Group {
|
||||
readonly handle: GroupManager;
|
||||
|
||||
readonly events: Registry<GroupEvents>;
|
||||
readonly properties: GroupProperties = new GroupProperties();
|
||||
|
||||
readonly id: number;
|
||||
readonly target: GroupTarget;
|
||||
readonly type: GroupType;
|
||||
|
@ -46,6 +58,8 @@ export class Group {
|
|||
|
||||
|
||||
constructor(handle: GroupManager, id: number, target: GroupTarget, type: GroupType, name: string) {
|
||||
this.events = new Registry<GroupEvents>();
|
||||
|
||||
this.handle = handle;
|
||||
this.id = id;
|
||||
this.target = target;
|
||||
|
@ -53,20 +67,21 @@ export class Group {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
updateProperty(key, value) {
|
||||
if(!JSON.map_field_to(this.properties, value, key))
|
||||
return; /* no updates */
|
||||
updateProperties(properties: {key: string, value: string}[]) {
|
||||
let updates = {};
|
||||
|
||||
if(key == "iconid") {
|
||||
this.properties.iconid = (new Uint32Array([this.properties.iconid]))[0];
|
||||
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
|
||||
client.updateGroupIcon(this);
|
||||
});
|
||||
} else if(key == "sortid")
|
||||
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
|
||||
client.update_group_icon_order();
|
||||
});
|
||||
for(const { key, value } of properties) {
|
||||
if(!JSON.map_field_to(this.properties, value, key))
|
||||
continue; /* no updates */
|
||||
if(key === "iconid")
|
||||
this.properties.iconid = this.properties.iconid >>> 0;
|
||||
updates[key] = this.properties[key];
|
||||
}
|
||||
|
||||
this.events.fire("notify_properties_updated", {
|
||||
group_properties: this.properties,
|
||||
updated_properties: updates as any
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,44 +165,45 @@ export class GroupManager extends AbstractCommandHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
if(target == GroupTarget.SERVER)
|
||||
this.serverGroups = [];
|
||||
else
|
||||
this.channelGroups = [];
|
||||
let group_list = target == GroupTarget.SERVER ? this.serverGroups : this.channelGroups;
|
||||
const deleted_groups = group_list.slice(0);
|
||||
|
||||
for(let groupData of json) {
|
||||
for(const group_data of json) {
|
||||
let type : GroupType;
|
||||
switch (Number.parseInt(groupData["type"])) {
|
||||
switch (parseInt(group_data["type"])) {
|
||||
case 0: type = GroupType.TEMPLATE; break;
|
||||
case 1: type = GroupType.NORMAL; break;
|
||||
case 2: type = GroupType.QUERY; break;
|
||||
default:
|
||||
log.error(LogCategory.CLIENT, tr("Invalid group type: %o for group %s"), groupData["type"],groupData["name"]);
|
||||
log.error(LogCategory.CLIENT, tr("Invalid group type: %o for group %s"), group_data["type"],group_data["name"]);
|
||||
continue;
|
||||
}
|
||||
|
||||
let group = new Group(this,parseInt(target == GroupTarget.SERVER ? groupData["sgid"] : groupData["cgid"]), target, type, groupData["name"]);
|
||||
for(let key of Object.keys(groupData)) {
|
||||
if(key == "sgid") continue;
|
||||
if(key == "cgid") continue;
|
||||
if(key == "type") continue;
|
||||
if(key == "name") continue;
|
||||
const group_id = parseInt(target == GroupTarget.SERVER ? group_data["sgid"] : group_data["cgid"]);
|
||||
let group_index = deleted_groups.findIndex(e => e.id === group_id);
|
||||
let group: Group;
|
||||
if(group_index === -1) {
|
||||
group = new Group(this, group_id, target, type, group_data["name"]);
|
||||
group_list.push(group);
|
||||
} else
|
||||
group = deleted_groups.splice(group_index, 1)[0];
|
||||
|
||||
group.updateProperty(key, groupData[key]);
|
||||
}
|
||||
const property_blacklist = [
|
||||
"sgid", "cgid", "type", "name",
|
||||
|
||||
group.requiredMemberRemovePower = parseInt(groupData["n_member_removep"]);
|
||||
group.requiredMemberAddPower = parseInt(groupData["n_member_addp"]);
|
||||
group.requiredModifyPower = parseInt(groupData["n_modifyp"]);
|
||||
"n_member_removep", "n_member_addp", "n_modifyp"
|
||||
];
|
||||
group.updateProperties(Object.keys(group_data).filter(e => property_blacklist.findIndex(a => a === e) === -1).map(e => { return { key: e, value: group_data[e] } }));
|
||||
|
||||
if(target == GroupTarget.SERVER)
|
||||
this.serverGroups.push(group);
|
||||
else
|
||||
this.channelGroups.push(group);
|
||||
group.requiredMemberRemovePower = parseInt(group_data["n_member_removep"]);
|
||||
group.requiredMemberAddPower = parseInt(group_data["n_member_addp"]);
|
||||
group.requiredModifyPower = parseInt(group_data["n_modifyp"]);
|
||||
}
|
||||
|
||||
for(const client of this.handle.channelTree.clients)
|
||||
client.update_displayed_client_groups();
|
||||
for(const deleted of deleted_groups) {
|
||||
group_list.remove(deleted);
|
||||
deleted.events.fire("notify_deleted");
|
||||
}
|
||||
}
|
||||
|
||||
request_permissions(group: Group) : Promise<PermissionValue[]> { //database_empty_result
|
||||
|
|
|
@ -492,7 +492,8 @@ export class PermissionManager extends AbstractCommandHandler {
|
|||
|
||||
requestClientChannelPermissions(client_id: number, channel_id: number) : Promise<PermissionValue[]> {
|
||||
const keys: PermissionRequestKeys = {
|
||||
client_id: client_id
|
||||
client_id: client_id,
|
||||
channel_id: channel_id
|
||||
};
|
||||
return this.execute_permission_request("requests_client_channel_permissions", keys, this.execute_client_channel_permission_request.bind(this));
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ declare global {
|
|||
readonly webkitAudioContext: typeof AudioContext;
|
||||
readonly AudioContext: typeof OfflineAudioContext;
|
||||
readonly OfflineAudioContext: typeof OfflineAudioContext;
|
||||
readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext;
|
||||
readonly webkitOfflineAudioContext: typeof OfflineAudioContext;
|
||||
readonly RTCPeerConnection: typeof RTCPeerConnection;
|
||||
readonly Pointer_stringify: any;
|
||||
readonly jsrender: any;
|
||||
|
|
|
@ -156,7 +156,8 @@ export class Settings extends StaticSettings {
|
|||
|
||||
static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
|
||||
key: 'disableContextMenu',
|
||||
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier'
|
||||
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier',
|
||||
default_value: false
|
||||
};
|
||||
|
||||
static readonly KEY_DISABLE_GLOBAL_CONTEXT_MENU: SettingsKey<boolean> = {
|
||||
|
|
|
@ -242,3 +242,20 @@ namespace connection {
|
|||
handler["notifyusercount"] = handle_notify_user_count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
{
|
||||
var X; /* just declare the identifier so we'll not getting a reference error */
|
||||
const A = () => {
|
||||
console.log("Variable X: %o", X);
|
||||
};
|
||||
|
||||
{
|
||||
class X {
|
||||
|
||||
}
|
||||
A();
|
||||
}
|
||||
A();
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
|
||||
export interface ChannelTreeEntryEvents {
|
||||
notify_select_state_change: { selected: boolean },
|
||||
notify_unread_state_change: { unread: boolean }
|
||||
}
|
||||
|
||||
export class ChannelTreeEntry<Events extends ChannelTreeEntryEvents> {
|
||||
readonly events: Registry<Events>;
|
||||
|
||||
protected selected_: boolean = false;
|
||||
protected unread_: boolean = false;
|
||||
|
||||
/* called from the channel tree */
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
if(this.selected_ === true) return;
|
||||
this.selected_ = true;
|
||||
|
||||
this.events.fire("notify_select_state_change", { selected: true });
|
||||
}
|
||||
|
||||
/* called from the channel tree */
|
||||
protected onUnselect() {
|
||||
if(this.selected_ === false) return;
|
||||
this.selected_ = false;
|
||||
|
||||
this.events.fire("notify_select_state_change", { selected: false });
|
||||
}
|
||||
|
||||
isSelected() { return this.selected_; }
|
||||
|
||||
setUnread(flag: boolean) {
|
||||
if(this.unread_ === flag) return;
|
||||
this.unread_ = flag;
|
||||
|
||||
this.events.fire("notify_unread_state_change", { unread: flag });
|
||||
}
|
||||
isUnread() { return this.unread_; }
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {ChannelTree} from "tc-shared/ui/view";
|
||||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import {ClientEntry, ClientEvents} from "tc-shared/ui/client";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory, LogType} from "tc-shared/log";
|
||||
import {PermissionType} from "tc-shared/permission/PermissionType";
|
||||
|
@ -15,6 +15,11 @@ import {openChannelInfo} from "tc-shared/ui/modal/ModalChannelInfo";
|
|||
import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
|
||||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||
import { ChannelEntryView as ChannelEntryView } from "./tree/Channel";
|
||||
|
||||
export enum ChannelType {
|
||||
PERMANENT,
|
||||
SEMI_PERMANENT,
|
||||
|
@ -69,7 +74,79 @@ export class ChannelProperties {
|
|||
channel_conversation_history_length: number = -1;
|
||||
}
|
||||
|
||||
export class ChannelEntry {
|
||||
export interface ChannelEvents extends ChannelTreeEntryEvents {
|
||||
notify_properties_updated: {
|
||||
updated_properties: {[Key in keyof ChannelProperties]: ChannelProperties[Key]};
|
||||
channel_properties: ChannelProperties
|
||||
},
|
||||
|
||||
notify_cached_password_updated: {
|
||||
reason: "channel-password-changed" | "password-miss-match" | "password-entered";
|
||||
new_hash?: string;
|
||||
},
|
||||
|
||||
notify_subscribe_state_changed: {
|
||||
channel_subscribed: boolean
|
||||
},
|
||||
|
||||
notify_children_changed: {},
|
||||
notify_clients_changed: {}, /* will also be fired when clients haven been reordered */
|
||||
}
|
||||
|
||||
export class ParsedChannelName {
|
||||
readonly original_name: string;
|
||||
alignment: "center" | "right" | "left" | "normal";
|
||||
repetitive: boolean;
|
||||
text: string; /* does not contain any alignment codes */
|
||||
|
||||
constructor(name: string, has_parent_channel: boolean) {
|
||||
this.original_name = name;
|
||||
this.parse(has_parent_channel);
|
||||
}
|
||||
|
||||
private parse(has_parent_channel: boolean) {
|
||||
this.alignment = "normal";
|
||||
|
||||
parse_type:
|
||||
if(!has_parent_channel && this.original_name.charAt(0) == '[') {
|
||||
let end = this.original_name.indexOf(']');
|
||||
if(end === -1) break parse_type;
|
||||
|
||||
let options = this.original_name.substr(1, end - 1);
|
||||
if(options.indexOf("spacer") === -1) break parse_type;
|
||||
options = options.substr(0, options.indexOf("spacer"));
|
||||
|
||||
if(options.length == 0)
|
||||
options = "l";
|
||||
else if(options.length > 1)
|
||||
options = options[0];
|
||||
|
||||
switch (options) {
|
||||
case "r":
|
||||
this.alignment = "right";
|
||||
break;
|
||||
case "l":
|
||||
this.alignment = "center";
|
||||
break;
|
||||
case "c":
|
||||
this.alignment = "center";
|
||||
break;
|
||||
case "*":
|
||||
this.alignment = "center";
|
||||
this.repetitive = true;
|
||||
break;
|
||||
default:
|
||||
break parse_type;
|
||||
}
|
||||
|
||||
this.text = this.original_name.substr(end + 1);
|
||||
}
|
||||
if(!this.text && this.alignment === "normal")
|
||||
this.text = this.original_name;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||
channelTree: ChannelTree;
|
||||
channelId: number;
|
||||
parent?: ChannelEntry;
|
||||
|
@ -78,15 +155,14 @@ export class ChannelEntry {
|
|||
channel_previous?: ChannelEntry;
|
||||
channel_next?: ChannelEntry;
|
||||
|
||||
private _channel_name_alignment: string = undefined;
|
||||
private _channel_name_formatted: string = undefined;
|
||||
readonly events: Registry<ChannelEvents>;
|
||||
readonly view: React.Ref<ChannelEntryView>;
|
||||
|
||||
parsed_channel_name: ParsedChannelName;
|
||||
|
||||
private _family_index: number = 0;
|
||||
|
||||
//HTML DOM elements
|
||||
private _tag_root: JQuery<HTMLElement>; /* container for the channel, client and children tag */
|
||||
private _tag_siblings: JQuery<HTMLElement>; /* container for all sub channels */
|
||||
private _tag_clients: JQuery<HTMLElement>; /* container for all clients */
|
||||
private _tag_channel: JQuery<HTMLElement>; /* container for the channel info itself */
|
||||
private _destroyed = false;
|
||||
|
||||
private _cachedPassword: string;
|
||||
|
@ -98,26 +174,33 @@ export class ChannelEntry {
|
|||
private _flag_subscribed: boolean;
|
||||
private _subscribe_mode: ChannelSubscribeMode;
|
||||
|
||||
constructor(channelId, channelName, parent = null) {
|
||||
private client_list: ClientEntry[] = []; /* this list is sorted correctly! */
|
||||
private readonly client_property_listener;
|
||||
|
||||
constructor(channelId, channelName) {
|
||||
super();
|
||||
|
||||
this.events = new Registry<ChannelEvents>();
|
||||
this.view = React.createRef<ChannelEntryView>();
|
||||
|
||||
this.properties = new ChannelProperties();
|
||||
this.channelId = channelId;
|
||||
this.properties.channel_name = channelName;
|
||||
this.parent = parent;
|
||||
this.channelTree = null;
|
||||
|
||||
this.initializeTag();
|
||||
this.__updateChannelName();
|
||||
this.parsed_channel_name = new ParsedChannelName("undefined", false);
|
||||
|
||||
this.client_property_listener = (event: ClientEvents["notify_properties_updated"]) => {
|
||||
if(typeof event.updated_properties.client_nickname !== "undefined" || typeof event.updated_properties.client_talk_power !== "undefined")
|
||||
this.reorderClientList(true);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
if(this._tag_root) {
|
||||
this._tag_root.remove(); /* removes also all other tags */
|
||||
this._tag_root = undefined;
|
||||
}
|
||||
this._tag_siblings = undefined;
|
||||
this._tag_channel = undefined;
|
||||
this._tag_clients = undefined;
|
||||
|
||||
this.client_list.forEach(e => this.unregisterClient(e, true));
|
||||
this.client_list = [];
|
||||
|
||||
this._cached_channel_description_promise = undefined;
|
||||
this._cached_channel_description_promise_resolve = undefined;
|
||||
|
@ -134,7 +217,7 @@ export class ChannelEntry {
|
|||
}
|
||||
|
||||
formattedChannelName() {
|
||||
return this._channel_name_formatted || this.properties.channel_name;
|
||||
return this.parsed_channel_name.text;
|
||||
}
|
||||
|
||||
getChannelDescription() : Promise<string> {
|
||||
|
@ -151,11 +234,57 @@ export class ChannelEntry {
|
|||
});
|
||||
}
|
||||
|
||||
registerClient(client: ClientEntry) {
|
||||
client.events.on("notify_properties_updated", this.client_property_listener);
|
||||
this.client_list.push(client);
|
||||
this.reorderClientList(false);
|
||||
|
||||
this.events.fire("notify_clients_changed");
|
||||
}
|
||||
|
||||
unregisterClient(client: ClientEntry, no_event?: boolean) {
|
||||
client.events.off("notify_properties_updated", this.client_property_listener);
|
||||
if(!this.client_list.remove(client))
|
||||
log.warn(LogCategory.CHANNEL, tr("Unregistered unknown client from channel %s"), this.channelName());
|
||||
|
||||
if(!no_event)
|
||||
this.events.fire("notify_clients_changed");
|
||||
}
|
||||
|
||||
private reorderClientList(fire_event: boolean) {
|
||||
const original_list = this.client_list.slice(0);
|
||||
|
||||
this.client_list.sort((a, b) => {
|
||||
if(a.properties.client_talk_power < b.properties.client_talk_power)
|
||||
return 1;
|
||||
if(a.properties.client_talk_power > b.properties.client_talk_power)
|
||||
return -1;
|
||||
|
||||
if(a.properties.client_nickname > b.properties.client_nickname)
|
||||
return 1;
|
||||
if(a.properties.client_nickname < b.properties.client_nickname)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
if(fire_event) {
|
||||
/* only fire if really something has changed ;) */
|
||||
for(let index = 0; index < this.client_list.length; index++) {
|
||||
if(this.client_list[index] !== original_list[index]) {
|
||||
this.events.fire("notify_clients_changed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent_channel() { return this.parent; }
|
||||
hasParent(){ return this.parent != null; }
|
||||
getChannelId(){ return this.channelId; }
|
||||
|
||||
children(deep = false) : ChannelEntry[] {
|
||||
//TODO: Speed this up by caching the children!
|
||||
const result: ChannelEntry[] = [];
|
||||
if(this.channelTree == null) return [];
|
||||
|
||||
|
@ -178,53 +307,17 @@ export class ChannelEntry {
|
|||
}
|
||||
|
||||
clients(deep = false) : ClientEntry[] {
|
||||
const result: ClientEntry[] = [];
|
||||
if(this.channelTree == null) return [];
|
||||
const result: ClientEntry[] = this.client_list.slice(0);
|
||||
if(!deep) return result;
|
||||
|
||||
const self = this;
|
||||
this.channelTree.clients.forEach(function (entry) {
|
||||
let current = entry.currentChannel();
|
||||
if(deep) {
|
||||
while(current) {
|
||||
if(current == self) {
|
||||
result.push(entry);
|
||||
break;
|
||||
}
|
||||
current = current.parent_channel();
|
||||
}
|
||||
} else
|
||||
if(current == self)
|
||||
result.push(entry);
|
||||
});
|
||||
return result;
|
||||
return this.children(true).map(e => e.clients(false)).reduce((prev, cur) => {
|
||||
prev.push(...cur);
|
||||
return cur;
|
||||
}, result);
|
||||
}
|
||||
|
||||
clients_ordered() : ClientEntry[] {
|
||||
const clients = this.clients(false);
|
||||
|
||||
clients.sort((a, b) => {
|
||||
if(a.properties.client_talk_power < b.properties.client_talk_power)
|
||||
return 1;
|
||||
if(a.properties.client_talk_power > b.properties.client_talk_power)
|
||||
return -1;
|
||||
|
||||
if(a.properties.client_nickname > b.properties.client_nickname)
|
||||
return 1;
|
||||
if(a.properties.client_nickname < b.properties.client_nickname)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
return clients;
|
||||
}
|
||||
|
||||
update_family_index(enforce?: boolean) {
|
||||
const current_index = this._family_index;
|
||||
const new_index = this.calculate_family_index(true);
|
||||
if(current_index == new_index && !enforce) return;
|
||||
|
||||
this._tag_channel.css("z-index", this._family_index);
|
||||
this._tag_channel.css("padding-left", ((this._family_index + 1) * 16 + 10) + "px");
|
||||
return this.client_list;
|
||||
}
|
||||
|
||||
calculate_family_index(enforce_recalculate: boolean = false) : number {
|
||||
|
@ -242,235 +335,13 @@ export class ChannelEntry {
|
|||
return this._family_index;
|
||||
}
|
||||
|
||||
private initializeTag() {
|
||||
const tag_channel = $.spawn("div").addClass("tree-entry channel");
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
super.onSelect(singleSelect);
|
||||
if(!singleSelect) return;
|
||||
|
||||
{
|
||||
const container_entry = $.spawn("div").addClass("container-channel");
|
||||
|
||||
container_entry.attr("channel-id", this.channelId);
|
||||
container_entry.addClass(this._channel_name_alignment);
|
||||
|
||||
/* unread marker */
|
||||
{
|
||||
container_entry.append(
|
||||
$.spawn("div")
|
||||
.addClass("marker-text-unread hidden")
|
||||
.attr("conversation", this.channelId)
|
||||
);
|
||||
}
|
||||
|
||||
/* channel icon (type) */
|
||||
{
|
||||
container_entry.append(
|
||||
$.spawn("div")
|
||||
.addClass("show-channel-normal-only channel-type icon client-channel_green_subscribed")
|
||||
);
|
||||
}
|
||||
|
||||
/* channel name */
|
||||
{
|
||||
container_entry.append(
|
||||
$.spawn("div")
|
||||
.addClass("container-channel-name")
|
||||
.append(
|
||||
$.spawn("a")
|
||||
.addClass("channel-name")
|
||||
.text(this.channelName())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/* all icons (last element) */
|
||||
{
|
||||
//Icons
|
||||
let container_icons = $.spawn("span").addClass("icons");
|
||||
|
||||
//Default icon (5)
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("show-channel-normal-only icon_entry icon_default icon client-channel_default")
|
||||
.attr("title", tr("Default channel"))
|
||||
);
|
||||
|
||||
//Password icon (4)
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("show-channel-normal-only icon_entry icon_password icon client-register")
|
||||
.attr("title", tr("The channel is password protected"))
|
||||
);
|
||||
|
||||
//Music icon (3)
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("show-channel-normal-only icon_entry icon_music icon client-music")
|
||||
.attr("title", tr("Music quality"))
|
||||
);
|
||||
|
||||
//Channel moderated (2)
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("show-channel-normal-only icon_entry icon_moderated icon client-moderated")
|
||||
.attr("title", tr("Channel is moderated"))
|
||||
);
|
||||
|
||||
//Channel Icon (1)
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("show-channel-normal-only icon_entry channel_icon")
|
||||
.attr("title", tr("Channel icon"))
|
||||
);
|
||||
|
||||
//Default no sound (0)
|
||||
let container = $.spawn("div")
|
||||
.css("position", "relative")
|
||||
.addClass("icon_no_sound");
|
||||
|
||||
let noSound = $.spawn("div")
|
||||
.addClass("icon_entry icon client-conflict-icon")
|
||||
.attr("title", "You don't support the channel codec");
|
||||
|
||||
let bg = $.spawn("div")
|
||||
.width(10)
|
||||
.height(14)
|
||||
.css("background", "red")
|
||||
.css("position", "absolute")
|
||||
.css("top", "1px")
|
||||
.css("left", "3px")
|
||||
.css("z-index", "-1");
|
||||
bg.appendTo(container);
|
||||
noSound.appendTo(container);
|
||||
container_icons.append(container);
|
||||
|
||||
container_icons.appendTo(container_entry);
|
||||
}
|
||||
|
||||
tag_channel.append(this._tag_channel = container_entry);
|
||||
this.update_family_index(true);
|
||||
}
|
||||
{
|
||||
const container_client = $.spawn("div").addClass("container-clients");
|
||||
|
||||
|
||||
tag_channel.append(this._tag_clients = container_client);
|
||||
}
|
||||
{
|
||||
const container_children = $.spawn("div").addClass("container-children");
|
||||
|
||||
|
||||
tag_channel.append(this._tag_siblings = container_children);
|
||||
}
|
||||
|
||||
/*
|
||||
setInterval(() => {
|
||||
let color = (Math.random() * 10000000).toString(16).substr(0, 6);
|
||||
tag_channel.css("background", "#" + color);
|
||||
}, 150);
|
||||
*/
|
||||
|
||||
this._tag_root = tag_channel;
|
||||
}
|
||||
|
||||
rootTag() : JQuery<HTMLElement> {
|
||||
return this._tag_root;
|
||||
}
|
||||
|
||||
channelTag() : JQuery<HTMLElement> {
|
||||
return this._tag_channel;
|
||||
}
|
||||
|
||||
siblingTag() : JQuery<HTMLElement> {
|
||||
return this._tag_siblings;
|
||||
}
|
||||
clientTag() : JQuery<HTMLElement>{
|
||||
return this._tag_clients;
|
||||
}
|
||||
|
||||
private _reorder_timer: number;
|
||||
reorderClients(sync?: boolean) {
|
||||
if(this._reorder_timer) {
|
||||
if(!sync) return;
|
||||
clearTimeout(this._reorder_timer);
|
||||
this._reorder_timer = undefined;
|
||||
} else if(!sync) {
|
||||
this._reorder_timer = setTimeout(() => {
|
||||
this._reorder_timer = undefined;
|
||||
this.reorderClients(true);
|
||||
}, 5) as any;
|
||||
return;
|
||||
}
|
||||
|
||||
let clients = this.clients();
|
||||
|
||||
if(clients.length > 1) {
|
||||
clients.sort((a, b) => {
|
||||
if(a.properties.client_talk_power < b.properties.client_talk_power)
|
||||
return 1;
|
||||
if(a.properties.client_talk_power > b.properties.client_talk_power)
|
||||
return -1;
|
||||
|
||||
if(a.properties.client_nickname > b.properties.client_nickname)
|
||||
return 1;
|
||||
if(a.properties.client_nickname < b.properties.client_nickname)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
clients.reverse();
|
||||
|
||||
for(let index = 0; index + 1 < clients.length; index++)
|
||||
clients[index].tag.before(clients[index + 1].tag);
|
||||
|
||||
log.debug(LogCategory.CHANNEL, tr("Reordered channel clients: %d"), clients.length);
|
||||
for(let client of clients) {
|
||||
log.debug(LogCategory.CHANNEL, "- %i %s", client.properties.client_talk_power, client.properties.client_nickname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initializeListener() {
|
||||
const tag_channel = this.channelTag();
|
||||
tag_channel.on('click', () => this.channelTree.onSelect(this));
|
||||
tag_channel.on('dblclick', () => {
|
||||
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
return;
|
||||
}
|
||||
this.joinChannel()
|
||||
});
|
||||
|
||||
let last_touch: number = 0;
|
||||
let touch_start: number = 0;
|
||||
tag_channel.on('touchend', event => {
|
||||
/* if over 250ms then its not a click its more a drag */
|
||||
if(Date.now() - touch_start > 250) {
|
||||
touch_start = 0;
|
||||
return;
|
||||
}
|
||||
if(Date.now() - last_touch > 750) {
|
||||
last_touch = Date.now();
|
||||
return;
|
||||
}
|
||||
last_touch = Date.now();
|
||||
/* double touch */
|
||||
tag_channel.trigger('dblclick');
|
||||
});
|
||||
tag_channel.on('touchstart', event => {
|
||||
touch_start = Date.now();
|
||||
});
|
||||
|
||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||
this.channelTag().on("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
(this.channelTree.currently_selected_context_callback || ((_) => null))(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelTree.onSelect(this, true);
|
||||
this.showContextMenu(event.pageX, event.pageY, () => {
|
||||
this.channelTree.onSelect(undefined, true);
|
||||
});
|
||||
});
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.channelTree.client.side_bar.channel_conversations().set_current_channel(this.channelId);
|
||||
this.channelTree.client.side_bar.show_channel_conversations();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -653,76 +524,8 @@ export class ChannelEntry {
|
|||
);
|
||||
}
|
||||
|
||||
handle_frame_resized() {
|
||||
if(this._channel_name_formatted === "align-repetitive")
|
||||
this.__updateChannelName();
|
||||
}
|
||||
|
||||
private static NAME_ALIGNMENTS: string[] = ["align-left", "align-center", "align-right", "align-repetitive"];
|
||||
private __updateChannelName() {
|
||||
this._channel_name_formatted = undefined;
|
||||
|
||||
parse_type:
|
||||
if(this.parent_channel() == null && this.properties.channel_name.charAt(0) == '[') {
|
||||
let end = this.properties.channel_name.indexOf(']');
|
||||
if(end == -1) break parse_type;
|
||||
|
||||
let options = this.properties.channel_name.substr(1, end - 1);
|
||||
if(options.indexOf("spacer") == -1) break parse_type;
|
||||
options = options.substr(0, options.indexOf("spacer"));
|
||||
|
||||
if(options.length == 0)
|
||||
options = "l";
|
||||
else if(options.length > 1)
|
||||
options = options[0];
|
||||
|
||||
switch (options) {
|
||||
case "r":
|
||||
this._channel_name_alignment = "align-right";
|
||||
break;
|
||||
case "l":
|
||||
this._channel_name_alignment = "align-left";
|
||||
break;
|
||||
case "c":
|
||||
this._channel_name_alignment = "align-center";
|
||||
break;
|
||||
case "*":
|
||||
this._channel_name_alignment = "align-repetitive";
|
||||
break;
|
||||
default:
|
||||
this._channel_name_alignment = undefined;
|
||||
break parse_type;
|
||||
}
|
||||
|
||||
this._channel_name_formatted = this.properties.channel_name.substr(end + 1) || "";
|
||||
}
|
||||
|
||||
this._tag_channel.find(".show-channel-normal-only").toggleClass("channel-normal", this._channel_name_formatted === undefined);
|
||||
|
||||
const tag_container_name = this._tag_channel.find(".container-channel-name");
|
||||
tag_container_name.removeClass(ChannelEntry.NAME_ALIGNMENTS.join(" "));
|
||||
|
||||
const tag_name = tag_container_name.find(".channel-name");
|
||||
let text = this._channel_name_formatted === undefined ? this.properties.channel_name : this._channel_name_formatted;
|
||||
|
||||
if(this._channel_name_formatted !== undefined) {
|
||||
tag_container_name.addClass(this._channel_name_alignment);
|
||||
|
||||
if(this._channel_name_alignment == "align-repetitive" && text.length > 0) {
|
||||
while(text.length < 1024 * 8)
|
||||
text += text;
|
||||
}
|
||||
}
|
||||
|
||||
tag_name.text(text);
|
||||
}
|
||||
|
||||
recalculate_repetitive_name() {
|
||||
if(this._channel_name_alignment == "align-repetitive")
|
||||
this.__updateChannelName();
|
||||
}
|
||||
|
||||
updateVariables(...variables: {key: string, value: string}[]) {
|
||||
/* devel-block(log-channel-property-updates) */
|
||||
let group = log.group(log.LogType.DEBUG, LogCategory.CHANNEL_PROPERTIES, tr("Update properties (%i) of %s (%i)"), variables.length, this.channelName(), this.getChannelId());
|
||||
|
||||
{
|
||||
|
@ -735,6 +538,7 @@ export class ChannelEntry {
|
|||
});
|
||||
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Clannel update properties", entries);
|
||||
}
|
||||
/* devel-block-end */
|
||||
|
||||
let info_update = false;
|
||||
for(let variable of variables) {
|
||||
|
@ -743,36 +547,14 @@ export class ChannelEntry {
|
|||
JSON.map_field_to(this.properties, value, variable.key);
|
||||
|
||||
if(key == "channel_name") {
|
||||
this.__updateChannelName();
|
||||
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||
info_update = true;
|
||||
} else if(key == "channel_order") {
|
||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||
this.channelTree.moveChannel(this, order, this.parent);
|
||||
} else if(key == "channel_icon_id") {
|
||||
/* For more detail lookup client::updateVariables and client_icon_id!
|
||||
* ATTENTION: This is required!
|
||||
*/
|
||||
this.properties.channel_icon_id = variable.value as any >>> 0;
|
||||
|
||||
let tag = this.channelTag().find(".icons .channel_icon");
|
||||
(this.properties.channel_icon_id > 0 ? $.fn.show : $.fn.hide).apply(tag);
|
||||
if(this.properties.channel_icon_id > 0) {
|
||||
tag.children().detach();
|
||||
this.channelTree.client.fileManager.icons.generateTag(this.properties.channel_icon_id).appendTo(tag);
|
||||
}
|
||||
info_update = true;
|
||||
} else if(key == "channel_codec") {
|
||||
(this.properties.channel_codec == 5 || this.properties.channel_codec == 3 ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_music"));
|
||||
this.channelTag().find(".icons .icon_no_sound").toggle(!(
|
||||
this.channelTree.client.serverConnection.support_voice() &&
|
||||
this.channelTree.client.serverConnection.voice_connection().decoding_supported(this.properties.channel_codec)
|
||||
));
|
||||
} else if(key == "channel_flag_default") {
|
||||
(this.properties.channel_flag_default ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_default"));
|
||||
} else if(key == "channel_flag_password")
|
||||
(this.properties.channel_flag_password ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_password"));
|
||||
else if(key == "channel_needed_talk_power")
|
||||
(this.properties.channel_needed_talk_power > 0 ? $.fn.show : $.fn.hide).apply(this.channelTag().find(".icons .icon_moderated"));
|
||||
} else if(key === "channel_icon_id") {
|
||||
this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */
|
||||
}
|
||||
else if(key == "channel_description") {
|
||||
this._cached_channel_description = undefined;
|
||||
if(this._cached_channel_description_promise_resolve)
|
||||
|
@ -781,10 +563,6 @@ export class ChannelEntry {
|
|||
this._cached_channel_description_promise_resolve = undefined;
|
||||
this._cached_channel_description_promise_reject = undefined;
|
||||
}
|
||||
if(key == "channel_maxclients" || key == "channel_maxfamilyclients" || key == "channel_flag_private" || key == "channel_flag_password") {
|
||||
this.updateChannelTypeIcon();
|
||||
info_update = true;
|
||||
}
|
||||
if(key == "channel_flag_conversation_private") {
|
||||
const conversations = this.channelTree.client.side_bar.channel_conversations();
|
||||
const conversation = conversations.conversation(this.channelId, false);
|
||||
|
@ -792,7 +570,15 @@ export class ChannelEntry {
|
|||
conversation.set_flag_private(this.properties.channel_flag_conversation_private);
|
||||
}
|
||||
}
|
||||
/* devel-block(log-channel-property-updates) */
|
||||
group.end();
|
||||
/* devel-block-end */
|
||||
{
|
||||
let properties = {};
|
||||
for(const property of variables)
|
||||
properties[property.key] = this.properties[property.key];
|
||||
this.events.fire("notify_properties_updated", { updated_properties: properties as any, channel_properties: this.properties });
|
||||
}
|
||||
|
||||
if(info_update) {
|
||||
const _client = this.channelTree.client.getClient();
|
||||
|
@ -802,28 +588,6 @@ export class ChannelEntry {
|
|||
}
|
||||
}
|
||||
|
||||
updateChannelTypeIcon() {
|
||||
let tag = this.channelTag().find(".channel-type");
|
||||
tag.removeAttr('class');
|
||||
tag.addClass("show-channel-normal-only channel-type icon");
|
||||
|
||||
if(this._channel_name_formatted === undefined)
|
||||
tag.addClass("channel-normal");
|
||||
|
||||
let type;
|
||||
if(this.properties.channel_flag_password == true && !this._cachedPassword)
|
||||
type = "yellow";
|
||||
else if(
|
||||
(!this.properties.channel_flag_maxclients_unlimited && this.clients().length >= this.properties.channel_maxclients) ||
|
||||
(!this.properties.channel_flag_maxfamilyclients_unlimited && this.properties.channel_maxfamilyclients >= 0 && this.clients(true).length >= this.properties.channel_maxfamilyclients)
|
||||
)
|
||||
type = "red";
|
||||
else
|
||||
type = "green";
|
||||
|
||||
tag.addClass("client-channel_" + type + (this._flag_subscribed ? "_subscribed" : ""));
|
||||
}
|
||||
|
||||
generate_bbcode() {
|
||||
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
|
||||
}
|
||||
|
@ -847,11 +611,12 @@ export class ChannelEntry {
|
|||
!this._cachedPassword &&
|
||||
!this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).granted(1)) {
|
||||
createInputModal(tr("Channel password"), tr("Channel password:"), () => true, text => {
|
||||
if(typeof(text) == typeof(true)) return;
|
||||
hashPassword(text as string).then(result => {
|
||||
if(typeof(text) !== "string") return;
|
||||
|
||||
hashPassword(text).then(result => {
|
||||
this._cachedPassword = result;
|
||||
this.events.fire("notify_cached_password_updated", { reason: "password-entered", new_hash: result });
|
||||
this.joinChannel();
|
||||
this.updateChannelTypeIcon();
|
||||
});
|
||||
}).open();
|
||||
} else if(this.channelTree.client.getClient().currentChannel() != this)
|
||||
|
@ -861,7 +626,7 @@ export class ChannelEntry {
|
|||
if(error instanceof CommandResult) {
|
||||
if(error.id == 781) { //Invalid password
|
||||
this._cachedPassword = undefined;
|
||||
this.updateChannelTypeIcon();
|
||||
this.events.fire("notify_cached_password_updated", { reason: "password-miss-match" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -890,7 +655,7 @@ export class ChannelEntry {
|
|||
|
||||
if(inherited_subscription_mode) {
|
||||
this.subscribe_mode = ChannelSubscribeMode.INHERITED;
|
||||
unsubscribe = this.flag_subscribed && !this.channelTree.client.client_status.channel_subscribe_all;
|
||||
unsubscribe = this.flag_subscribed && !this.channelTree.client.isSubscribeToAllChannels();
|
||||
} else {
|
||||
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
|
||||
unsubscribe = this.flag_subscribed;
|
||||
|
@ -918,7 +683,7 @@ export class ChannelEntry {
|
|||
return;
|
||||
|
||||
this._flag_subscribed = flag;
|
||||
this.updateChannelTypeIcon();
|
||||
this.events.fire("notify_subscribe_state_changed", { channel_subscribed: flag });
|
||||
}
|
||||
|
||||
get subscribe_mode() : ChannelSubscribeMode {
|
||||
|
@ -933,10 +698,6 @@ export class ChannelEntry {
|
|||
this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId), mode);
|
||||
}
|
||||
|
||||
set flag_text_unread(flag: boolean) {
|
||||
this._tag_channel.find(".marker-text-unread").toggleClass("hidden", !flag);
|
||||
}
|
||||
|
||||
log_data() : server_log.base.Channel {
|
||||
return {
|
||||
channel_name: this.channelName(),
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {channel_tree, Registry} from "tc-shared/events";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelTree} from "tc-shared/ui/view";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory, LogType} from "tc-shared/log";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {KeyCode, SpecialKey} from "tc-shared/PPTListener";
|
||||
import {Sound} from "tc-shared/sound/Sounds";
|
||||
import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/GroupManager";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import * as htmltags from "tc-shared/ui/htmltags";
|
||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {CommandResult, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
||||
|
@ -25,8 +24,10 @@ import {spawnChangeLatency} from "tc-shared/ui/modal/ModalChangeLatency";
|
|||
import {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import * as ppt from "tc-backend/ppt";
|
||||
import * as hex from "tc-shared/crypto/hex";
|
||||
import { ClientEntry as ClientEntryView } from "./tree/Client";
|
||||
import * as React from "react";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||
|
||||
export enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
|
@ -135,12 +136,39 @@ export class ClientConnectionInfo {
|
|||
connection_client_port: number = -1;
|
||||
}
|
||||
|
||||
export class ClientEntry {
|
||||
readonly events: Registry<channel_tree.client>;
|
||||
export interface ClientEvents extends ChannelTreeEntryEvents {
|
||||
"notify_enter_view": {},
|
||||
"notify_left_view": {},
|
||||
|
||||
notify_properties_updated: {
|
||||
updated_properties: {[Key in keyof ClientProperties]: ClientProperties[Key]};
|
||||
client_properties: ClientProperties
|
||||
},
|
||||
notify_mute_state_change: { muted: boolean }
|
||||
notify_speak_state_change: { speaking: boolean }
|
||||
|
||||
"music_status_update": {
|
||||
player_buffered_index: number,
|
||||
player_replay_index: number
|
||||
},
|
||||
"music_song_change": {
|
||||
"song": SongInfo
|
||||
},
|
||||
|
||||
/* TODO: Move this out of the music bots interface? */
|
||||
"playlist_song_add": { song: PlaylistSong },
|
||||
"playlist_song_remove": { song_id: number },
|
||||
"playlist_song_reorder": { song_id: number, previous_song_id: number },
|
||||
"playlist_song_loaded": { song_id: number, success: boolean, error_msg?: string, metadata?: string },
|
||||
|
||||
}
|
||||
|
||||
export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||
readonly events: Registry<ClientEvents>;
|
||||
readonly view: React.RefObject<ClientEntryView> = React.createRef<ClientEntryView>();
|
||||
|
||||
protected _clientId: number;
|
||||
protected _channel: ChannelEntry;
|
||||
protected _tag: JQuery<HTMLElement>;
|
||||
|
||||
protected _properties: ClientProperties;
|
||||
protected lastVariableUpdate: number = 0;
|
||||
|
@ -162,7 +190,8 @@ export class ClientEntry {
|
|||
channelTree: ChannelTree;
|
||||
|
||||
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
|
||||
this.events = new Registry<channel_tree.client>();
|
||||
super();
|
||||
this.events = new Registry<ClientEvents>();
|
||||
|
||||
this._properties = properties;
|
||||
this._properties.client_nickname = clientName;
|
||||
|
@ -172,10 +201,6 @@ export class ClientEntry {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
if(this._tag) {
|
||||
this._tag.remove();
|
||||
this._tag = undefined;
|
||||
}
|
||||
if(this._audio_handle) {
|
||||
log.warn(LogCategory.AUDIO, tr("Destroying client with an active audio handle. This could cause memory leaks!"));
|
||||
try {
|
||||
|
@ -240,7 +265,7 @@ export class ClientEntry {
|
|||
clientId(){ return this._clientId; }
|
||||
|
||||
is_muted() { return !!this._audio_muted; }
|
||||
set_muted(flag: boolean, update_icon: boolean, force?: boolean) {
|
||||
set_muted(flag: boolean, force: boolean) {
|
||||
if(this._audio_muted === flag && !force)
|
||||
return;
|
||||
|
||||
|
@ -264,13 +289,11 @@ export class ClientEntry {
|
|||
}
|
||||
}
|
||||
|
||||
if(update_icon)
|
||||
this.updateClientSpeakIcon();
|
||||
|
||||
this.events.fire("notify_mute_state_change", { muted: flag });
|
||||
for(const client of this.channelTree.clients) {
|
||||
if(client === this || client.properties.client_unique_identifier != this.properties.client_unique_identifier)
|
||||
if(client === this || client.properties.client_unique_identifier !== this.properties.client_unique_identifier)
|
||||
continue;
|
||||
client.set_muted(flag, true);
|
||||
client.set_muted(flag, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,34 +301,8 @@ export class ClientEntry {
|
|||
if(this._listener_initialized) return;
|
||||
this._listener_initialized = true;
|
||||
|
||||
this.tag.on('mouseup', event => {
|
||||
if(!this.channelTree.client_mover.is_active()) {
|
||||
this.channelTree.onSelect(this);
|
||||
}
|
||||
});
|
||||
|
||||
if(!(this instanceof LocalClientEntry) && !(this instanceof MusicClientEntry))
|
||||
this.tag.dblclick(event => {
|
||||
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
return;
|
||||
}
|
||||
this.open_text_chat();
|
||||
});
|
||||
|
||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||
this.tag.on("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
(this.channelTree.currently_selected_context_callback || ((_) => null))(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelTree.onSelect(this, true);
|
||||
this.showContextMenu(event.pageX, event.pageY, () => {});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
//FIXME: TODO!
|
||||
/*
|
||||
this.tag.on('mousedown', event => {
|
||||
if(event.which != 1) return; //Only the left button
|
||||
|
||||
|
@ -340,6 +337,19 @@ export class ClientEntry {
|
|||
this.channelTree.onSelect();
|
||||
}, event);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
super.onSelect(singleSelect);
|
||||
if(!singleSelect) return;
|
||||
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) {
|
||||
if(this instanceof MusicClientEntry)
|
||||
this.channelTree.client.side_bar.show_music_player(this);
|
||||
else
|
||||
this.channelTree.client.side_bar.show_client_info(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected contextmenu_info() : contextmenu.MenuEntry[] {
|
||||
|
@ -674,85 +684,18 @@ export class ClientEntry {
|
|||
icon_class: "client-input_muted_local",
|
||||
name: tr("Mute client"),
|
||||
visible: !this._audio_muted,
|
||||
callback: () => this.set_muted(true, true)
|
||||
callback: () => this.set_muted(true, false)
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-input_muted_local",
|
||||
name: tr("Unmute client"),
|
||||
visible: this._audio_muted,
|
||||
callback: () => this.set_muted(false, true)
|
||||
callback: () => this.set_muted(false, false)
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => trigger_close && on_close ? on_close() : {})
|
||||
);
|
||||
}
|
||||
|
||||
get tag() : JQuery<HTMLElement> {
|
||||
if(this._tag) return this._tag;
|
||||
|
||||
let container_client = $.spawn("div")
|
||||
.addClass("tree-entry client")
|
||||
.attr("client-id", this.clientId());
|
||||
|
||||
|
||||
/* unread marker */
|
||||
{
|
||||
container_client.append(
|
||||
$.spawn("div")
|
||||
.addClass("marker-text-unread hidden")
|
||||
.attr("private-conversation", this._clientId)
|
||||
);
|
||||
}
|
||||
container_client.append(
|
||||
$.spawn("div")
|
||||
.addClass("icon_client_state")
|
||||
.attr("title", "Client state")
|
||||
);
|
||||
|
||||
container_client.append(
|
||||
$.spawn("div")
|
||||
.addClass("group-prefix")
|
||||
.attr("title", "Server groups prefixes")
|
||||
.hide()
|
||||
);
|
||||
container_client.append(
|
||||
$.spawn("div")
|
||||
.addClass("client-name")
|
||||
.text(this.clientNickName())
|
||||
);
|
||||
container_client.append(
|
||||
$.spawn("div")
|
||||
.addClass("group-suffix")
|
||||
.attr("title", "Server groups suffix")
|
||||
.hide()
|
||||
);
|
||||
container_client.append(
|
||||
$.spawn("div")
|
||||
.addClass("client-away-message")
|
||||
.text(this.clientNickName())
|
||||
);
|
||||
|
||||
let container_icons = $.spawn("div").addClass("container-icons");
|
||||
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("icon icon_talk_power client-input_muted")
|
||||
.hide()
|
||||
);
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("container-icons-group")
|
||||
);
|
||||
container_icons.append(
|
||||
$.spawn("div")
|
||||
.addClass("container-icon-client")
|
||||
);
|
||||
container_client.append(container_icons);
|
||||
|
||||
this._tag = container_client;
|
||||
this.initializeListener();
|
||||
return this._tag;
|
||||
}
|
||||
|
||||
static bbcodeTag(id: number, name: string, uid: string) : string {
|
||||
return "[url=client://" + id + "/" + uid + "~" + encodeURIComponent(name) + "]" + name + "[/url]";
|
||||
}
|
||||
|
@ -777,80 +720,18 @@ export class ClientEntry {
|
|||
set speaking(flag) {
|
||||
if(flag === this._speaking) return;
|
||||
this._speaking = flag;
|
||||
this.updateClientSpeakIcon();
|
||||
this.events.fire("notify_speak_state_change", { speaking: flag });
|
||||
}
|
||||
|
||||
updateClientStatusIcons() {
|
||||
let talk_power = this.properties.client_talk_power >= this._channel.properties.channel_needed_talk_power;
|
||||
if(talk_power)
|
||||
this.tag.find(".icon_talk_power").hide();
|
||||
else
|
||||
this.tag.find(".icon_talk_power").show();
|
||||
}
|
||||
|
||||
updateClientSpeakIcon() {
|
||||
let icon: string = "";
|
||||
let clicon: string = "";
|
||||
|
||||
if(this.properties.client_type_exact == ClientType.CLIENT_QUERY) {
|
||||
icon = "client-server_query";
|
||||
console.log("Server query!");
|
||||
} else {
|
||||
if (this.properties.client_away) {
|
||||
icon = "client-away";
|
||||
} else if (this._audio_muted && !(this instanceof LocalClientEntry)) {
|
||||
icon = "client-input_muted_local";
|
||||
} else if(!this.properties.client_output_hardware) {
|
||||
icon = "client-hardware_output_muted";
|
||||
} else if(this.properties.client_output_muted) {
|
||||
icon = "client-output_muted";
|
||||
} else if(!this.properties.client_input_hardware) {
|
||||
icon = "client-hardware_input_muted";
|
||||
} else if(this.properties.client_input_muted) {
|
||||
icon = "client-input_muted";
|
||||
} else {
|
||||
if(this._speaking) {
|
||||
if(this.properties.client_is_channel_commander)
|
||||
clicon = "client_cc_talk";
|
||||
else
|
||||
clicon = "client_talk";
|
||||
} else {
|
||||
if(this.properties.client_is_channel_commander)
|
||||
clicon = "client_cc_idle";
|
||||
else
|
||||
clicon = "client_idle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(clicon.length > 0)
|
||||
this.tag.find(".icon_client_state").attr('class', 'icon_client_state clicon ' + clicon);
|
||||
else if(icon.length > 0)
|
||||
this.tag.find(".icon_client_state").attr('class', 'icon_client_state icon ' + icon);
|
||||
else
|
||||
this.tag.find(".icon_client_state").attr('class', 'icon_client_state icon_empty');
|
||||
}
|
||||
|
||||
updateAwayMessage() {
|
||||
let tag = this.tag.find(".client-away-message");
|
||||
if(this.properties.client_away == true && this.properties.client_away_message){
|
||||
tag.text("[" + this.properties.client_away_message + "]");
|
||||
tag.show();
|
||||
} else {
|
||||
tag.hide();
|
||||
}
|
||||
}
|
||||
isSpeaking() { return this._speaking; }
|
||||
|
||||
updateVariables(...variables: {key: string, value: string}[]) {
|
||||
let group = log.group(log.LogType.DEBUG, LogCategory.CLIENT, tr("Update properties (%i) of %s (%i)"), variables.length, this.clientNickName(), this.clientId());
|
||||
|
||||
let update_icon_status = false;
|
||||
let update_icon_speech = false;
|
||||
let update_away = false;
|
||||
let reorder_channel = false;
|
||||
let update_avatar = false;
|
||||
|
||||
/* devel-block(log-client-property-updates) */
|
||||
let group = log.group(log.LogType.DEBUG, LogCategory.CLIENT, tr("Update properties (%i) of %s (%i)"), variables.length, this.clientNickName(), this.clientId());
|
||||
{
|
||||
const entries = [];
|
||||
for(const variable of variables)
|
||||
|
@ -861,6 +742,7 @@ export class ClientEntry {
|
|||
});
|
||||
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Client update properties", entries);
|
||||
}
|
||||
/* devel-block-end */
|
||||
|
||||
for(const variable of variables) {
|
||||
const old_value = this._properties[variable.key];
|
||||
|
@ -878,8 +760,6 @@ export class ClientEntry {
|
|||
}
|
||||
}
|
||||
|
||||
this.tag.find(".client-name").text(variable.value);
|
||||
|
||||
const chat = this.channelTree.client.side_bar;
|
||||
const conversation = chat.private_conversations().find_conversation({
|
||||
name: this.clientNickName(),
|
||||
|
@ -893,32 +773,19 @@ export class ClientEntry {
|
|||
conversation.set_client_name(variable.value);
|
||||
reorder_channel = true;
|
||||
}
|
||||
if(
|
||||
variable.key == "client_away" ||
|
||||
variable.key == "client_input_hardware" ||
|
||||
variable.key == "client_output_hardware" ||
|
||||
variable.key == "client_output_muted" ||
|
||||
variable.key == "client_input_muted" ||
|
||||
variable.key == "client_is_channel_commander"){
|
||||
update_icon_speech = true;
|
||||
}
|
||||
if(variable.key == "client_away_message" || variable.key == "client_away") {
|
||||
update_away = true;
|
||||
}
|
||||
if(variable.key == "client_unique_identifier") {
|
||||
this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
|
||||
const mute_status = this.channelTree.client.settings.server("mute_client_" + this.clientUid(), false);
|
||||
this.set_muted(mute_status, false, mute_status); /* force only needed when we want to mute the client */
|
||||
this.set_muted(mute_status, mute_status); /* force only needed when we want to mute the client */
|
||||
|
||||
if(this._audio_handle)
|
||||
this._audio_handle.set_volume(this._audio_muted ? 0 : this._audio_volume);
|
||||
|
||||
update_icon_speech = true;
|
||||
log.debug(LogCategory.CLIENT, tr("Loaded client (%s) server specific properties. Volume: %o Muted: %o."), this.clientUid(), this._audio_volume, this._audio_muted);
|
||||
}
|
||||
if(variable.key == "client_talk_power") {
|
||||
reorder_channel = true;
|
||||
update_icon_status = true;
|
||||
//update_icon_status = true; DONE
|
||||
}
|
||||
if(variable.key == "client_icon_id") {
|
||||
/* yeah we like javascript. Due to JS wiered integer behaviour parsing for example fails for 18446744073409829863.
|
||||
|
@ -926,24 +793,12 @@ export class ClientEntry {
|
|||
* In opposite "18446744073409829863" >>> 0 evaluates to 3995244544, which is the icon id :)
|
||||
*/
|
||||
this.properties.client_icon_id = variable.value as any >>> 0;
|
||||
this.updateClientIcon();
|
||||
}
|
||||
if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups")
|
||||
this.update_displayed_client_groups();
|
||||
else if(variable.key == "client_flag_avatar")
|
||||
update_avatar = true;
|
||||
}
|
||||
|
||||
/* process updates after variables have been set */
|
||||
if(this._channel && reorder_channel)
|
||||
this._channel.reorderClients();
|
||||
if(update_icon_speech)
|
||||
this.updateClientSpeakIcon();
|
||||
if(update_icon_status)
|
||||
this.updateClientStatusIcons();
|
||||
if(update_away)
|
||||
this.updateAwayMessage();
|
||||
|
||||
const side_bar = this.channelTree.client.side_bar;
|
||||
{
|
||||
const client_info = side_bar.client_info();
|
||||
|
@ -959,44 +814,15 @@ export class ClientEntry {
|
|||
conversation.update_avatar();
|
||||
}
|
||||
|
||||
/* devel-block(log-client-property-updates) */
|
||||
group.end();
|
||||
this.events.fire("property_update", {
|
||||
properties: variables.map(e => e.key)
|
||||
});
|
||||
}
|
||||
/* devel-block-end */
|
||||
|
||||
update_displayed_client_groups() {
|
||||
this.tag.find(".container-icons-group").children().remove();
|
||||
|
||||
for(let id of this.assignedServerGroupIds())
|
||||
this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id));
|
||||
this.update_group_icon_order();
|
||||
this.updateGroupIcon(this.channelTree.client.groups.channelGroup(this.properties.client_channel_group_id));
|
||||
|
||||
let prefix_groups: string[] = [];
|
||||
let suffix_groups: string[] = [];
|
||||
for(const group_id of this.assignedServerGroupIds()) {
|
||||
const group = this.channelTree.client.groups.serverGroup(group_id);
|
||||
if(!group) continue;
|
||||
|
||||
if(group.properties.namemode == 1)
|
||||
prefix_groups.push(group.name);
|
||||
else if(group.properties.namemode == 2)
|
||||
suffix_groups.push(group.name);
|
||||
}
|
||||
|
||||
const tag_group_prefix = this.tag.find(".group-prefix");
|
||||
const tag_group_suffix = this.tag.find(".group-suffix");
|
||||
if(prefix_groups.length > 0) {
|
||||
tag_group_prefix.text("[" + prefix_groups.join("][") + "]").show();
|
||||
} else {
|
||||
tag_group_prefix.hide()
|
||||
}
|
||||
|
||||
if(suffix_groups.length > 0) {
|
||||
tag_group_suffix.text("[" + suffix_groups.join("][") + "]").show();
|
||||
} else {
|
||||
tag_group_suffix.hide()
|
||||
{
|
||||
let properties = {};
|
||||
for(const property of variables)
|
||||
properties[property.key] = this.properties[property.key];
|
||||
this.events.fire("notify_properties_updated", { updated_properties: properties as any, client_properties: this.properties });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1013,35 +839,6 @@ export class ClientEntry {
|
|||
}));
|
||||
}
|
||||
|
||||
updateClientIcon() {
|
||||
this.tag.find(".container-icon-client").children().remove();
|
||||
if(this.properties.client_icon_id > 0) {
|
||||
this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon")
|
||||
.appendTo(this.tag.find(".container-icon-client"));
|
||||
}
|
||||
}
|
||||
|
||||
updateGroupIcon(group: Group) {
|
||||
if(!group) return;
|
||||
|
||||
const container = this.tag.find(".container-icons-group");
|
||||
container.find(".icon_group_" + group.id).remove();
|
||||
|
||||
if (group.properties.iconid > 0) {
|
||||
container.append(
|
||||
$.spawn("div").attr('group-power', group.properties.sortid)
|
||||
.addClass("container-group-icon icon_group_" + group.id)
|
||||
.append(this.channelTree.client.fileManager.icons.generateTag(group.properties.iconid)).attr("title", group.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update_group_icon_order() {
|
||||
const container = this.tag.find(".container-icons-group");
|
||||
|
||||
container.append(...[...container.children()].sort((a, b) => parseInt(a.getAttribute("group-power")) - parseInt(b.getAttribute("group-power"))));
|
||||
}
|
||||
|
||||
assignedServerGroupIds() : number[] {
|
||||
let result = [];
|
||||
for(let id of this.properties.client_servergroups.split(",")){
|
||||
|
@ -1101,13 +898,6 @@ export class ClientEntry {
|
|||
}
|
||||
}
|
||||
|
||||
update_family_index() {
|
||||
if(!this._channel) return;
|
||||
const index = this._channel.calculate_family_index();
|
||||
|
||||
this.tag.css('padding-left', (5 + (index + 2) * 16) + "px");
|
||||
}
|
||||
|
||||
log_data() : server_log.base.Client {
|
||||
return {
|
||||
client_unique_id: this.properties.client_unique_identifier,
|
||||
|
@ -1143,10 +933,6 @@ export class ClientEntry {
|
|||
this._info_connection_promise_resolve = undefined;
|
||||
this._info_connection_promise_reject = undefined;
|
||||
}
|
||||
|
||||
set flag_text_unread(flag: boolean) {
|
||||
this._tag.find(".marker-text-unread").toggleClass("hidden", !flag);
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalClientEntry extends ClientEntry {
|
||||
|
@ -1195,64 +981,42 @@ export class LocalClientEntry extends ClientEntry {
|
|||
}
|
||||
|
||||
initializeListener(): void {
|
||||
if(this._listener_initialized)
|
||||
this.tag.off();
|
||||
this._listener_initialized = false; /* could there be a better system */
|
||||
super.initializeListener();
|
||||
this.tag.find(".client-name").addClass("client-name-own");
|
||||
}
|
||||
|
||||
this.tag.on('dblclick', () => {
|
||||
if(Array.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
return;
|
||||
}
|
||||
this.openRename();
|
||||
renameSelf(new_name: string) : Promise<boolean> {
|
||||
const old_name = this.properties.client_nickname;
|
||||
this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */
|
||||
return this.handle.serverConnection.command_helper.updateClient("client_nickname", new_name).then((e) => {
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, new_name);
|
||||
this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGED, {
|
||||
client: this.log_data(),
|
||||
old_name: old_name,
|
||||
new_name: new_name,
|
||||
own_client: true
|
||||
});
|
||||
return true;
|
||||
}).catch((e: CommandResult) => {
|
||||
this.updateVariables({ key: "client_nickname", value: old_name }); /* change it back */
|
||||
this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGE_FAILED, {
|
||||
reason: e.extra_message
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
openRename() : void {
|
||||
this.channelTree.client_mover.enabled = false;
|
||||
|
||||
const elm = this.tag.find(".client-name");
|
||||
elm.attr("contenteditable", "true");
|
||||
elm.removeClass("client-name-own");
|
||||
elm.css("background-color", "white");
|
||||
elm.focus();
|
||||
this.renaming = true;
|
||||
|
||||
elm.on('keypress', event => {
|
||||
if(event.keyCode == KeyCode.KEY_RETURN) {
|
||||
$(event.target).trigger("focusout");
|
||||
return false;
|
||||
const view = this.channelTree.view.current;
|
||||
if(!view) return; //TODO: Fallback input modal
|
||||
view.scrollEntryInView(this, () => {
|
||||
const own_view = this.view.current;
|
||||
if(!own_view) {
|
||||
return; //TODO: Fallback input modal
|
||||
}
|
||||
});
|
||||
|
||||
elm.on('focusout', event => {
|
||||
this.channelTree.client_mover.enabled = true;
|
||||
|
||||
if(!this.renaming) return;
|
||||
this.renaming = false;
|
||||
|
||||
elm.css("background-color", "");
|
||||
elm.removeAttr("contenteditable");
|
||||
elm.addClass("client-name-own");
|
||||
let text = elm.text().toString();
|
||||
if(this.clientNickName() == text) return;
|
||||
|
||||
elm.text(this.clientNickName());
|
||||
const old_name = this.clientNickName();
|
||||
this.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => {
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text);
|
||||
this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGED, {
|
||||
client: this.log_data(),
|
||||
old_name: old_name,
|
||||
new_name: text,
|
||||
own_client: true
|
||||
});
|
||||
}).catch((e: CommandResult) => {
|
||||
this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGE_FAILED, {
|
||||
reason: e.extra_message
|
||||
});
|
||||
this.openRename();
|
||||
own_view.setState({
|
||||
rename: true,
|
||||
renameInitialName: this.properties.client_nickname
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Icon, IconManager} from "tc-shared/FileManager";
|
||||
import {icon_cache_loader, IconManager, LocalIcon} from "tc-shared/FileManager";
|
||||
import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks";
|
||||
import {
|
||||
add_server_to_bookmarks,
|
||||
|
@ -33,7 +33,7 @@ export interface MenuItem {
|
|||
delete_item(item: MenuItem | HRItem);
|
||||
items() : (MenuItem | HRItem)[];
|
||||
|
||||
icon(klass?: string | Promise<Icon> | Icon) : string;
|
||||
icon(klass?: string | LocalIcon) : string; //FIXME: Native client must work as well!
|
||||
label(value?: string) : string;
|
||||
visible(value?: boolean) : boolean;
|
||||
disabled(value?: boolean) : boolean;
|
||||
|
@ -178,7 +178,7 @@ namespace html {
|
|||
return this;
|
||||
}
|
||||
|
||||
icon(klass?: string | Promise<Icon> | Icon): string {
|
||||
icon(klass?: string | LocalIcon): string {
|
||||
this._label_icon_tag.children().remove();
|
||||
if(typeof(klass) === "string")
|
||||
$.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag);
|
||||
|
@ -288,7 +288,8 @@ export function rebuild_bookmarks() {
|
|||
} else {
|
||||
const bookmark = entry as Bookmark;
|
||||
const item = root.append_item(bookmark.display_name);
|
||||
item.icon(IconManager.load_cached_icon(bookmark.last_icon_id || 0));
|
||||
|
||||
item.icon(icon_cache_loader.load_icon(bookmark.last_icon_id, bookmark.last_icon_server_id));
|
||||
item.click(() => boorkmak_connect(bookmark));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ export interface ButtonProperties {
|
|||
}
|
||||
|
||||
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||
protected default_state(): ButtonState {
|
||||
protected defaultState(): ButtonState {
|
||||
return {
|
||||
switched: false,
|
||||
dropdownShowed: false,
|
||||
|
@ -66,13 +66,13 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
|||
}
|
||||
|
||||
private onMouseEnter() {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
dropdownShowed: true
|
||||
});
|
||||
}
|
||||
|
||||
private onMouseLeave() {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
dropdownShowed: false
|
||||
});
|
||||
}
|
||||
|
@ -81,6 +81,6 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
|||
const new_state = !(this.state.switched || this.props.switched);
|
||||
const result = this.props.onToggle?.call(undefined, new_state);
|
||||
if(this.props.autoSwitch)
|
||||
this.updateState({ switched: typeof result === "boolean" ? result : new_state });
|
||||
this.setState({ switched: typeof result === "boolean" ? result : new_state });
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import * as React from "react";
|
||||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import {IconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {LocalIcon} from "tc-shared/FileManager";
|
||||
const cssStyle = require("./button.scss");
|
||||
|
||||
export interface DropdownEntryProperties {
|
||||
icon?: string | JQuery<HTMLDivElement>;
|
||||
icon?: string | LocalIcon;
|
||||
text: JSX.Element | string;
|
||||
|
||||
onClick?: (event) => void;
|
||||
|
@ -12,7 +13,7 @@ export interface DropdownEntryProperties {
|
|||
}
|
||||
|
||||
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
|
||||
protected default_state() { return {}; }
|
||||
protected defaultState() { return {}; }
|
||||
|
||||
render() {
|
||||
if(this.props.children) {
|
||||
|
@ -41,7 +42,7 @@ export interface DropdownContainerProperties { }
|
|||
export interface DropdownContainerState { }
|
||||
|
||||
export class DropdownContainer extends ReactComponentBase<DropdownContainerProperties, DropdownContainerState> {
|
||||
protected default_state() {
|
||||
protected defaultState() {
|
||||
return { };
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
DirectoryBookmark,
|
||||
find_bookmark
|
||||
} from "tc-shared/bookmarks";
|
||||
import {IconManager} from "tc-shared/FileManager";
|
||||
import {icon_cache_loader, IconManager} from "tc-shared/FileManager";
|
||||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {default_recorder} from "tc-shared/voice/RecorderProfile";
|
||||
|
@ -32,7 +32,7 @@ export interface ConnectionState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<InternalControlBarEvents> }, ConnectionState> {
|
||||
protected default_state(): ConnectionState {
|
||||
protected defaultState(): ConnectionState {
|
||||
return {
|
||||
connected: false,
|
||||
connectedAnywhere: false
|
||||
|
@ -84,7 +84,7 @@ class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_re
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_connect_state")
|
||||
private handleStateUpdate(state: ConnectionState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Inter
|
|||
this.button_ref = React.createRef();
|
||||
}
|
||||
|
||||
protected default_state() {
|
||||
protected defaultState() {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Inter
|
|||
private renderBookmark(bookmark: Bookmark) {
|
||||
return (
|
||||
<DropdownEntry key={bookmark.unique_id}
|
||||
icon={IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false})}
|
||||
icon={icon_cache_loader.load_icon(bookmark.last_icon_id, bookmark.last_icon_server_id)}
|
||||
text={bookmark.display_name}
|
||||
onClick={BookmarkButton.onBookmarkClick.bind(undefined, bookmark.unique_id)}
|
||||
onContextMenu={this.onBookmarkContextMenu.bind(this, bookmark.unique_id)}/>
|
||||
|
@ -146,7 +146,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Inter
|
|||
const bookmark = find_bookmark(bookmark_id) as Bookmark;
|
||||
if(!bookmark) return;
|
||||
|
||||
this.button_ref.current?.updateState({ dropdownForceShow: true });
|
||||
this.button_ref.current?.setState({ dropdownForceShow: true });
|
||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Connect"),
|
||||
|
@ -159,7 +159,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Inter
|
|||
callback: () => boorkmak_connect(bookmark, true),
|
||||
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
|
||||
}, contextmenu.Entry.CLOSE(() => {
|
||||
this.button_ref.current?.updateState({ dropdownForceShow: false });
|
||||
this.button_ref.current?.setState({ dropdownForceShow: false });
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ export interface AwayState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, AwayState> {
|
||||
protected default_state(): AwayState {
|
||||
protected defaultState(): AwayState {
|
||||
return {
|
||||
away: false,
|
||||
awayAnywhere: false,
|
||||
|
@ -226,7 +226,7 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalC
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_away_state")
|
||||
private handleStateUpdate(state: AwayState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,7 +236,7 @@ export interface ChannelSubscribeState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, ChannelSubscribeState> {
|
||||
protected default_state(): ChannelSubscribeState {
|
||||
protected defaultState(): ChannelSubscribeState {
|
||||
return { subscribeEnabled: false };
|
||||
}
|
||||
|
||||
|
@ -247,7 +247,7 @@ class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Regist
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_subscribe_state")
|
||||
private handleStateUpdate(state: ChannelSubscribeState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,7 +258,7 @@ export interface MicrophoneState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, MicrophoneState> {
|
||||
protected default_state(): MicrophoneState {
|
||||
protected defaultState(): MicrophoneState {
|
||||
return {
|
||||
enabled: false,
|
||||
muted: false
|
||||
|
@ -278,7 +278,7 @@ class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<Int
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_microphone_state")
|
||||
private handleStateUpdate(state: MicrophoneState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,7 +288,7 @@ export interface SpeakerState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, SpeakerState> {
|
||||
protected default_state(): SpeakerState {
|
||||
protected defaultState(): SpeakerState {
|
||||
return {
|
||||
muted: false
|
||||
};
|
||||
|
@ -304,7 +304,7 @@ class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<Intern
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_speaker_state")
|
||||
private handleStateUpdate(state: SpeakerState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,7 +314,7 @@ export interface QueryState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class QueryButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, QueryState> {
|
||||
protected default_state() {
|
||||
protected defaultState() {
|
||||
return {
|
||||
queryShown: false
|
||||
};
|
||||
|
@ -340,7 +340,7 @@ class QueryButton extends ReactComponentBase<{ event_registry: Registry<Internal
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_query_state")
|
||||
private handleStateUpdate(state: QueryState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,7 +352,7 @@ export interface HostButtonState {
|
|||
|
||||
@ReactEventHandler(obj => obj.props.event_registry)
|
||||
class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, HostButtonState> {
|
||||
protected default_state() {
|
||||
protected defaultState() {
|
||||
return {
|
||||
url: undefined,
|
||||
target_url: undefined
|
||||
|
@ -382,7 +382,7 @@ class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalC
|
|||
|
||||
@EventHandler<InternalControlBarEvents>("update_host_button")
|
||||
private handleStateUpdate(state: HostButtonState) {
|
||||
this.updateState(state);
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,6 +446,7 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
|
|||
const events = target.events();
|
||||
events.off("notify_state_updated", this.connection_handler_callbacks.notify_state_updated);
|
||||
events.off("notify_connection_state_changed", this.connection_handler_callbacks.notify_connection_state_changed);
|
||||
//FIXME: Add the host button here!
|
||||
}
|
||||
|
||||
private registerConnectionHandlerEvents(target: ConnectionHandler) {
|
||||
|
@ -455,7 +456,6 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
|
|||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
console.error(server_connections.events());
|
||||
server_connections.events().on("notify_active_handler_changed", this.connection_manager_callbacks.active_handler_changed);
|
||||
this.event_registry.fire("set_connection_handler", { handler: server_connections.active_connection() });
|
||||
}
|
||||
|
|
|
@ -140,8 +140,12 @@ export class Conversation {
|
|||
this._first_unread_message = undefined;
|
||||
|
||||
const ctree = this.handle.handle.handle.channelTree;
|
||||
if(ctree && ctree.tag_tree())
|
||||
ctree.tag_tree().find(".marker-text-unread[conversation='" + this.channel_id + "']").addClass("hidden");
|
||||
if(ctree && ctree.tag_tree()) {
|
||||
if(this.channel_id === 0)
|
||||
ctree.server.setUnread(false);
|
||||
else
|
||||
ctree.findChannel(this.channel_id).setUnread(false);
|
||||
}
|
||||
}
|
||||
this._first_unread_message_pointer.html_element.detach();
|
||||
}
|
||||
|
@ -276,6 +280,9 @@ export class Conversation {
|
|||
return; /* we already have that message */
|
||||
}
|
||||
}
|
||||
if(this._last_messages.length === 0)
|
||||
_new_message = true;
|
||||
|
||||
if(!spliced && this._last_messages.length < this._view_max_messages) {
|
||||
this._last_messages.push(message);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
|
||||
import * as events from "tc-shared/events";
|
||||
import {MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
||||
import PlayerState = voice.PlayerState;
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
|
@ -328,9 +328,9 @@ export class MusicInfo {
|
|||
});
|
||||
|
||||
/* bot property listener */
|
||||
const callback_property = event => this.events.fire("bot_property_update", { properties: event.properties });
|
||||
const callback_time_update = event => this.events.fire("player_time_update", event);
|
||||
const callback_song_change = event => this.events.fire("player_song_change", event);
|
||||
const callback_property = (event: ClientEvents["notify_properties_updated"]) => this.events.fire("bot_property_update", { properties: Object.keys(event.updated_properties) });
|
||||
const callback_time_update = (event: ClientEvents["music_status_update"]) => this.events.fire("player_time_update", event, true);
|
||||
const callback_song_change = (event: ClientEvents["music_song_change"]) => this.events.fire("player_song_change", event, true);
|
||||
this.events.on("bot_change", event => {
|
||||
if(event.old) {
|
||||
event.old.events.off(callback_property);
|
||||
|
@ -339,7 +339,7 @@ export class MusicInfo {
|
|||
event.old.events.disconnect_all(this.events);
|
||||
}
|
||||
if(event.new) {
|
||||
event.new.events.on("property_update", callback_property);
|
||||
event.new.events.on("notify_properties_updated", callback_property);
|
||||
|
||||
event.new.events.on("music_status_update", callback_time_update);
|
||||
event.new.events.on("music_song_change", callback_song_change);
|
||||
|
|
|
@ -543,7 +543,7 @@ export class PrivateConveration {
|
|||
} else {
|
||||
const ctree = this.handle.handle.handle.channelTree;
|
||||
if(ctree && ctree.tag_tree() && this.client_id)
|
||||
ctree.tag_tree().find(".marker-text-unread[private-conversation='" + this.client_id + "']").addClass("hidden");
|
||||
ctree.findClient(this.client_id)?.setUnread(false);
|
||||
|
||||
if(this._spacer_unread_message) {
|
||||
this._destroy_view_entry(this._spacer_unread_message.tag_unread);
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
save_bookmark
|
||||
} from "tc-shared/bookmarks";
|
||||
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
|
||||
import {IconManager} from "tc-shared/FileManager";
|
||||
import {icon_cache_loader, IconManager} from "tc-shared/FileManager";
|
||||
import {profiles} from "tc-shared/profiles/ConnectionProfile";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
@ -147,7 +147,7 @@ export function spawnBookmarkModal() {
|
|||
const bookmark = entry as Bookmark;
|
||||
container.append(
|
||||
bookmark.last_icon_id ?
|
||||
IconManager.generate_tag(IconManager.load_cached_icon(bookmark.last_icon_id || 0), {animate: false}) :
|
||||
IconManager.generate_tag(icon_cache_loader.load_icon(bookmark.last_icon_id, bookmark.last_icon_server_id), {animate: false}) :
|
||||
$.spawn("div").addClass("icon-container icon_em")
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as loader from "tc-loader";
|
|||
import {createModal} from "tc-shared/ui/elements/Modal";
|
||||
import {ConnectionProfile, default_profile, find_profile, profiles} from "tc-shared/profiles/ConnectionProfile";
|
||||
import {KeyCode} from "tc-shared/PPTListener";
|
||||
import {IconManager} from "tc-shared/FileManager";
|
||||
import {icon_cache_loader, IconManager} from "tc-shared/FileManager";
|
||||
import * as i18nc from "tc-shared/i18n/country";
|
||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||
|
@ -15,7 +15,10 @@ export namespace connection_log {
|
|||
//TODO: Save password data
|
||||
export type ConnectionData = {
|
||||
name: string;
|
||||
|
||||
icon_id: number;
|
||||
server_unique_id: string;
|
||||
|
||||
country: string;
|
||||
clients_online: number;
|
||||
clients_total: number;
|
||||
|
@ -45,6 +48,7 @@ export namespace connection_log {
|
|||
country: 'unknown',
|
||||
name: 'Unknown',
|
||||
icon_id: 0,
|
||||
server_unique_id: "unknown",
|
||||
total_connection: 0,
|
||||
|
||||
flag_password: false,
|
||||
|
@ -289,7 +293,7 @@ export function spawnConnectModal(options: {
|
|||
})
|
||||
).append(
|
||||
$.spawn("div").addClass("column name").append([
|
||||
IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)),
|
||||
IconManager.generate_tag(icon_cache_loader.load_icon(entry.icon_id, entry.server_unique_id)),
|
||||
$.spawn("a").text(entry.name)
|
||||
])
|
||||
).append(
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
|||
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {IconManager} from "tc-shared/FileManager";
|
||||
import {icon_cache_loader, IconManager} from "tc-shared/FileManager";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as log from "tc-shared/log";
|
||||
import {
|
||||
|
@ -712,7 +712,7 @@ export class HTMLPermissionEditor extends AbstractPermissionEditor {
|
|||
|
||||
let resolve: Promise<JQuery<HTMLDivElement>>;
|
||||
if(icon_id >= 0 && icon_id <= 1000)
|
||||
resolve = Promise.resolve(IconManager.generate_tag({id: icon_id, url: ""}));
|
||||
resolve = Promise.resolve(IconManager.generate_tag(icon_cache_loader.load_icon(icon_id, "general")));
|
||||
else
|
||||
resolve = this.icon_resolver(permission ? permission.get_value() : 0).then(e => $(e));
|
||||
|
||||
|
|
|
@ -64,18 +64,18 @@ export function spawnPermissionEdit<T extends keyof OptionMap>(connection: Conne
|
|||
const permission_editor: AbstractPermissionEditor = (() => {
|
||||
const editor = new HTMLPermissionEditor();
|
||||
editor.initialize(connection.permissions.groupedPermissions());
|
||||
editor.icon_resolver = id => connection.fileManager.icons.resolve_icon(id).then(async icon => {
|
||||
if(!icon)
|
||||
return undefined;
|
||||
editor.icon_resolver = async id => {
|
||||
const icon = connection.fileManager.icons.load_icon(id);
|
||||
await icon.await_loading();
|
||||
|
||||
const tag = document.createElement("img");
|
||||
await new Promise((resolve, reject) => {
|
||||
tag.onerror = reject;
|
||||
tag.onload = resolve;
|
||||
tag.src = icon.url;
|
||||
tag.src = icon.loaded_url || "nope";
|
||||
});
|
||||
return tag;
|
||||
});
|
||||
};
|
||||
editor.icon_selector = current_icon => new Promise<number>(resolve => {
|
||||
spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon);
|
||||
});
|
||||
|
|
|
@ -61,7 +61,7 @@ interface KeyActionEntryProperties {
|
|||
|
||||
@ReactEventHandler(e => e.props.eventRegistry)
|
||||
class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyActionEntryState> {
|
||||
protected default_state() : KeyActionEntryState {
|
||||
protected defaultState() : KeyActionEntryState {
|
||||
return {
|
||||
assignedKey: undefined,
|
||||
selected: false,
|
||||
|
@ -133,7 +133,7 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
|||
|
||||
@EventHandler<KeyMapEvents>("set_selected_action")
|
||||
private handleSelectedChange(event: KeyMapEvents["set_selected_action"]) {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
selected: this.props.action === event.action
|
||||
});
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
|||
if(event.action !== this.props.action) return;
|
||||
if(event.query_type !== "general") return;
|
||||
|
||||
this.updateState({
|
||||
this.setState({
|
||||
state: "loading"
|
||||
});
|
||||
}
|
||||
|
@ -153,12 +153,12 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
|||
if(event.action !== this.props.action) return;
|
||||
|
||||
if(event.status === "success") {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
state: "loaded",
|
||||
assignedKey: event.key
|
||||
});
|
||||
} else {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
state: "error",
|
||||
error: event.status === "timeout" ? tr("query timeout") : event.error
|
||||
});
|
||||
|
@ -169,7 +169,7 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
|||
private handleSetKeymap(event: KeyMapEvents["set_keymap"]) {
|
||||
if(event.action !== this.props.action) return;
|
||||
|
||||
this.updateState({ state: "applying" });
|
||||
this.setState({ state: "applying" });
|
||||
}
|
||||
|
||||
@EventHandler<KeyMapEvents>("set_keymap_result")
|
||||
|
@ -177,12 +177,12 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
|||
if(event.action !== this.props.action) return;
|
||||
|
||||
if(event.status === "success") {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
state: "loaded",
|
||||
assignedKey: event.key
|
||||
});
|
||||
} else {
|
||||
this.updateState({ state: "loaded" });
|
||||
this.setState({ state: "loaded" });
|
||||
createErrorModal(tr("Failed to change key"), tra("Failed to change key for action \"{}\":{:br:}{}", this.props.action, event.status === "timeout" ? tr("timeout") : event.error));
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ interface KeyActionGroupProperties {
|
|||
}
|
||||
|
||||
class KeyActionGroup extends ReactComponentBase<KeyActionGroupProperties, { collapsed: boolean }> {
|
||||
protected default_state(): { collapsed: boolean } {
|
||||
protected defaultState(): { collapsed: boolean } {
|
||||
return { collapsed: false }
|
||||
}
|
||||
|
||||
|
@ -213,7 +213,7 @@ class KeyActionGroup extends ReactComponentBase<KeyActionGroupProperties, { coll
|
|||
}
|
||||
|
||||
private toggleCollapsed() {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ interface KeyActionListProperties {
|
|||
}
|
||||
|
||||
class KeyActionList extends ReactComponentBase<KeyActionListProperties, {}> {
|
||||
protected default_state(): {} {
|
||||
protected defaultState(): {} {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -251,7 +251,7 @@ interface ButtonBarState {
|
|||
|
||||
@ReactEventHandler(e => e.props.event_registry)
|
||||
class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEvents> }, ButtonBarState> {
|
||||
protected default_state(): ButtonBarState {
|
||||
protected defaultState(): ButtonBarState {
|
||||
return {
|
||||
active_action: undefined,
|
||||
loading: true,
|
||||
|
@ -271,7 +271,7 @@ class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEven
|
|||
|
||||
@EventHandler<KeyMapEvents>("set_selected_action")
|
||||
private handleSetSelectedAction(event: KeyMapEvents["set_selected_action"]) {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
active_action: event.action,
|
||||
loading: true
|
||||
}, () => {
|
||||
|
@ -281,7 +281,7 @@ class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEven
|
|||
|
||||
@EventHandler<KeyMapEvents>("query_keymap_result")
|
||||
private handleQueryKeymapResult(event: KeyMapEvents["query_keymap_result"]) {
|
||||
this.updateState({
|
||||
this.setState({
|
||||
loading: false,
|
||||
has_key: event.status === "success" && !!event.key
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface ButtonState {
|
|||
}
|
||||
|
||||
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||
protected default_state(): ButtonState {
|
||||
protected defaultState(): ButtonState {
|
||||
return {
|
||||
disabled: undefined
|
||||
};
|
||||
|
|
|
@ -1,35 +1,63 @@
|
|||
import * as React from "react";
|
||||
import {LocalIcon} from "tc-shared/FileManager";
|
||||
|
||||
export interface IconProperties {
|
||||
icon: string | JQuery<HTMLDivElement>;
|
||||
icon: string | LocalIcon;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class IconRenderer extends React.Component<IconProperties, {}> {
|
||||
private readonly icon_ref: React.RefObject<HTMLDivElement>;
|
||||
render() {
|
||||
if(!this.props.icon)
|
||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
else if(typeof this.props.icon === "string")
|
||||
return <div className={"icon " + this.props.icon} title={this.props.title} />;
|
||||
else if(this.props.icon instanceof LocalIcon)
|
||||
return <LocalIconRenderer icon={this.props.icon} title={this.props.title} />;
|
||||
else throw "JQuery icons are not longer supported";
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadedIconRenderer {
|
||||
icon: LocalIcon;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
||||
private readonly callback_state_update;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if(typeof this.props.icon === "object")
|
||||
this.icon_ref = React.createRef();
|
||||
this.callback_state_update = () => {
|
||||
const icon = this.props.icon;
|
||||
if(icon.status !== "destroyed")
|
||||
this.forceUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if(!this.props.icon)
|
||||
return <div className={"icon-container icon-empty"} />;
|
||||
else if(typeof this.props.icon === "string")
|
||||
return <div className={"icon " + this.props.icon} />;
|
||||
|
||||
|
||||
return <div ref={this.icon_ref} />;
|
||||
const icon = this.props.icon;
|
||||
if(icon.status === "loaded") {
|
||||
if(icon.icon_id >= 0 && icon.icon_id <= 1000) {
|
||||
if(icon.icon_id === 0)
|
||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
return <div className={"icon_em client-group_" + icon.icon_id} />;
|
||||
}
|
||||
return <div key={"icon"} className={"icon-container"}><img src={icon.loaded_url} alt={this.props.title || ("icon " + icon.icon_id)} /></div>;
|
||||
} else if(icon.status === "loading")
|
||||
return <div key={"loading"} className={"icon-container"} title={this.props.title}><div className={"icon_loading"} /></div>;
|
||||
else if(icon.status === "error")
|
||||
return <div key={"error"} className={"icon client-warning"} title={icon.error_message || tr("Failed to load icon")} />;
|
||||
else if(icon.status === "empty" || icon.status === "destroyed")
|
||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if(this.icon_ref)
|
||||
$(this.icon_ref.current).replaceWith(this.props.icon);
|
||||
this.props.icon.status_change_callbacks.push(this.callback_state_update);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if(this.icon_ref)
|
||||
$(this.icon_ref.current).empty();
|
||||
this.props.icon.status_change_callbacks.remove(this.callback_state_update);
|
||||
}
|
||||
}
|
|
@ -1,22 +1,100 @@
|
|||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
|
||||
export enum BatchUpdateType {
|
||||
UNSET = -1,
|
||||
GENERAL = 0,
|
||||
CHANNEL_TREE = 1
|
||||
}
|
||||
|
||||
interface UpdateBatch {
|
||||
enabled: boolean;
|
||||
enable_count: number;
|
||||
|
||||
update: {
|
||||
c: any;
|
||||
s: any;
|
||||
b: () => void;
|
||||
}[];
|
||||
|
||||
force: {
|
||||
c: any;
|
||||
b: () => void;
|
||||
}[];
|
||||
}
|
||||
|
||||
const generate_batch = () => { return { enabled: false, enable_count: 0, update: [], force: [] }};
|
||||
let update_batches: {[key: number]:UpdateBatch} = {
|
||||
0: generate_batch(),
|
||||
1: generate_batch()
|
||||
};
|
||||
|
||||
export function BatchUpdateAssignment(type: BatchUpdateType) {
|
||||
return function (constructor: Function) {
|
||||
if(!ReactComponentBase.prototype.isPrototypeOf(constructor.prototype))
|
||||
throw "Class/object isn't an instance of ReactComponentBase";
|
||||
|
||||
const didMount = constructor.prototype.componentDidMount;
|
||||
constructor.prototype.componentDidMount = function() {
|
||||
if(typeof this.update_batch === "undefined")
|
||||
this.update_batch = update_batches[type];
|
||||
|
||||
if(typeof didMount === "function")
|
||||
didMount.call(this, arguments);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ReactComponentBase<Properties, State> extends React.Component<Properties, State> {
|
||||
private update_batch: UpdateBatch;
|
||||
|
||||
private batch_component_id: number;
|
||||
private batch_component_force_id: number;
|
||||
|
||||
constructor(props: Properties) {
|
||||
super(props);
|
||||
this.batch_component_id = -1;
|
||||
this.batch_component_force_id = -1;
|
||||
|
||||
this.state = this.default_state();
|
||||
this.state = this.defaultState();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
protected initialize() { }
|
||||
protected abstract default_state() : State;
|
||||
protected defaultState() : State { return {} as State; }
|
||||
|
||||
updateState(updates: {[key in keyof State]?: State[key]}, callback?: () => void) {
|
||||
if(Object.keys(updates).findIndex(e => updates[e] !== this.state[e]) === -1) {
|
||||
if(callback) setTimeout(callback, 0);
|
||||
return; /* no state has been changed */
|
||||
setState<K extends keyof State>(
|
||||
state: ((prevState: Readonly<State>, props: Readonly<Properties>) => (Pick<State, K> | State | null)) | (Pick<State, K> | State | null),
|
||||
callback?: () => void
|
||||
): void {
|
||||
if(typeof this.update_batch !== "undefined" && this.update_batch.enabled) {
|
||||
const obj = {
|
||||
c: this,
|
||||
s: Object.assign(this.update_batch.update[this.batch_component_id]?.s || {}, state),
|
||||
b: callback
|
||||
};
|
||||
if(this.batch_component_id === -1)
|
||||
this.batch_component_id = this.update_batch.update.push(obj) - 1;
|
||||
else
|
||||
this.update_batch.update[this.batch_component_id] = obj;
|
||||
} else {
|
||||
super.setState(state, callback);
|
||||
}
|
||||
}
|
||||
|
||||
forceUpdate(callback?: () => void): void {
|
||||
if(typeof this.update_batch !== "undefined" && this.update_batch.enabled) {
|
||||
const obj = {
|
||||
c: this,
|
||||
b: callback
|
||||
};
|
||||
if(this.batch_component_force_id === -1)
|
||||
this.batch_component_force_id = this.update_batch.force.push(obj) - 1;
|
||||
else
|
||||
this.update_batch.force[this.batch_component_force_id] = obj;
|
||||
} else {
|
||||
super.forceUpdate(callback);
|
||||
}
|
||||
this.setState(Object.assign(this.state, updates), callback);
|
||||
}
|
||||
|
||||
protected classList(...classes: (string | undefined)[]) {
|
||||
|
@ -30,3 +108,44 @@ export abstract class ReactComponentBase<Properties, State> extends React.Compon
|
|||
return Array.isArray(this.props.children) ? this.props.children.length > 0 : true;
|
||||
}
|
||||
}
|
||||
|
||||
export function batch_updates(type: BatchUpdateType) {
|
||||
const batch = update_batches[type];
|
||||
if(typeof batch === "undefined") throw "unknown batch type";
|
||||
|
||||
batch.enabled = true;
|
||||
batch.enable_count++;
|
||||
}
|
||||
|
||||
export function flush_batched_updates(type: BatchUpdateType, force?: boolean) {
|
||||
const batch = update_batches[type];
|
||||
if(typeof batch === "undefined") throw "unknown batch type";
|
||||
if(--batch.enable_count > 0 && !force) return;
|
||||
if(batch.enable_count < 0) throw "flush_batched_updates called more than batch_updates!";
|
||||
|
||||
const updates = batch.update;
|
||||
const forces = batch.force;
|
||||
|
||||
batch.update = [];
|
||||
batch.force = [];
|
||||
batch.enabled = batch.enable_count > 0;
|
||||
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
{
|
||||
let index = updates.length;
|
||||
while(index--) { /* fastest way to iterate */
|
||||
const update = updates[index];
|
||||
update.c.batch_component_id = -1;
|
||||
update.c.setState(update.s, update.b);
|
||||
}
|
||||
}
|
||||
{
|
||||
let index = forces.length;
|
||||
while(index--) { /* fastest way to iterate */
|
||||
const update = forces[index];
|
||||
update.c.batch_component_force_id = -1;
|
||||
update.c.forceUpdate(update.b);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -14,6 +14,10 @@ import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
|||
import {connection_log} from "tc-shared/ui/modal/ModalConnect";
|
||||
import * as top_menu from "./frames/MenuBar";
|
||||
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
||||
import { ServerEntry as ServerEntryView } from "./tree/Server";
|
||||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||
|
||||
export class ServerProperties {
|
||||
virtualserver_host: string = "";
|
||||
|
@ -122,11 +126,21 @@ export interface ServerAddress {
|
|||
port: number;
|
||||
}
|
||||
|
||||
export class ServerEntry {
|
||||
export interface ServerEvents extends ChannelTreeEntryEvents {
|
||||
notify_properties_updated: {
|
||||
updated_properties: {[Key in keyof ServerProperties]: ServerProperties[Key]};
|
||||
server_properties: ServerProperties
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||
remote_address: ServerAddress;
|
||||
channelTree: ChannelTree;
|
||||
properties: ServerProperties;
|
||||
|
||||
readonly events: Registry<ServerEvents>;
|
||||
readonly view: React.Ref<ServerEntryView>;
|
||||
|
||||
private info_request_promise: Promise<void> = undefined;
|
||||
private info_request_promise_resolve: any = undefined;
|
||||
private info_request_promise_reject: any = undefined;
|
||||
|
@ -138,56 +152,22 @@ export class ServerEntry {
|
|||
|
||||
lastInfoRequest: number = 0;
|
||||
nextInfoRequest: number = 0;
|
||||
private _htmlTag: JQuery<HTMLElement>;
|
||||
private _destroyed = false;
|
||||
|
||||
constructor(tree, name, address: ServerAddress) {
|
||||
super();
|
||||
|
||||
this.events = new Registry<ServerEvents>();
|
||||
this.view = React.createRef();
|
||||
|
||||
this.properties = new ServerProperties();
|
||||
this.channelTree = tree;
|
||||
this.remote_address = Object.assign({}, address); /* close the address because it might get changed due to the DNS resolve */
|
||||
this.remote_address = Object.assign({}, address); /* copy the address because it might get changed due to the DNS resolve */
|
||||
this.properties.virtualserver_name = name;
|
||||
}
|
||||
|
||||
get htmlTag() {
|
||||
if(this._destroyed) throw "destoryed";
|
||||
if(this._htmlTag) return this._htmlTag;
|
||||
|
||||
let tag = $.spawn("div").addClass("tree-entry server");
|
||||
|
||||
/* unread marker */
|
||||
{
|
||||
tag.append(
|
||||
$.spawn("div")
|
||||
.addClass("marker-text-unread hidden")
|
||||
.attr("conversation", 0)
|
||||
);
|
||||
}
|
||||
|
||||
tag.append(
|
||||
$.spawn("div")
|
||||
.addClass("server_type icon client-server_green")
|
||||
);
|
||||
|
||||
tag.append(
|
||||
$.spawn("div")
|
||||
.addClass("name")
|
||||
.text(this.properties.virtualserver_name)
|
||||
);
|
||||
|
||||
tag.append(
|
||||
$.spawn("div")
|
||||
.addClass("icon_property icon_empty")
|
||||
);
|
||||
|
||||
return this._htmlTag = tag;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
if(this._htmlTag) {
|
||||
this._htmlTag.remove();
|
||||
this._htmlTag = undefined;
|
||||
}
|
||||
this.info_request_promise = undefined;
|
||||
this.info_request_promise_resolve = undefined;
|
||||
this.info_request_promise_reject = undefined;
|
||||
|
@ -196,33 +176,22 @@ export class ServerEntry {
|
|||
this.remote_address = undefined;
|
||||
}
|
||||
|
||||
initializeListener(){
|
||||
this._htmlTag.on('click' ,() => {
|
||||
this.channelTree.onSelect(this);
|
||||
this.updateProperties(); /* just prepare to show some server info */
|
||||
});
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
super.onSelect(singleSelect);
|
||||
if(!singleSelect) return;
|
||||
|
||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||
this.htmlTag.on("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
if($.isArray(this.channelTree.currently_selected)) { //Multiselect
|
||||
(this.channelTree.currently_selected_context_callback || ((_) => null))(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelTree.onSelect(this, true);
|
||||
this.spawnContextMenu(event.pageX, event.pageY, () => { this.channelTree.onSelect(undefined, true); });
|
||||
});
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.channelTree.client.side_bar.channel_conversations().set_current_channel(0);
|
||||
this.channelTree.client.side_bar.show_channel_conversations();
|
||||
}
|
||||
}
|
||||
|
||||
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
|
||||
let trigger_close = true;
|
||||
contextmenu.spawn_context_menu(x, y, {
|
||||
contextMenuItems() : contextmenu.MenuEntry[] {
|
||||
return [
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
name: tr("Show server info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
openServerInfo(this);
|
||||
},
|
||||
icon_class: "client-about"
|
||||
|
@ -276,8 +245,13 @@ export class ServerEntry {
|
|||
name: tr("View avatars"),
|
||||
visible: false, //TODO: Enable again as soon the new design is finished
|
||||
callback: () => spawnAvatarList(this.channelTree.client)
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => trigger_close ? on_close() : {})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
|
||||
contextmenu.spawn_context_menu(x, y, ...this.contextMenuItems(),
|
||||
contextmenu.Entry.CLOSE(on_close)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -295,12 +269,11 @@ export class ServerEntry {
|
|||
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Server update properties", entries);
|
||||
}
|
||||
|
||||
let update_bannner = false, update_button = false;
|
||||
let update_bannner = false, update_button = false, update_bookmarks = false;
|
||||
for(let variable of variables) {
|
||||
JSON.map_field_to(this.properties, variable.value, variable.key);
|
||||
|
||||
if(variable.key == "virtualserver_name") {
|
||||
this.htmlTag.find(".name").text(variable.value);
|
||||
this.channelTree.client.tag_connection_handler.find(".server-name").text(variable.value);
|
||||
server_connections.update_ui();
|
||||
} else if(variable.key == "virtualserver_icon_id") {
|
||||
|
@ -308,30 +281,38 @@ export class ServerEntry {
|
|||
* ATTENTION: This is required!
|
||||
*/
|
||||
this.properties.virtualserver_icon_id = variable.value as any >>> 0;
|
||||
|
||||
const bmarks = bookmarks.bookmarks_flat()
|
||||
.filter(e => e.server_properties.server_address === this.remote_address.host && e.server_properties.server_port == this.remote_address.port)
|
||||
.filter(e => e.last_icon_id !== this.properties.virtualserver_icon_id);
|
||||
if(bmarks.length > 0) {
|
||||
bmarks.forEach(e => {
|
||||
e.last_icon_id = this.properties.virtualserver_icon_id;
|
||||
});
|
||||
bookmarks.save_bookmark();
|
||||
top_menu.rebuild_bookmarks();
|
||||
|
||||
control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
|
||||
}
|
||||
|
||||
if(this.channelTree.client.fileManager && this.channelTree.client.fileManager.icons)
|
||||
this.htmlTag.find(".icon_property").replaceWith(this.channelTree.client.fileManager.icons.generateTag(this.properties.virtualserver_icon_id).addClass("icon_property"));
|
||||
update_bookmarks = true;
|
||||
} else if(variable.key.indexOf('hostbanner') != -1) {
|
||||
update_bannner = true;
|
||||
} else if(variable.key.indexOf('hostbutton') != -1) {
|
||||
update_button = true;
|
||||
}
|
||||
}
|
||||
{
|
||||
let properties = {};
|
||||
for(const property of variables)
|
||||
properties[property.key] = this.properties[property.key];
|
||||
this.events.fire("notify_properties_updated", { updated_properties: properties as any, server_properties: this.properties });
|
||||
}
|
||||
if(update_bookmarks) {
|
||||
const bmarks = bookmarks.bookmarks_flat()
|
||||
.filter(e => e.server_properties.server_address === this.remote_address.host && e.server_properties.server_port == this.remote_address.port)
|
||||
.filter(e => e.last_icon_id !== this.properties.virtualserver_icon_id || e.last_icon_server_id !== this.properties.virtualserver_unique_identifier);
|
||||
if(bmarks.length > 0) {
|
||||
bmarks.forEach(e => {
|
||||
e.last_icon_id = this.properties.virtualserver_icon_id;
|
||||
e.last_icon_server_id = this.properties.virtualserver_unique_identifier;
|
||||
});
|
||||
bookmarks.save_bookmark();
|
||||
top_menu.rebuild_bookmarks();
|
||||
|
||||
control_bar_instance()?.events().fire("update_state", { state: "bookmarks" });
|
||||
}
|
||||
}
|
||||
|
||||
if(update_bannner)
|
||||
this.channelTree.client.hostbanner.update();
|
||||
|
||||
if(update_button)
|
||||
control_bar_instance()?.events().fire("server_updated", { handler: this.channelTree.client, category: "hostbanner" });
|
||||
|
||||
|
@ -353,6 +334,7 @@ export class ServerEntry {
|
|||
flag_password: this.properties.virtualserver_flag_password,
|
||||
name: this.properties.virtualserver_name,
|
||||
icon_id: this.properties.virtualserver_icon_id,
|
||||
server_unique_id: this.properties.virtualserver_unique_identifier,
|
||||
|
||||
password_hash: undefined /* we've here no clue */
|
||||
});
|
||||
|
@ -413,7 +395,11 @@ export class ServerEntry {
|
|||
return this.properties.virtualserver_uptime + (new Date().getTime() - this.lastInfoRequest) / 1000;
|
||||
}
|
||||
|
||||
set flag_text_unread(flag: boolean) {
|
||||
this._htmlTag.find(".marker-text-unread").toggleClass("hidden", !flag);
|
||||
reset() {
|
||||
this.properties = new ServerProperties();
|
||||
this._info_connection_promise = undefined;
|
||||
this._info_connection_promise_reject = undefined;
|
||||
this._info_connection_promise_resolve = undefined;
|
||||
this._info_connection_promise_timestamp = undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
.channelEntry {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
min-height: 16px;
|
||||
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.channelType {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.containerChannelName {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
justify-content: left;
|
||||
|
||||
max-width: 100%; /* important for the repetitive channel name! */
|
||||
overflow-x: hidden;
|
||||
height: 16px;
|
||||
|
||||
&.align-right {
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
&.align-center, &.align-repetitive {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.channelName {
|
||||
align-self: center;
|
||||
color: var(--channel-tree-entry-color);
|
||||
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.align-repetitive {
|
||||
.channelName {
|
||||
text-overflow: clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
padding-right: 5px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.moveSelected {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.showChannelNormalOnly {
|
||||
display: none;
|
||||
|
||||
&.channelNormal {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.icon_no_sound {
|
||||
z-index: 0;
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.background {
|
||||
height: 14px;
|
||||
width: 10px;
|
||||
|
||||
background: red;
|
||||
position: absolute;
|
||||
|
||||
top: 1px;
|
||||
left: 3px;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
import {
|
||||
BatchUpdateAssignment,
|
||||
BatchUpdateType,
|
||||
ReactComponentBase
|
||||
} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import * as React from "react";
|
||||
import {ChannelEntry as ChannelEntryController, ChannelEvents, ChannelProperties} from "../channel";
|
||||
import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry";
|
||||
|
||||
const channelStyle = require("./Channel.scss");
|
||||
const viewStyle = require("./View.scss");
|
||||
|
||||
interface ChannelEntryIconsProperties {
|
||||
channel: ChannelEntryController;
|
||||
}
|
||||
|
||||
interface ChannelEntryIconsState {
|
||||
icons_shown: boolean;
|
||||
|
||||
is_default: boolean;
|
||||
is_password_protected: boolean;
|
||||
is_music_quality: boolean;
|
||||
is_moderated: boolean;
|
||||
is_codec_supported: boolean;
|
||||
|
||||
custom_icon_id: number;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ChannelEntryIcons>(e => e.props.channel.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ChannelEntryIcons extends ReactComponentBase<ChannelEntryIconsProperties, ChannelEntryIconsState> {
|
||||
private static readonly SimpleIcon = (props: { iconClass: string, title: string }) => {
|
||||
return <div className={"icon " + props.iconClass} title={props.title} />
|
||||
};
|
||||
|
||||
protected defaultState(): ChannelEntryIconsState {
|
||||
const properties = this.props.channel.properties;
|
||||
const server_connection = this.props.channel.channelTree.client.serverConnection;
|
||||
|
||||
return {
|
||||
icons_shown: this.props.channel.parsed_channel_name.alignment === "normal",
|
||||
custom_icon_id: properties.channel_icon_id,
|
||||
is_music_quality: properties.channel_codec === 3 || properties.channel_codec === 5,
|
||||
is_codec_supported: server_connection.support_voice() && server_connection.voice_connection().decoding_supported(properties.channel_codec),
|
||||
is_default: properties.channel_flag_default,
|
||||
is_password_protected: properties.channel_flag_password,
|
||||
is_moderated: properties.channel_needed_talk_power !== 0
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let icons = [];
|
||||
|
||||
if(!this.state.icons_shown)
|
||||
return null;
|
||||
|
||||
if(this.state.is_default)
|
||||
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-default"} iconClass={"client-channel_default"} title={tr("Default channel")} />);
|
||||
|
||||
if(this.state.is_password_protected)
|
||||
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-password"} iconClass={"client-register"} title={tr("The channel is password protected")} />); //TODO: "client-register" is really the right icon?
|
||||
|
||||
if(this.state.is_music_quality)
|
||||
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-music"} iconClass={"client-music"} title={tr("Music quality")} />);
|
||||
|
||||
if(this.state.is_moderated)
|
||||
icons.push(<ChannelEntryIcons.SimpleIcon key={"icon-moderated"} iconClass={"client-moderated"} title={tr("Channel is moderated")} />);
|
||||
|
||||
if(this.state.custom_icon_id)
|
||||
icons.push(<LocalIconRenderer key={"icon-custom"} icon={this.props.channel.channelTree.client.fileManager.icons.load_icon(this.state.custom_icon_id)} title={tr("Client icon")} />);
|
||||
|
||||
if(!this.state.is_codec_supported) {
|
||||
icons.push(<div key={"icon-unsupported"} className={channelStyle.icon_no_sound}>
|
||||
<div className={"icon_entry icon client-conflict-icon"} title={tr("You don't support the channel codec")} />
|
||||
<div className={channelStyle.background} />
|
||||
</div>);
|
||||
}
|
||||
|
||||
return <span className={channelStyle.icons}>
|
||||
{icons}
|
||||
</span>
|
||||
}
|
||||
|
||||
@EventHandler<ChannelEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.channel_icon_id !== "undefined")
|
||||
this.setState({ custom_icon_id: event.updated_properties.channel_icon_id });
|
||||
|
||||
if(typeof event.updated_properties.channel_codec !== "undefined" || typeof event.updated_properties.channel_codec_quality !== "undefined") {
|
||||
const codec = event.channel_properties.channel_codec;
|
||||
this.setState({ is_music_quality: codec === 3 || codec === 5 });
|
||||
}
|
||||
|
||||
if(typeof event.updated_properties.channel_codec !== "undefined") {
|
||||
const server_connection = this.props.channel.channelTree.client.serverConnection;
|
||||
this.setState({ is_codec_supported: server_connection.support_voice() && server_connection.voice_connection().decoding_supported(event.channel_properties.channel_codec) });
|
||||
}
|
||||
|
||||
if(typeof event.updated_properties.channel_flag_default !== "undefined")
|
||||
this.setState({ is_default: event.updated_properties.channel_flag_default });
|
||||
|
||||
if(typeof event.updated_properties.channel_flag_password !== "undefined")
|
||||
this.setState({ is_password_protected: event.updated_properties.channel_flag_password });
|
||||
|
||||
if(typeof event.updated_properties.channel_needed_talk_power !== "undefined")
|
||||
this.setState({ is_moderated: event.channel_properties.channel_needed_talk_power !== 0 });
|
||||
|
||||
if(typeof event.updated_properties.channel_name !== "undefined")
|
||||
this.setState({ icons_shown: this.props.channel.parsed_channel_name.alignment === "normal" });
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelEntryIconProperties {
|
||||
channel: ChannelEntryController;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ChannelEntryIcon>(e => e.props.channel.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ChannelEntryIcon extends ReactComponentBase<ChannelEntryIconProperties, {}> {
|
||||
private static readonly IconUpdateKeys: (keyof ChannelProperties)[] = [
|
||||
"channel_name",
|
||||
"channel_flag_password",
|
||||
|
||||
"channel_maxclients",
|
||||
"channel_flag_maxclients_unlimited",
|
||||
|
||||
"channel_maxfamilyclients",
|
||||
"channel_flag_maxfamilyclients_inherited",
|
||||
"channel_flag_maxfamilyclients_unlimited",
|
||||
];
|
||||
|
||||
render() {
|
||||
if(this.props.channel.formattedChannelName() !== this.props.channel.channelName())
|
||||
return null;
|
||||
|
||||
const channel_properties = this.props.channel.properties;
|
||||
|
||||
let type;
|
||||
if(channel_properties.channel_flag_password === true && !this.props.channel.cached_password())
|
||||
type = "yellow";
|
||||
else if(!channel_properties.channel_flag_maxclients_unlimited && this.props.channel.clients().length >= channel_properties.channel_maxclients)
|
||||
type = "red";
|
||||
else if(!channel_properties.channel_flag_maxfamilyclients_unlimited && channel_properties.channel_maxfamilyclients >= 0 && this.props.channel.clients(true).length >= channel_properties.channel_maxfamilyclients)
|
||||
type = "red";
|
||||
else
|
||||
type = "green";
|
||||
|
||||
return <div className={"icon client-channel_" + type + (this.props.channel.flag_subscribed ? "_subscribed" : "") + " " + channelStyle.channelType} />;
|
||||
}
|
||||
|
||||
@EventHandler<ChannelEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) {
|
||||
for(const key of ChannelEntryIcon.IconUpdateKeys) {
|
||||
if(key in event.updated_properties) {
|
||||
this.forceUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A client change may cause the channel to show another flag */
|
||||
@EventHandler<ChannelEvents>("notify_clients_changed")
|
||||
private handleClientsUpdated() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelEvents>("notify_cached_password_updated")
|
||||
private handleCachedPasswordUpdate() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelEvents>("notify_subscribe_state_changed")
|
||||
private handleSubscribeModeChanges() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelEntryNameProperties {
|
||||
channel: ChannelEntryController;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ChannelEntryName>(e => e.props.channel.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ChannelEntryName extends ReactComponentBase<ChannelEntryNameProperties, {}> {
|
||||
|
||||
render() {
|
||||
const name = this.props.channel.parsed_channel_name;
|
||||
let class_name: string;
|
||||
let text: string;
|
||||
if(name.repetitive) {
|
||||
class_name = "align-repetitive";
|
||||
text = name.text;
|
||||
if(text.length) {
|
||||
while(text.length < 8000)
|
||||
text += text;
|
||||
}
|
||||
} else {
|
||||
text = name.text;
|
||||
class_name = "align-" + name.alignment;
|
||||
}
|
||||
|
||||
return <div className={this.classList(channelStyle.containerChannelName, channelStyle[class_name])}>
|
||||
<a className={channelStyle.channelName}>{text}</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@EventHandler<ChannelEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.channel_name !== "undefined")
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelEntryViewProperties {
|
||||
channel: ChannelEntryController;
|
||||
depth: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ChannelEntryView>(e => e.props.channel.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}> {
|
||||
shouldComponentUpdate(nextProps: Readonly<ChannelEntryViewProperties>, nextState: Readonly<{}>, nextContext: any): boolean {
|
||||
if(nextProps.offset !== this.props.offset)
|
||||
return true;
|
||||
if(nextProps.depth !== this.props.depth)
|
||||
return true;
|
||||
|
||||
return nextProps.channel !== this.props.channel;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
|
||||
style={{ paddingLeft: this.props.depth * 16, top: this.props.offset }}
|
||||
onMouseDown={e => this.onMouseDown(e as any)}
|
||||
onDoubleClick={() => this.onDoubleClick()}
|
||||
onContextMenu={e => this.onContextMenu(e as any)}
|
||||
>
|
||||
<UnreadMarker entry={this.props.channel} />
|
||||
<ChannelEntryIcon channel={this.props.channel} />
|
||||
<ChannelEntryName channel={this.props.channel} />
|
||||
<ChannelEntryIcons channel={this.props.channel} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
private onMouseDown(event: MouseEvent) {
|
||||
if(event.button !== 0) return; /* only left mouse clicks */
|
||||
|
||||
const channel = this.props.channel;
|
||||
channel.channelTree.events.fire("action_select_entries", {
|
||||
entries: [ channel ],
|
||||
mode: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
private onDoubleClick() {
|
||||
const channel = this.props.channel;
|
||||
if(channel.channelTree.selection.is_multi_select()) return;
|
||||
|
||||
channel.joinChannel();
|
||||
}
|
||||
|
||||
private onContextMenu(event: MouseEvent) {
|
||||
if(settings.static(Settings.KEY_DISABLE_CONTEXT_MENU))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
const channel = this.props.channel;
|
||||
if(channel.channelTree.selection.is_multi_select() && channel.isSelected())
|
||||
return;
|
||||
|
||||
channel.channelTree.events.fire("action_select_entries", {
|
||||
entries: [ channel ],
|
||||
mode: "exclusive"
|
||||
});
|
||||
channel.showContextMenu(event.pageX, event.pageY);
|
||||
}
|
||||
|
||||
@EventHandler<ChannelEvents>("notify_select_state_change")
|
||||
private handleSelectStateChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
@import "../../../css/static/mixin";
|
||||
|
||||
.clientEntry {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.clientName {
|
||||
line-height: 16px;
|
||||
min-width: 2em;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
padding-right: .25em;
|
||||
color: var(--channel-tree-entry-color);
|
||||
|
||||
&:not(.edit) {
|
||||
@include text-dotdotdot();
|
||||
}
|
||||
|
||||
&.clientNameOwn {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.edit {
|
||||
width: 100%;
|
||||
font-weight: normal;
|
||||
|
||||
color: black;
|
||||
background-color: white;
|
||||
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.clientAwayMessage {
|
||||
color: var(--channel-tree-entry-color);
|
||||
}
|
||||
|
||||
.containerIcons {
|
||||
margin-right: 0; /* override from previous thing */
|
||||
height: 100%;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding-right: 5px;
|
||||
padding-left: 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
.containerIconsGroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.containerHroupIcon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
&:focus-within {
|
||||
.containerIcons {
|
||||
background-color: var(--channel-tree-entry-selected); /* overpaint the name change box */
|
||||
|
||||
padding-left: 5px;
|
||||
z-index: 1001; /* show before client name */
|
||||
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.clientName {
|
||||
&:focus {
|
||||
position: absolute;
|
||||
color: black;
|
||||
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
margin-right: -10px;
|
||||
margin-left: 18px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
import {
|
||||
BatchUpdateAssignment,
|
||||
BatchUpdateType,
|
||||
ReactComponentBase
|
||||
} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import * as React from "react";
|
||||
import {
|
||||
ClientEntry as ClientEntryController,
|
||||
ClientEvents,
|
||||
ClientProperties,
|
||||
ClientType,
|
||||
LocalClientEntry, MusicClientEntry
|
||||
} from "../client";
|
||||
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
||||
import {Group, GroupEvents} from "tc-shared/permission/GroupManager";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry";
|
||||
import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
const clientStyle = require("./Client.scss");
|
||||
const viewStyle = require("./View.scss");
|
||||
|
||||
interface ClientIconProperties {
|
||||
client: ClientEntryController;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ClientSpeakIcon>(e => e.props.client.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ClientSpeakIcon extends ReactComponentBase<ClientIconProperties, {}> {
|
||||
private static readonly IconUpdateKeys: (keyof ClientProperties)[] = [
|
||||
"client_away",
|
||||
"client_input_hardware",
|
||||
"client_output_hardware",
|
||||
"client_output_muted",
|
||||
"client_input_muted",
|
||||
"client_is_channel_commander",
|
||||
"client_talk_power"
|
||||
];
|
||||
|
||||
render() {
|
||||
let icon: string = "";
|
||||
let clicon: string = "";
|
||||
|
||||
const client = this.props.client;
|
||||
const properties = client.properties;
|
||||
|
||||
if(properties.client_type_exact == ClientType.CLIENT_QUERY) {
|
||||
icon = "client-server_query";
|
||||
} else {
|
||||
if (properties.client_away) {
|
||||
icon = "client-away";
|
||||
} else if (!client.get_audio_handle() && !(this instanceof LocalClientEntry)) {
|
||||
icon = "client-input_muted_local";
|
||||
} else if(!properties.client_output_hardware) {
|
||||
icon = "client-hardware_output_muted";
|
||||
} else if(properties.client_output_muted) {
|
||||
icon = "client-output_muted";
|
||||
} else if(!properties.client_input_hardware) {
|
||||
icon = "client-hardware_input_muted";
|
||||
} else if(properties.client_input_muted) {
|
||||
icon = "client-input_muted";
|
||||
} else {
|
||||
if(client.isSpeaking()) {
|
||||
if(properties.client_is_channel_commander)
|
||||
clicon = "client_cc_talk";
|
||||
else
|
||||
clicon = "client_talk";
|
||||
} else {
|
||||
if(properties.client_is_channel_commander)
|
||||
clicon = "client_cc_idle";
|
||||
else
|
||||
clicon = "client_idle";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(clicon.length > 0)
|
||||
return <div className={"clicon " + clicon} />;
|
||||
else if(icon.length > 0)
|
||||
return <div className={"icon " + icon} />;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) {
|
||||
for(const key of ClientSpeakIcon.IconUpdateKeys)
|
||||
if(key in event.updated_properties) {
|
||||
this.forceUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_mute_state_change")
|
||||
private handleMuteStateChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_speak_state_change")
|
||||
private handleSpeakStateChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
interface ClientServerGroupIconsProperties {
|
||||
client: ClientEntryController;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ClientServerGroupIcons>(e => e.props.client.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ClientServerGroupIcons extends ReactComponentBase<ClientServerGroupIconsProperties, {}> {
|
||||
private subscribed_groups: Group[] = [];
|
||||
private group_updated_callback;
|
||||
|
||||
protected initialize() {
|
||||
this.group_updated_callback = (event: GroupEvents["notify_properties_updated"]) => {
|
||||
if(typeof event.updated_properties.iconid !== "undefined" || typeof event.updated_properties.sortid !== "undefined")
|
||||
this.forceUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
private unsubscribeGroupEvents() {
|
||||
this.subscribed_groups.forEach(e => e.events.off("notify_properties_updated", this.group_updated_callback));
|
||||
this.subscribed_groups = [];
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.unsubscribeGroupEvents();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.unsubscribeGroupEvents();
|
||||
|
||||
const groups = this.props.client.assignedServerGroupIds()
|
||||
.map(e => this.props.client.channelTree.client.groups.serverGroup(e)).filter(e => !!e);
|
||||
if(groups.length === 0) return null;
|
||||
|
||||
groups.forEach(e => {
|
||||
e.events.on("notify_properties_updated", this.group_updated_callback);
|
||||
this.subscribed_groups.push(e);
|
||||
});
|
||||
|
||||
const group_icons = groups.filter(e => e?.properties.iconid)
|
||||
.sort((a, b) => a.properties.sortid - b.properties.sortid);
|
||||
if(group_icons.length === 0) return null;
|
||||
return [
|
||||
group_icons.map(e => {
|
||||
return <LocalIconRenderer key={"group-icon-" + e.id} icon={this.props.client.channelTree.client.fileManager.icons.load_icon(e.properties.iconid)} />;
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.client_servergroups)
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
interface ClientChannelGroupIconProperties {
|
||||
client: ClientEntryController;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ClientChannelGroupIcon>(e => e.props.client.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ClientChannelGroupIcon extends ReactComponentBase<ClientChannelGroupIconProperties, {}> {
|
||||
private subscribed_group: Group | undefined;
|
||||
private group_updated_callback;
|
||||
|
||||
protected initialize() {
|
||||
this.group_updated_callback = (event: GroupEvents["notify_properties_updated"]) => {
|
||||
if(typeof event.updated_properties.iconid !== "undefined" || typeof event.updated_properties.sortid !== "undefined")
|
||||
this.forceUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
private unsubscribeGroupEvent() {
|
||||
this.subscribed_group?.events.off("notify_properties_updated", this.group_updated_callback);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.unsubscribeGroupEvent();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.unsubscribeGroupEvent();
|
||||
|
||||
const cgid = this.props.client.assignedChannelGroup();
|
||||
if(cgid === 0) return null;
|
||||
|
||||
const channel_group = this.props.client.channelTree.client.groups.channelGroup(cgid);
|
||||
if(!channel_group) return null;
|
||||
|
||||
channel_group.events.on("notify_properties_updated", this.group_updated_callback);
|
||||
this.subscribed_group = channel_group;
|
||||
|
||||
if(channel_group.properties.iconid === 0) return null;
|
||||
return <LocalIconRenderer key={"cg-icon"} icon={this.props.client.channelTree.client.fileManager.icons.load_icon(channel_group.properties.iconid)} />;
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.client_servergroups)
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
interface ClientIconsProperties {
|
||||
client: ClientEntryController;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ClientIcons>(e => e.props.client.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ClientIcons extends ReactComponentBase<ClientIconsProperties, {}> {
|
||||
render() {
|
||||
const icons = [];
|
||||
const talk_power = this.props.client.properties.client_talk_power;
|
||||
const needed_talk_power = this.props.client.currentChannel()?.properties.channel_needed_talk_power || 0;
|
||||
if(talk_power !== -1 && needed_talk_power !== 0 && needed_talk_power > talk_power)
|
||||
icons.push(<div key={"muted"} className={"icon icon_talk_power client-input_muted"} />);
|
||||
|
||||
icons.push(<ClientServerGroupIcons key={"sg-icons"} client={this.props.client} />);
|
||||
icons.push(<ClientChannelGroupIcon key={"channel-icons"} client={this.props.client} />);
|
||||
if(this.props.client.properties.client_icon_id !== 0)
|
||||
icons.push(<LocalIconRenderer key={"client-icon"} icon={this.props.client.channelTree.client.fileManager.icons.load_icon(this.props.client.properties.client_icon_id)} />);
|
||||
|
||||
return (
|
||||
<div className={clientStyle.containerIcons}>
|
||||
{icons}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.client_channel_group_id !== "undefined" || typeof event.updated_properties.client_talk_power !== "undefined" || typeof event.updated_properties.client_icon_id !== "undefined")
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
interface ClientNameProperties {
|
||||
client: ClientEntryController;
|
||||
}
|
||||
|
||||
interface ClientNameState {
|
||||
group_prefix: string;
|
||||
group_suffix: string;
|
||||
|
||||
away_message: string;
|
||||
}
|
||||
|
||||
/* group prefix & suffix, away message */
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
class ClientName extends ReactComponentBase<ClientNameProperties, ClientNameState> {
|
||||
protected defaultState(): ClientNameState {
|
||||
return {
|
||||
group_prefix: "",
|
||||
away_message: "",
|
||||
group_suffix: ""
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className={this.classList(clientStyle.clientName, this.props.client instanceof LocalClientEntry && clientStyle.clientNameOwn)}>
|
||||
{this.state.group_prefix + this.props.client.clientNickName() + this.state.group_suffix + this.state.away_message}
|
||||
</div>
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_properties_updated")
|
||||
private handlePropertiesChanged(event: ClientEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.client_away !== "undefined" || typeof event.updated_properties.client_away_message !== "undefined") {
|
||||
this.setState({
|
||||
away_message: event.client_properties.client_away_message && " [" + event.client_properties.client_away_message + "]"
|
||||
});
|
||||
}
|
||||
if(typeof event.updated_properties.client_servergroups !== "undefined" || typeof event.updated_properties.client_channel_group_id !== "undefined") {
|
||||
let prefix_groups: string[] = [];
|
||||
let suffix_groups: string[] = [];
|
||||
for(const group_id of this.props.client.assignedServerGroupIds()) {
|
||||
const group = this.props.client.channelTree.client.groups.serverGroup(group_id);
|
||||
if(!group) continue;
|
||||
|
||||
if(group.properties.namemode == 1)
|
||||
prefix_groups.push(group.name);
|
||||
else if(group.properties.namemode == 2)
|
||||
suffix_groups.push(group.name);
|
||||
}
|
||||
|
||||
const channel_group = this.props.client.channelTree.client.groups.channelGroup(this.props.client.assignedChannelGroup());
|
||||
if(channel_group) {
|
||||
if(channel_group.properties.namemode == 1)
|
||||
prefix_groups.push(channel_group.name);
|
||||
else if(channel_group.properties.namemode == 2)
|
||||
suffix_groups.splice(0, 0, channel_group.name);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
group_suffix: suffix_groups.map(e => "[" + e + "]").join(""),
|
||||
group_prefix: prefix_groups.map(e => "[" + e + "]").join("")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ClientNameEditProps {
|
||||
editFinished: (new_name?: string) => void;
|
||||
initialName: string;
|
||||
}
|
||||
|
||||
class ClientNameEdit extends ReactComponentBase<ClientNameEditProps, {}> {
|
||||
private readonly ref_div: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
componentDidMount(): void {
|
||||
this.ref_div.current.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
className={this.classList(clientStyle.clientName, clientStyle.edit)}
|
||||
contentEditable={true}
|
||||
ref={this.ref_div}
|
||||
dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(this.props.initialName)}}
|
||||
onBlur={e => this.onBlur()}
|
||||
onKeyPress={e => this.onKeyPress(e as any)}
|
||||
/>
|
||||
}
|
||||
|
||||
private onBlur() {
|
||||
this.props.editFinished(this.ref_div.current.textContent);
|
||||
}
|
||||
|
||||
private onKeyPress(event: KeyboardEvent) {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.onBlur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientEntryProperties {
|
||||
client: ClientEntryController;
|
||||
depth: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface ClientEntryState {
|
||||
rename: boolean;
|
||||
renameInitialName?: string;
|
||||
}
|
||||
|
||||
@ReactEventHandler<ClientEntry>(e => e.props.client.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
export class ClientEntry extends TreeEntry<ClientEntryProperties, ClientEntryState> {
|
||||
shouldComponentUpdate(nextProps: Readonly<ClientEntryProperties>, nextState: Readonly<ClientEntryState>, nextContext: any): boolean {
|
||||
return nextState.rename !== this.state.rename ||
|
||||
nextProps.offset !== this.props.offset ||
|
||||
nextProps.client !== this.props.client ||
|
||||
nextProps.depth !== this.props.depth;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.classList(clientStyle.clientEntry, viewStyle.treeEntry, this.props.client.isSelected() && viewStyle.selected)}
|
||||
style={{ paddingLeft: (this.props.depth * 16) + "px", top: this.props.offset }}
|
||||
onDoubleClick={() => this.onDoubleClick()}
|
||||
onMouseDown={e => this.onMouseDown(e as any)}
|
||||
onContextMenu={e => this.onContextMenu(e as any)}
|
||||
>
|
||||
<UnreadMarker entry={this.props.client} />
|
||||
<ClientSpeakIcon client={this.props.client} />
|
||||
{this.state.rename ?
|
||||
<ClientNameEdit key={"rename"} editFinished={name => this.onEditFinished(name)} initialName={this.state.renameInitialName || this.props.client.properties.client_nickname} /> :
|
||||
[<ClientName key={"name"} client={this.props.client} />, <ClientIcons key={"icons"} client={this.props.client} />] }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onDoubleClick() {
|
||||
const client = this.props.client;
|
||||
if(client.channelTree.selection.is_multi_select()) return;
|
||||
|
||||
if(this.props.client instanceof LocalClientEntry) {
|
||||
this.props.client.openRename();
|
||||
} else if(this.props.client instanceof MusicClientEntry) {
|
||||
/* no action defined yet */
|
||||
} else {
|
||||
this.props.client.open_text_chat();
|
||||
}
|
||||
}
|
||||
|
||||
private onEditFinished(new_name?: string) {
|
||||
if(!(this.props.client instanceof LocalClientEntry))
|
||||
throw "Only local clients could be renamed";
|
||||
|
||||
if(new_name && new_name !== this.state.renameInitialName) {
|
||||
const client = this.props.client;
|
||||
client.renameSelf(new_name).then(result => {
|
||||
if(!result)
|
||||
this.setState({ rename: true, renameInitialName: new_name }); //TODO: Keep last name?
|
||||
});
|
||||
}
|
||||
this.setState({ rename: false });
|
||||
}
|
||||
|
||||
private onMouseDown(event: MouseEvent) {
|
||||
if(event.button !== 0) return; /* only left mouse clicks */
|
||||
|
||||
const tree = this.props.client.channelTree;
|
||||
tree.events.fire("action_select_entries", { entries: [this.props.client], mode: "auto" });
|
||||
}
|
||||
|
||||
private onContextMenu(event: MouseEvent) {
|
||||
if(settings.static(Settings.KEY_DISABLE_CONTEXT_MENU))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
const client = this.props.client;
|
||||
if(client.channelTree.selection.is_multi_select() && client.isSelected()) return;
|
||||
|
||||
client.channelTree.events.fire("action_select_entries", {
|
||||
entries: [ client ],
|
||||
mode: "exclusive"
|
||||
});
|
||||
client.showContextMenu(event.pageX, event.pageY);
|
||||
}
|
||||
|
||||
@EventHandler<ClientEvents>("notify_select_state_change")
|
||||
private handleSelectChangeState() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
.serverEntry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
position: relative;
|
||||
|
||||
cursor: pointer;
|
||||
margin-left: 2px;
|
||||
margin-right: 5px;
|
||||
|
||||
.server_type {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
align-self: center;
|
||||
color: var(--channel-tree-entry-color);
|
||||
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import {
|
||||
BatchUpdateAssignment,
|
||||
BatchUpdateType,
|
||||
ReactComponentBase
|
||||
} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import {ServerEntry as ServerEntryController, ServerEvents} from "../server";
|
||||
import * as React from "react";
|
||||
import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry";
|
||||
import {ConnectionEvents, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
|
||||
const serverStyle = require("./Server.scss");
|
||||
const viewStyle = require("./View.scss");
|
||||
|
||||
|
||||
export interface ServerEntryProperties {
|
||||
server: ServerEntryController;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface ServerEntryState {
|
||||
connection_state: "connected" | "connecting" | "disconnected";
|
||||
}
|
||||
|
||||
@ReactEventHandler<ServerEntry>(e => e.props.server.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
export class ServerEntry extends TreeEntry<ServerEntryProperties, ServerEntryState> {
|
||||
private handle_connection_state_change;
|
||||
|
||||
protected defaultState(): ServerEntryState {
|
||||
return { connection_state: "disconnected" };
|
||||
}
|
||||
|
||||
protected initialize() {
|
||||
this.handle_connection_state_change = (event: ConnectionEvents["notify_connection_state_changed"]) => {
|
||||
switch (event.new_state) {
|
||||
case ConnectionState.AUTHENTICATING:
|
||||
case ConnectionState.CONNECTING:
|
||||
case ConnectionState.INITIALISING:
|
||||
this.setState({ connection_state: "connecting" });
|
||||
break;
|
||||
|
||||
case ConnectionState.CONNECTED:
|
||||
this.setState({ connection_state: "connected" });
|
||||
break;
|
||||
|
||||
case ConnectionState.DISCONNECTING:
|
||||
case ConnectionState.UNCONNECTED:
|
||||
this.setState({ connection_state: "disconnected" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Readonly<ServerEntryProperties>, nextState: Readonly<ServerEntryState>, nextContext: any): boolean {
|
||||
return this.state.connection_state !== nextState.connection_state ||
|
||||
this.props.offset !== nextProps.offset ||
|
||||
this.props.server !== nextProps.server;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.server.channelTree.client.events().on("notify_connection_state_changed", this.handle_connection_state_change);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.props.server.channelTree.client.events().off("notify_connection_state_changed", this.handle_connection_state_change);
|
||||
}
|
||||
|
||||
render() {
|
||||
let name = this.props.server.properties.virtualserver_name;
|
||||
if(this.state.connection_state === "disconnected")
|
||||
name = tr("Not connected to any server");
|
||||
else if(this.state.connection_state === "connecting")
|
||||
name = tr("Connecting to ") + this.props.server.remote_address.host + (this.props.server.remote_address.port !== 9987 ? ":" + this.props.server.remote_address.host : "");
|
||||
|
||||
return <div className={this.classList(serverStyle.serverEntry, viewStyle.treeEntry, this.props.server.isSelected() && viewStyle.selected )}
|
||||
style={{ top: this.props.offset }}
|
||||
onMouseDown={e => this.onMouseDown(e as any)}
|
||||
onContextMenu={e => this.onContextMenu(e as any)}
|
||||
>
|
||||
<UnreadMarker entry={this.props.server} />
|
||||
<div className={"icon client-server_green " + serverStyle.server_type} />
|
||||
<div className={this.classList(serverStyle.name)}>{name}</div>
|
||||
<LocalIconRenderer icon={this.props.server.channelTree.client.fileManager?.icons.load_icon(this.props.server.properties.virtualserver_icon_id)} />
|
||||
</div>
|
||||
}
|
||||
|
||||
private onMouseDown(event: MouseEvent) {
|
||||
if(event.button !== 0) return; /* only left mouse clicks */
|
||||
|
||||
this.props.server.channelTree.events.fire("action_select_entries", {
|
||||
entries: [ this.props.server ],
|
||||
mode: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
private onContextMenu(event: MouseEvent) {
|
||||
if(settings.static(Settings.KEY_DISABLE_CONTEXT_MENU))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
const server = this.props.server;
|
||||
if(server.channelTree.selection.is_multi_select() && server.isSelected())
|
||||
return;
|
||||
|
||||
server.channelTree.events.fire("action_select_entries", {
|
||||
entries: [ server ],
|
||||
mode: "exclusive"
|
||||
});
|
||||
server.spawnContextMenu(event.pageX, event.pageY);
|
||||
}
|
||||
|
||||
@EventHandler<ServerEvents>("notify_properties_updated")
|
||||
private handlePropertiesUpdated(event: ServerEvents["notify_properties_updated"]) {
|
||||
if(typeof event.updated_properties.virtualserver_name !== "undefined" || typeof event.updated_properties.virtualserver_icon_id !== "undefined") {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<ServerEvents>("notify_select_state_change")
|
||||
private handleServerSelectStateChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||
import * as React from "react";
|
||||
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
||||
|
||||
const viewStyle = require("./View.scss");
|
||||
|
||||
export interface UnreadMarkerProperties {
|
||||
entry: ChannelTreeEntry<any>;
|
||||
}
|
||||
|
||||
@ReactEventHandler<UnreadMarker>(e => e.props.entry.events)
|
||||
export class UnreadMarker extends ReactComponentBase<UnreadMarkerProperties, {}> {
|
||||
render() {
|
||||
if(!this.props.entry.isUnread())
|
||||
return null;
|
||||
return <div className={viewStyle.markerUnread} />;
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEntryEvents>("notify_unread_state_change")
|
||||
private handleUnreadStateChange() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export class TreeEntry<Props, State> extends ReactComponentBase<Props, State> { }
|
|
@ -0,0 +1,116 @@
|
|||
@import "../../../css/static/properties";
|
||||
@import "../../../css/static/mixin";
|
||||
|
||||
html:root {
|
||||
--channel-tree-entry-selected: #2d2d2d;
|
||||
--channel-tree-entry-hovered: #393939;
|
||||
--channel-tree-entry-color: #828282;
|
||||
|
||||
--channel-tree-entry-marker-unread: rgba(168, 20, 20, 0.5);
|
||||
}
|
||||
|
||||
@if 0 {
|
||||
/* the channel tree */
|
||||
.channel-tree {
|
||||
|
||||
.tree-entry {
|
||||
&.client {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channelTree {
|
||||
@include user-select(none);
|
||||
width: 100%;
|
||||
|
||||
min-width: 10em;
|
||||
min-height: 5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
white-space: pre;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
.treeEntry {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 18px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--channel-tree-entry-hovered);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--channel-tree-entry-selected);
|
||||
}
|
||||
|
||||
|
||||
.markerUnread {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 1px;
|
||||
background-color: var(--channel-tree-entry-marker-unread);
|
||||
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 24px;
|
||||
|
||||
background: linear-gradient(to right, var(--channel-tree-entry-marker-unread) 0%, rgba(0, 0, 0, 0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include transition(opacity $button_hover_animation_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channelTreeContainer {
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
scroll-behavior: smooth;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
import {
|
||||
BatchUpdateAssignment,
|
||||
BatchUpdateType,
|
||||
ReactComponentBase
|
||||
} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import {ChannelTree, ChannelTreeEvents} from "tc-shared/ui/view";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import {EventHandler, ReactEventHandler} from "tc-shared/events";
|
||||
|
||||
import {ChannelEntryView as ChannelEntryView} from "./Channel";
|
||||
import {ServerEntry as ServerEntryView} from "./Server";
|
||||
import {ClientEntry as ClientEntryView} from "./Client";
|
||||
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
import {ServerEntry} from "tc-shared/ui/server";
|
||||
import {ClientEntry, LocalClientEntry} from "tc-shared/ui/client";
|
||||
|
||||
const viewStyle = require("./View.scss");
|
||||
|
||||
|
||||
export interface ChannelTreeViewProperties {
|
||||
tree: ChannelTree;
|
||||
}
|
||||
|
||||
export interface ChannelTreeViewState {
|
||||
element_scroll_offset?: number; /* in px */
|
||||
scroll_offset: number; /* in px */
|
||||
view_height: number; /* in px */
|
||||
}
|
||||
|
||||
type TreeEntry = ChannelEntry | ServerEntry | ClientEntry;
|
||||
type FlatTreeEntry = {
|
||||
rendered: any;
|
||||
entry: TreeEntry;
|
||||
}
|
||||
|
||||
//TODO: Only register listeners when channel is in view ;)
|
||||
@ReactEventHandler<ChannelTreeView>(e => e.props.tree.events)
|
||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewProperties, ChannelTreeViewState> {
|
||||
private static readonly EntryHeight = 18;
|
||||
|
||||
private readonly ref_container = React.createRef<HTMLDivElement>();
|
||||
private resize_observer: ResizeObserver;
|
||||
|
||||
private flat_tree: FlatTreeEntry[] = [];
|
||||
private listener_client_change;
|
||||
private listener_channel_change;
|
||||
private update_timeout;
|
||||
|
||||
private in_view_callbacks: {
|
||||
index: number,
|
||||
callback: () => void,
|
||||
timeout
|
||||
}[] = [];
|
||||
|
||||
protected defaultState(): ChannelTreeViewState {
|
||||
return {
|
||||
scroll_offset: 0,
|
||||
view_height: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.resize_observer = new ResizeObserver(entries => {
|
||||
if(entries.length !== 1) {
|
||||
if(entries.length === 0)
|
||||
console.warn("Channel resize observer fired resize event with no entries!");
|
||||
else
|
||||
console.warn("Channel resize observer fired resize event with more than one entry which should not be possible (%d)!", entries.length);
|
||||
return;
|
||||
}
|
||||
const bounds = entries[0].contentRect;
|
||||
if(this.state.view_height !== bounds.height) {
|
||||
console.log("Handling height update and change tree height to %d from %d", bounds.height, this.state.view_height);
|
||||
this.setState({
|
||||
view_height: bounds.height
|
||||
});
|
||||
}
|
||||
});
|
||||
this.resize_observer.observe(this.ref_container.current);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.resize_observer.disconnect();
|
||||
this.resize_observer = undefined;
|
||||
}
|
||||
|
||||
protected initialize() {
|
||||
(window as any).do_tree_update = () => this.handleTreeUpdate();
|
||||
this.listener_client_change = () => this.handleTreeUpdate();
|
||||
this.listener_channel_change = () => this.handleTreeUpdate();
|
||||
}
|
||||
|
||||
private handleTreeUpdate() {
|
||||
clearTimeout(this.update_timeout);
|
||||
this.update_timeout = setTimeout(() => {
|
||||
this.rebuild_tree();
|
||||
this.forceUpdate();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private visibleEntries() {
|
||||
let view_entry_count = Math.ceil(this.state.view_height / ChannelTreeView.EntryHeight);
|
||||
const view_entry_begin = Math.floor(this.state.scroll_offset / ChannelTreeView.EntryHeight);
|
||||
const view_entry_end = Math.min(this.flat_tree.length, view_entry_begin + view_entry_count);
|
||||
|
||||
return {
|
||||
begin: view_entry_begin,
|
||||
end: view_entry_end
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const entry_prerender_count = 5;
|
||||
const entry_postrender_count = 5;
|
||||
|
||||
const elements = [];
|
||||
const renderedRange = this.visibleEntries();
|
||||
const view_entry_begin = Math.max(0, renderedRange.begin - entry_prerender_count);
|
||||
const view_entry_end = Math.min(this.flat_tree.length, renderedRange.end + entry_postrender_count);
|
||||
|
||||
for (let index = view_entry_begin; index < view_entry_end; index++)
|
||||
elements.push(this.flat_tree[index].rendered);
|
||||
|
||||
for(const callback of this.in_view_callbacks.slice(0)) {
|
||||
if(callback.index >= renderedRange.begin && callback.index <= renderedRange.end) {
|
||||
clearTimeout(callback.timeout);
|
||||
callback.callback();
|
||||
this.in_view_callbacks.remove(callback);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={viewStyle.channelTreeContainer} onScroll={() => this.onScroll()} ref={this.ref_container} >
|
||||
<div className={viewStyle.channelTree} style={{height: (this.flat_tree.length * ChannelTreeView.EntryHeight) + "px"}}>
|
||||
{elements}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private build_top_offset: number;
|
||||
private build_sub_tree(entry: ChannelEntry, depth: number) {
|
||||
entry.events.on("notify_clients_changed", this.listener_client_change);
|
||||
entry.events.on("notify_children_changed", this.listener_channel_change);
|
||||
|
||||
this.flat_tree.push({
|
||||
entry: entry,
|
||||
rendered: <ChannelEntryView key={"channel-" + entry.channelId} channel={entry} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth} ref={entry.view} />
|
||||
});
|
||||
this.flat_tree.push(...entry.clients(false).map(e => {
|
||||
return {
|
||||
entry: e,
|
||||
rendered: <ClientEntryView key={"client-" + e.clientId()} client={e} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth + 1} ref={e.view} />
|
||||
};
|
||||
}));
|
||||
for (const channel of entry.children(false))
|
||||
this.build_sub_tree(channel, depth + 1);
|
||||
}
|
||||
|
||||
private rebuild_tree() {
|
||||
const tree = this.props.tree;
|
||||
{
|
||||
let index = this.flat_tree.length;
|
||||
while(index--) {
|
||||
const entry = this.flat_tree[index].entry;
|
||||
if(entry instanceof ChannelEntry) {
|
||||
entry.events.off("notify_clients_changed", this.listener_client_change);
|
||||
entry.events.off("notify_children_changed", this.listener_channel_change);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.build_top_offset = -ChannelTreeView.EntryHeight; /* because of the += */
|
||||
this.flat_tree = [{
|
||||
entry: tree.server,
|
||||
rendered: <ServerEntryView key={"server"} server={tree.server} offset={this.build_top_offset += ChannelTreeView.EntryHeight} ref={tree.server.view} />
|
||||
}];
|
||||
|
||||
for (const channel of tree.rootChannel())
|
||||
this.build_sub_tree(channel, 1);
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_root_channel_changed")
|
||||
private handleRootChannelChanged() {
|
||||
this.handleTreeUpdate();
|
||||
}
|
||||
|
||||
private onScroll() {
|
||||
this.setState({
|
||||
scroll_offset: this.ref_container.current.scrollTop
|
||||
});
|
||||
}
|
||||
|
||||
scrollEntryInView(entry: TreeEntry, callback?: () => void) {
|
||||
const index = this.flat_tree.findIndex(e => e.entry === entry);
|
||||
if(index === -1) {
|
||||
if(callback) callback();
|
||||
console.warn("Failed to scroll tree entry in view because its not registered within the view. Entry: %o", entry);
|
||||
return;
|
||||
}
|
||||
|
||||
let new_index;
|
||||
const currentRange = this.visibleEntries();
|
||||
if(index >= currentRange.end - 1) {
|
||||
new_index = index - (currentRange.end - currentRange.begin) + 2;
|
||||
} else if(index < currentRange.begin) {
|
||||
new_index = index;
|
||||
} else {
|
||||
if(callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ref_container.current.scrollTop = new_index * ChannelTreeView.EntryHeight;
|
||||
|
||||
if(callback) {
|
||||
let cb = {
|
||||
index: index,
|
||||
callback: callback,
|
||||
timeout: setTimeout(() => {
|
||||
this.in_view_callbacks.remove(cb);
|
||||
callback();
|
||||
}, (Math.abs(new_index - currentRange.begin) / (currentRange.end - currentRange.begin)) * 1500)
|
||||
};
|
||||
this.in_view_callbacks.push(cb);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,940 +0,0 @@
|
|||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import * as log from "tc-shared/log";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {PermissionType} from "tc-shared/permission/PermissionType";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {KeyCode, SpecialKey} from "tc-shared/PPTListener";
|
||||
import {createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {Sound} from "tc-shared/sound/Sounds";
|
||||
import {Group} from "tc-shared/permission/GroupManager";
|
||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
||||
import {ServerAddress, ServerEntry} from "tc-shared/ui/server";
|
||||
import {ClientMover} from "tc-shared/ui/client_move";
|
||||
import {ChannelEntry, ChannelSubscribeMode} from "tc-shared/ui/channel";
|
||||
import {ClientEntry, ClientType, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client";
|
||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
|
||||
import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel";
|
||||
import * as ppt from "tc-backend/ppt";
|
||||
|
||||
export class ChannelTree {
|
||||
client: ConnectionHandler;
|
||||
server: ServerEntry;
|
||||
|
||||
channels: ChannelEntry[] = [];
|
||||
clients: ClientEntry[] = [];
|
||||
|
||||
currently_selected: ClientEntry | ServerEntry | ChannelEntry | (ClientEntry | ServerEntry)[] = undefined;
|
||||
currently_selected_context_callback: (event) => any = undefined;
|
||||
readonly client_mover: ClientMover;
|
||||
|
||||
private _tag_container: JQuery;
|
||||
private _tag_entries: JQuery;
|
||||
|
||||
private _tree_detached: boolean = false;
|
||||
private _show_queries: boolean;
|
||||
private channel_last?: ChannelEntry;
|
||||
private channel_first?: ChannelEntry;
|
||||
|
||||
private _focused = false;
|
||||
private _listener_document_click;
|
||||
private _listener_document_key;
|
||||
|
||||
private _scroll_bar/*: SimpleBar*/;
|
||||
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
this._tag_container = $.spawn("div").addClass("channel-tree-container");
|
||||
this._tag_entries = $.spawn("div").addClass("channel-tree");
|
||||
//if('SimpleBar' in window) /* for MSEdge, and may consider Firefox? */
|
||||
// this._scroll_bar = new SimpleBar(this._tag_container[0]);
|
||||
|
||||
this.client_mover = new ClientMover(this);
|
||||
this.reset();
|
||||
|
||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||
this._tag_container.on("contextmenu", (event) => {
|
||||
if(event.isDefaultPrevented()) return;
|
||||
|
||||
for(const element of document.elementsFromPoint(event.pageX, event.pageY))
|
||||
if(element.classList.contains("channelLine") || element.classList.contains("client"))
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
if($.isArray(this.currently_selected)) { //Multiselect
|
||||
(this.currently_selected_context_callback || ((_) => null))(event);
|
||||
} else {
|
||||
this.onSelect(undefined);
|
||||
this.showContextMenu(event.pageX, event.pageY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._tag_container.on('resize', this.handle_resized.bind(this));
|
||||
this._listener_document_key = event => this.handle_key_press(event);
|
||||
this._listener_document_click = event => {
|
||||
this._focused = false;
|
||||
let element = event.target as HTMLElement;
|
||||
while(element) {
|
||||
if(element === this._tag_container[0]) {
|
||||
this._focused = true;
|
||||
break;
|
||||
}
|
||||
element = element.parentNode as HTMLElement;
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', this._listener_document_click);
|
||||
document.addEventListener('keydown', this._listener_document_key);
|
||||
}
|
||||
|
||||
tag_tree() : JQuery {
|
||||
return this._tag_container;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._listener_document_click && document.removeEventListener('click', this._listener_document_click);
|
||||
this._listener_document_click = undefined;
|
||||
|
||||
this._listener_document_key && document.removeEventListener('keydown', this._listener_document_key);
|
||||
this._listener_document_key = undefined;
|
||||
|
||||
if(this.server) {
|
||||
this.server.destroy();
|
||||
this.server = undefined;
|
||||
}
|
||||
this.reset(); /* cleanup channel and clients */
|
||||
|
||||
this.channel_first = undefined;
|
||||
this.channel_last = undefined;
|
||||
|
||||
this._tag_container.remove();
|
||||
this.currently_selected = undefined;
|
||||
this.currently_selected_context_callback = undefined;
|
||||
}
|
||||
|
||||
hide_channel_tree() {
|
||||
this._tag_entries.detach();
|
||||
this._tree_detached = true;
|
||||
}
|
||||
|
||||
show_channel_tree() {
|
||||
this._tree_detached = false;
|
||||
if(this._scroll_bar)
|
||||
this._tag_entries.appendTo(this._scroll_bar.getContentElement());
|
||||
else
|
||||
this._tag_entries.appendTo(this._tag_container);
|
||||
|
||||
this.channels.forEach(e => {
|
||||
e.recalculate_repetitive_name();
|
||||
e.reorderClients();
|
||||
});
|
||||
this._scroll_bar?.recalculate();
|
||||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
|
||||
let channelCreate =
|
||||
this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_TEMPORARY).granted(1) ||
|
||||
this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT).granted(1) ||
|
||||
this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_PERMANENT).granted(1);
|
||||
|
||||
contextmenu.spawn_context_menu(x, y,
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_create",
|
||||
name: tr("Create channel"),
|
||||
invalidPermission: !channelCreate,
|
||||
callback: () => this.spawnCreateChannel()
|
||||
},
|
||||
contextmenu.Entry.CLOSE(on_close)
|
||||
);
|
||||
}
|
||||
|
||||
initialiseHead(serverName: string, address: ServerAddress) {
|
||||
if(this.server) {
|
||||
this.server.destroy();
|
||||
this.server = undefined;
|
||||
}
|
||||
this.server = new ServerEntry(this, serverName, address);
|
||||
this.server.htmlTag.appendTo(this._tag_entries);
|
||||
this.server.initializeListener();
|
||||
}
|
||||
|
||||
private __deleteAnimation(element: ChannelEntry | ClientEntry) {
|
||||
let tag = element instanceof ChannelEntry ? element.rootTag() : element.tag;
|
||||
tag.fadeOut("slow", () => {
|
||||
tag.detach();
|
||||
element.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
rootChannel() : ChannelEntry[] {
|
||||
return this.channels.filter(e => e.parent == undefined);
|
||||
}
|
||||
|
||||
deleteChannel(channel: ChannelEntry) {
|
||||
const _this = this;
|
||||
for(let index = 0; index < this.channels.length; index++) {
|
||||
let entry = this.channels[index];
|
||||
let currentEntry = this.channels[index];
|
||||
while(currentEntry != undefined && currentEntry != null) {
|
||||
if(currentEntry == channel) {
|
||||
_this.channels.remove(entry);
|
||||
_this.__deleteAnimation(entry);
|
||||
entry.channelTree = null;
|
||||
index--;
|
||||
break;
|
||||
} else currentEntry = currentEntry.parent_channel();
|
||||
}
|
||||
}
|
||||
|
||||
this.channels.remove(channel);
|
||||
this.__deleteAnimation(channel);
|
||||
channel.channelTree = null;
|
||||
|
||||
if(channel.channel_previous)
|
||||
channel.channel_previous.channel_next = channel.channel_next;
|
||||
|
||||
if(channel.channel_next)
|
||||
channel.channel_next.channel_previous = channel.channel_previous;
|
||||
|
||||
if(channel == this.channel_first)
|
||||
this.channel_first = channel.channel_next;
|
||||
|
||||
if(channel == this.channel_last)
|
||||
this.channel_last = channel.channel_previous;
|
||||
|
||||
if(this._scroll_bar) this._scroll_bar.recalculate();
|
||||
}
|
||||
|
||||
insertChannel(channel: ChannelEntry) {
|
||||
channel.channelTree = this;
|
||||
this.channels.push(channel);
|
||||
|
||||
let elm = undefined;
|
||||
let tag = this._tag_entries;
|
||||
|
||||
let previous_channel = null;
|
||||
if(channel.hasParent()) {
|
||||
let parent = channel.parent_channel();
|
||||
let siblings = parent.children();
|
||||
if(siblings.length == 0) {
|
||||
elm = parent.rootTag();
|
||||
previous_channel = null;
|
||||
} else {
|
||||
previous_channel = siblings.last();
|
||||
elm = previous_channel.tag;
|
||||
}
|
||||
tag = parent.siblingTag();
|
||||
} else {
|
||||
previous_channel = this.channel_last;
|
||||
|
||||
if(!this.channel_last)
|
||||
this.channel_last = channel;
|
||||
|
||||
if(!this.channel_first)
|
||||
this.channel_first = channel;
|
||||
}
|
||||
|
||||
channel.channel_previous = previous_channel;
|
||||
channel.channel_next = undefined;
|
||||
|
||||
if(previous_channel) {
|
||||
channel.channel_next = previous_channel.channel_next;
|
||||
previous_channel.channel_next = channel;
|
||||
|
||||
if(channel.channel_next)
|
||||
channel.channel_next.channel_previous = channel;
|
||||
}
|
||||
|
||||
let entry = channel.rootTag();
|
||||
if(!this._tree_detached)
|
||||
entry.css({display: "none"}).fadeIn("slow");
|
||||
entry.appendTo(tag);
|
||||
|
||||
if(elm != undefined)
|
||||
elm.after(entry);
|
||||
|
||||
if(channel.channel_previous == channel) /* shall never happen */
|
||||
channel.channel_previous = undefined;
|
||||
if(channel.channel_next == channel) /* shall never happen */
|
||||
channel.channel_next = undefined;
|
||||
|
||||
channel.initializeListener();
|
||||
channel.update_family_index();
|
||||
if(this._scroll_bar) this._scroll_bar.recalculate();
|
||||
}
|
||||
|
||||
findChannel(channelId: number) : ChannelEntry | undefined {
|
||||
for(let index = 0; index < this.channels.length; index++)
|
||||
if(this.channels[index].getChannelId() == channelId) return this.channels[index];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
find_channel_by_name(name: string, parent?: ChannelEntry, force_parent: boolean = true) : ChannelEntry | undefined {
|
||||
for(let index = 0; index < this.channels.length; index++)
|
||||
if(this.channels[index].channelName() == name && (!force_parent || parent == this.channels[index].parent))
|
||||
return this.channels[index];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
moveChannel(channel: ChannelEntry, channel_previous: ChannelEntry, parent: ChannelEntry) {
|
||||
if(channel_previous != null && channel_previous.parent != parent) {
|
||||
console.error(tr("Invalid channel move (different parents! (%o|%o)"), channel_previous.parent, parent);
|
||||
return;
|
||||
}
|
||||
|
||||
if(channel.channel_next)
|
||||
channel.channel_next.channel_previous = channel.channel_previous;
|
||||
|
||||
if(channel.channel_previous)
|
||||
channel.channel_previous.channel_next = channel.channel_next;
|
||||
|
||||
if(channel == this.channel_last)
|
||||
this.channel_last = channel.channel_previous;
|
||||
|
||||
if(channel == this.channel_first)
|
||||
this.channel_first = channel.channel_next;
|
||||
|
||||
|
||||
channel.channel_next = undefined;
|
||||
channel.channel_previous = channel_previous;
|
||||
channel.parent = parent;
|
||||
|
||||
if(channel_previous) {
|
||||
if(channel_previous == this.channel_last)
|
||||
this.channel_last = channel;
|
||||
|
||||
channel.channel_next = channel_previous.channel_next;
|
||||
channel_previous.channel_next = channel;
|
||||
channel_previous.rootTag().after(channel.rootTag());
|
||||
|
||||
if(channel.channel_next)
|
||||
channel.channel_next.channel_previous = channel;
|
||||
} else {
|
||||
if(parent) {
|
||||
let children = parent.children();
|
||||
if(children.length <= 1) { //Self should be already in there
|
||||
let left = channel.rootTag();
|
||||
left.appendTo(parent.siblingTag());
|
||||
|
||||
channel.channel_next = undefined;
|
||||
} else {
|
||||
channel.channel_previous = undefined;
|
||||
channel.rootTag().prependTo(parent.siblingTag());
|
||||
|
||||
channel.channel_next = children[1]; /* children 0 shall be the channel itself */
|
||||
channel.channel_next.channel_previous = channel;
|
||||
}
|
||||
} else {
|
||||
this._tag_entries.find(".server").after(channel.rootTag());
|
||||
|
||||
channel.channel_next = this.channel_first;
|
||||
if(this.channel_first)
|
||||
this.channel_first.channel_previous = channel;
|
||||
|
||||
this.channel_first = channel;
|
||||
}
|
||||
}
|
||||
|
||||
channel.update_family_index();
|
||||
channel.children(true).forEach(e => e.update_family_index());
|
||||
channel.clients(true).forEach(e => e.update_family_index());
|
||||
|
||||
if(channel.channel_previous == channel) { /* shall never happen */
|
||||
channel.channel_previous = undefined;
|
||||
debugger;
|
||||
}
|
||||
if(channel.channel_next == channel) { /* shall never happen */
|
||||
channel.channel_next = undefined;
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
|
||||
deleteClient(client: ClientEntry, animate_tag?: boolean) {
|
||||
const old_channel = client.currentChannel();
|
||||
this.clients.remove(client);
|
||||
if(typeof(animate_tag) !== "boolean" || animate_tag)
|
||||
this.__deleteAnimation(client);
|
||||
else
|
||||
client.tag.detach();
|
||||
client.onDelete();
|
||||
|
||||
if(old_channel) {
|
||||
this.client.side_bar.info_frame().update_channel_client_count(old_channel);
|
||||
}
|
||||
|
||||
const voice_connection = this.client.serverConnection.voice_connection();
|
||||
if(client.get_audio_handle()) {
|
||||
if(!voice_connection) {
|
||||
log.warn(LogCategory.VOICE, tr("Deleting client with a voice handle, but we haven't a voice connection!"));
|
||||
} else {
|
||||
voice_connection.unregister_client(client.get_audio_handle());
|
||||
}
|
||||
}
|
||||
client.set_audio_handle(undefined);
|
||||
if(this._scroll_bar) this._scroll_bar.recalculate();
|
||||
}
|
||||
|
||||
registerClient(client: ClientEntry) {
|
||||
this.clients.push(client);
|
||||
client.channelTree = this;
|
||||
|
||||
const voice_connection = this.client.serverConnection.voice_connection();
|
||||
if(voice_connection)
|
||||
client.set_audio_handle(voice_connection.register_client(client.clientId()));
|
||||
}
|
||||
|
||||
unregisterClient(client: ClientEntry) {
|
||||
if(!this.clients.remove(client))
|
||||
return;
|
||||
}
|
||||
|
||||
private _update_timer: number;
|
||||
private _reorder_channels = new Set<ChannelEntry>();
|
||||
|
||||
insertClient(client: ClientEntry, channel: ChannelEntry) : ClientEntry {
|
||||
let newClient = this.findClient(client.clientId());
|
||||
if(newClient)
|
||||
client = newClient; //Got new client :)
|
||||
else {
|
||||
this.registerClient(client);
|
||||
}
|
||||
|
||||
client["_channel"] = channel;
|
||||
let tag = client.tag;
|
||||
|
||||
if(!this._show_queries && client.properties.client_type == ClientType.CLIENT_QUERY)
|
||||
client.tag.hide();
|
||||
else if(!this._tree_detached)
|
||||
tag.css("display", "none").fadeIn("slow");
|
||||
|
||||
tag.appendTo(channel.clientTag());
|
||||
channel.reorderClients();
|
||||
|
||||
/* schedule a reorder for this channel. */
|
||||
this._reorder_channels.add(client.currentChannel());
|
||||
if(!this._update_timer) {
|
||||
this._update_timer = setTimeout(() => {
|
||||
this._update_timer = undefined;
|
||||
for(const channel of this._reorder_channels) {
|
||||
channel.updateChannelTypeIcon();
|
||||
this.client.side_bar.info_frame().update_channel_client_count(channel);
|
||||
}
|
||||
this._reorder_channels.clear();
|
||||
}, 5) as any;
|
||||
}
|
||||
|
||||
client.update_family_index(); /* why the hell is this here?! */
|
||||
if(this._scroll_bar) this._scroll_bar.recalculate();
|
||||
return client;
|
||||
}
|
||||
|
||||
moveClient(client: ClientEntry, channel: ChannelEntry) {
|
||||
let oldChannel = client.currentChannel();
|
||||
client["_channel"] = channel;
|
||||
|
||||
let tag = client.tag;
|
||||
tag.detach();
|
||||
tag.appendTo(client.currentChannel().clientTag());
|
||||
if(oldChannel) {
|
||||
oldChannel.updateChannelTypeIcon();
|
||||
this.client.side_bar.info_frame().update_channel_client_count(oldChannel);
|
||||
}
|
||||
if(channel) {
|
||||
channel.reorderClients();
|
||||
channel.updateChannelTypeIcon();
|
||||
this.client.side_bar.info_frame().update_channel_client_count(channel);
|
||||
}
|
||||
client.updateClientStatusIcons();
|
||||
client.update_family_index();
|
||||
}
|
||||
|
||||
findClient?(clientId: number) : ClientEntry {
|
||||
for(let index = 0; index < this.clients.length; index++) {
|
||||
if(this.clients[index].clientId() == clientId)
|
||||
return this.clients[index];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
find_client_by_dbid?(client_dbid: number) : ClientEntry {
|
||||
for(let index = 0; index < this.clients.length; index++) {
|
||||
if(this.clients[index].properties.client_database_id == client_dbid)
|
||||
return this.clients[index];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
find_client_by_unique_id?(unique_id: string) : ClientEntry {
|
||||
for(let index = 0; index < this.clients.length; index++) {
|
||||
if(this.clients[index].properties.client_unique_identifier == unique_id)
|
||||
return this.clients[index];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static same_selected_type(a, b) {
|
||||
if(a instanceof ChannelEntry)
|
||||
return b instanceof ChannelEntry;
|
||||
if(a instanceof ClientEntry)
|
||||
return b instanceof ClientEntry;
|
||||
if(a instanceof ServerEntry)
|
||||
return b instanceof ServerEntry;
|
||||
return a == b;
|
||||
}
|
||||
|
||||
onSelect(entry?: ChannelEntry | ClientEntry | ServerEntry, enforce_single?: boolean, flag_shift?: boolean) {
|
||||
if(this.currently_selected && (ppt.key_pressed(SpecialKey.SHIFT) || flag_shift) && entry instanceof ClientEntry) { //Currently we're only supporting client multiselects :D
|
||||
if(!entry) return; //Nowhere
|
||||
|
||||
if($.isArray(this.currently_selected)) {
|
||||
if(!ChannelTree.same_selected_type(this.currently_selected[0], entry)) return; //Not the same type
|
||||
} else if(ChannelTree.same_selected_type(this.currently_selected, entry)) {
|
||||
this.currently_selected = [this.currently_selected] as any;
|
||||
}
|
||||
if(entry instanceof ChannelEntry)
|
||||
this.currently_selected_context_callback = this.callback_multiselect_channel.bind(this);
|
||||
if(entry instanceof ClientEntry)
|
||||
this.currently_selected_context_callback = this.callback_multiselect_client.bind(this);
|
||||
} else
|
||||
this.currently_selected = undefined;
|
||||
|
||||
if(!$.isArray(this.currently_selected) || enforce_single) {
|
||||
this.currently_selected = entry;
|
||||
this._tag_entries.find(".selected").each(function (idx, e) {
|
||||
$(e).removeClass("selected");
|
||||
});
|
||||
} else {
|
||||
for(const e of this.currently_selected)
|
||||
if(e == entry) {
|
||||
this.currently_selected.remove(e);
|
||||
if(entry instanceof ChannelEntry)
|
||||
(entry as ChannelEntry).channelTag().removeClass("selected");
|
||||
else if(entry instanceof ClientEntry)
|
||||
(entry as ClientEntry).tag.removeClass("selected");
|
||||
else if(entry instanceof ServerEntry)
|
||||
(entry as ServerEntry).htmlTag.removeClass("selected");
|
||||
if(this.currently_selected.length == 1)
|
||||
this.currently_selected = this.currently_selected[0];
|
||||
else if(this.currently_selected.length == 0)
|
||||
this.currently_selected = undefined;
|
||||
//Already selected
|
||||
return;
|
||||
}
|
||||
this.currently_selected.push(entry as any);
|
||||
}
|
||||
|
||||
if(entry instanceof ChannelEntry)
|
||||
(entry as ChannelEntry).channelTag().addClass("selected");
|
||||
else if(entry instanceof ClientEntry)
|
||||
(entry as ClientEntry).tag.addClass("selected");
|
||||
else if(entry instanceof ServerEntry)
|
||||
(entry as ServerEntry).htmlTag.addClass("selected");
|
||||
|
||||
if(!$.isArray(this.currently_selected)) {
|
||||
if(this.currently_selected instanceof ClientEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) {
|
||||
if(this.currently_selected instanceof MusicClientEntry)
|
||||
this.client.side_bar.show_music_player(this.currently_selected as MusicClientEntry);
|
||||
else
|
||||
this.client.side_bar.show_client_info(this.currently_selected);
|
||||
} else if(this.currently_selected instanceof ChannelEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.client.side_bar.channel_conversations().set_current_channel(this.currently_selected.channelId);
|
||||
this.client.side_bar.show_channel_conversations();
|
||||
} else if(this.currently_selected instanceof ServerEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.client.side_bar.channel_conversations().set_current_channel(0);
|
||||
this.client.side_bar.show_channel_conversations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private callback_multiselect_channel(event) {
|
||||
console.log(tr("Multiselect channel"));
|
||||
}
|
||||
private callback_multiselect_client(event) {
|
||||
console.log(tr("Multiselect client"));
|
||||
const clients = this.currently_selected as ClientEntry[];
|
||||
const music_only = clients.map(e => e instanceof MusicClientEntry ? 0 : 1).reduce((a, b) => a + b, 0) == 0;
|
||||
const music_entry = clients.map(e => e instanceof MusicClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0;
|
||||
const local_client = clients.map(e => e instanceof LocalClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0;
|
||||
let entries: contextmenu.MenuEntry[] = [];
|
||||
if (!music_entry && !local_client) { //Music bots or local client cant be poked
|
||||
entries.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-poke",
|
||||
name: tr("Poke clients"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => {
|
||||
if (typeof(result) === "string") {
|
||||
for (const client of this.currently_selected as ClientEntry[])
|
||||
this.client.serverConnection.send_command("clientpoke", {
|
||||
clid: client.clientId(),
|
||||
msg: result
|
||||
});
|
||||
|
||||
}
|
||||
}, {width: 400, maxLength: 512}).open();
|
||||
}
|
||||
});
|
||||
}
|
||||
entries.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-move_client_to_own_channel",
|
||||
name: tr("Move clients to your channel"),
|
||||
callback: () => {
|
||||
const target = this.client.getClient().currentChannel().getChannelId();
|
||||
for(const client of clients)
|
||||
this.client.serverConnection.send_command("clientmove", {
|
||||
clid: client.clientId(),
|
||||
cid: target
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!local_client) {//local client cant be kicked and/or banned or kicked
|
||||
entries.push(contextmenu.Entry.HR());
|
||||
entries.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-kick_channel",
|
||||
name: tr("Kick clients from channel"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Kick clients from channel"), tr("Kick reason:<br>"), text => true, result => {
|
||||
if (result) {
|
||||
for (const client of clients)
|
||||
this.client.serverConnection.send_command("clientkick", {
|
||||
clid: client.clientId(),
|
||||
reasonid: ViewReasonId.VREASON_CHANNEL_KICK,
|
||||
reasonmsg: result
|
||||
});
|
||||
|
||||
}
|
||||
}, {width: 400, maxLength: 255}).open();
|
||||
}
|
||||
});
|
||||
|
||||
if (!music_entry) { //Music bots cant be banned or kicked
|
||||
entries.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-kick_server",
|
||||
name: tr("Kick clients fom server"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Kick clients from server"), tr("Kick reason:<br>"), text => true, result => {
|
||||
if (result) {
|
||||
for (const client of clients)
|
||||
this.client.serverConnection.send_command("clientkick", {
|
||||
clid: client.clientId(),
|
||||
reasonid: ViewReasonId.VREASON_SERVER_KICK,
|
||||
reasonmsg: result
|
||||
});
|
||||
|
||||
}
|
||||
}, {width: 400, maxLength: 255}).open();
|
||||
}
|
||||
}, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-ban_client",
|
||||
name: tr("Ban clients"),
|
||||
invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
|
||||
callback: () => {
|
||||
spawnBanClient(this.client, (clients).map(entry => {
|
||||
return {
|
||||
name: entry.clientNickName(),
|
||||
unique_id: entry.properties.client_unique_identifier
|
||||
}
|
||||
}), (data) => {
|
||||
for (const client of clients)
|
||||
this.client.serverConnection.send_command("banclient", {
|
||||
uid: client.properties.client_unique_identifier,
|
||||
banreason: data.reason,
|
||||
time: data.length
|
||||
}, {
|
||||
flagset: [data.no_ip ? "no-ip" : "", data.no_hwid ? "no-hardware-id" : "", data.no_name ? "no-nickname" : ""]
|
||||
}).then(() => {
|
||||
this.client.sound.play(Sound.USER_BANNED);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if(music_only) {
|
||||
entries.push(contextmenu.Entry.HR());
|
||||
entries.push({
|
||||
name: tr("Delete bots"),
|
||||
icon_class: "client-delete",
|
||||
disabled: false,
|
||||
callback: () => {
|
||||
const param_string = clients.map((_, index) => "{" + index + "}").join(', ');
|
||||
const param_values = clients.map(client => client.createChatTag(true));
|
||||
const tag = $.spawn("div").append(...formatMessage(tr("Do you really want to delete ") + param_string, ...param_values));
|
||||
const tag_container = $.spawn("div").append(tag);
|
||||
spawnYesNo(tr("Are you sure?"), tag_container, result => {
|
||||
if(result) {
|
||||
for(const client of clients)
|
||||
this.client.serverConnection.send_command("musicbotdelete", {
|
||||
botid: client.properties.client_database_id
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
type: contextmenu.MenuEntryType.ENTRY
|
||||
});
|
||||
}
|
||||
}
|
||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries);
|
||||
}
|
||||
|
||||
clientsByGroup(group: Group) : ClientEntry[] {
|
||||
let result = [];
|
||||
|
||||
for(let client of this.clients) {
|
||||
if(client.groupAssigned(group))
|
||||
result.push(client);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
clientsByChannel(channel: ChannelEntry) : ClientEntry[] {
|
||||
let result = [];
|
||||
|
||||
for(let client of this.clients) {
|
||||
if(client.currentChannel() == channel)
|
||||
result.push(client);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
reset(){
|
||||
const voice_connection = this.client.serverConnection ? this.client.serverConnection.voice_connection() : undefined;
|
||||
for(const client of this.clients) {
|
||||
if(client.get_audio_handle() && voice_connection) {
|
||||
voice_connection.unregister_client(client.get_audio_handle());
|
||||
client.set_audio_handle(undefined);
|
||||
}
|
||||
client.destroy();
|
||||
}
|
||||
this.clients = [];
|
||||
|
||||
for(const channel of this.channels)
|
||||
channel.destroy();
|
||||
this.channels = [];
|
||||
|
||||
this._tag_entries.children().detach(); //Dont remove listeners
|
||||
|
||||
this.channel_first = undefined;
|
||||
this.channel_last = undefined;
|
||||
}
|
||||
|
||||
spawnCreateChannel(parent?: ChannelEntry) {
|
||||
createChannelModal(this.client, undefined, parent, this.client.permissions, (properties?, permissions?) => {
|
||||
if(!properties) return;
|
||||
properties["cpid"] = parent ? parent.channelId : 0;
|
||||
log.debug(LogCategory.CHANNEL, tr("Creating a new channel.\nProperties: %o\nPermissions: %o"), properties);
|
||||
this.client.serverConnection.send_command("channelcreate", properties).then(() => {
|
||||
let channel = this.find_channel_by_name(properties.channel_name, parent, true);
|
||||
if(!channel) {
|
||||
log.error(LogCategory.CHANNEL, tr("Failed to resolve channel after creation. Could not apply permissions!"));
|
||||
return;
|
||||
}
|
||||
if(permissions && permissions.length > 0) {
|
||||
let perms = [];
|
||||
for(let perm of permissions) {
|
||||
perms.push({
|
||||
permvalue: perm.value,
|
||||
permnegated: false,
|
||||
permskip: false,
|
||||
permid: perm.type.id
|
||||
});
|
||||
}
|
||||
|
||||
perms[0]["cid"] = channel.channelId;
|
||||
return this.client.serverConnection.send_command("channeladdperm", perms, {
|
||||
flagset: ["continueonerror"]
|
||||
}).then(() => new Promise<ChannelEntry>(resolve => { resolve(channel); }));
|
||||
}
|
||||
|
||||
return new Promise<ChannelEntry>(resolve => { resolve(channel); })
|
||||
}).then(channel => {
|
||||
this.client.log.log(server_log.Type.CHANNEL_CREATE, {
|
||||
channel: channel.log_data(),
|
||||
creator: this.client.getClient().log_data(),
|
||||
own_action: true
|
||||
});
|
||||
this.client.sound.play(Sound.CHANNEL_CREATED);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handle_resized() {
|
||||
for(let channel of this.channels)
|
||||
channel.handle_frame_resized();
|
||||
}
|
||||
|
||||
private select_next_channel(channel: ChannelEntry, select_client: boolean) {
|
||||
if(select_client) {
|
||||
const clients = channel.clients_ordered();
|
||||
if(clients.length > 0) {
|
||||
this.onSelect(clients[0], true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const children = channel.children();
|
||||
if(children.length > 0) {
|
||||
this.onSelect(children[0], true);
|
||||
return;
|
||||
}
|
||||
|
||||
const next = channel.channel_next;
|
||||
if(next) {
|
||||
this.onSelect(next, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let parent = channel.parent_channel();
|
||||
while(parent) {
|
||||
const p_next = parent.channel_next;
|
||||
if(p_next) {
|
||||
this.onSelect(p_next, true);
|
||||
return;
|
||||
}
|
||||
|
||||
parent = parent.parent_channel();
|
||||
}
|
||||
}
|
||||
|
||||
handle_key_press(event: KeyboardEvent) {
|
||||
//console.log("Keydown: %o | %o | %o", this._focused, this.currently_selected, Array.isArray(this.currently_selected));
|
||||
if(!this._focused || !this.currently_selected || Array.isArray(this.currently_selected)) return;
|
||||
|
||||
if(event.keyCode == KeyCode.KEY_UP) {
|
||||
event.preventDefault();
|
||||
if(this.currently_selected instanceof ChannelEntry) {
|
||||
let previous = this.currently_selected.channel_previous;
|
||||
|
||||
if(previous) {
|
||||
while(true) {
|
||||
const siblings = previous.children();
|
||||
if(siblings.length == 0) break;
|
||||
previous = siblings.last();
|
||||
}
|
||||
const clients = previous.clients_ordered();
|
||||
if(clients.length > 0) {
|
||||
this.onSelect(clients.last(), true);
|
||||
return;
|
||||
} else {
|
||||
this.onSelect(previous, true);
|
||||
return;
|
||||
}
|
||||
} else if(this.currently_selected.hasParent()) {
|
||||
const channel = this.currently_selected.parent_channel();
|
||||
const clients = channel.clients_ordered();
|
||||
if(clients.length > 0) {
|
||||
this.onSelect(clients.last(), true);
|
||||
return;
|
||||
} else {
|
||||
this.onSelect(channel, true);
|
||||
return;
|
||||
}
|
||||
} else
|
||||
this.onSelect(this.server, true);
|
||||
} else if(this.currently_selected instanceof ClientEntry) {
|
||||
const channel = this.currently_selected.currentChannel();
|
||||
const clients = channel.clients_ordered();
|
||||
const index = clients.indexOf(this.currently_selected);
|
||||
if(index > 0) {
|
||||
this.onSelect(clients[index - 1], true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSelect(channel, true);
|
||||
return;
|
||||
}
|
||||
|
||||
} else if(event.keyCode == KeyCode.KEY_DOWN) {
|
||||
event.preventDefault();
|
||||
if(this.currently_selected instanceof ChannelEntry) {
|
||||
this.select_next_channel(this.currently_selected, true);
|
||||
} else if(this.currently_selected instanceof ClientEntry){
|
||||
const channel = this.currently_selected.currentChannel();
|
||||
const clients = channel.clients_ordered();
|
||||
const index = clients.indexOf(this.currently_selected);
|
||||
if(index + 1 < clients.length) {
|
||||
this.onSelect(clients[index + 1], true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.select_next_channel(channel, false);
|
||||
} else if(this.currently_selected instanceof ServerEntry)
|
||||
this.onSelect(this.channel_first, true);
|
||||
} else if(event.keyCode == KeyCode.KEY_RETURN) {
|
||||
if(this.currently_selected instanceof ChannelEntry) {
|
||||
this.currently_selected.joinChannel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggle_server_queries(flag: boolean) {
|
||||
if(this._show_queries == flag) return;
|
||||
this._show_queries = flag;
|
||||
|
||||
const channels: ChannelEntry[] = []
|
||||
for(const client of this.clients)
|
||||
if(client.properties.client_type == ClientType.CLIENT_QUERY) {
|
||||
if(this._show_queries)
|
||||
client.tag.show();
|
||||
else
|
||||
client.tag.hide();
|
||||
if(channels.indexOf(client.currentChannel()) == -1)
|
||||
channels.push(client.currentChannel());
|
||||
}
|
||||
}
|
||||
|
||||
get_first_channel?() : ChannelEntry {
|
||||
return this.channel_first;
|
||||
}
|
||||
|
||||
unsubscribe_all_channels(subscribe_specified?: boolean) {
|
||||
if(!this.client.serverConnection || !this.client.serverConnection.connected())
|
||||
return;
|
||||
|
||||
this.client.serverConnection.send_command('channelunsubscribeall').then(() => {
|
||||
const channels: number[] = [];
|
||||
for(const channel of this.channels) {
|
||||
if(channel.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
|
||||
channels.push(channel.getChannelId());
|
||||
}
|
||||
|
||||
if(channels.length > 0) {
|
||||
this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
|
||||
console.warn(tr("Failed to subscribe to specific channels (%o)"), channels);
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn(tr("Failed to unsubscribe to all channels! (%o)"), error);
|
||||
});
|
||||
}
|
||||
|
||||
subscribe_all_channels() {
|
||||
if(!this.client.serverConnection || !this.client.serverConnection.connected())
|
||||
return;
|
||||
|
||||
this.client.serverConnection.send_command('channelsubscribeall').then(() => {
|
||||
const channels: number[] = [];
|
||||
for(const channel of this.channels) {
|
||||
if(channel.subscribe_mode == ChannelSubscribeMode.UNSUBSCRIBED)
|
||||
channels.push(channel.getChannelId());
|
||||
}
|
||||
|
||||
if(channels.length > 0) {
|
||||
this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
|
||||
console.warn(tr("Failed to unsubscribe to specific channels (%o)"), channels);
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn(tr("Failed to subscribe to all channels! (%o)"), error);
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -13,3 +13,5 @@ export function fix_declare_global(nodes: ts.Node[]) : ts.Node[] {
|
|||
|
||||
return [];
|
||||
}
|
||||
|
||||
SyntaxKind.PlusEqualsToken
|
|
@ -54,19 +54,22 @@ export class CodecWrapperWorker extends BasicCodec {
|
|||
|
||||
async initialise() : Promise<Boolean> {
|
||||
if(this._initialized) return;
|
||||
if(this._initialize_promise)
|
||||
return await this._initialize_promise;
|
||||
|
||||
this._initialize_promise = this.spawn_worker().then(() => this.execute("initialise", {
|
||||
type: this.type,
|
||||
channelCount: this.channelCount,
|
||||
})).then(result => {
|
||||
if(result.success)
|
||||
if(result.success) {
|
||||
this._initialized = true;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
log.error(LogCategory.VOICE, tr("Failed to initialize codec %s: %s"), CodecType[this.type], result.error);
|
||||
return Promise.reject(result.error);
|
||||
});
|
||||
|
||||
this._initialized = true;
|
||||
await this._initialize_promise;
|
||||
}
|
||||
|
||||
|
@ -81,6 +84,8 @@ export class CodecWrapperWorker extends BasicCodec {
|
|||
}
|
||||
|
||||
async decode(data: Uint8Array): Promise<AudioBuffer> {
|
||||
if(!this.initialized()) throw "codec not initialized/initialize failed";
|
||||
|
||||
const result = await this.execute("decodeSamples", { data: data, length: data.length });
|
||||
if(result.timings.downstream > 5 || result.timings.upstream > 5 || result.timings.handle > 5)
|
||||
log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream);
|
||||
|
@ -102,6 +107,8 @@ export class CodecWrapperWorker extends BasicCodec {
|
|||
}
|
||||
|
||||
async encode(data: AudioBuffer) : Promise<Uint8Array> {
|
||||
if(!this.initialized()) throw "codec not initialized/initialize failed";
|
||||
|
||||
let buffer = new Float32Array(this.channelCount * data.length);
|
||||
for (let offset = 0; offset < data.length; offset++) {
|
||||
for (let channel = 0; channel < this.channelCount; channel++)
|
||||
|
|
|
@ -64,6 +64,9 @@ async function handle_message(command: string, data: any) : Promise<string | obj
|
|||
|
||||
return {};
|
||||
case "encodeSamples":
|
||||
if(!codec_instance)
|
||||
return "codec not initialized/initialize failed";
|
||||
|
||||
let encodeArray = new Float32Array(data.length);
|
||||
for(let index = 0; index < encodeArray.length; index++)
|
||||
encodeArray[index] = data.data[index];
|
||||
|
@ -74,6 +77,9 @@ async function handle_message(command: string, data: any) : Promise<string | obj
|
|||
else
|
||||
return { data: encodeResult, length: encodeResult.length };
|
||||
case "decodeSamples":
|
||||
if(!codec_instance)
|
||||
return "codec not initialized/initialize failed";
|
||||
|
||||
let decodeArray = new Uint8Array(data.length);
|
||||
for(let index = 0; index < decodeArray.length; index++)
|
||||
decodeArray[index] = data.data[index];
|
||||
|
|
|
@ -111,7 +111,10 @@ export const config = async (target: "web" | "client") => { return {
|
|||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
modules: {
|
||||
mode: "local",
|
||||
localIdentName: '[path][name]__[local]--[hash:base64:5]', //FIXME: Debug mode only!
|
||||
},
|
||||
sourceMap: isDevelopment
|
||||
}
|
||||
},
|
||||
|
@ -141,6 +144,12 @@ export const config = async (target: "web" | "client") => { return {
|
|||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: "./webpack/DevelBlocks.js",
|
||||
options: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import {RawSourceMap} from "source-map";
|
||||
import * as webpack from "webpack";
|
||||
import * as loaderUtils from "loader-utils";
|
||||
|
||||
import LoaderContext = webpack.loader.LoaderContext;
|
||||
|
||||
export default function loader(this: LoaderContext, source: string | Buffer, sourceMap?: RawSourceMap): string | Buffer | void | undefined {
|
||||
this.cacheable();
|
||||
|
||||
const options = loaderUtils.getOptions(this);
|
||||
if(!options.enabled) {
|
||||
this.callback(null, source);
|
||||
return;
|
||||
}
|
||||
|
||||
const start_regex = "devel-block\\((?<name>\\S+)\\)";
|
||||
const end_regex = "devel-block-end";
|
||||
|
||||
const pattern = new RegExp("[\\t ]*\\/\\* ?" + start_regex + " ?\\*\\/[\\s\\S]*?\\/\\* ?" + end_regex + " ?\\*\\/[\\t ]*\\n?", "g");
|
||||
source = (source as string).replace(pattern, (value, type) => {
|
||||
return "/* snipped block \"" + type + "\" */";
|
||||
});
|
||||
this.callback(null, source);
|
||||
}
|
Loading…
Reference in New Issue