commit
056dc7c8d5
77 changed files with 4248 additions and 3632 deletions
23
ChangeLog.md
23
ChangeLog.md
|
@ -1,12 +1,29 @@
|
|||
# Changelog:
|
||||
* **11.03.20**
|
||||
* **21.04.20**
|
||||
- Clicking on the music bot does not longer results in the insufficient permission sound when the client has no permissions
|
||||
- Fixed permission editor overflow
|
||||
- Fixed the bookmark edit window (bookmarks have failed to save)
|
||||
|
||||
* **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)
|
||||
- Added the ability to collapse/expend the channel tree
|
||||
- Removed PHP dependencies from the project. PHP isn't needed anymore
|
||||
|
||||
* **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
|
||||
|
|
11
auth/.gitignore
vendored
11
auth/.gitignore
vendored
|
@ -1,11 +0,0 @@
|
|||
#Nope :)
|
||||
certs/
|
||||
|
||||
#A local link just for browsing the files
|
||||
xf/
|
||||
|
||||
css/**/*.css
|
||||
css/**/*.css.map
|
||||
|
||||
js/**/*.js
|
||||
js/**/*.js.map
|
250
auth/auth.php
250
auth/auth.php
|
@ -1,250 +0,0 @@
|
|||
<?php
|
||||
$GLOBALS["COOKIE_NAME_USER_DATA"] = "user_data";
|
||||
$GLOBALS["COOKIE_NAME_USER_SIGN"] = "user_sign";
|
||||
|
||||
$host = gethostname();
|
||||
$localhost = false;
|
||||
if($host == "WolverinDEV")
|
||||
$localhost = true;
|
||||
|
||||
function authPath() {
|
||||
if (file_exists("auth")) {
|
||||
return "auth/";
|
||||
} else return "";
|
||||
}
|
||||
|
||||
function mainPath() {
|
||||
global $localhost;
|
||||
if ($localhost) {
|
||||
return "../";
|
||||
} else return "";
|
||||
}
|
||||
|
||||
function remoteAddress()
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/** @return \XF\App */
|
||||
function getXF()
|
||||
{
|
||||
if (isset($GLOBALS["XF_APP"])) return $GLOBALS["XF_APP"];
|
||||
|
||||
if (file_exists("/var/www/forum.teaspeak"))
|
||||
$dir = "/var/www/forum.teaspeak";
|
||||
else if (file_exists(__DIR__ . "/xf"))
|
||||
$dir = __DIR__ . "/xf";
|
||||
else if (file_exists(__DIR__ . "/auth/xf"))
|
||||
$dir = __DIR__ . "/auth/xf";
|
||||
else
|
||||
return null;
|
||||
|
||||
require($dir . '/src/XF.php');
|
||||
|
||||
XF::start($dir);
|
||||
return ($GLOBALS["XF_APP"] = XF::setupApp('XF\Pub\App'));
|
||||
}
|
||||
|
||||
function milliseconds()
|
||||
{
|
||||
$mt = explode(' ', microtime());
|
||||
return ((int)$mt[1]) * 1000 + ((int)round($mt[0] * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $username
|
||||
* @param $password
|
||||
* @return array
|
||||
*/
|
||||
function checkLogin($username, $password) {
|
||||
$allowedXFGroups = [
|
||||
3, //Administrator
|
||||
6, //Web tester
|
||||
5 //Premium
|
||||
];
|
||||
$app = getXF();
|
||||
|
||||
$response = [];
|
||||
$response["success"] = false;
|
||||
if(!$app) goto _return;
|
||||
|
||||
if (!isset($username) || !isset($password)) {
|
||||
$response["msg"] = "missing credentials";
|
||||
goto _return;
|
||||
}
|
||||
|
||||
/** @var \XF\Service\User\Login $loginService */
|
||||
$loginService = $app->service('XF:User\Login', $username, "");
|
||||
if (!$loginService->isLoginLimited()) {
|
||||
$error = "";
|
||||
$user = $loginService->validate($password, $error);
|
||||
if ($user) {
|
||||
$response["success"] = true;
|
||||
$allowed = false;
|
||||
foreach ($allowedXFGroups as $id) {
|
||||
foreach ($user->secondary_group_ids as $assigned)
|
||||
if ($assigned == $id) {
|
||||
$allowed = true;
|
||||
break;
|
||||
}
|
||||
$allowed |= $user->user_group_id == $id;
|
||||
if ($allowed) break;
|
||||
}
|
||||
if ($allowed) {
|
||||
$response["allowed"] = true;
|
||||
|
||||
try {
|
||||
/** @var $session XF\Session\Session */
|
||||
$session = $app->session();
|
||||
if (!$session->exists()) {
|
||||
$session->expunge();
|
||||
if (!$session->start(remoteAddress())) {
|
||||
$response["success"] = false;
|
||||
$response["msg"] = "could not create session";
|
||||
goto _return;
|
||||
}
|
||||
}
|
||||
$session->changeUser($user);
|
||||
$session->save();
|
||||
$response["sessionName"] = $session->getCookieName();
|
||||
$response["sessionId"] = $session->getSessionId();
|
||||
$response["user_name"] = $user->username;
|
||||
} catch (Exception $error) {
|
||||
$response["success"] = false;
|
||||
$response["msg"] = $error->getMessage();
|
||||
}
|
||||
goto _return;
|
||||
} else {
|
||||
$response["allowed"] = false;
|
||||
}
|
||||
} else {
|
||||
$response["msg"] = $error;
|
||||
}
|
||||
} else {
|
||||
$response["msg"] = "Too many login's!";
|
||||
}
|
||||
|
||||
_return:
|
||||
return $response;
|
||||
}
|
||||
|
||||
function logged_in() {
|
||||
return test_session() == 0;
|
||||
}
|
||||
|
||||
function logout()
|
||||
{
|
||||
$app = getXF();
|
||||
if(!$app) return false;
|
||||
|
||||
$session = $app->session();
|
||||
$session->expunge();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $sessionId
|
||||
* @return int 0 = Success | 1 = Invalid coocie | 2 = invalid session
|
||||
*/
|
||||
function test_session($sessionId = null) {
|
||||
$app = getXF();
|
||||
if(!$app) return -1;
|
||||
|
||||
if(!isset($sessionId)) {
|
||||
if (!isset($_COOKIE[$app->session()->getCookieName()]))
|
||||
return 1;
|
||||
$sessionId = $_COOKIE[$app->session()->getCookieName()];
|
||||
}
|
||||
$app->session()->expunge();
|
||||
if (!$app->session()->start(remoteAddress(), $sessionId) || !$app->session()->exists())
|
||||
return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function redirectOnInvalidSession() {
|
||||
$app = getXF();
|
||||
if(!$app) return;
|
||||
|
||||
$status = test_session();
|
||||
if ($status != 0) {
|
||||
$type = "undefined";
|
||||
switch ($status) {
|
||||
case 1:
|
||||
$type = "nocookie";
|
||||
break;
|
||||
case 2:
|
||||
$type = "expired";
|
||||
break;
|
||||
default:
|
||||
$type = "unknown";
|
||||
break;
|
||||
}
|
||||
header('Location: ' . authPath() . 'login.php?error=' . $type);
|
||||
setcookie($app->session()->getCookieName(), "", 1);
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
function setup_forum_auth() {
|
||||
getXF(); /* Initialize XF */
|
||||
}
|
||||
|
||||
if(!defined("_AUTH_API_ONLY")) {
|
||||
$app = getXF();
|
||||
if(!$app) {
|
||||
die("failed to start app");
|
||||
}
|
||||
|
||||
if (isset($_GET["type"])) {
|
||||
error_log("Got authX request!");
|
||||
if ($_GET["type"] == "login") {
|
||||
die(json_encode(checkLogin($_POST["user"], $_POST["pass"])));
|
||||
} else if ($_GET["type"] == "logout") {
|
||||
logout();
|
||||
global $localhost;
|
||||
if($localhost)
|
||||
header("Location: login.php");
|
||||
else
|
||||
header("Location: https://web.teaspeak.de/");
|
||||
|
||||
$session = $app->session();
|
||||
setcookie($session->getCookieName(), '', time() - 3600, '/');
|
||||
setcookie("session", '', time() - 3600, '/');
|
||||
setcookie("user_data", '', time() - 3600, '/');
|
||||
setcookie("user_sign", '', time() - 3600, '/');
|
||||
} else die("unknown type!");
|
||||
} else if(isset($_POST["action"])) {
|
||||
error_log("Got auth post request!");
|
||||
if($_POST["action"] === "login") {
|
||||
die(json_encode(checkLogin($_POST["user"], $_POST["pass"])));
|
||||
} else if ($_POST["action"] === "logout") {
|
||||
logout();
|
||||
die(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
} else if($_POST["action"] === "validate") {
|
||||
$app = getXF();
|
||||
if(test_session($_POST["token"]) === 0)
|
||||
die(json_encode([
|
||||
"success" => true,
|
||||
"token" => $app->session()->getSessionId()
|
||||
]));
|
||||
else
|
||||
die(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
} else
|
||||
die(json_encode([
|
||||
"success" => false,
|
||||
"msg" => "Invalid action"
|
||||
]));
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
body{
|
||||
padding:0;
|
||||
margin:0;
|
||||
}
|
||||
.inner {
|
||||
position: absolute;
|
||||
}
|
||||
.inner-container{
|
||||
width:400px;
|
||||
height:400px;
|
||||
position:absolute;
|
||||
top:calc(50vh - 200px);
|
||||
left:calc(50vw - 200px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.box{
|
||||
position:absolute;
|
||||
height:100%;
|
||||
width:100%;
|
||||
font-family:Helvetica;
|
||||
color:#fff;
|
||||
background:rgba(0,0,0,0.13);
|
||||
padding:30px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
.box h1{
|
||||
text-align:center;
|
||||
margin:30px 0;
|
||||
font-size:30px;
|
||||
}
|
||||
.box input{
|
||||
display:block;
|
||||
width:300px;
|
||||
margin:20px auto;
|
||||
padding:15px;
|
||||
background:rgba(0,0,0,0.2);
|
||||
color:#fff;
|
||||
border:0;
|
||||
}
|
||||
.box input:focus,.box input:active,.box button:focus,.box button:active{
|
||||
outline:none;
|
||||
}
|
||||
.box button {
|
||||
background:#742ECC;
|
||||
border:0;
|
||||
color:#fff;
|
||||
padding:10px;
|
||||
font-size:20px;
|
||||
width:330px;
|
||||
margin:20px auto;
|
||||
display:block;
|
||||
cursor:pointer;
|
||||
}
|
||||
.box button:disabled {
|
||||
background:rgba(0,0,0,0.2);
|
||||
}
|
||||
.box button:active{
|
||||
background:#27ae60;
|
||||
}
|
||||
.box p{
|
||||
font-size:14px;
|
||||
text-align:center;
|
||||
}
|
||||
.box p span{
|
||||
cursor:pointer;
|
||||
color:#666;
|
||||
}
|
||||
|
||||
.box .error {
|
||||
color: darkred;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#login {
|
||||
display: block;
|
||||
}
|
||||
#success {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
const btn_login = $("#btn_login");
|
||||
|
||||
btn_login.on('click', () => {
|
||||
btn_login
|
||||
.prop("disabled", true)
|
||||
.empty()
|
||||
.append($(document.createElement("i")).addClass("fa fa-circle-o-notch fa-spin"));
|
||||
submitLogin($("#user").val() as string, $("#pass").val() as string);
|
||||
});
|
||||
|
||||
function submitLogin(user: string, pass: string) {
|
||||
$.ajax({
|
||||
url: "auth.php?type=login",
|
||||
type: "POST",
|
||||
cache: false,
|
||||
data: {
|
||||
user: user,
|
||||
pass: pass
|
||||
},
|
||||
success: (result: string) => {
|
||||
setTimeout(() => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(result);
|
||||
} catch (e) {
|
||||
loginFailed("Invalid response: " + result);
|
||||
return;
|
||||
}
|
||||
if (data["success"] == false) {
|
||||
loginFailed(data["msg"]);
|
||||
return;
|
||||
}
|
||||
if (data["allowed"] == false) {
|
||||
loginFailed("You're not allowed for the closed beta!");
|
||||
return;
|
||||
}
|
||||
$("#login").hide(500);
|
||||
$("#success").show(500);
|
||||
|
||||
document.cookie = data["sessionName"] + "=" + data["sessionId"] + ";path=/";
|
||||
document.cookie = data["cookie_name_data"] + "=" + data["user_data"] + ";path=/";
|
||||
document.cookie = data["cookie_name_sign"] + "=" + data["user_sign"] + ";path=/";
|
||||
console.log(result);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = btn_login.attr("target");
|
||||
}, 1000 + Math.random() % 1500);
|
||||
}, 500 + Math.random() % 500);
|
||||
},
|
||||
error: function (xhr,status,error) {
|
||||
loginFailed("Invalid request (" + status + ") => " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loginFailed(err: string = "") {
|
||||
btn_login
|
||||
.prop("disabled", false)
|
||||
.empty()
|
||||
.append($(document.createElement("a")).text("Login"));
|
||||
|
||||
let errTag = $(".box .error");
|
||||
if(err !== "") {
|
||||
errTag.text(err).show(500);
|
||||
} else errTag.hide(500);
|
||||
}
|
||||
|
||||
//<i class="fa fa-circle-o-notch fa-spin" id="login-loader"></i>
|
||||
|
||||
$("#user").on('keydown', event => {
|
||||
if(event.key == "Enter") $("#pass").focus();
|
||||
});
|
||||
|
||||
$("#pass").on('keydown', event => {
|
||||
if(event.key == "Enter") $("#btn_login").trigger("click");
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
include_once('auth.php');
|
||||
|
||||
$session = test_session();
|
||||
if($session == 0) {
|
||||
header('Location: ' . mainPath() . 'index.php');
|
||||
die();
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="css/auth.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="inner-container">
|
||||
<div class="box">
|
||||
<h1>Login</h1>
|
||||
<div id="login">
|
||||
<a class="error">some error code</a>
|
||||
<input type="text" placeholder="Username" id="user"/>
|
||||
<input type="password" placeholder="Password" id="pass"/>
|
||||
<button id="btn_login" target="<?php echo mainPath() . "index.php"; ?>">Login</button>
|
||||
<p>Create a account on <a href="//forum.teaspeak.de">forum.teaspeak.de</a></p>
|
||||
</div>
|
||||
<div id="success">
|
||||
<a> Successful logged in!</a><br>
|
||||
<a>You will be redirected in 3 seconds</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/auth.js"></script>
|
||||
</body>
|
||||
</html>
|
137
file.ts
137
file.ts
|
@ -30,9 +30,9 @@ type ProjectResource = {
|
|||
}
|
||||
|
||||
const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
|
||||
{ /* shared html and php files */
|
||||
{ /* shared html files */
|
||||
"type": "html",
|
||||
"search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/,
|
||||
"search-pattern": /^.*([a-zA-Z]+)\.(html|json)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "./",
|
||||
|
@ -191,7 +191,7 @@ const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
|
|||
{ /* web html files */
|
||||
"web-only": true,
|
||||
"type": "html",
|
||||
"search-pattern": /.*\.(php|html)/,
|
||||
"search-pattern": /.*\.(html)/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "./",
|
||||
|
@ -208,56 +208,11 @@ const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
|
|||
}
|
||||
];
|
||||
|
||||
//TODO: This isn't needed anymore
|
||||
const APP_FILE_LIST_WEB_TEASPEAK: ProjectResource[] = [
|
||||
/* special web.teaspeak.de only auth files */
|
||||
{ /* login page and api */
|
||||
"web-only": true,
|
||||
"type": "html",
|
||||
"search-pattern": /[a-zA-Z_0-9]+\.(php|html)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "./",
|
||||
"local-path": "./auth/",
|
||||
"req-parm": ["-xf"]
|
||||
},
|
||||
{ /* javascript */
|
||||
"web-only": true,
|
||||
"type": "js",
|
||||
"search-pattern": /.*\.js$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "js/",
|
||||
"local-path": "./auth/js/",
|
||||
"req-parm": ["-xf"]
|
||||
},
|
||||
{ /* web css files */
|
||||
"web-only": true,
|
||||
"type": "css",
|
||||
"search-pattern": /.*\.css$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "css/",
|
||||
"local-path": "./auth/css/",
|
||||
"req-parm": ["-xf"]
|
||||
},
|
||||
{ /* certificates */
|
||||
"web-only": true,
|
||||
"type": "pem",
|
||||
"search-pattern": /.*\.pem$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "certs/",
|
||||
"local-path": "./auth/certs/",
|
||||
"req-parm": ["-xf"]
|
||||
}
|
||||
];
|
||||
|
||||
//FIXME: This isn't working right now
|
||||
const CERTACCEPT_FILE_LIST: ProjectResource[] = [
|
||||
{ /* html files */
|
||||
"type": "html",
|
||||
"search-pattern": /^([a-zA-Z]+)\.(html|php|json)$/,
|
||||
"search-pattern": /^([a-zA-Z]+)\.(html|json)$/,
|
||||
"build-target": "dev|rel",
|
||||
|
||||
"path": "./popup/certaccept/",
|
||||
|
@ -355,7 +310,6 @@ const WEB_APP_FILE_LIST = [
|
|||
...APP_FILE_LIST_SHARED_SOURCE,
|
||||
...APP_FILE_LIST_SHARED_VENDORS,
|
||||
...APP_FILE_LIST_WEB_SOURCE,
|
||||
...APP_FILE_LIST_WEB_TEASPEAK,
|
||||
...CERTACCEPT_FILE_LIST,
|
||||
];
|
||||
|
||||
|
@ -501,8 +455,6 @@ namespace server {
|
|||
import SearchOptions = generator.SearchOptions;
|
||||
export type Options = {
|
||||
port: number;
|
||||
php: string;
|
||||
|
||||
search_options: SearchOptions;
|
||||
}
|
||||
|
||||
|
@ -510,7 +462,6 @@ namespace server {
|
|||
|
||||
let files: ProjectResource[] = [];
|
||||
let server: http.Server;
|
||||
let php: string;
|
||||
let options: Options;
|
||||
|
||||
const use_https = false;
|
||||
|
@ -518,21 +469,6 @@ namespace server {
|
|||
options = options_;
|
||||
files = _files;
|
||||
|
||||
try {
|
||||
const info = await exec(options.php + " --version");
|
||||
if(info.stderr)
|
||||
throw info.stderr;
|
||||
|
||||
if(!info.stdout.startsWith("PHP 7."))
|
||||
throw "invalid php interpreter version (Require at least 7)";
|
||||
|
||||
console.debug("Found PHP interpreter:\n%s", info.stdout);
|
||||
php = options.php;
|
||||
} catch(error) {
|
||||
console.error("failed to validate php interpreter: %o", error);
|
||||
throw "invalid php interpreter";
|
||||
}
|
||||
|
||||
if(process.env["ssl_enabled"] || use_https) {
|
||||
//openssl req -nodes -new -x509 -keyout files_key.pem -out files_cert.pem
|
||||
const key_file = process.env["ssl_key"] || path.join(__dirname, "files_key.pem");
|
||||
|
@ -566,40 +502,6 @@ namespace server {
|
|||
}
|
||||
}
|
||||
|
||||
function serve_php(file: string, query: any, response: http.ServerResponse) {
|
||||
if(!fs.existsSync("tmp"))
|
||||
fs.mkdirSync("tmp");
|
||||
let tmp_script_name = path.join("tmp", Math.random().toFixed(32).substr(2));
|
||||
let script = "<?php\n";
|
||||
script += "$params = json_decode(urldecode(\"" + encodeURIComponent(JSON.stringify(query)) + "\")); \n";
|
||||
script += "foreach($params as $key => $value) $_GET[$key] = $value;\n";
|
||||
script += "chdir(urldecode(\"" + encodeURIComponent(path.dirname(file)) + "\"));";
|
||||
script += "?>";
|
||||
fs.writeFileSync(tmp_script_name, script, {flag: 'w'});
|
||||
exec(php + " -d auto_prepend_file=" + tmp_script_name + " " + file).then(result => {
|
||||
if(result.stderr && !result.stdout) {
|
||||
response.writeHead(500);
|
||||
response.write("Encountered error while interpreting PHP script:\n");
|
||||
response.write(result.stderr);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, "success", {
|
||||
"Content-Type": "text/html; charset=utf-8"
|
||||
});
|
||||
response.write(result.stdout);
|
||||
response.end();
|
||||
}).catch(error => {
|
||||
response.writeHead(500);
|
||||
response.write("Received an exception while interpreting PHP script:\n");
|
||||
response.write(error.toString());
|
||||
response.end();
|
||||
}).then(() => fs.unlink(tmp_script_name)).catch(error => {
|
||||
console.error("[SERVER] Failed to delete tmp PHP prepend file: %o", error);
|
||||
});
|
||||
}
|
||||
|
||||
async function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||
const file = await generator.search_http_file(files, pathname, options.search_options);
|
||||
if(!file) {
|
||||
|
@ -612,10 +514,6 @@ namespace server {
|
|||
|
||||
let type = mt.lookup(path.extname(file)) || "text/html";
|
||||
console.log("[SERVER] Serving file %s", file, type);
|
||||
if(path.extname(file) === ".php") {
|
||||
serve_php(file, query, response);
|
||||
return;
|
||||
}
|
||||
const fis = fs.createReadStream(file);
|
||||
|
||||
response.writeHead(200, "success", {
|
||||
|
@ -634,19 +532,12 @@ namespace server {
|
|||
response.writeHead(200, { "info-version": 1 });
|
||||
response.write("type\thash\tpath\tname\n");
|
||||
for(const file of await generator.search_files(files, options.search_options))
|
||||
if(file.name.endsWith(".php"))
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + path.basename(file.name, ".php") + ".html" + "\n");
|
||||
else
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + file.name + "\n");
|
||||
response.write(file.type + "\t" + file.hash + "\t" + path.dirname(file.target_path) + "\t" + file.name + "\n");
|
||||
response.end();
|
||||
return;
|
||||
} else if(url.query["type"] === "file") {
|
||||
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
||||
if(!p.startsWith("/")) p = "/" + p;
|
||||
if(p.endsWith(".html")) {
|
||||
const np = await generator.search_http_file(files, p.substr(0, p.length - 5) + ".php", options.search_options);
|
||||
if(np) p = p.substr(0, p.length - 5) + ".php";
|
||||
}
|
||||
serve_file(p, url.query, response);
|
||||
return;
|
||||
}
|
||||
|
@ -676,7 +567,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 +579,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, {
|
||||
|
@ -835,19 +726,9 @@ namespace watcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function php_exe() : string {
|
||||
if(process.env["PHP_EXE"])
|
||||
return process.env["PHP_EXE"];
|
||||
if(os.platform() === "win32")
|
||||
return "php.exe";
|
||||
return "php";
|
||||
}
|
||||
|
||||
async function main_serve(target: "client" | "web", mode: "rel" | "dev", port: number) {
|
||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
port: port,
|
||||
php: php_exe(),
|
||||
search_options: {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
|
@ -882,7 +763,6 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
|
|||
try {
|
||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||
port: port,
|
||||
php: php_exe(),
|
||||
search_options: {
|
||||
source_path: __dirname,
|
||||
parameter: [],
|
||||
|
@ -1125,9 +1005,6 @@ async function main(args: string[]) {
|
|||
console.log(" node files.js list <client|web> <dev|rel> | List all project files");
|
||||
console.log(" node files.js develop <client|web> [port] | Start a developer session. All typescript an SASS files will generated automatically");
|
||||
console.log(" | You could access your current build via http://localhost:8081");
|
||||
console.log("");
|
||||
console.log("Influential environment variables:");
|
||||
console.log(" PHP_EXE | Path to the PHP CLI interpreter");
|
||||
}
|
||||
|
||||
/* proxy log for better format */
|
||||
|
|
|
@ -4,4 +4,6 @@ window["loader"] = loader_base;
|
|||
/* let the loader register himself at the window first */
|
||||
setTimeout(loader.run, 0);
|
||||
|
||||
export {};
|
||||
export {};
|
||||
|
||||
//window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {};
|
|
@ -191,7 +191,6 @@ const loader_style = {
|
|||
"css/static/overlay-image-preview.css",
|
||||
"css/static/music/info_plate.css",
|
||||
"css/static/frame/SelectInfo.css",
|
||||
"css/static/control_bar.css",
|
||||
"css/static/context_menu.css",
|
||||
"css/static/frame-chat.css",
|
||||
"css/static/connection_handlers.css",
|
||||
|
@ -345,10 +344,6 @@ loader.register_task(loader.Stage.SETUP, {
|
|||
const container = document.createElement("div");
|
||||
container.setAttribute('id', "mouse-move");
|
||||
|
||||
const inner_container = document.createElement("div");
|
||||
inner_container.classList.add("container");
|
||||
container.append(inner_container);
|
||||
|
||||
body.append(container);
|
||||
}
|
||||
/* tooltip container */
|
||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,11 +148,21 @@ if [[ -e "$LOG_FILE" ]]; then
|
|||
fi
|
||||
|
||||
chmod +x ./web/native-codec/build.sh
|
||||
execute \
|
||||
"Building native codes" \
|
||||
"Failed to build native opus codec" \
|
||||
"docker exec -it emscripten bash -c 'web/native-codec/build.sh'"
|
||||
if hash emcmake 2>/dev/null; then
|
||||
hash cmake 2>/dev/null || { echo "Missing cmake. Please install cmake before retrying. (apt-get install cmake)"; exit 1; }
|
||||
hash make 2>/dev/null || { echo "Missing make. Please install build-essential before retrying. (apt-get install build-essential)"; exit 1; }
|
||||
|
||||
echo "Found installation of emcmake locally. Don't use docker in order to build the native parts."
|
||||
execute \
|
||||
"Building native codes" \
|
||||
"Failed to build native opus codec" \
|
||||
"./web/native-codec/build.sh"
|
||||
else
|
||||
execute \
|
||||
"Building native codes" \
|
||||
"Failed to build native opus codec" \
|
||||
"docker exec -it emscripten bash -c 'web/native-codec/build.sh'"
|
||||
fi
|
||||
echo "---------- Web client ----------"
|
||||
|
||||
function move_target_file() {
|
||||
|
|
|
@ -2,38 +2,42 @@
|
|||
## 1.0 Requirements
|
||||
The following tools or applications are required to develop the web client:
|
||||
- [1.1 IDE](#11-ide)
|
||||
- [1.2 PHP](#12-php)
|
||||
- [1.3 NodeJS](#13-nodejs)
|
||||
- [1.3.2 NPM](#132-npm)
|
||||
- [1.4 Git bash](#14-git-bash)
|
||||
- [1.2 NodeJS](#12-nodejs)
|
||||
- [1.2.2 NPM](#122-npm)
|
||||
- [1.3 Git bash](#13-git-bash)
|
||||
- [1.4 Docker](#14-docker)
|
||||
|
||||
### 1.1 IDE
|
||||
It does not matter which IDE you use,
|
||||
you could even use a command line text editor for developing.
|
||||
|
||||
### 1.2 PHP
|
||||
For having a test environment you require an installation of PHP 5 or grater.
|
||||
You could just download PHP from [here](https://windows.php.net/download#php-7.4).
|
||||
Note:
|
||||
`php.exe` must be accessible via the command line.
|
||||
This means you'll have to add the `bin` folder to your `PATH` variable.
|
||||
|
||||
### 1.3 NodeJS
|
||||
### 1.2 NodeJS
|
||||
For building and serving you require `nodejs` grater than 8.
|
||||
Nodejs is easily downloadable from [here]().
|
||||
Ensure you've added `node.exe` to the environment path!
|
||||
|
||||
### 1.3.2 NPM
|
||||
### 1.2.2 NPM
|
||||
Normally NPM already comes with the NodeJS installer.
|
||||
So you don't really have to worry about it.
|
||||
NPM min 6.X is required to develop this project.
|
||||
With NPM you could easily download all required dependencies by just typing `npm install`.
|
||||
IMPORTANT: NPM must be available within the PATH environment variable!
|
||||
|
||||
### 1.4 Git bash
|
||||
### 1.3 Git bash
|
||||
For using the `.sh` build scripts you require Git Bash.
|
||||
A minimum of 4.2 is recommend, but in general every modern version should work.
|
||||
|
||||
### 1.4 Docker
|
||||
For building the native scripts you need docker.
|
||||
Just install docker from [docker.com](https://docs.docker.com).
|
||||
Attention: If you're having Windows make sure you're running linux containers!
|
||||
|
||||
In order to setup the later required containers, just execute these commands:
|
||||
Make sure you're within the web source directory! If not replace the `$(pwd)` the the path the web client is located at
|
||||
```shell script
|
||||
docker run -dit --name emscripten -v "$(pwd)":"/src/" trzeci/emscripten:sdk-incoming-64bit bash
|
||||
```
|
||||
|
||||
## 2.0 Project initialization
|
||||
|
||||
### 2.1 Cloning the WebClient
|
||||
|
@ -50,26 +54,18 @@ git submodule update --init
|
|||
### 2.2 Setting up native scripts
|
||||
TeaWeb uses the Opus audio codec. Because almost no browser supports it out of the box
|
||||
we've to implement our own de/encoder. For this we're using `emscripten` to compile the codec.
|
||||
Because this is a quite time consuming task we already offer prebuild javascript and wasm files.
|
||||
So we just need to download them. Just execute the `download_compiled_files.sh` shell script within the `asm` folder.
|
||||
In order to build the required javascript and wasm files just executing this in your git bash:
|
||||
```shell script
|
||||
./asm/download_compiled_files.sh
|
||||
docker exec -it emscripten bash -c 'web/native-codec/build.sh'
|
||||
```
|
||||
|
||||
|
||||
### 2.3 Initializing NPM
|
||||
To download all required packages simply type:
|
||||
```shell script
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2.4 Initial client compilation
|
||||
Before you could start ahead with developing you've to compile everything.
|
||||
Just execute the `web_build.sh` script:
|
||||
```shell script
|
||||
./scripts/web_build.sh development
|
||||
```
|
||||
|
||||
### 2.5 Starting the development environment
|
||||
### 2.4 You're ready to go and start developing
|
||||
To start the development environment which automatically compiles all your changed
|
||||
scripts and style sheets you simply have to execute:
|
||||
```shell script
|
||||
|
@ -78,5 +74,13 @@ npm start web
|
|||
This will also spin up a temporary web server where you could test out your newest changes.
|
||||
The server will by default listen on `http://localhost:8081`
|
||||
|
||||
### 2.6 You're ready
|
||||
Now you're ready to start ahead and implement your own great ideas.
|
||||
### 2.5 Using your UI within the TeaClient
|
||||
An explanation how this works will come later. Stay tuned!
|
||||
|
||||
### 2.6 Generate a release package
|
||||
In order to build your own TeaWeb-Package just execute the `scripts/build.sh` script.
|
||||
```shell script
|
||||
./scripts/build.sh web rel # Build the web client
|
||||
./scripts/web_package.sh rel # Bundle the webclient into one .zip archive
|
||||
```
|
||||
You could also create a debug packaged just by replacing `rel` with `dev`.
|
|
@ -12,7 +12,6 @@ files=(
|
|||
"css/static/channel-tree.css"
|
||||
"css/static/connection_handlers.css"
|
||||
"css/static/context_menu.css"
|
||||
"css/static/control_bar.css"
|
||||
"css/static/frame-chat.css"
|
||||
"css/static/server-log.css"
|
||||
"css/static/scroll.css"
|
||||
|
|
|
@ -1,314 +1,3 @@
|
|||
@import "properties";
|
||||
@import "mixin";
|
||||
|
||||
.channel-tree-container {
|
||||
height: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
}
|
||||
|
||||
/* the channel tree */
|
||||
.channel-tree {
|
||||
@include user-select(none);
|
||||
width: 100%;
|
||||
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
white-space: pre;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tree-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
/* margin-left: 16px; */
|
||||
min-height: 16px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.server, > .container-channel, &.client {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: $channel_tree_entry_hovered;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $channel_tree_entry_selected;
|
||||
.channel-name {
|
||||
color: whitesmoke;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.server {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
position: relative;
|
||||
|
||||
cursor: pointer;
|
||||
margin-left: 0;
|
||||
|
||||
.server_type {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
align-self: center;
|
||||
color: $channel_tree_entry_text_color;
|
||||
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon_property {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.channel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.container-channel {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
min-height: 16px;
|
||||
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.channel-type {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.container-channel-name {
|
||||
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;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
align-self: center;
|
||||
color: $channel_tree_entry_text_color;
|
||||
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.align-repetitive {
|
||||
.channel-name {
|
||||
text-overflow: clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.move-selected {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.show-channel-normal-only {
|
||||
display: none;
|
||||
|
||||
&.channel-normal {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.icon_no_sound {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.container-clients {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&.client {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
line-height: 16px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
padding-right: .25em;
|
||||
color: $channel_tree_entry_text_color;
|
||||
|
||||
&.client-name-own {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.client-away-message {
|
||||
color: $channel_tree_entry_text_color;
|
||||
}
|
||||
|
||||
.container-icons {
|
||||
margin-right: 0; /* override from previous thing */
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding-right: 5px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
.container-icons-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.container-group-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
&:focus-within {
|
||||
.container-icons {
|
||||
background-color: $channel_tree_entry_selected;
|
||||
padding-left: 5px;
|
||||
z-index: 1001; /* show before client name */
|
||||
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.client-name {
|
||||
&:focus {
|
||||
position: absolute;
|
||||
color: black;
|
||||
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
margin-right: -10px;
|
||||
margin-left: 18px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.channel .container-channel, &.client, &.server {
|
||||
.marker-text-unread {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 1px;
|
||||
@include background-color(#a814147F);
|
||||
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 24px;
|
||||
|
||||
background: -moz-linear-gradient(left, rgba(168,20,20,.18) 0%, rgba(168,20,20,0) 100%); /* FF3.6-15 */
|
||||
background: -webkit-linear-gradient(left, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* Chrome10-25,Safari5.1-6 */
|
||||
background: linear-gradient(to right, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include transition(opacity $button_hover_animation_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* all icons related to basic_icons */
|
||||
.clicon {
|
||||
width:16px;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
@import "properties";
|
||||
@import "mixin";
|
||||
|
||||
$border_color_activated: rgba(255, 255, 255, .75);
|
||||
|
||||
/* max height is 2em */
|
||||
.control_bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@include user-select(none);
|
||||
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
/* tmp fix for ultra small devices */
|
||||
overflow-y: visible;
|
||||
|
||||
.divider {
|
||||
border-left:2px solid #393838;
|
||||
height: calc(100% - 3px);
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
/* border etc */
|
||||
.button, .dropdown-arrow {
|
||||
text-align: center;
|
||||
|
||||
border: .05em solid rgba(0, 0, 0, 0);
|
||||
border-radius: $border_radius_small;
|
||||
|
||||
background-color: #454545;
|
||||
|
||||
&:hover {
|
||||
background-color: #393c43;
|
||||
border-color: #4a4c55;
|
||||
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||
}
|
||||
|
||||
&.activated {
|
||||
background-color: #2f3841;
|
||||
border-color: #005fa1;
|
||||
|
||||
&:hover {
|
||||
background-color: #263340;
|
||||
border-color: #005fa1;
|
||||
}
|
||||
|
||||
&.button-red {
|
||||
background-color: #412f2f;
|
||||
border-color: #a10000;
|
||||
|
||||
&:hover {
|
||||
background-color: #402626;
|
||||
border-color: #a10000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include transition(background-color $button_hover_animation_time ease-in-out, border-color $button_hover_animation_time ease-in-out);
|
||||
|
||||
> .icon_x24 {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
|
||||
&:not(.icon_x24) {
|
||||
min-width: 2em;
|
||||
max-width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.icon_em {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
&.button-hostbutton {
|
||||
img {
|
||||
min-width: 1.5em;
|
||||
max-width: 1.5em;
|
||||
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
padding: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.button-dropdown {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.buttons {
|
||||
height: 2em;
|
||||
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.dropdown-arrow {
|
||||
height: 2em;
|
||||
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
width: 1.5em;
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 0 $border_radius_small $border_radius_small 0;
|
||||
align-items: center;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.button, .dropdown-arrow {
|
||||
background-color: #393c43;
|
||||
border-color: #4a4c55;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-right-color: transparent;
|
||||
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
margin-left: 5px;
|
||||
|
||||
color: #c4c5c5;
|
||||
|
||||
background-color: #2d3032;
|
||||
align-items: center;
|
||||
border: .05em solid #2c2525;
|
||||
border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle;
|
||||
|
||||
width: 230px;
|
||||
|
||||
z-index: 1000;
|
||||
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.icon, .icon-container, .icon_em {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
padding: 1px 2px 1px 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: #252729;
|
||||
}
|
||||
}
|
||||
|
||||
& > div:first-of-type {
|
||||
border-radius: .1em .1em 0 0;
|
||||
}
|
||||
|
||||
& > div:last-of-type {
|
||||
border-radius: 0 0 .1em .1em;
|
||||
}
|
||||
|
||||
&.display_left {
|
||||
margin-left: -179px;
|
||||
border-radius: $border_radius_middle 0 $border_radius_middle $border_radius_middle;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover.dropdownDisplayed, &.force-show {
|
||||
.dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.button, .dropdown-arrow {
|
||||
background-color: #393c43;
|
||||
border-color: #4a4c55;
|
||||
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-right-color: transparent;
|
||||
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
hr {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.bookmark-dropdown {
|
||||
hr:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
|
||||
}
|
||||
|
||||
.bookmark, .directory {
|
||||
display: flex!important;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.icon, .arrow {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.directory {
|
||||
&:hover {
|
||||
> .sub-container, > .sub-container .sub-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:hover) {
|
||||
.sub-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-container {
|
||||
padding-right: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
display: none;
|
||||
left: 100%;
|
||||
top: -13px;
|
||||
position: absolute;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_em {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -74,6 +74,14 @@ $animation_length: .5s;
|
|||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.channel-tree-container {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,22 +265,6 @@ $animation_seperator_length: .1s;
|
|||
}
|
||||
}
|
||||
|
||||
#mouse-move {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
border: 2px solid gray;
|
||||
-webkit-border-radius: 2px;
|
||||
-moz-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -166,6 +166,7 @@
|
|||
width: 25%;
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: #222226;
|
||||
|
||||
|
|
|
@ -279,7 +279,9 @@ export class ConnectionHandler {
|
|||
}
|
||||
|
||||
const original_address = {host: server_address.host, port: server_address.port};
|
||||
if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
|
||||
if(server_address.host === "localhost") {
|
||||
server_address.host = "127.0.0.1";
|
||||
} else if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
|
||||
const id = ++this._connect_initialize_id;
|
||||
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {});
|
||||
try {
|
||||
|
@ -636,6 +638,7 @@ export class ConnectionHandler {
|
|||
this.serverConnection.disconnect();
|
||||
|
||||
this.side_bar.private_conversations().clear_client_ids();
|
||||
this.side_bar.channel_conversations().set_current_channel(0);
|
||||
this.hostbanner.update();
|
||||
|
||||
if(auto_reconnect) {
|
||||
|
@ -657,6 +660,8 @@ export class ConnectionHandler {
|
|||
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
this.serverConnection.updateConnectionState(ConnectionState.UNCONNECTED); /* Fix for the native client... */
|
||||
}
|
||||
|
||||
cancel_reconnect(log_event: boolean) {
|
||||
|
@ -807,7 +812,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -262,7 +262,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
});
|
||||
}
|
||||
|
||||
request_playlist_songs(playlist_id: number) : Promise<PlaylistSong[]> {
|
||||
request_playlist_songs(playlist_id: number, process_result?: boolean) : Promise<PlaylistSong[]> {
|
||||
let bulked_response = false;
|
||||
let bulk_index = 0;
|
||||
|
||||
|
@ -314,7 +314,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
};
|
||||
this.handler_boss.register_single_handler(single_handler);
|
||||
|
||||
this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}).catch(error => {
|
||||
this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}, { process_result: process_result }).catch(error => {
|
||||
this.handler_boss.remove_single_handler(single_handler);
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id == ErrorID.EMPTY_RESULT) {
|
||||
|
|
|
@ -50,6 +50,8 @@ export abstract class AbstractServerConnection {
|
|||
|
||||
//FIXME: Remove this this is currently only some kind of hack
|
||||
updateConnectionState(state: ConnectionState) {
|
||||
if(state === this.connection_state_) return;
|
||||
|
||||
const old_state = this.connection_state_;
|
||||
this.connection_state_ = state;
|
||||
if(this.onconnectionstatechanged)
|
||||
|
|
|
@ -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,5 +1,4 @@
|
|||
import {MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||
import {guid} from "tc-shared/crypto/uid";
|
||||
import * as React from "react";
|
||||
|
||||
|
@ -31,7 +30,7 @@ export class Registry<Events> {
|
|||
handlers: {[key: string]: ((event) => void)[]}
|
||||
}[] = [];
|
||||
private debug_prefix = undefined;
|
||||
private warn_unhandled_events = true;
|
||||
private warn_unhandled_events = false;
|
||||
|
||||
constructor() {
|
||||
this.registry_uuid = "evreg_data_" + guid();
|
||||
|
@ -74,8 +73,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 +106,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);
|
||||
if(this.warn_unhandled_events && invoke_count === 0) {
|
||||
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() {
|
||||
|
@ -219,7 +231,11 @@ export function ReactEventHandler<ObjectClass = React.Component<any, any>, Event
|
|||
constructor.prototype.componentWillUnmount = function () {
|
||||
const registry = registry_callback(this);
|
||||
if(!registry) throw "Event registry returned for an event object is invalid";
|
||||
registry.unregister_handler(this);
|
||||
try {
|
||||
registry.unregister_handler(this);
|
||||
} catch (error) {
|
||||
console.warn("Failed to unregister event handler: %o", error);
|
||||
}
|
||||
|
||||
if(typeof willUnmount === "function")
|
||||
willUnmount.call(this, arguments);
|
||||
|
@ -227,31 +243,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 +280,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 +691,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);
|
||||
eclient.connect("playlist_song_loaded", emusic);
|
||||
*/
|
|
@ -29,8 +29,7 @@ import * as React from "react";
|
|||
import * as ReactDOM from "react-dom";
|
||||
import * as cbar from "./ui/frames/control-bar";
|
||||
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
||||
import {ClientGlobalControlEvents, global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||
|
||||
/* required import for init */
|
||||
require("./proto").initialize();
|
||||
|
|
|
@ -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> = {
|
||||
|
@ -365,6 +366,13 @@ export class Settings extends StaticSettings {
|
|||
}
|
||||
};
|
||||
|
||||
static readonly FN_SERVER_CHANNEL_COLLAPSED: (channel_id: number) => SettingsKey<boolean> = channel => {
|
||||
return {
|
||||
key: 'channel_collapsed_' + channel,
|
||||
default_value: false
|
||||
}
|
||||
};
|
||||
|
||||
static readonly FN_PROFILE_RECORD: (name: string) => SettingsKey<any> = name => {
|
||||
return {
|
||||
key: 'profile_record' + name
|
||||
|
@ -485,14 +493,15 @@ export class ServerSettings extends SettingsBase {
|
|||
|
||||
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
if(this._destroyed) throw "destroyed";
|
||||
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
|
||||
const kkey = Settings.keyify(key);
|
||||
return StaticSettings.resolveKey(kkey, typeof _default === "undefined" ? kkey.default_value : _default, key => this.cacheServer[key]);
|
||||
}
|
||||
|
||||
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
||||
if(this._destroyed) throw "destroyed";
|
||||
key = Settings.keyify(key);
|
||||
|
||||
if(this.cacheServer[key.key] == value) return;
|
||||
if(this.cacheServer[key.key] === value) return;
|
||||
|
||||
this._server_settings_updated = true;
|
||||
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
||||
|
|
|
@ -241,4 +241,21 @@ namespace connection {
|
|||
handler["notifyinitialized"] = handle_notify_initialized;
|
||||
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();
|
||||
}
|
39
shared/js/ui/TreeEntry.ts
Normal file
39
shared/js/ui/TreeEntry.ts
Normal file
|
@ -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,12 @@ 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";
|
||||
import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu";
|
||||
|
||||
export enum ChannelType {
|
||||
PERMANENT,
|
||||
SEMI_PERMANENT,
|
||||
|
@ -69,7 +75,82 @@ 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_collapsed_state_changed: {
|
||||
collapsed: 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;
|
||||
|
@ -77,16 +158,16 @@ export class ChannelEntry {
|
|||
|
||||
channel_previous?: ChannelEntry;
|
||||
channel_next?: ChannelEntry;
|
||||
child_channel_head?: ChannelEntry;
|
||||
|
||||
readonly events: Registry<ChannelEvents>;
|
||||
readonly view: React.RefObject<ChannelEntryView>;
|
||||
|
||||
parsed_channel_name: ParsedChannelName;
|
||||
|
||||
private _channel_name_alignment: string = undefined;
|
||||
private _channel_name_formatted: string = undefined;
|
||||
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;
|
||||
|
@ -95,29 +176,37 @@ export class ChannelEntry {
|
|||
private _cached_channel_description_promise_resolve: any = undefined;
|
||||
private _cached_channel_description_promise_reject: any = undefined;
|
||||
|
||||
private _flag_collapsed: boolean;
|
||||
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 +223,7 @@ export class ChannelEntry {
|
|||
}
|
||||
|
||||
formattedChannelName() {
|
||||
return this._channel_name_formatted || this.properties.channel_name;
|
||||
return this.parsed_channel_name.text;
|
||||
}
|
||||
|
||||
getChannelDescription() : Promise<string> {
|
||||
|
@ -151,58 +240,27 @@ export class ChannelEntry {
|
|||
});
|
||||
}
|
||||
|
||||
parent_channel() { return this.parent; }
|
||||
hasParent(){ return this.parent != null; }
|
||||
getChannelId(){ return this.channelId; }
|
||||
registerClient(client: ClientEntry) {
|
||||
client.events.on("notify_properties_updated", this.client_property_listener);
|
||||
this.client_list.push(client);
|
||||
this.reorderClientList(false);
|
||||
|
||||
children(deep = false) : ChannelEntry[] {
|
||||
const result: ChannelEntry[] = [];
|
||||
if(this.channelTree == null) return [];
|
||||
|
||||
const self = this;
|
||||
this.channelTree.channels.forEach(function (entry) {
|
||||
let current = entry;
|
||||
if(deep) {
|
||||
while(current) {
|
||||
if(current.parent_channel() == self) {
|
||||
result.push(entry);
|
||||
break;
|
||||
}
|
||||
current = current.parent_channel();
|
||||
}
|
||||
} else
|
||||
if(current.parent_channel() == self)
|
||||
result.push(entry);
|
||||
});
|
||||
return result;
|
||||
this.events.fire("notify_clients_changed");
|
||||
}
|
||||
|
||||
clients(deep = false) : ClientEntry[] {
|
||||
const result: ClientEntry[] = [];
|
||||
if(this.channelTree == null) return [];
|
||||
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());
|
||||
|
||||
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;
|
||||
if(!no_event)
|
||||
this.events.fire("notify_clients_changed");
|
||||
}
|
||||
|
||||
clients_ordered() : ClientEntry[] {
|
||||
const clients = this.clients(false);
|
||||
private reorderClientList(fire_event: boolean) {
|
||||
const original_list = this.client_list.slice(0);
|
||||
|
||||
clients.sort((a, b) => {
|
||||
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)
|
||||
|
@ -215,16 +273,47 @@ export class ChannelEntry {
|
|||
|
||||
return 0;
|
||||
});
|
||||
return clients;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
parent_channel() { return this.parent; }
|
||||
hasParent(){ return this.parent != null; }
|
||||
getChannelId(){ return this.channelId; }
|
||||
|
||||
this._tag_channel.css("z-index", this._family_index);
|
||||
this._tag_channel.css("padding-left", ((this._family_index + 1) * 16 + 10) + "px");
|
||||
children(deep = false) : ChannelEntry[] {
|
||||
const result: ChannelEntry[] = [];
|
||||
let head = this.child_channel_head;
|
||||
while(head) {
|
||||
result.push(head);
|
||||
head = head.channel_next;
|
||||
}
|
||||
|
||||
if(deep)
|
||||
return result.map(e => e.children(true)).reduce((prv, now) => { prv.push(...now); return prv; }, []);
|
||||
return result;
|
||||
}
|
||||
|
||||
clients(deep = false) : ClientEntry[] {
|
||||
const result: ClientEntry[] = this.client_list.slice(0);
|
||||
if(!deep) return result;
|
||||
|
||||
return this.children(true).map(e => e.clients(false)).reduce((prev, cur) => {
|
||||
prev.push(...cur);
|
||||
return cur;
|
||||
}, result);
|
||||
}
|
||||
|
||||
clients_ordered() : ClientEntry[] {
|
||||
return this.client_list;
|
||||
}
|
||||
|
||||
calculate_family_index(enforce_recalculate: boolean = false) : number {
|
||||
|
@ -242,235 +331,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -516,6 +383,7 @@ export class ChannelEntry {
|
|||
|
||||
let trigger_close = true;
|
||||
|
||||
const collapse_expendable = !!this.child_channel_head || this.client_list.length > 0;
|
||||
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
|
||||
contextmenu.spawn_context_menu(x, y, {
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
@ -579,7 +447,7 @@ export class ChannelEntry {
|
|||
name: tr("Edit channel"),
|
||||
invalidPermission: !channelModify,
|
||||
callback: () => {
|
||||
createChannelModal(this.channelTree.client, this, undefined, this.channelTree.client.permissions, (changes?, permissions?) => {
|
||||
createChannelModal(this.channelTree.client, this, this.parent, this.channelTree.client.permissions, (changes?, permissions?) => {
|
||||
if(changes) {
|
||||
changes["cid"] = this.channelId;
|
||||
this.channelTree.client.serverConnection.send_command("channeledit", changes);
|
||||
|
@ -635,6 +503,25 @@ export class ChannelEntry {
|
|||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: MenuEntryType.HR,
|
||||
name: "",
|
||||
visible: collapse_expendable
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_collapse_all",
|
||||
name: tr("Collapse sub channels"),
|
||||
visible: collapse_expendable,
|
||||
callback: () => this.channelTree.collapse_channels(this)
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_expand_all",
|
||||
name: tr("Expend sub channels"),
|
||||
visible: collapse_expendable,
|
||||
callback: () => this.channelTree.expand_channels(this)
|
||||
},
|
||||
contextmenu.Entry.HR(),
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
@ -649,80 +536,12 @@ export class ChannelEntry {
|
|||
invalidPermission: !channelCreate,
|
||||
callback: () => this.channelTree.spawnCreateChannel()
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => trigger_close ? on_close() : {})
|
||||
contextmenu.Entry.CLOSE(() => trigger_close && on_close ? on_close() : {})
|
||||
);
|
||||
}
|
||||
|
||||
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 +554,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 +563,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 +579,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 +586,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 +604,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 +627,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 +642,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 +671,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;
|
||||
|
@ -909,6 +690,21 @@ export class ChannelEntry {
|
|||
}
|
||||
}
|
||||
|
||||
get collapsed() : boolean {
|
||||
if(typeof this._flag_collapsed === "undefined")
|
||||
this._flag_collapsed = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId));
|
||||
return this._flag_collapsed;
|
||||
}
|
||||
|
||||
set collapsed(flag: boolean) {
|
||||
if(this._flag_collapsed === flag)
|
||||
return;
|
||||
this._flag_collapsed = flag;
|
||||
this.events.fire("notify_collapsed_state_changed", { collapsed: flag });
|
||||
this.view.current?.forceUpdate();
|
||||
this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId), flag);
|
||||
}
|
||||
|
||||
get flag_subscribed() : boolean {
|
||||
return this._flag_subscribed;
|
||||
}
|
||||
|
@ -918,7 +714,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 +729,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,167 +0,0 @@
|
|||
import {ChannelTree} from "tc-shared/ui/view";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {ClientEntry} from "tc-shared/ui/client";
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
|
||||
export class ClientMover {
|
||||
static readonly listener_root = $(document);
|
||||
static readonly move_element = $("#mouse-move");
|
||||
readonly channel_tree: ChannelTree;
|
||||
|
||||
selected_client: ClientEntry | ClientEntry[];
|
||||
|
||||
hovered_channel: HTMLDivElement;
|
||||
callback: (channel?: ChannelEntry) => any;
|
||||
|
||||
enabled: boolean = true;
|
||||
|
||||
private _bound_finish;
|
||||
private _bound_move;
|
||||
private _active: boolean = false;
|
||||
|
||||
private origin_point: {x: number, y: number} = undefined;
|
||||
|
||||
constructor(tree: ChannelTree) {
|
||||
this.channel_tree = tree;
|
||||
}
|
||||
|
||||
is_active() { return this._active; }
|
||||
|
||||
private hover_text() {
|
||||
if($.isArray(this.selected_client)) {
|
||||
return this.selected_client.filter(client => !!client).map(client => client.clientNickName()).join(", ");
|
||||
} else if(this.selected_client) {
|
||||
return (<ClientEntry>this.selected_client).clientNickName();
|
||||
} else
|
||||
return "";
|
||||
}
|
||||
|
||||
private bbcode_text() {
|
||||
if($.isArray(this.selected_client)) {
|
||||
return this.selected_client.filter(client => !!client).map(client => client.create_bbcode()).join(", ");
|
||||
} else if(this.selected_client) {
|
||||
return (<ClientEntry>this.selected_client).create_bbcode();
|
||||
} else
|
||||
return "";
|
||||
}
|
||||
|
||||
activate(client: ClientEntry | ClientEntry[], callback: (channel?: ChannelEntry) => any, event: any) {
|
||||
this.finish_listener(undefined);
|
||||
|
||||
if(!this.enabled)
|
||||
return false;
|
||||
|
||||
this.selected_client = client;
|
||||
this.callback = callback;
|
||||
log.debug(LogCategory.GENERAL, tr("Starting mouse move"));
|
||||
|
||||
ClientMover.listener_root.on('mouseup', this._bound_finish = this.finish_listener.bind(this)).on('mousemove', this._bound_move = this.move_listener.bind(this));
|
||||
|
||||
{
|
||||
const content = ClientMover.move_element.find(".container");
|
||||
content.empty();
|
||||
content.append($.spawn("a").text(this.hover_text()));
|
||||
}
|
||||
this.move_listener(event);
|
||||
}
|
||||
|
||||
private move_listener(event) {
|
||||
if(!this.enabled)
|
||||
return;
|
||||
|
||||
//console.log("Mouse move: " + event.pageX + " - " + event.pageY);
|
||||
if(!event.pageX || !event.pageY) return;
|
||||
if(!this.origin_point)
|
||||
this.origin_point = {x: event.pageX, y: event.pageY};
|
||||
|
||||
ClientMover.move_element.css({
|
||||
"top": (event.pageY - 1) + "px",
|
||||
"left": (event.pageX + 10) + "px"
|
||||
});
|
||||
|
||||
if(!this._active) {
|
||||
const d_x = this.origin_point.x - event.pageX;
|
||||
const d_y = this.origin_point.y - event.pageY;
|
||||
this._active = Math.sqrt(d_x * d_x + d_y * d_y) > 5 * 5;
|
||||
|
||||
if(this._active) {
|
||||
if($.isArray(this.selected_client)) {
|
||||
this.channel_tree.onSelect(this.selected_client[0], true);
|
||||
for(const client of this.selected_client.slice(1))
|
||||
this.channel_tree.onSelect(client, false, true);
|
||||
} else {
|
||||
this.channel_tree.onSelect(this.selected_client, true);
|
||||
}
|
||||
|
||||
ClientMover.move_element.show();
|
||||
}
|
||||
}
|
||||
|
||||
const elements = document.elementsFromPoint(event.pageX, event.pageY);
|
||||
while(elements.length > 0) {
|
||||
if(elements[0].classList.contains("container-channel")) break;
|
||||
elements.pop_front();
|
||||
}
|
||||
|
||||
if(this.hovered_channel) {
|
||||
this.hovered_channel.classList.remove("move-selected");
|
||||
this.hovered_channel = undefined;
|
||||
}
|
||||
if(elements.length > 0) {
|
||||
elements[0].classList.add("move-selected");
|
||||
this.hovered_channel = elements[0] as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
||||
private finish_listener(event) {
|
||||
ClientMover.move_element.hide();
|
||||
log.debug(LogCategory.GENERAL, tr("Finishing mouse move"));
|
||||
|
||||
const channel_id = this.hovered_channel ? parseInt(this.hovered_channel.getAttribute("channel-id")) : 0;
|
||||
ClientMover.listener_root.unbind('mouseleave', this._bound_finish);
|
||||
ClientMover.listener_root.unbind('mouseup', this._bound_finish);
|
||||
ClientMover.listener_root.unbind('mousemove', this._bound_move);
|
||||
if(this.hovered_channel) {
|
||||
this.hovered_channel.classList.remove("move-selected");
|
||||
this.hovered_channel = undefined;
|
||||
}
|
||||
|
||||
this.origin_point = undefined;
|
||||
if(!this._active) {
|
||||
this.selected_client = undefined;
|
||||
this.callback = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._active = false;
|
||||
if(this.callback) {
|
||||
if(!channel_id)
|
||||
this.callback(undefined);
|
||||
else {
|
||||
this.callback(this.channel_tree.findChannel(channel_id));
|
||||
}
|
||||
this.callback = undefined;
|
||||
}
|
||||
|
||||
/* test for the chat box */
|
||||
{
|
||||
const elements = document.elementsFromPoint(event.pageX, event.pageY);
|
||||
console.error(elements);
|
||||
while(elements.length > 0) {
|
||||
if(elements[0].classList.contains("client-chat-box-field")) break;
|
||||
elements.pop_front();
|
||||
}
|
||||
|
||||
if(elements.length > 0) {
|
||||
const element = $(<HTMLTextAreaElement>elements[0]);
|
||||
element.val((element.val() || "") + this.bbcode_text());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this.callback = undefined;
|
||||
this.finish_listener(undefined);
|
||||
}
|
||||
}
|
|
@ -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,6 +16,7 @@ html:root {
|
|||
height: 100%;
|
||||
align-items: center;
|
||||
background: var(--menu-bar-background);
|
||||
border-radius: 5px;
|
||||
|
||||
/* tmp fix for ultra small devices */
|
||||
overflow-y: visible;
|
||||
|
|
|
@ -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);
|
||||
|
@ -736,7 +736,7 @@ export class MusicInfo {
|
|||
this._current_bot.updateClientVariables(true).catch(error => {
|
||||
log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
||||
}).then(() => {
|
||||
this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id).then(songs => {
|
||||
this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id, false).then(songs => {
|
||||
this.playlist_subscribe(false); /* we're allowed to see the playlist */
|
||||
if(!songs) {
|
||||
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -2,18 +2,21 @@ import {createInputModal, createModal, Modal} from "tc-shared/ui/elements/Modal"
|
|||
import {
|
||||
Bookmark,
|
||||
bookmarks,
|
||||
BookmarkType, boorkmak_connect, create_bookmark, create_bookmark_directory,
|
||||
BookmarkType,
|
||||
boorkmak_connect,
|
||||
create_bookmark,
|
||||
create_bookmark_directory,
|
||||
delete_bookmark,
|
||||
DirectoryBookmark,
|
||||
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";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import * as i18nc from "tc-shared/i18n/country";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import * as top_menu from "../frames/MenuBar";
|
||||
|
@ -107,8 +110,11 @@ export function spawnBookmarkModal() {
|
|||
input_server_address.val(address);
|
||||
|
||||
let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']");
|
||||
if(profile.length == 0)
|
||||
console.error("%o - %s", profile, entry.connect_profile);
|
||||
if(profile.length == 0) {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to find bookmark profile %s. Displaying default one."), entry.connect_profile);
|
||||
profile = input_connect_profile.find("option[value=default]");
|
||||
}
|
||||
profile.prop("selected", true);
|
||||
|
||||
input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : "");
|
||||
|
@ -147,7 +153,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 {
|
||||
|
@ -287,6 +293,7 @@ export function spawnBookmarkModal() {
|
|||
if(event.type === "change" && valid) {
|
||||
selected_bookmark.display_name = name;
|
||||
label_bookmark_name.text(name);
|
||||
save_bookmark(selected_bookmark);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -306,17 +313,25 @@ export function spawnBookmarkModal() {
|
|||
entry.server_properties.server_address = address;
|
||||
entry.server_properties.server_port = 9987;
|
||||
}
|
||||
save_bookmark(selected_bookmark);
|
||||
|
||||
label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port)));
|
||||
update_connect_info();
|
||||
}
|
||||
});
|
||||
|
||||
input_server_password.on("change keydown", event => {
|
||||
const password = input_server_password.val() as string;
|
||||
(selected_bookmark as Bookmark).server_properties.server_password = password;
|
||||
save_bookmark(selected_bookmark);
|
||||
});
|
||||
|
||||
input_connect_profile.on('change', event => {
|
||||
const id = input_connect_profile.val() as string;
|
||||
const profile = profiles().find(e => e.id === id);
|
||||
if(profile) {
|
||||
(selected_bookmark as Bookmark).connect_profile = id;
|
||||
save_bookmark(selected_bookmark);
|
||||
} else {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id);
|
||||
}
|
||||
|
|
|
@ -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 || icon.status === "empty" || icon.status === "destroyed")
|
||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
else 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")} />;
|
||||
}
|
||||
|
||||
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,101 @@
|
|||
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()
|
||||
};
|
||||
(window as any).update_batches = update_batches;
|
||||
|
||||
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)[]) {
|
||||
|
@ -29,4 +108,45 @@ 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"
|
||||
|
@ -277,7 +246,28 @@ export class ServerEntry {
|
|||
visible: false, //TODO: Enable again as soon the new design is finished
|
||||
callback: () => spawnAvatarList(this.channelTree.client)
|
||||
},
|
||||
contextmenu.Entry.CLOSE(() => trigger_close ? on_close() : {})
|
||||
{
|
||||
type: contextmenu.MenuEntryType.HR,
|
||||
name: ''
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_collapse_all",
|
||||
name: tr("Collapse all channels"),
|
||||
callback: () => this.channelTree.collapse_channels()
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-channel_expand_all",
|
||||
name: tr("Expend all channels"),
|
||||
callback: () => this.channelTree.expand_channels()
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
|
||||
contextmenu.spawn_context_menu(x, y, ...this.contextMenuItems(),
|
||||
contextmenu.Entry.CLOSE(on_close)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -295,12 +285,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 +297,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 +350,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 +411,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;
|
||||
}
|
||||
}
|
113
shared/js/ui/tree/Channel.scss
Normal file
113
shared/js/ui/tree/Channel.scss
Normal file
|
@ -0,0 +1,113 @@
|
|||
.channelEntry {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 100%;
|
||||
min-height: 16px;
|
||||
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.containerArrow {
|
||||
width: 16px;
|
||||
margin-left: -16px;
|
||||
text-align: center;
|
||||
|
||||
&.down {
|
||||
align-self: normal;
|
||||
}
|
||||
|
||||
:global .arrow {
|
||||
border-color: hsla(220, 5%, 30%, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
302
shared/js/ui/tree/Channel.tsx
Normal file
302
shared/js/ui/tree/Channel.tsx
Normal file
|
@ -0,0 +1,302 @@
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
const ChannelCollapsedIndicator = (props: { collapsed: boolean, onToggle: () => void }) => {
|
||||
return <div className={channelStyle.containerArrow + (!props.collapsed ? " " + channelStyle.down : "")}><div className={"arrow " + (props.collapsed ? "right" : "down")} onClick={event => {
|
||||
event.preventDefault();
|
||||
props.onToggle();
|
||||
}} /></div>
|
||||
};
|
||||
|
||||
@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() {
|
||||
const collapsed_indicator = this.props.channel.child_channel_head || this.props.channel.clients(false).length > 0;
|
||||
return <div className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
|
||||
style={{ paddingLeft: this.props.depth * 16 + 2, top: this.props.offset }}
|
||||
onMouseUp={e => this.onMouseUp(e)}
|
||||
onDoubleClick={() => this.onDoubleClick()}
|
||||
onContextMenu={e => this.onContextMenu(e)}
|
||||
>
|
||||
<UnreadMarker entry={this.props.channel} />
|
||||
{collapsed_indicator && <ChannelCollapsedIndicator key={"collapsed-indicator"} onToggle={() => this.onCollapsedToggle()} collapsed={this.props.channel.collapsed} />}
|
||||
<ChannelEntryIcon channel={this.props.channel} />
|
||||
<ChannelEntryName channel={this.props.channel} />
|
||||
<ChannelEntryIcons channel={this.props.channel} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
private onCollapsedToggle() {
|
||||
this.props.channel.collapsed = !this.props.channel.collapsed;
|
||||
}
|
||||
|
||||
private onMouseUp(event: React.MouseEvent) {
|
||||
if(event.button !== 0) return; /* only left mouse clicks */
|
||||
|
||||
const channel = this.props.channel;
|
||||
if(channel.channelTree.isClientMoveActive()) return;
|
||||
|
||||
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: React.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();
|
||||
}
|
||||
}
|
104
shared/js/ui/tree/Client.scss
Normal file
104
shared/js/ui/tree/Client.scss
Normal file
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
433
shared/js/ui/tree/Client.tsx
Normal file
433
shared/js/ui/tree/Client.tsx
Normal file
|
@ -0,0 +1,433 @@
|
|||
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)}
|
||||
/>
|
||||
}
|
||||
|
||||
private onBlur() {
|
||||
this.props.editFinished(this.ref_div.current.textContent);
|
||||
}
|
||||
|
||||
private onKeyPress(event: React.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 + 2) + "px", top: this.props.offset }}
|
||||
onDoubleClick={() => this.onDoubleClick()}
|
||||
onMouseUp={e => this.onMouseUp(e)}
|
||||
onContextMenu={e => this.onContextMenu(e)}
|
||||
>
|
||||
<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 onMouseUp(event: React.MouseEvent) {
|
||||
if(event.button !== 0) return; /* only left mouse clicks */
|
||||
const tree = this.props.client.channelTree;
|
||||
if(tree.isClientMoveActive()) return;
|
||||
|
||||
tree.events.fire("action_select_entries", { entries: [this.props.client], mode: "auto" });
|
||||
}
|
||||
|
||||
private onContextMenu(event: React.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();
|
||||
}
|
||||
}
|
33
shared/js/ui/tree/Server.scss
Normal file
33
shared/js/ui/tree/Server.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
.serverEntry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
position: relative;
|
||||
|
||||
cursor: pointer;
|
||||
padding-left: 2px;
|
||||
padding-right: 5px;
|
||||
|
||||
.server_type {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: 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;
|
||||
}
|
||||
}
|
127
shared/js/ui/tree/Server.tsx
Normal file
127
shared/js/ui/tree/Server.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
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 }}
|
||||
onMouseUp={e => this.onMouseUp(e)}
|
||||
onContextMenu={e => this.onContextMenu(e)}
|
||||
>
|
||||
<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 onMouseUp(event: React.MouseEvent) {
|
||||
if(event.button !== 0) return; /* only left mouse clicks */
|
||||
if(this.props.server.channelTree.isClientMoveActive()) return;
|
||||
|
||||
this.props.server.channelTree.events.fire("action_select_entries", {
|
||||
entries: [ this.props.server ],
|
||||
mode: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
private onContextMenu(event: React.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();
|
||||
}
|
||||
}
|
26
shared/js/ui/tree/TreeEntry.tsx
Normal file
26
shared/js/ui/tree/TreeEntry.tsx
Normal file
|
@ -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> { }
|
22
shared/js/ui/tree/TreeEntryMove.scss
Normal file
22
shared/js/ui/tree/TreeEntryMove.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
html:root {
|
||||
--channel-tree-move-color: hsla(220, 5%, 2%, 1);
|
||||
--channel-tree-move-background: hsla(0, 0%, 25%, 1);
|
||||
--channel-tree-move-border: hsla(220, 4%, 40%, 1);
|
||||
}
|
||||
|
||||
.moveContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
||||
border: 2px solid var(--channel-tree-move-border);
|
||||
background-color: var(--channel-tree-move-background);
|
||||
|
||||
z-index: 10000;
|
||||
margin-left: 5px;
|
||||
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
|
||||
border-radius: 2px;
|
||||
color: var(--channel-tree-move-color);
|
||||
}
|
95
shared/js/ui/tree/TreeEntryMove.tsx
Normal file
95
shared/js/ui/tree/TreeEntryMove.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {ChannelTreeView} from "tc-shared/ui/tree/View";
|
||||
const moveStyle = require("./TreeEntryMove.scss");
|
||||
|
||||
export interface TreeEntryMoveProps {
|
||||
onMoveEnd: (point: { x: number, y: number }) => void;
|
||||
}
|
||||
|
||||
export interface TreeEntryMoveState {
|
||||
tree_view: ChannelTreeView;
|
||||
|
||||
begin: { x: number, y: number };
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class TreeEntryMove extends ReactComponentBase<TreeEntryMoveProps, TreeEntryMoveState> {
|
||||
private readonly domContainer;
|
||||
private readonly document_mouse_out_listener;
|
||||
private readonly document_mouse_listener;
|
||||
private readonly ref_container: React.RefObject<HTMLDivElement>;
|
||||
|
||||
private current: { x: number, y: number };
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.ref_container = React.createRef();
|
||||
this.domContainer = document.getElementById("mouse-move");
|
||||
this.document_mouse_out_listener = (e: MouseEvent) => {
|
||||
if(e.type === "mouseup") {
|
||||
if(e.button !== 0) return;
|
||||
|
||||
this.props.onMoveEnd({ x: e.pageX, y: e.pageY });
|
||||
}
|
||||
|
||||
this.disableEntryMove();
|
||||
};
|
||||
|
||||
this.document_mouse_listener = (e: MouseEvent) => {
|
||||
this.current = { x: e.pageX, y: e.pageY };
|
||||
const container = this.ref_container.current;
|
||||
if(!container) return;
|
||||
|
||||
container.style.top = e.pageY + "px";
|
||||
container.style.left = e.pageX + "px";
|
||||
};
|
||||
}
|
||||
|
||||
enableEntryMove(view: ChannelTreeView, description: string, begin: { x: number, y: null }, current: { x: number, y: null }, callback_enabled?: () => void) {
|
||||
this.setState({
|
||||
tree_view: view,
|
||||
begin: begin,
|
||||
description: description
|
||||
}, callback_enabled);
|
||||
|
||||
this.current = current;
|
||||
document.addEventListener("mousemove", this.document_mouse_listener);
|
||||
document.addEventListener("mouseleave", this.document_mouse_out_listener);
|
||||
document.addEventListener("mouseup", this.document_mouse_out_listener);
|
||||
}
|
||||
|
||||
private disableEntryMove() {
|
||||
this.setState({
|
||||
tree_view: null
|
||||
});
|
||||
document.removeEventListener("mousemove", this.document_mouse_listener);
|
||||
document.removeEventListener("mouseleave", this.document_mouse_out_listener);
|
||||
document.removeEventListener("mouseup", this.document_mouse_out_listener);
|
||||
}
|
||||
|
||||
protected defaultState(): TreeEntryMoveState {
|
||||
return {
|
||||
tree_view: null,
|
||||
begin: { x: 0, y: 0},
|
||||
description: ""
|
||||
}
|
||||
}
|
||||
|
||||
isActive() { return !!this.state.tree_view; }
|
||||
|
||||
render() {
|
||||
if(!this.state.tree_view)
|
||||
return null;
|
||||
|
||||
return ReactDOM.createPortal(this.renderPortal(), this.domContainer);
|
||||
}
|
||||
|
||||
private renderPortal() {
|
||||
return <div style={{ top: this.current.y, left: this.current.x }} className={moveStyle.moveContainer} ref={this.ref_container} >
|
||||
{this.state.description}
|
||||
</div>;
|
||||
}
|
||||
}
|
122
shared/js/ui/tree/View.scss
Normal file
122
shared/js/ui/tree/View.scss
Normal file
|
@ -0,0 +1,122 @@
|
|||
@import "../../../css/static/properties";
|
||||
@import "../../../css/static/mixin";
|
||||
|
||||
html:root {
|
||||
--channel-tree-entry-move: #313235;
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
&.move {
|
||||
.treeEntry.selected {
|
||||
background-color: var(--channel-tree-entry-move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channelTreeContainer {
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
scroll-behavior: smooth;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
331
shared/js/ui/tree/View.tsx
Normal file
331
shared/js/ui/tree/View.tsx
Normal file
|
@ -0,0 +1,331 @@
|
|||
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, ClientType} from "tc-shared/ui/client";
|
||||
|
||||
const viewStyle = require("./View.scss");
|
||||
|
||||
|
||||
export interface ChannelTreeViewProperties {
|
||||
tree: ChannelTree;
|
||||
onMoveStart: (start: { x: number, y: number }, current: { x: number, y: number }) => void;
|
||||
moveThreshold?: number;
|
||||
}
|
||||
|
||||
export interface ChannelTreeViewState {
|
||||
element_scroll_offset?: number; /* in px */
|
||||
scroll_offset: number; /* in px */
|
||||
view_height: number; /* in px */
|
||||
|
||||
tree_version: number;
|
||||
}
|
||||
|
||||
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 listener_state_collapsed;
|
||||
private update_timeout;
|
||||
|
||||
private mouse_move: { x: number, y: number, down: boolean, fired: boolean } = { x: 0, y: 0, down: false, fired: false };
|
||||
private document_mouse_listener;
|
||||
|
||||
private in_view_callbacks: {
|
||||
index: number,
|
||||
callback: () => void,
|
||||
timeout
|
||||
}[] = [];
|
||||
|
||||
protected defaultState(): ChannelTreeViewState {
|
||||
return {
|
||||
scroll_offset: 0,
|
||||
view_height: 0,
|
||||
tree_version: 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();
|
||||
this.listener_state_collapsed = () => this.handleTreeUpdate();
|
||||
|
||||
this.document_mouse_listener = (e: MouseEvent) => {
|
||||
if(e.type !== "mouseleave" && e.button !== 0)
|
||||
return;
|
||||
|
||||
this.mouse_move.down = false;
|
||||
this.mouse_move.fired = false;
|
||||
|
||||
this.removeDocumentMouseListener();
|
||||
}
|
||||
}
|
||||
|
||||
private registerDocumentMouseListener() {
|
||||
document.addEventListener("mouseleave", this.document_mouse_listener);
|
||||
document.addEventListener("mouseup", this.document_mouse_listener);
|
||||
}
|
||||
|
||||
private removeDocumentMouseListener() {
|
||||
document.removeEventListener("mouseleave", this.document_mouse_listener);
|
||||
document.removeEventListener("mouseup", this.document_mouse_listener);
|
||||
}
|
||||
|
||||
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}
|
||||
onMouseDown={e => this.onMouseDown(e)}
|
||||
onMouseMove={e => this.onMouseMove(e)} >
|
||||
<div className={this.classList(viewStyle.channelTree, this.props.tree.isClientMoveActive() && viewStyle.move)} 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);
|
||||
entry.events.on("notify_collapsed_state_changed", this.listener_state_collapsed);
|
||||
|
||||
this.flat_tree.push({
|
||||
entry: entry,
|
||||
rendered: <ChannelEntryView key={this.state.tree_version + "-channel-" + entry.channelId} channel={entry} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth} ref={entry.view} />
|
||||
});
|
||||
|
||||
if(entry.collapsed) return;
|
||||
let clients = entry.clients(false);
|
||||
if(!this.props.tree.areServerQueriesShown())
|
||||
clients = clients.filter(e => e.properties.client_type_exact !== ClientType.CLIENT_QUERY);
|
||||
this.flat_tree.push(...clients.map(e => {
|
||||
return {
|
||||
entry: e,
|
||||
rendered: <ClientEntryView key={this.state.tree_version + "-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);
|
||||
entry.events.off("notify_collapsed_state_changed", this.listener_state_collapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.build_top_offset = -ChannelTreeView.EntryHeight; /* because of the += */
|
||||
this.flat_tree = [{
|
||||
entry: tree.server,
|
||||
rendered: <ServerEntryView key={this.state.tree_version + "-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();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_query_view_state_changed")
|
||||
private handleQueryViewStateChange() {
|
||||
this.handleTreeUpdate();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_entry_move_begin")
|
||||
private handleEntryMoveBegin() {
|
||||
this.handleTreeUpdate();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_entry_move_end")
|
||||
private handleEntryMoveEnd() {
|
||||
this.handleTreeUpdate();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_tree_reset")
|
||||
private handleTreeReset() {
|
||||
this.rebuild_tree();
|
||||
this.setState({
|
||||
tree_version: this.state.tree_version + 1
|
||||
});
|
||||
}
|
||||
|
||||
private onScroll() {
|
||||
this.setState({
|
||||
scroll_offset: this.ref_container.current.scrollTop
|
||||
});
|
||||
}
|
||||
|
||||
private onMouseDown(e: React.MouseEvent) {
|
||||
if(e.button !== 0) return; /* left button only */
|
||||
|
||||
this.mouse_move.down = true;
|
||||
this.mouse_move.x = e.pageX;
|
||||
this.mouse_move.y = e.pageY;
|
||||
this.registerDocumentMouseListener();
|
||||
}
|
||||
|
||||
private onMouseMove(e: React.MouseEvent) {
|
||||
if(!this.mouse_move.down || this.mouse_move.fired) return;
|
||||
if(Math.abs((this.mouse_move.x - e.pageX) * (this.mouse_move.y - e.pageY)) > (this.props.moveThreshold || 9)) {
|
||||
this.mouse_move.fired = true;
|
||||
this.props.onMoveStart({x: this.mouse_move.x, y: this.mouse_move.y}, {x: e.pageX, y: e.pageY});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
getEntryFromPoint(pageX: number, pageY: number) {
|
||||
const container = this.ref_container.current;
|
||||
if(!container) return;
|
||||
|
||||
const bounds = container.getBoundingClientRect();
|
||||
pageY -= bounds.y;
|
||||
pageX -= bounds.x;
|
||||
|
||||
if(pageX < 0 || pageY < 0)
|
||||
return undefined;
|
||||
|
||||
if(pageX > container.clientWidth)
|
||||
return undefined;
|
||||
|
||||
const total_offset = container.scrollTop + pageY;
|
||||
return this.flat_tree[Math.floor(total_offset / ChannelTreeView.EntryHeight)]?.entry;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
1122
shared/js/ui/view.tsx
Normal file
1122
shared/js/ui/view.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -12,4 +12,6 @@ export function fix_declare_global(nodes: ts.Node[]) : ts.Node[] {
|
|||
if(has_export) return nodes;
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxKind.PlusEqualsToken
|
|
@ -16,6 +16,7 @@
|
|||
"webpack/ManifestPlugin.ts",
|
||||
"webpack/EJSGenerator.ts",
|
||||
"webpack/WatLoader.ts",
|
||||
"webpack/DevelBlocks.ts",
|
||||
|
||||
"file.ts"
|
||||
],
|
||||
|
|
|
@ -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++)
|
||||
|
|
|
@ -345,9 +345,11 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
return;
|
||||
}
|
||||
if(json["type"] === "command") {
|
||||
/* devel-block(log-networking-commands) */
|
||||
let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]);
|
||||
group.log(tr("Handling command '%s'"), json["command"]);
|
||||
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
|
||||
/* devel-block-end */
|
||||
|
||||
this._command_boss.invoke_handle({
|
||||
command: json["command"],
|
||||
|
@ -361,7 +363,9 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
if(this._voice_connection)
|
||||
this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */
|
||||
}
|
||||
/* devel-block(log-networking-commands) */
|
||||
group.end();
|
||||
/* devel-block-end */
|
||||
} else if(json["type"] === "WebRTC") {
|
||||
if(this._voice_connection)
|
||||
this._voice_connection.handleControlPacket(json);
|
||||
|
|
|
@ -77,10 +77,7 @@ export namespace codec {
|
|||
this.entries[index].instance.initialise().then((flag) => {
|
||||
//TODO test success flag
|
||||
this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject);
|
||||
}).catch(error => {
|
||||
log.error(LogCategory.VOICE, tr("Could not initialize codec!\nError: %o"), error);
|
||||
reject(typeof(error) === 'string' ? error : tr("Could not initialize codec!"));
|
||||
});
|
||||
}).catch(reject);
|
||||
}
|
||||
return;
|
||||
} else if(this.entries[index].owner == 0) {
|
||||
|
@ -388,8 +385,9 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
let config: RTCConfiguration = {};
|
||||
config.iceServers = [];
|
||||
config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
|
||||
//config.iceServers.push({ urls: "stun:stun.teaspeak.de:3478" });
|
||||
this.rtcPeerConnection = new RTCPeerConnection(config);
|
||||
const dataChannelConfig = { ordered: true, maxRetransmits: 0 };
|
||||
const dataChannelConfig = { ordered: false, maxRetransmits: 0 };
|
||||
|
||||
this.dataChannel = this.rtcPeerConnection.createDataChannel('main', dataChannelConfig);
|
||||
this.dataChannel.onmessage = this.on_data_channel_message.bind(this);
|
||||
|
@ -401,6 +399,8 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
sdpConstraints.offerToReceiveVideo = false;
|
||||
sdpConstraints.voiceActivityDetection = true;
|
||||
|
||||
this.rtcPeerConnection.onicegatheringstatechange = () => console.log("ICE gathering state changed to %s", this.rtcPeerConnection.iceGatheringState);
|
||||
this.rtcPeerConnection.oniceconnectionstatechange = () => console.log("ICE connection state changed to %s", this.rtcPeerConnection.iceConnectionState);
|
||||
this.rtcPeerConnection.onicecandidate = this.on_local_ice_candidate.bind(this);
|
||||
if(this.local_audio_stream) { //May a typecheck?
|
||||
this.rtcPeerConnection.addStream(this.local_audio_stream.stream);
|
||||
|
@ -431,8 +431,26 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
this.connection.client.update_voice_status(undefined);
|
||||
}
|
||||
|
||||
private registerRemoteICECandidate(candidate: RTCIceCandidate) {
|
||||
if(candidate.candidate === "") {
|
||||
console.log("Adding end candidate");
|
||||
this.rtcPeerConnection.addIceCandidate(null).catch(error => {
|
||||
log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate finish: %o"), error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const pcandidate = new RTCIceCandidate(candidate);
|
||||
if(pcandidate.protocol !== "tcp") return; /* UDP does not work currently */
|
||||
|
||||
log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), pcandidate);
|
||||
this.rtcPeerConnection.addIceCandidate(pcandidate).catch(error => {
|
||||
log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %o: %o"), candidate, error);
|
||||
});
|
||||
}
|
||||
|
||||
private _ice_use_cache: boolean = true;
|
||||
private _ice_cache: any[] = [];
|
||||
private _ice_cache: RTCIceCandidate[] = [];
|
||||
handleControlPacket(json) {
|
||||
if(json["request"] === "answer") {
|
||||
const session_description = new RTCSessionDescription(json["msg"]);
|
||||
|
@ -440,24 +458,20 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
this.rtcPeerConnection.setRemoteDescription(session_description).then(() => {
|
||||
log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length);
|
||||
this._ice_use_cache = false;
|
||||
for(let msg of this._ice_cache) {
|
||||
this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg)).catch(error => {
|
||||
log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %s: %o"), msg, error);
|
||||
});
|
||||
}
|
||||
|
||||
for(let candidate of this._ice_cache)
|
||||
this.registerRemoteICECandidate(candidate);
|
||||
this._ice_cache = [];
|
||||
}).catch(error => {
|
||||
log.info(LogCategory.VOICE, tr("Failed to apply remote description: %o"), error); //FIXME error handling!
|
||||
});
|
||||
} else if(json["request"] === "ice") {
|
||||
} else if(json["request"] === "ice" || json["request"] === "ice_finish") {
|
||||
const candidate = new RTCIceCandidate(json["msg"]);
|
||||
if(!this._ice_use_cache) {
|
||||
log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), json["msg"]);
|
||||
this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(json["msg"])).catch(error => {
|
||||
log.info(LogCategory.VOICE, tr("Failed to add remote ice candidate %s: %o"), json["msg"], error);
|
||||
});
|
||||
this.registerRemoteICECandidate(candidate);
|
||||
} else {
|
||||
log.info(LogCategory.VOICE, tr("Cache remote ice! (%o)"), json["msg"]);
|
||||
this._ice_cache.push(json["msg"]);
|
||||
this._ice_cache.push(candidate);
|
||||
}
|
||||
} else if(json["request"] == "status") {
|
||||
if(json["state"] == "failed") {
|
||||
|
@ -472,22 +486,25 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
}
|
||||
//TODO handle fail specially when its not allowed to reconnect
|
||||
}
|
||||
} else {
|
||||
log.warn(LogCategory.NETWORKING, tr("Received unknown web client control packet: %s"), json["request"]);
|
||||
}
|
||||
}
|
||||
|
||||
private on_local_ice_candidate(event: RTCPeerConnectionIceEvent) {
|
||||
if (event) {
|
||||
//if(event.candidate && event.candidate.protocol !== "udp")
|
||||
// return;
|
||||
if(event.candidate && event.candidate.protocol !== "tcp")
|
||||
return;
|
||||
|
||||
log.info(LogCategory.VOICE, tr("Gathered local ice candidate %o."), event.candidate);
|
||||
if(event.candidate) {
|
||||
log.info(LogCategory.VOICE, tr("Gathered local ice candidate for stream %d: %s"), event.candidate.sdpMLineIndex, event.candidate.candidate);
|
||||
this.connection.sendData(JSON.stringify({
|
||||
type: 'WebRTC',
|
||||
request: "ice",
|
||||
msg: event.candidate,
|
||||
}));
|
||||
} else {
|
||||
log.info(LogCategory.VOICE, tr("Local ICE candidate gathering finish."));
|
||||
this.connection.sendData(JSON.stringify({
|
||||
type: 'WebRTC',
|
||||
request: "ice_finish"
|
||||
|
|
|
@ -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: isDevelopment ? "[path][name]__[local]--[hash:base64:5]" : "[hash:base64]",
|
||||
},
|
||||
sourceMap: isDevelopment
|
||||
}
|
||||
},
|
||||
|
@ -141,6 +144,12 @@ export const config = async (target: "web" | "client") => { return {
|
|||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: "./webpack/DevelBlocks.js",
|
||||
options: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
27
webpack/DevelBlocks.ts
Normal file
27
webpack/DevelBlocks.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
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) => {
|
||||
if(type === "log-networking-commands")
|
||||
return value;
|
||||
|
||||
return "/* snipped block \"" + type + "\" */";
|
||||
});
|
||||
this.callback(null, source);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import * as webpack from "webpack";
|
||||
import * as fs from "fs";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
|
||||
interface Options {
|
||||
|
@ -79,6 +79,8 @@ class ManifestGenerator {
|
|||
});
|
||||
|
||||
compiler.hooks.done.tap(this.constructor.name, () => {
|
||||
const file = this.options.file || "manifest.json";
|
||||
fs.mkdirpSync(path.dirname(file));
|
||||
fs.writeFileSync(this.options.file || "manifest.json", JSON.stringify(this.manifest_content));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue