commit
056dc7c8d5
77 changed files with 4248 additions and 3632 deletions
23
ChangeLog.md
23
ChangeLog.md
|
@ -1,12 +1,29 @@
|
||||||
# Changelog:
|
# 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
|
- Only show the host message when its not empty
|
||||||
|
|
||||||
* **10.03.20**
|
* **10.04.20**
|
||||||
- Improved key code displaying
|
- Improved key code displaying
|
||||||
- Added a keymap system (Hotkeys)
|
- Added a keymap system (Hotkeys)
|
||||||
|
|
||||||
* **09.03.20**
|
* **09.04.20**
|
||||||
- Using React for the client control bar
|
- Using React for the client control bar
|
||||||
- Saving last away state and message
|
- Saving last away state and message
|
||||||
- Saving last query show state
|
- 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[] = [
|
const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
|
||||||
{ /* shared html and php files */
|
{ /* shared html files */
|
||||||
"type": "html",
|
"type": "html",
|
||||||
"search-pattern": /^.*([a-zA-Z]+)\.(html|php|json)$/,
|
"search-pattern": /^.*([a-zA-Z]+)\.(html|json)$/,
|
||||||
"build-target": "dev|rel",
|
"build-target": "dev|rel",
|
||||||
|
|
||||||
"path": "./",
|
"path": "./",
|
||||||
|
@ -191,7 +191,7 @@ const APP_FILE_LIST_WEB_SOURCE: ProjectResource[] = [
|
||||||
{ /* web html files */
|
{ /* web html files */
|
||||||
"web-only": true,
|
"web-only": true,
|
||||||
"type": "html",
|
"type": "html",
|
||||||
"search-pattern": /.*\.(php|html)/,
|
"search-pattern": /.*\.(html)/,
|
||||||
"build-target": "dev|rel",
|
"build-target": "dev|rel",
|
||||||
|
|
||||||
"path": "./",
|
"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
|
//FIXME: This isn't working right now
|
||||||
const CERTACCEPT_FILE_LIST: ProjectResource[] = [
|
const CERTACCEPT_FILE_LIST: ProjectResource[] = [
|
||||||
{ /* html files */
|
{ /* html files */
|
||||||
"type": "html",
|
"type": "html",
|
||||||
"search-pattern": /^([a-zA-Z]+)\.(html|php|json)$/,
|
"search-pattern": /^([a-zA-Z]+)\.(html|json)$/,
|
||||||
"build-target": "dev|rel",
|
"build-target": "dev|rel",
|
||||||
|
|
||||||
"path": "./popup/certaccept/",
|
"path": "./popup/certaccept/",
|
||||||
|
@ -355,7 +310,6 @@ const WEB_APP_FILE_LIST = [
|
||||||
...APP_FILE_LIST_SHARED_SOURCE,
|
...APP_FILE_LIST_SHARED_SOURCE,
|
||||||
...APP_FILE_LIST_SHARED_VENDORS,
|
...APP_FILE_LIST_SHARED_VENDORS,
|
||||||
...APP_FILE_LIST_WEB_SOURCE,
|
...APP_FILE_LIST_WEB_SOURCE,
|
||||||
...APP_FILE_LIST_WEB_TEASPEAK,
|
|
||||||
...CERTACCEPT_FILE_LIST,
|
...CERTACCEPT_FILE_LIST,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -501,8 +455,6 @@ namespace server {
|
||||||
import SearchOptions = generator.SearchOptions;
|
import SearchOptions = generator.SearchOptions;
|
||||||
export type Options = {
|
export type Options = {
|
||||||
port: number;
|
port: number;
|
||||||
php: string;
|
|
||||||
|
|
||||||
search_options: SearchOptions;
|
search_options: SearchOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,7 +462,6 @@ namespace server {
|
||||||
|
|
||||||
let files: ProjectResource[] = [];
|
let files: ProjectResource[] = [];
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
let php: string;
|
|
||||||
let options: Options;
|
let options: Options;
|
||||||
|
|
||||||
const use_https = false;
|
const use_https = false;
|
||||||
|
@ -518,21 +469,6 @@ namespace server {
|
||||||
options = options_;
|
options = options_;
|
||||||
files = _files;
|
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) {
|
if(process.env["ssl_enabled"] || use_https) {
|
||||||
//openssl req -nodes -new -x509 -keyout files_key.pem -out files_cert.pem
|
//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");
|
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) {
|
async function serve_file(pathname: string, query: any, response: http.ServerResponse) {
|
||||||
const file = await generator.search_http_file(files, pathname, options.search_options);
|
const file = await generator.search_http_file(files, pathname, options.search_options);
|
||||||
if(!file) {
|
if(!file) {
|
||||||
|
@ -612,10 +514,6 @@ namespace server {
|
||||||
|
|
||||||
let type = mt.lookup(path.extname(file)) || "text/html";
|
let type = mt.lookup(path.extname(file)) || "text/html";
|
||||||
console.log("[SERVER] Serving file %s", file, type);
|
console.log("[SERVER] Serving file %s", file, type);
|
||||||
if(path.extname(file) === ".php") {
|
|
||||||
serve_php(file, query, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fis = fs.createReadStream(file);
|
const fis = fs.createReadStream(file);
|
||||||
|
|
||||||
response.writeHead(200, "success", {
|
response.writeHead(200, "success", {
|
||||||
|
@ -634,19 +532,12 @@ namespace server {
|
||||||
response.writeHead(200, { "info-version": 1 });
|
response.writeHead(200, { "info-version": 1 });
|
||||||
response.write("type\thash\tpath\tname\n");
|
response.write("type\thash\tpath\tname\n");
|
||||||
for(const file of await generator.search_files(files, options.search_options))
|
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" + file.name + "\n");
|
||||||
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.end();
|
response.end();
|
||||||
return;
|
return;
|
||||||
} else if(url.query["type"] === "file") {
|
} else if(url.query["type"] === "file") {
|
||||||
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
let p = path.join(url.query["path"] as string, url.query["name"] as string).replace(/\\/g, "/");
|
||||||
if(!p.startsWith("/")) p = "/" + p;
|
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);
|
serve_file(p, url.query, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -676,7 +567,7 @@ namespace server {
|
||||||
handle_api_request(request, response, url);
|
handle_api_request(request, response, url);
|
||||||
return;
|
return;
|
||||||
} else if(url.pathname === "/") {
|
} else if(url.pathname === "/") {
|
||||||
url.pathname = "/index.php";
|
url.pathname = "/index.html";
|
||||||
}
|
}
|
||||||
serve_file(url.pathname, url.query, response);
|
serve_file(url.pathname, url.query, response);
|
||||||
}
|
}
|
||||||
|
@ -688,7 +579,7 @@ namespace watcher {
|
||||||
return cp.spawn(process.env.comspec, ["/C", cmd, ...args], {
|
return cp.spawn(process.env.comspec, ["/C", cmd, ...args], {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
cwd: __dirname,
|
cwd: __dirname,
|
||||||
env: process.env
|
env: Object.assign({ NODE_ENV: "development" }, process.env)
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
return cp.spawn(cmd, args, {
|
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) {
|
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, {
|
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||||
port: port,
|
port: port,
|
||||||
php: php_exe(),
|
|
||||||
search_options: {
|
search_options: {
|
||||||
source_path: __dirname,
|
source_path: __dirname,
|
||||||
parameter: [],
|
parameter: [],
|
||||||
|
@ -882,7 +763,6 @@ async function main_develop(node: boolean, target: "client" | "web", port: numbe
|
||||||
try {
|
try {
|
||||||
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
await server.launch(target === "client" ? CLIENT_APP_FILE_LIST : WEB_APP_FILE_LIST, {
|
||||||
port: port,
|
port: port,
|
||||||
php: php_exe(),
|
|
||||||
search_options: {
|
search_options: {
|
||||||
source_path: __dirname,
|
source_path: __dirname,
|
||||||
parameter: [],
|
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 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(" 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(" | 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 */
|
/* proxy log for better format */
|
||||||
|
|
|
@ -4,4 +4,6 @@ window["loader"] = loader_base;
|
||||||
/* let the loader register himself at the window first */
|
/* let the loader register himself at the window first */
|
||||||
setTimeout(loader.run, 0);
|
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/overlay-image-preview.css",
|
||||||
"css/static/music/info_plate.css",
|
"css/static/music/info_plate.css",
|
||||||
"css/static/frame/SelectInfo.css",
|
"css/static/frame/SelectInfo.css",
|
||||||
"css/static/control_bar.css",
|
|
||||||
"css/static/context_menu.css",
|
"css/static/context_menu.css",
|
||||||
"css/static/frame-chat.css",
|
"css/static/frame-chat.css",
|
||||||
"css/static/connection_handlers.css",
|
"css/static/connection_handlers.css",
|
||||||
|
@ -345,10 +344,6 @@ loader.register_task(loader.Stage.SETUP, {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.setAttribute('id', "mouse-move");
|
container.setAttribute('id', "mouse-move");
|
||||||
|
|
||||||
const inner_container = document.createElement("div");
|
|
||||||
inner_container.classList.add("container");
|
|
||||||
container.append(inner_container);
|
|
||||||
|
|
||||||
body.append(container);
|
body.append(container);
|
||||||
}
|
}
|
||||||
/* tooltip container */
|
/* tooltip container */
|
||||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -242,6 +242,16 @@
|
||||||
"@types/sizzle": "*"
|
"@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": {
|
"@types/lodash": {
|
||||||
"version": "4.14.149",
|
"version": "4.14.149",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
||||||
|
@ -8421,6 +8431,11 @@
|
||||||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
||||||
"dev": true
|
"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": {
|
"resolve": {
|
||||||
"version": "1.15.1",
|
"version": "1.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
|
||||||
|
|
|
@ -13,12 +13,10 @@
|
||||||
"csso": "csso",
|
"csso": "csso",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"start": "npm run compile-project-base && node file.js ndevelop",
|
"start": "npm run compile-project-base && node file.js ndevelop",
|
||||||
|
|
||||||
"build-web": "webpack --config webpack-web.config.js",
|
"build-web": "webpack --config webpack-web.config.js",
|
||||||
"develop-web": "npm run compile-project-base && node file.js develop web",
|
"develop-web": "npm run compile-project-base && node file.js develop web",
|
||||||
"build-client": "webpack --config webpack-client.config.js",
|
"build-client": "webpack --config webpack-client.config.js",
|
||||||
"develop-client": "npm run compile-project-base && node file.js develop client",
|
"develop-client": "npm run compile-project-base && node file.js develop client",
|
||||||
|
|
||||||
"webpack-web": "webpack --config webpack-web.config.js",
|
"webpack-web": "webpack --config webpack-web.config.js",
|
||||||
"webpack-client": "webpack --config webpack-client.config.js",
|
"webpack-client": "webpack --config webpack-client.config.js",
|
||||||
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js"
|
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js"
|
||||||
|
@ -33,6 +31,7 @@
|
||||||
"@types/fs-extra": "^8.0.1",
|
"@types/fs-extra": "^8.0.1",
|
||||||
"@types/html-minifier": "^3.5.3",
|
"@types/html-minifier": "^3.5.3",
|
||||||
"@types/jquery": "^3.3.34",
|
"@types/jquery": "^3.3.34",
|
||||||
|
"@types/loader-utils": "^1.1.3",
|
||||||
"@types/lodash": "^4.14.149",
|
"@types/lodash": "^4.14.149",
|
||||||
"@types/moment": "^2.13.0",
|
"@types/moment": "^2.13.0",
|
||||||
"@types/node": "^12.7.2",
|
"@types/node": "^12.7.2",
|
||||||
|
@ -84,6 +83,7 @@
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"webrtc-adapter": "^7.5.1"
|
"webrtc-adapter": "^7.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,11 +148,21 @@ if [[ -e "$LOG_FILE" ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chmod +x ./web/native-codec/build.sh
|
chmod +x ./web/native-codec/build.sh
|
||||||
execute \
|
if hash emcmake 2>/dev/null; then
|
||||||
"Building native codes" \
|
hash cmake 2>/dev/null || { echo "Missing cmake. Please install cmake before retrying. (apt-get install cmake)"; exit 1; }
|
||||||
"Failed to build native opus codec" \
|
hash make 2>/dev/null || { echo "Missing make. Please install build-essential before retrying. (apt-get install build-essential)"; exit 1; }
|
||||||
"docker exec -it emscripten bash -c 'web/native-codec/build.sh'"
|
|
||||||
|
|
||||||
|
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 ----------"
|
echo "---------- Web client ----------"
|
||||||
|
|
||||||
function move_target_file() {
|
function move_target_file() {
|
||||||
|
|
|
@ -2,38 +2,42 @@
|
||||||
## 1.0 Requirements
|
## 1.0 Requirements
|
||||||
The following tools or applications are required to develop the web client:
|
The following tools or applications are required to develop the web client:
|
||||||
- [1.1 IDE](#11-ide)
|
- [1.1 IDE](#11-ide)
|
||||||
- [1.2 PHP](#12-php)
|
- [1.2 NodeJS](#12-nodejs)
|
||||||
- [1.3 NodeJS](#13-nodejs)
|
- [1.2.2 NPM](#122-npm)
|
||||||
- [1.3.2 NPM](#132-npm)
|
- [1.3 Git bash](#13-git-bash)
|
||||||
- [1.4 Git bash](#14-git-bash)
|
- [1.4 Docker](#14-docker)
|
||||||
|
|
||||||
### 1.1 IDE
|
### 1.1 IDE
|
||||||
It does not matter which IDE you use,
|
It does not matter which IDE you use,
|
||||||
you could even use a command line text editor for developing.
|
you could even use a command line text editor for developing.
|
||||||
|
|
||||||
### 1.2 PHP
|
### 1.2 NodeJS
|
||||||
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
|
|
||||||
For building and serving you require `nodejs` grater than 8.
|
For building and serving you require `nodejs` grater than 8.
|
||||||
Nodejs is easily downloadable from [here]().
|
Nodejs is easily downloadable from [here]().
|
||||||
Ensure you've added `node.exe` to the environment path!
|
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.
|
Normally NPM already comes with the NodeJS installer.
|
||||||
So you don't really have to worry about it.
|
So you don't really have to worry about it.
|
||||||
NPM min 6.X is required to develop this project.
|
NPM min 6.X is required to develop this project.
|
||||||
With NPM you could easily download all required dependencies by just typing `npm install`.
|
With NPM you could easily download all required dependencies by just typing `npm install`.
|
||||||
IMPORTANT: NPM must be available within the PATH environment variable!
|
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.
|
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.
|
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.0 Project initialization
|
||||||
|
|
||||||
### 2.1 Cloning the WebClient
|
### 2.1 Cloning the WebClient
|
||||||
|
@ -50,26 +54,18 @@ git submodule update --init
|
||||||
### 2.2 Setting up native scripts
|
### 2.2 Setting up native scripts
|
||||||
TeaWeb uses the Opus audio codec. Because almost no browser supports it out of the box
|
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.
|
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.
|
In order to build the required javascript and wasm files just executing this in your git bash:
|
||||||
So we just need to download them. Just execute the `download_compiled_files.sh` shell script within the `asm` folder.
|
|
||||||
```shell script
|
```shell script
|
||||||
./asm/download_compiled_files.sh
|
docker exec -it emscripten bash -c 'web/native-codec/build.sh'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 Initializing NPM
|
### 2.3 Initializing NPM
|
||||||
To download all required packages simply type:
|
To download all required packages simply type:
|
||||||
```shell script
|
```shell script
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.4 Initial client compilation
|
### 2.4 You're ready to go and start developing
|
||||||
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
|
|
||||||
To start the development environment which automatically compiles all your changed
|
To start the development environment which automatically compiles all your changed
|
||||||
scripts and style sheets you simply have to execute:
|
scripts and style sheets you simply have to execute:
|
||||||
```shell script
|
```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.
|
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`
|
The server will by default listen on `http://localhost:8081`
|
||||||
|
|
||||||
### 2.6 You're ready
|
### 2.5 Using your UI within the TeaClient
|
||||||
Now you're ready to start ahead and implement your own great ideas.
|
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/channel-tree.css"
|
||||||
"css/static/connection_handlers.css"
|
"css/static/connection_handlers.css"
|
||||||
"css/static/context_menu.css"
|
"css/static/context_menu.css"
|
||||||
"css/static/control_bar.css"
|
|
||||||
"css/static/frame-chat.css"
|
"css/static/frame-chat.css"
|
||||||
"css/static/server-log.css"
|
"css/static/server-log.css"
|
||||||
"css/static/scroll.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 */
|
/* all icons related to basic_icons */
|
||||||
.clicon {
|
.clicon {
|
||||||
width:16px;
|
width:16px;
|
||||||
|
|
|
@ -70,7 +70,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-container {
|
.sub-container {
|
||||||
padding-right: 3px;
|
margin-right: -3px;
|
||||||
|
padding-right: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -85,7 +86,7 @@
|
||||||
left: 100%;
|
left: 100%;
|
||||||
top: -4px;
|
top: -4px;
|
||||||
position: absolute;
|
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-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
|
|
|
@ -74,6 +74,14 @@ $animation_length: .5s;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 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 {
|
html, body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,7 @@
|
||||||
width: 25%;
|
width: 25%;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
min-height: 10em;
|
min-height: 10em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
background-color: #222226;
|
background-color: #222226;
|
||||||
|
|
||||||
|
|
|
@ -279,7 +279,9 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
const original_address = {host: server_address.host, port: server_address.port};
|
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;
|
const id = ++this._connect_initialize_id;
|
||||||
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {});
|
this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {});
|
||||||
try {
|
try {
|
||||||
|
@ -636,6 +638,7 @@ export class ConnectionHandler {
|
||||||
this.serverConnection.disconnect();
|
this.serverConnection.disconnect();
|
||||||
|
|
||||||
this.side_bar.private_conversations().clear_client_ids();
|
this.side_bar.private_conversations().clear_client_ids();
|
||||||
|
this.side_bar.channel_conversations().set_current_channel(0);
|
||||||
this.hostbanner.update();
|
this.hostbanner.update();
|
||||||
|
|
||||||
if(auto_reconnect) {
|
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}));
|
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.serverConnection.updateConnectionState(ConnectionState.UNCONNECTED); /* Fix for the native client... */
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel_reconnect(log_event: boolean) {
|
cancel_reconnect(log_event: boolean) {
|
||||||
|
@ -807,7 +812,6 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
resize_elements() {
|
resize_elements() {
|
||||||
this.channelTree.handle_resized();
|
|
||||||
this.invoke_resized_on_activate = false;
|
this.invoke_resized_on_activate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import * as hex from "tc-shared/crypto/hex";
|
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
import * as hex from "tc-shared/crypto/hex";
|
||||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
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 {ClientEntry} from "tc-shared/ui/client";
|
||||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
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 {
|
export class FileEntry {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -130,7 +132,7 @@ export class RequestFileUpload implements UploadTransfer {
|
||||||
throw "invalid size";
|
throw "invalid size";
|
||||||
form_data.append("file", new Blob([data], { type: "application/octet-stream" }));
|
form_data.append("file", new Blob([data], { type: "application/octet-stream" }));
|
||||||
} else {
|
} else {
|
||||||
const buffer = <BufferSource>data;
|
const buffer = data as BufferSource;
|
||||||
if(buffer.byteLength != this.transfer_key.total_size)
|
if(buffer.byteLength != this.transfer_key.total_size)
|
||||||
throw "invalid size";
|
throw "invalid size";
|
||||||
|
|
||||||
|
@ -416,11 +418,6 @@ export class FileManager extends AbstractCommandHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Icon {
|
|
||||||
id: number;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ImageType {
|
export enum ImageType {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
BITMAP,
|
BITMAP,
|
||||||
|
@ -552,34 +549,228 @@ export class CacheManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IconManager {
|
const icon_cache: CacheManager = new CacheManager("icons");
|
||||||
private static 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;
|
handle: FileManager;
|
||||||
private _id_urls: {[id:number]:string} = {};
|
readonly events: Registry<IconManagerEvents>;
|
||||||
private _loading_promises: {[id:number]:Promise<Icon>} = {};
|
private loading_timestamps: {[key: number]: IconManagerLoadingData} = {};
|
||||||
|
|
||||||
constructor(handle: FileManager) {
|
constructor(handle: FileManager) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
|
this.events = new Registry<IconManagerEvents>();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if(URL.revokeObjectURL) {
|
this.loading_timestamps = {};
|
||||||
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 = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete_icon(id: number) : Promise<void> {
|
async delete_icon(id: number) : Promise<void> {
|
||||||
|
@ -599,83 +790,31 @@ export class IconManager {
|
||||||
return this.handle.download_file("", "/icon_" + id);
|
return this.handle.download_file("", "/icon_" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async _response_url(response: Response) {
|
private async server_icon_loader(icon: LocalIcon) : Promise<Response> {
|
||||||
if(!response.headers.has('X-media-bytes'))
|
const loading_data: IconManagerLoadingData = this.loading_timestamps[icon.icon_id] || (this.loading_timestamps[icon.icon_id] = { result: "unset" });
|
||||||
throw "missing media bytes";
|
if(loading_data.result === "error") {
|
||||||
|
if(!loading_data.next_retry || loading_data.next_retry > Date.now()) {
|
||||||
const type = image_type(response.headers.get('X-media-bytes'));
|
log.debug(LogCategory.GENERAL, tr("Don't retry icon download from server. We'll try again in %s"),
|
||||||
const media = media_image_type(type);
|
!loading_data.next_retry ? tr("never") : format_time(loading_data.next_retry - Date.now(), tr("1 second")));
|
||||||
|
throw loading_data.error;
|
||||||
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 _load_icon(id: number) : Promise<Icon> {
|
|
||||||
try {
|
try {
|
||||||
let download_key: DownloadKey;
|
let download_key: DownloadKey;
|
||||||
try {
|
try {
|
||||||
download_key = await this.create_icon_download(id);
|
download_key = await this.create_icon_download(icon.icon_id);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), id, error);
|
if(error instanceof CommandResult) {
|
||||||
throw "Failed to request icon";
|
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);
|
const downloader = spawn_download_transfer(download_key);
|
||||||
|
@ -683,58 +822,21 @@ export class IconManager {
|
||||||
try {
|
try {
|
||||||
response = await downloader.request_file();
|
response = await downloader.request_file();
|
||||||
} catch(error) {
|
} 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";
|
throw "failed to download icon";
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = image_type(response.headers.get('X-media-bytes'));
|
loading_data.result = "success";
|
||||||
const media = media_image_type(type);
|
return response;
|
||||||
|
} catch (error) {
|
||||||
await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media);
|
loading_data.result = "error";
|
||||||
const url = await IconManager._response_url(response.clone());
|
loading_data.error = error as string;
|
||||||
if(this._id_urls[id])
|
loading_data.next_retry = Date.now() + 300 * 1000;
|
||||||
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 */
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
download_icon(id: number) : Promise<Icon> {
|
static generate_tag(icon: LocalIcon | undefined, options?: {
|
||||||
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?: {
|
|
||||||
animate?: boolean
|
animate?: boolean
|
||||||
}) : JQuery<HTMLDivElement> {
|
}) : JQuery<HTMLDivElement> {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
@ -743,42 +845,50 @@ export class IconManager {
|
||||||
let icon_load_image = $.spawn("div").addClass("icon_loading");
|
let icon_load_image = $.spawn("div").addClass("icon_loading");
|
||||||
|
|
||||||
const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", "");
|
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);
|
if (icon.icon_id == 0) {
|
||||||
return;
|
icon_load_image = undefined;
|
||||||
}
|
} else if (icon.icon_id < 1000) {
|
||||||
|
icon_load_image = undefined;
|
||||||
icon_image.attr("src", icon.url);
|
icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + icon.icon_id);
|
||||||
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");
|
|
||||||
});
|
|
||||||
} else {
|
} 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)
|
if(icon_load_image)
|
||||||
|
@ -790,19 +900,20 @@ export class IconManager {
|
||||||
animate?: boolean
|
animate?: boolean
|
||||||
}) : JQuery<HTMLDivElement> {
|
}) : JQuery<HTMLDivElement> {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
return IconManager.generate_tag(this.load_icon(id), options);
|
||||||
|
}
|
||||||
|
|
||||||
id = id >>> 0;
|
load_icon(id: number) : LocalIcon {
|
||||||
if(id == 0 || !id)
|
const server_uid = this.handle.handle.channelTree.server.properties.virtualserver_unique_identifier;
|
||||||
return IconManager.generate_tag({id: id, url: ""}, options);
|
let icon = icon_cache_loader.load_icon(id, server_uid, this.server_icon_loader.bind(this));
|
||||||
else if(id < 1000)
|
if(icon.status !== "loading" && icon.status !== "loaded") {
|
||||||
return IconManager.generate_tag({id: id, url: ""}, options);
|
this.server_icon_loader(icon).then(response => {
|
||||||
|
return icon.set_image(response);
|
||||||
|
}).catch(error => {
|
||||||
if(this._id_urls[id]) {
|
console.warn("Failed to update broken cached icon from server: %o", error);
|
||||||
return IconManager.generate_tag({id: id, url: this._id_urls[id]}, options);
|
})
|
||||||
} else {
|
|
||||||
return IconManager.generate_tag(this.resolve_icon(id), options);
|
|
||||||
}
|
}
|
||||||
|
return icon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -818,7 +929,7 @@ export class AvatarManager {
|
||||||
|
|
||||||
private static cache: CacheManager;
|
private static cache: CacheManager;
|
||||||
private _cached_avatars: {[response_avatar_id:number]:Avatar} = {};
|
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) {
|
constructor(handle: FileManager) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
|
@ -68,6 +68,7 @@ export interface Bookmark {
|
||||||
connect_profile: string;
|
connect_profile: string;
|
||||||
|
|
||||||
last_icon_id?: number;
|
last_icon_id?: number;
|
||||||
|
last_icon_server_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirectoryBookmark {
|
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 {PrivateConversationState} from "tc-shared/ui/frames/side/private_conversations";
|
||||||
import {Conversation} from "tc-shared/ui/frames/side/conversations";
|
import {Conversation} from "tc-shared/ui/frames/side/conversations";
|
||||||
import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
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 {
|
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||||
constructor(connection: AbstractServerConnection) {
|
constructor(connection: AbstractServerConnection) {
|
||||||
|
@ -137,7 +138,13 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
|
|
||||||
handle_command(command: ServerCommand) : boolean {
|
handle_command(command: ServerCommand) : boolean {
|
||||||
if(this[command.command]) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,24 +304,25 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
private createChannelFromJson(json, ignoreOrder: boolean = false) {
|
private createChannelFromJson(json, ignoreOrder: boolean = false) {
|
||||||
let tree = this.connection.client.channelTree;
|
let tree = this.connection.client.channelTree;
|
||||||
|
|
||||||
let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"], tree.findChannel(json["cpid"]));
|
let channel = new ChannelEntry(parseInt(json["cid"]), json["channel_name"]);
|
||||||
tree.insertChannel(channel);
|
let parent, previous;
|
||||||
if(json["channel_order"] !== "0") {
|
if(json["channel_order"] !== "0") {
|
||||||
let prev = tree.findChannel(json["channel_order"]);
|
previous = tree.findChannel(json["channel_order"]);
|
||||||
if(!prev && json["channel_order"] != 0) {
|
if(!previous && json["channel_order"] != 0) {
|
||||||
if(!ignoreOrder) {
|
if(!ignoreOrder) {
|
||||||
log.error(LogCategory.NETWORKING, tr("Invalid channel order id!"));
|
log.error(LogCategory.NETWORKING, tr("Invalid channel order id!"));
|
||||||
return;
|
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) {
|
if(ignoreOrder) {
|
||||||
for(let ch of tree.channels) {
|
for(let ch of tree.channels) {
|
||||||
if(ch.properties.channel_order == channel.channelId) {
|
if(ch.properties.channel_order == channel.channelId) {
|
||||||
|
@ -340,16 +348,30 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
channel.updateVariables(...updates);
|
channel.updateVariables(...updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private batch_update_finished_timeout;
|
||||||
handleCommandChannelList(json) {
|
handleCommandChannelList(json) {
|
||||||
this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */
|
if(this.batch_update_finished_timeout) {
|
||||||
log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length);
|
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++)
|
for(let index = 0; index < json.length; index++)
|
||||||
this.createChannelFromJson(json[index], true);
|
this.createChannelFromJson(json[index], true);
|
||||||
|
|
||||||
|
this.batch_update_finished_timeout = setTimeout(() => {
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleCommandChannelListFinished(json) {
|
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) {
|
handleCommandChannelCreate(json) {
|
||||||
|
@ -795,7 +817,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
||||||
const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
const client = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||||
if(client) /* the client itself might be invisible */
|
if(client) /* the client itself might be invisible */
|
||||||
client.flag_text_unread = conversation.is_unread();
|
client.setUnread(conversation.is_unread());
|
||||||
} else {
|
} else {
|
||||||
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
||||||
}
|
}
|
||||||
|
@ -822,7 +844,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
message: json["msg"]
|
message: json["msg"]
|
||||||
});
|
});
|
||||||
if(conversation.is_unread() && channel)
|
if(conversation.is_unread() && channel)
|
||||||
channel.flag_text_unread = true;
|
channel.setUnread(true);
|
||||||
} else if(mode == 3) {
|
} else if(mode == 3) {
|
||||||
this.connection_handler.log.log(server_log.Type.GLOBAL_MESSAGE, {
|
this.connection_handler.log.log(server_log.Type.GLOBAL_MESSAGE, {
|
||||||
message: json["msg"],
|
message: json["msg"],
|
||||||
|
@ -844,7 +866,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
|
timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]),
|
||||||
message: json["msg"]
|
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) {
|
handleNotifyChannelSubscribed(json) {
|
||||||
for(const entry of json) {
|
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
|
try {
|
||||||
if(!channel) {
|
for(const entry of json) {
|
||||||
console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']);
|
const channel = this.connection.client.channelTree.findChannel(parseInt(entry["cid"]));
|
||||||
continue;
|
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 bulked_response = false;
|
||||||
let bulk_index = 0;
|
let bulk_index = 0;
|
||||||
|
|
||||||
|
@ -314,7 +314,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
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);
|
this.handler_boss.remove_single_handler(single_handler);
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
if(error.id == ErrorID.EMPTY_RESULT) {
|
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
|
//FIXME: Remove this this is currently only some kind of hack
|
||||||
updateConnectionState(state: ConnectionState) {
|
updateConnectionState(state: ConnectionState) {
|
||||||
|
if(state === this.connection_state_) return;
|
||||||
|
|
||||||
const old_state = this.connection_state_;
|
const old_state = this.connection_state_;
|
||||||
this.connection_state_ = state;
|
this.connection_state_ = state;
|
||||||
if(this.onconnectionstatechanged)
|
if(this.onconnectionstatechanged)
|
||||||
|
|
|
@ -9,6 +9,7 @@ export enum ErrorID {
|
||||||
PLAYLIST_IS_IN_USE = 0x2103,
|
PLAYLIST_IS_IN_USE = 0x2103,
|
||||||
|
|
||||||
FILE_ALREADY_EXISTS = 2050,
|
FILE_ALREADY_EXISTS = 2050,
|
||||||
|
FILE_NOT_FOUND = 2051,
|
||||||
|
|
||||||
CLIENT_INVALID_ID = 0x0200,
|
CLIENT_INVALID_ID = 0x0200,
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
import {ClientEvents, MusicClientEntry, SongInfo} from "tc-shared/ui/client";
|
||||||
import {PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration";
|
|
||||||
import {guid} from "tc-shared/crypto/uid";
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@ export class Registry<Events> {
|
||||||
handlers: {[key: string]: ((event) => void)[]}
|
handlers: {[key: string]: ((event) => void)[]}
|
||||||
}[] = [];
|
}[] = [];
|
||||||
private debug_prefix = undefined;
|
private debug_prefix = undefined;
|
||||||
private warn_unhandled_events = true;
|
private warn_unhandled_events = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.registry_uuid = "evreg_data_" + guid();
|
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>(handler: (event?) => void);
|
||||||
off<T extends keyof Events>(event: T, handler: (event?: Event<Events, T>) => 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(event: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
|
||||||
off(handler_or_events, handler?) {
|
off(handler_or_events, handler?) {
|
||||||
if(typeof handler_or_events === "function") {
|
if(typeof handler_or_events === "function") {
|
||||||
|
@ -107,36 +106,49 @@ export class Registry<Events> {
|
||||||
this.connections[event].remove(target as any);
|
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(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, {
|
const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, {
|
||||||
type: event_type,
|
type: event_type,
|
||||||
as: function () { return this; }
|
as: function () { return this; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.fire_event(event_type as string, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fire_event(type: string, data: any) {
|
||||||
let invoke_count = 0;
|
let invoke_count = 0;
|
||||||
for(const handler of (this.handler[event_type as string] || [])) {
|
for(const handler of (this.handler[type]?.slice(0) || [])) {
|
||||||
handler(event);
|
handler(data);
|
||||||
invoke_count++;
|
invoke_count++;
|
||||||
|
|
||||||
const reg_data = handler[this.registry_uuid];
|
const reg_data = handler[this.registry_uuid];
|
||||||
if(typeof reg_data === "object" && reg_data.singleshot)
|
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] || [])) {
|
for(const evhandler of (this.connections[type]?.slice(0) || [])) {
|
||||||
evhandler.fire(event_type as any, event as any);
|
evhandler.fire_event(type, data);
|
||||||
invoke_count++;
|
invoke_count++;
|
||||||
}
|
}
|
||||||
if(invoke_count === 0) {
|
if(this.warn_unhandled_events && invoke_count === 0) {
|
||||||
console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debug_prefix, event_type);
|
console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debug_prefix, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fire_async<T extends keyof Events>(event_type: T, data?: Events[T]) {
|
fire_async<T extends keyof Events>(event_type: T, data?: Events[T], callback?: () => void) {
|
||||||
setTimeout(() => this.fire(event_type, data));
|
setTimeout(() => {
|
||||||
|
this.fire(event_type, data);
|
||||||
|
if(typeof callback === "function")
|
||||||
|
callback();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -219,7 +231,11 @@ export function ReactEventHandler<ObjectClass = React.Component<any, any>, Event
|
||||||
constructor.prototype.componentWillUnmount = function () {
|
constructor.prototype.componentWillUnmount = function () {
|
||||||
const registry = registry_callback(this);
|
const registry = registry_callback(this);
|
||||||
if(!registry) throw "Event registry returned for an event object is invalid";
|
if(!registry) throw "Event registry returned for an event object is invalid";
|
||||||
registry.unregister_handler(this);
|
try {
|
||||||
|
registry.unregister_handler(this);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to unregister event handler: %o", error);
|
||||||
|
}
|
||||||
|
|
||||||
if(typeof willUnmount === "function")
|
if(typeof willUnmount === "function")
|
||||||
willUnmount.call(this, arguments);
|
willUnmount.call(this, arguments);
|
||||||
|
@ -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 namespace sidebar {
|
||||||
export interface music {
|
export interface music {
|
||||||
"open": {}, /* triggers when frame should be shown */
|
"open": {}, /* triggers when frame should be shown */
|
||||||
|
@ -289,13 +280,13 @@ export namespace sidebar {
|
||||||
"reorder_begin": { song_id: number; entry: JQuery },
|
"reorder_begin": { song_id: number; entry: JQuery },
|
||||||
"reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number },
|
"reorder_end": { song_id: number; canceled: boolean; entry: JQuery; previous_entry?: number },
|
||||||
|
|
||||||
"player_time_update": channel_tree.client["music_status_update"],
|
"player_time_update": ClientEvents["music_status_update"],
|
||||||
"player_song_change": channel_tree.client["music_song_change"],
|
"player_song_change": ClientEvents["music_song_change"],
|
||||||
|
|
||||||
"playlist_song_add": channel_tree.client["playlist_song_add"] & { insert_effect?: boolean },
|
"playlist_song_add": ClientEvents["playlist_song_add"] & { insert_effect?: boolean },
|
||||||
"playlist_song_remove": channel_tree.client["playlist_song_remove"],
|
"playlist_song_remove": ClientEvents["playlist_song_remove"],
|
||||||
"playlist_song_reorder": channel_tree.client["playlist_song_reorder"],
|
"playlist_song_reorder": ClientEvents["playlist_song_reorder"],
|
||||||
"playlist_song_loaded": channel_tree.client["playlist_song_loaded"] & { html_entry?: JQuery },
|
"playlist_song_loaded": ClientEvents["playlist_song_loaded"] & { html_entry?: JQuery },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -700,9 +691,11 @@ export namespace modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Some test code
|
//Some test code
|
||||||
const eclient = new Registry<channel_tree.client>();
|
/*
|
||||||
|
const eclient = new Registry<ClientEvents>();
|
||||||
const emusic = new Registry<sidebar.music>();
|
const emusic = new Registry<sidebar.music>();
|
||||||
|
|
||||||
eclient.on("property_update", event => { event.as<"playlist_song_loaded">(); });
|
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);
|
eclient.connect("playlist_song_loaded", emusic);
|
||||||
|
*/
|
|
@ -29,8 +29,7 @@ import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import * as cbar from "./ui/frames/control-bar";
|
import * as cbar from "./ui/frames/control-bar";
|
||||||
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
||||||
import {ClientGlobalControlEvents, global_client_actions} from "tc-shared/events/GlobalEvents";
|
import {global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
|
||||||
|
|
||||||
/* required import for init */
|
/* required import for init */
|
||||||
require("./proto").initialize();
|
require("./proto").initialize();
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
|
||||||
export enum GroupType {
|
export enum GroupType {
|
||||||
QUERY,
|
QUERY,
|
||||||
|
@ -31,10 +32,21 @@ export class GroupPermissionRequest {
|
||||||
promise: LaterPromise<PermissionValue[]>;
|
promise: LaterPromise<PermissionValue[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Group {
|
export interface GroupEvents {
|
||||||
properties: GroupProperties = new GroupProperties();
|
notify_deleted: {},
|
||||||
|
|
||||||
|
notify_properties_updated: {
|
||||||
|
updated_properties: {[Key in keyof GroupProperties]: GroupProperties[Key]};
|
||||||
|
group_properties: GroupProperties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Group {
|
||||||
readonly handle: GroupManager;
|
readonly handle: GroupManager;
|
||||||
|
|
||||||
|
readonly events: Registry<GroupEvents>;
|
||||||
|
readonly properties: GroupProperties = new GroupProperties();
|
||||||
|
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
readonly target: GroupTarget;
|
readonly target: GroupTarget;
|
||||||
readonly type: GroupType;
|
readonly type: GroupType;
|
||||||
|
@ -46,6 +58,8 @@ export class Group {
|
||||||
|
|
||||||
|
|
||||||
constructor(handle: GroupManager, id: number, target: GroupTarget, type: GroupType, name: string) {
|
constructor(handle: GroupManager, id: number, target: GroupTarget, type: GroupType, name: string) {
|
||||||
|
this.events = new Registry<GroupEvents>();
|
||||||
|
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.target = target;
|
this.target = target;
|
||||||
|
@ -53,20 +67,21 @@ export class Group {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProperty(key, value) {
|
updateProperties(properties: {key: string, value: string}[]) {
|
||||||
if(!JSON.map_field_to(this.properties, value, key))
|
let updates = {};
|
||||||
return; /* no updates */
|
|
||||||
|
|
||||||
if(key == "iconid") {
|
for(const { key, value } of properties) {
|
||||||
this.properties.iconid = (new Uint32Array([this.properties.iconid]))[0];
|
if(!JSON.map_field_to(this.properties, value, key))
|
||||||
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
|
continue; /* no updates */
|
||||||
client.updateGroupIcon(this);
|
if(key === "iconid")
|
||||||
});
|
this.properties.iconid = this.properties.iconid >>> 0;
|
||||||
} else if(key == "sortid")
|
updates[key] = this.properties[key];
|
||||||
this.handle.handle.channelTree.clientsByGroup(this).forEach(client => {
|
}
|
||||||
client.update_group_icon_order();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(target == GroupTarget.SERVER)
|
let group_list = target == GroupTarget.SERVER ? this.serverGroups : this.channelGroups;
|
||||||
this.serverGroups = [];
|
const deleted_groups = group_list.slice(0);
|
||||||
else
|
|
||||||
this.channelGroups = [];
|
|
||||||
|
|
||||||
for(let groupData of json) {
|
for(const group_data of json) {
|
||||||
let type : GroupType;
|
let type : GroupType;
|
||||||
switch (Number.parseInt(groupData["type"])) {
|
switch (parseInt(group_data["type"])) {
|
||||||
case 0: type = GroupType.TEMPLATE; break;
|
case 0: type = GroupType.TEMPLATE; break;
|
||||||
case 1: type = GroupType.NORMAL; break;
|
case 1: type = GroupType.NORMAL; break;
|
||||||
case 2: type = GroupType.QUERY; break;
|
case 2: type = GroupType.QUERY; break;
|
||||||
default:
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let group = new Group(this,parseInt(target == GroupTarget.SERVER ? groupData["sgid"] : groupData["cgid"]), target, type, groupData["name"]);
|
const group_id = parseInt(target == GroupTarget.SERVER ? group_data["sgid"] : group_data["cgid"]);
|
||||||
for(let key of Object.keys(groupData)) {
|
let group_index = deleted_groups.findIndex(e => e.id === group_id);
|
||||||
if(key == "sgid") continue;
|
let group: Group;
|
||||||
if(key == "cgid") continue;
|
if(group_index === -1) {
|
||||||
if(key == "type") continue;
|
group = new Group(this, group_id, target, type, group_data["name"]);
|
||||||
if(key == "name") continue;
|
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"]);
|
"n_member_removep", "n_member_addp", "n_modifyp"
|
||||||
group.requiredMemberAddPower = parseInt(groupData["n_member_addp"]);
|
];
|
||||||
group.requiredModifyPower = parseInt(groupData["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)
|
group.requiredMemberRemovePower = parseInt(group_data["n_member_removep"]);
|
||||||
this.serverGroups.push(group);
|
group.requiredMemberAddPower = parseInt(group_data["n_member_addp"]);
|
||||||
else
|
group.requiredModifyPower = parseInt(group_data["n_modifyp"]);
|
||||||
this.channelGroups.push(group);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const client of this.handle.channelTree.clients)
|
for(const deleted of deleted_groups) {
|
||||||
client.update_displayed_client_groups();
|
group_list.remove(deleted);
|
||||||
|
deleted.events.fire("notify_deleted");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request_permissions(group: Group) : Promise<PermissionValue[]> { //database_empty_result
|
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[]> {
|
requestClientChannelPermissions(client_id: number, channel_id: number) : Promise<PermissionValue[]> {
|
||||||
const keys: PermissionRequestKeys = {
|
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));
|
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 webkitAudioContext: typeof AudioContext;
|
||||||
readonly AudioContext: typeof OfflineAudioContext;
|
readonly AudioContext: typeof OfflineAudioContext;
|
||||||
readonly OfflineAudioContext: typeof OfflineAudioContext;
|
readonly OfflineAudioContext: typeof OfflineAudioContext;
|
||||||
readonly webkitOfflineAudioContext: typeof webkitOfflineAudioContext;
|
readonly webkitOfflineAudioContext: typeof OfflineAudioContext;
|
||||||
readonly RTCPeerConnection: typeof RTCPeerConnection;
|
readonly RTCPeerConnection: typeof RTCPeerConnection;
|
||||||
readonly Pointer_stringify: any;
|
readonly Pointer_stringify: any;
|
||||||
readonly jsrender: any;
|
readonly jsrender: any;
|
||||||
|
|
|
@ -156,7 +156,8 @@ export class Settings extends StaticSettings {
|
||||||
|
|
||||||
static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
|
static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
|
||||||
key: 'disableContextMenu',
|
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> = {
|
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 => {
|
static readonly FN_PROFILE_RECORD: (name: string) => SettingsKey<any> = name => {
|
||||||
return {
|
return {
|
||||||
key: 'profile_record' + name
|
key: 'profile_record' + name
|
||||||
|
@ -485,14 +493,15 @@ export class ServerSettings extends SettingsBase {
|
||||||
|
|
||||||
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||||
if(this._destroyed) throw "destroyed";
|
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) {
|
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
||||||
if(this._destroyed) throw "destroyed";
|
if(this._destroyed) throw "destroyed";
|
||||||
key = Settings.keyify(key);
|
key = Settings.keyify(key);
|
||||||
|
|
||||||
if(this.cacheServer[key.key] == value) return;
|
if(this.cacheServer[key.key] === value) return;
|
||||||
|
|
||||||
this._server_settings_updated = true;
|
this._server_settings_updated = true;
|
||||||
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
||||||
|
|
|
@ -241,4 +241,21 @@ namespace connection {
|
||||||
handler["notifyinitialized"] = handle_notify_initialized;
|
handler["notifyinitialized"] = handle_notify_initialized;
|
||||||
handler["notifyusercount"] = handle_notify_user_count;
|
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 {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 * as log from "tc-shared/log";
|
||||||
import {LogCategory, LogType} from "tc-shared/log";
|
import {LogCategory, LogType} from "tc-shared/log";
|
||||||
import {PermissionType} from "tc-shared/permission/PermissionType";
|
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 {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
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 {
|
export enum ChannelType {
|
||||||
PERMANENT,
|
PERMANENT,
|
||||||
SEMI_PERMANENT,
|
SEMI_PERMANENT,
|
||||||
|
@ -69,7 +75,82 @@ export class ChannelProperties {
|
||||||
channel_conversation_history_length: number = -1;
|
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;
|
channelTree: ChannelTree;
|
||||||
channelId: number;
|
channelId: number;
|
||||||
parent?: ChannelEntry;
|
parent?: ChannelEntry;
|
||||||
|
@ -77,16 +158,16 @@ export class ChannelEntry {
|
||||||
|
|
||||||
channel_previous?: ChannelEntry;
|
channel_previous?: ChannelEntry;
|
||||||
channel_next?: 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;
|
private _family_index: number = 0;
|
||||||
|
|
||||||
//HTML DOM elements
|
//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 _destroyed = false;
|
||||||
|
|
||||||
private _cachedPassword: string;
|
private _cachedPassword: string;
|
||||||
|
@ -95,29 +176,37 @@ export class ChannelEntry {
|
||||||
private _cached_channel_description_promise_resolve: any = undefined;
|
private _cached_channel_description_promise_resolve: any = undefined;
|
||||||
private _cached_channel_description_promise_reject: any = undefined;
|
private _cached_channel_description_promise_reject: any = undefined;
|
||||||
|
|
||||||
|
private _flag_collapsed: boolean;
|
||||||
private _flag_subscribed: boolean;
|
private _flag_subscribed: boolean;
|
||||||
private _subscribe_mode: ChannelSubscribeMode;
|
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.properties = new ChannelProperties();
|
||||||
this.channelId = channelId;
|
this.channelId = channelId;
|
||||||
this.properties.channel_name = channelName;
|
this.properties.channel_name = channelName;
|
||||||
this.parent = parent;
|
|
||||||
this.channelTree = null;
|
this.channelTree = null;
|
||||||
|
|
||||||
this.initializeTag();
|
this.parsed_channel_name = new ParsedChannelName("undefined", false);
|
||||||
this.__updateChannelName();
|
|
||||||
|
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() {
|
destroy() {
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
if(this._tag_root) {
|
|
||||||
this._tag_root.remove(); /* removes also all other tags */
|
this.client_list.forEach(e => this.unregisterClient(e, true));
|
||||||
this._tag_root = undefined;
|
this.client_list = [];
|
||||||
}
|
|
||||||
this._tag_siblings = undefined;
|
|
||||||
this._tag_channel = undefined;
|
|
||||||
this._tag_clients = undefined;
|
|
||||||
|
|
||||||
this._cached_channel_description_promise = undefined;
|
this._cached_channel_description_promise = undefined;
|
||||||
this._cached_channel_description_promise_resolve = undefined;
|
this._cached_channel_description_promise_resolve = undefined;
|
||||||
|
@ -134,7 +223,7 @@ export class ChannelEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedChannelName() {
|
formattedChannelName() {
|
||||||
return this._channel_name_formatted || this.properties.channel_name;
|
return this.parsed_channel_name.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelDescription() : Promise<string> {
|
getChannelDescription() : Promise<string> {
|
||||||
|
@ -151,58 +240,27 @@ export class ChannelEntry {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parent_channel() { return this.parent; }
|
registerClient(client: ClientEntry) {
|
||||||
hasParent(){ return this.parent != null; }
|
client.events.on("notify_properties_updated", this.client_property_listener);
|
||||||
getChannelId(){ return this.channelId; }
|
this.client_list.push(client);
|
||||||
|
this.reorderClientList(false);
|
||||||
|
|
||||||
children(deep = false) : ChannelEntry[] {
|
this.events.fire("notify_clients_changed");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clients(deep = false) : ClientEntry[] {
|
unregisterClient(client: ClientEntry, no_event?: boolean) {
|
||||||
const result: ClientEntry[] = [];
|
client.events.off("notify_properties_updated", this.client_property_listener);
|
||||||
if(this.channelTree == null) return [];
|
if(!this.client_list.remove(client))
|
||||||
|
log.warn(LogCategory.CHANNEL, tr("Unregistered unknown client from channel %s"), this.channelName());
|
||||||
|
|
||||||
const self = this;
|
if(!no_event)
|
||||||
this.channelTree.clients.forEach(function (entry) {
|
this.events.fire("notify_clients_changed");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clients_ordered() : ClientEntry[] {
|
private reorderClientList(fire_event: boolean) {
|
||||||
const clients = this.clients(false);
|
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)
|
if(a.properties.client_talk_power < b.properties.client_talk_power)
|
||||||
return 1;
|
return 1;
|
||||||
if(a.properties.client_talk_power > b.properties.client_talk_power)
|
if(a.properties.client_talk_power > b.properties.client_talk_power)
|
||||||
|
@ -215,16 +273,47 @@ export class ChannelEntry {
|
||||||
|
|
||||||
return 0;
|
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) {
|
parent_channel() { return this.parent; }
|
||||||
const current_index = this._family_index;
|
hasParent(){ return this.parent != null; }
|
||||||
const new_index = this.calculate_family_index(true);
|
getChannelId(){ return this.channelId; }
|
||||||
if(current_index == new_index && !enforce) return;
|
|
||||||
|
|
||||||
this._tag_channel.css("z-index", this._family_index);
|
children(deep = false) : ChannelEntry[] {
|
||||||
this._tag_channel.css("padding-left", ((this._family_index + 1) * 16 + 10) + "px");
|
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 {
|
calculate_family_index(enforce_recalculate: boolean = false) : number {
|
||||||
|
@ -242,235 +331,13 @@ export class ChannelEntry {
|
||||||
return this._family_index;
|
return this._family_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeTag() {
|
protected onSelect(singleSelect: boolean) {
|
||||||
const tag_channel = $.spawn("div").addClass("tree-entry channel");
|
super.onSelect(singleSelect);
|
||||||
|
if(!singleSelect) return;
|
||||||
|
|
||||||
{
|
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||||
const container_entry = $.spawn("div").addClass("container-channel");
|
this.channelTree.client.side_bar.channel_conversations().set_current_channel(this.channelId);
|
||||||
|
this.channelTree.client.side_bar.show_channel_conversations();
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,6 +383,7 @@ export class ChannelEntry {
|
||||||
|
|
||||||
let trigger_close = true;
|
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;
|
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
|
||||||
contextmenu.spawn_context_menu(x, y, {
|
contextmenu.spawn_context_menu(x, y, {
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
@ -579,7 +447,7 @@ export class ChannelEntry {
|
||||||
name: tr("Edit channel"),
|
name: tr("Edit channel"),
|
||||||
invalidPermission: !channelModify,
|
invalidPermission: !channelModify,
|
||||||
callback: () => {
|
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) {
|
if(changes) {
|
||||||
changes["cid"] = this.channelId;
|
changes["cid"] = this.channelId;
|
||||||
this.channelTree.client.serverConnection.send_command("channeledit", changes);
|
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(),
|
contextmenu.Entry.HR(),
|
||||||
{
|
{
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
@ -649,80 +536,12 @@ export class ChannelEntry {
|
||||||
invalidPermission: !channelCreate,
|
invalidPermission: !channelCreate,
|
||||||
callback: () => this.channelTree.spawnCreateChannel()
|
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}[]) {
|
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());
|
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);
|
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Clannel update properties", entries);
|
||||||
}
|
}
|
||||||
|
/* devel-block-end */
|
||||||
|
|
||||||
let info_update = false;
|
let info_update = false;
|
||||||
for(let variable of variables) {
|
for(let variable of variables) {
|
||||||
|
@ -743,36 +563,14 @@ export class ChannelEntry {
|
||||||
JSON.map_field_to(this.properties, value, variable.key);
|
JSON.map_field_to(this.properties, value, variable.key);
|
||||||
|
|
||||||
if(key == "channel_name") {
|
if(key == "channel_name") {
|
||||||
this.__updateChannelName();
|
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||||
info_update = true;
|
info_update = true;
|
||||||
} else if(key == "channel_order") {
|
} else if(key == "channel_order") {
|
||||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||||
this.channelTree.moveChannel(this, order, this.parent);
|
this.channelTree.moveChannel(this, order, this.parent);
|
||||||
} else if(key == "channel_icon_id") {
|
} else if(key === "channel_icon_id") {
|
||||||
/* For more detail lookup client::updateVariables and client_icon_id!
|
this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */
|
||||||
* 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_description") {
|
else if(key == "channel_description") {
|
||||||
this._cached_channel_description = undefined;
|
this._cached_channel_description = undefined;
|
||||||
if(this._cached_channel_description_promise_resolve)
|
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_resolve = undefined;
|
||||||
this._cached_channel_description_promise_reject = 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") {
|
if(key == "channel_flag_conversation_private") {
|
||||||
const conversations = this.channelTree.client.side_bar.channel_conversations();
|
const conversations = this.channelTree.client.side_bar.channel_conversations();
|
||||||
const conversation = conversations.conversation(this.channelId, false);
|
const conversation = conversations.conversation(this.channelId, false);
|
||||||
|
@ -792,7 +586,15 @@ export class ChannelEntry {
|
||||||
conversation.set_flag_private(this.properties.channel_flag_conversation_private);
|
conversation.set_flag_private(this.properties.channel_flag_conversation_private);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* devel-block(log-channel-property-updates) */
|
||||||
group.end();
|
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) {
|
if(info_update) {
|
||||||
const _client = this.channelTree.client.getClient();
|
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() {
|
generate_bbcode() {
|
||||||
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
|
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
|
||||||
}
|
}
|
||||||
|
@ -847,11 +627,12 @@ export class ChannelEntry {
|
||||||
!this._cachedPassword &&
|
!this._cachedPassword &&
|
||||||
!this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).granted(1)) {
|
!this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).granted(1)) {
|
||||||
createInputModal(tr("Channel password"), tr("Channel password:"), () => true, text => {
|
createInputModal(tr("Channel password"), tr("Channel password:"), () => true, text => {
|
||||||
if(typeof(text) == typeof(true)) return;
|
if(typeof(text) !== "string") return;
|
||||||
hashPassword(text as string).then(result => {
|
|
||||||
|
hashPassword(text).then(result => {
|
||||||
this._cachedPassword = result;
|
this._cachedPassword = result;
|
||||||
|
this.events.fire("notify_cached_password_updated", { reason: "password-entered", new_hash: result });
|
||||||
this.joinChannel();
|
this.joinChannel();
|
||||||
this.updateChannelTypeIcon();
|
|
||||||
});
|
});
|
||||||
}).open();
|
}).open();
|
||||||
} else if(this.channelTree.client.getClient().currentChannel() != this)
|
} else if(this.channelTree.client.getClient().currentChannel() != this)
|
||||||
|
@ -861,7 +642,7 @@ export class ChannelEntry {
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
if(error.id == 781) { //Invalid password
|
if(error.id == 781) { //Invalid password
|
||||||
this._cachedPassword = undefined;
|
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) {
|
if(inherited_subscription_mode) {
|
||||||
this.subscribe_mode = ChannelSubscribeMode.INHERITED;
|
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 {
|
} else {
|
||||||
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
|
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
|
||||||
unsubscribe = this.flag_subscribed;
|
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 {
|
get flag_subscribed() : boolean {
|
||||||
return this._flag_subscribed;
|
return this._flag_subscribed;
|
||||||
}
|
}
|
||||||
|
@ -918,7 +714,7 @@ export class ChannelEntry {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this._flag_subscribed = flag;
|
this._flag_subscribed = flag;
|
||||||
this.updateChannelTypeIcon();
|
this.events.fire("notify_subscribe_state_changed", { channel_subscribed: flag });
|
||||||
}
|
}
|
||||||
|
|
||||||
get subscribe_mode() : ChannelSubscribeMode {
|
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);
|
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 {
|
log_data() : server_log.base.Channel {
|
||||||
return {
|
return {
|
||||||
channel_name: this.channelName(),
|
channel_name: this.channelName(),
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
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 {ChannelTree} from "tc-shared/ui/view";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {LogCategory, LogType} from "tc-shared/log";
|
import {LogCategory, LogType} from "tc-shared/log";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {KeyCode, SpecialKey} from "tc-shared/PPTListener";
|
|
||||||
import {Sound} from "tc-shared/sound/Sounds";
|
import {Sound} from "tc-shared/sound/Sounds";
|
||||||
import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/GroupManager";
|
import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/GroupManager";
|
||||||
import PermissionType from "tc-shared/permission/PermissionType";
|
import PermissionType from "tc-shared/permission/PermissionType";
|
||||||
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||||
import * as htmltags from "tc-shared/ui/htmltags";
|
import * as htmltags from "tc-shared/ui/htmltags";
|
||||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
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 {ChannelEntry} from "tc-shared/ui/channel";
|
||||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
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 {spawnPlaylistEdit} from "tc-shared/ui/modal/ModalPlaylistEdit";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import * as ppt from "tc-backend/ppt";
|
|
||||||
import * as hex from "tc-shared/crypto/hex";
|
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 {
|
export enum ClientType {
|
||||||
CLIENT_VOICE,
|
CLIENT_VOICE,
|
||||||
|
@ -135,12 +136,39 @@ export class ClientConnectionInfo {
|
||||||
connection_client_port: number = -1;
|
connection_client_port: number = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClientEntry {
|
export interface ClientEvents extends ChannelTreeEntryEvents {
|
||||||
readonly events: Registry<channel_tree.client>;
|
"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 _clientId: number;
|
||||||
protected _channel: ChannelEntry;
|
protected _channel: ChannelEntry;
|
||||||
protected _tag: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
protected _properties: ClientProperties;
|
protected _properties: ClientProperties;
|
||||||
protected lastVariableUpdate: number = 0;
|
protected lastVariableUpdate: number = 0;
|
||||||
|
@ -162,7 +190,8 @@ export class ClientEntry {
|
||||||
channelTree: ChannelTree;
|
channelTree: ChannelTree;
|
||||||
|
|
||||||
constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) {
|
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 = properties;
|
||||||
this._properties.client_nickname = clientName;
|
this._properties.client_nickname = clientName;
|
||||||
|
@ -172,10 +201,6 @@ export class ClientEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if(this._tag) {
|
|
||||||
this._tag.remove();
|
|
||||||
this._tag = undefined;
|
|
||||||
}
|
|
||||||
if(this._audio_handle) {
|
if(this._audio_handle) {
|
||||||
log.warn(LogCategory.AUDIO, tr("Destroying client with an active audio handle. This could cause memory leaks!"));
|
log.warn(LogCategory.AUDIO, tr("Destroying client with an active audio handle. This could cause memory leaks!"));
|
||||||
try {
|
try {
|
||||||
|
@ -240,7 +265,7 @@ export class ClientEntry {
|
||||||
clientId(){ return this._clientId; }
|
clientId(){ return this._clientId; }
|
||||||
|
|
||||||
is_muted() { return !!this._audio_muted; }
|
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)
|
if(this._audio_muted === flag && !force)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -264,13 +289,11 @@ export class ClientEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(update_icon)
|
this.events.fire("notify_mute_state_change", { muted: flag });
|
||||||
this.updateClientSpeakIcon();
|
|
||||||
|
|
||||||
for(const client of this.channelTree.clients) {
|
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;
|
continue;
|
||||||
client.set_muted(flag, true);
|
client.set_muted(flag, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,34 +301,8 @@ export class ClientEntry {
|
||||||
if(this._listener_initialized) return;
|
if(this._listener_initialized) return;
|
||||||
this._listener_initialized = true;
|
this._listener_initialized = true;
|
||||||
|
|
||||||
this.tag.on('mouseup', event => {
|
//FIXME: TODO!
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tag.on('mousedown', event => {
|
this.tag.on('mousedown', event => {
|
||||||
if(event.which != 1) return; //Only the left button
|
if(event.which != 1) return; //Only the left button
|
||||||
|
|
||||||
|
@ -340,6 +337,19 @@ export class ClientEntry {
|
||||||
this.channelTree.onSelect();
|
this.channelTree.onSelect();
|
||||||
}, event);
|
}, 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[] {
|
protected contextmenu_info() : contextmenu.MenuEntry[] {
|
||||||
|
@ -674,85 +684,18 @@ export class ClientEntry {
|
||||||
icon_class: "client-input_muted_local",
|
icon_class: "client-input_muted_local",
|
||||||
name: tr("Mute client"),
|
name: tr("Mute client"),
|
||||||
visible: !this._audio_muted,
|
visible: !this._audio_muted,
|
||||||
callback: () => this.set_muted(true, true)
|
callback: () => this.set_muted(true, false)
|
||||||
}, {
|
}, {
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
icon_class: "client-input_muted_local",
|
icon_class: "client-input_muted_local",
|
||||||
name: tr("Unmute client"),
|
name: tr("Unmute client"),
|
||||||
visible: this._audio_muted,
|
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() : {})
|
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 {
|
static bbcodeTag(id: number, name: string, uid: string) : string {
|
||||||
return "[url=client://" + id + "/" + uid + "~" + encodeURIComponent(name) + "]" + name + "[/url]";
|
return "[url=client://" + id + "/" + uid + "~" + encodeURIComponent(name) + "]" + name + "[/url]";
|
||||||
}
|
}
|
||||||
|
@ -777,80 +720,18 @@ export class ClientEntry {
|
||||||
set speaking(flag) {
|
set speaking(flag) {
|
||||||
if(flag === this._speaking) return;
|
if(flag === this._speaking) return;
|
||||||
this._speaking = flag;
|
this._speaking = flag;
|
||||||
this.updateClientSpeakIcon();
|
this.events.fire("notify_speak_state_change", { speaking: flag });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClientStatusIcons() {
|
isSpeaking() { return this._speaking; }
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVariables(...variables: {key: string, value: string}[]) {
|
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 reorder_channel = false;
|
||||||
let update_avatar = 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 = [];
|
const entries = [];
|
||||||
for(const variable of variables)
|
for(const variable of variables)
|
||||||
|
@ -861,6 +742,7 @@ export class ClientEntry {
|
||||||
});
|
});
|
||||||
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Client update properties", entries);
|
log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Client update properties", entries);
|
||||||
}
|
}
|
||||||
|
/* devel-block-end */
|
||||||
|
|
||||||
for(const variable of variables) {
|
for(const variable of variables) {
|
||||||
const old_value = this._properties[variable.key];
|
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 chat = this.channelTree.client.side_bar;
|
||||||
const conversation = chat.private_conversations().find_conversation({
|
const conversation = chat.private_conversations().find_conversation({
|
||||||
name: this.clientNickName(),
|
name: this.clientNickName(),
|
||||||
|
@ -893,32 +773,19 @@ export class ClientEntry {
|
||||||
conversation.set_client_name(variable.value);
|
conversation.set_client_name(variable.value);
|
||||||
reorder_channel = true;
|
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") {
|
if(variable.key == "client_unique_identifier") {
|
||||||
this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1"));
|
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);
|
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)
|
if(this._audio_handle)
|
||||||
this._audio_handle.set_volume(this._audio_muted ? 0 : this._audio_volume);
|
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);
|
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") {
|
if(variable.key == "client_talk_power") {
|
||||||
reorder_channel = true;
|
reorder_channel = true;
|
||||||
update_icon_status = true;
|
//update_icon_status = true; DONE
|
||||||
}
|
}
|
||||||
if(variable.key == "client_icon_id") {
|
if(variable.key == "client_icon_id") {
|
||||||
/* yeah we like javascript. Due to JS wiered integer behaviour parsing for example fails for 18446744073409829863.
|
/* 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 :)
|
* In opposite "18446744073409829863" >>> 0 evaluates to 3995244544, which is the icon id :)
|
||||||
*/
|
*/
|
||||||
this.properties.client_icon_id = variable.value as any >>> 0;
|
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")
|
else if(variable.key == "client_flag_avatar")
|
||||||
update_avatar = true;
|
update_avatar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* process updates after variables have been set */
|
/* 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 side_bar = this.channelTree.client.side_bar;
|
||||||
{
|
{
|
||||||
const client_info = side_bar.client_info();
|
const client_info = side_bar.client_info();
|
||||||
|
@ -959,44 +814,15 @@ export class ClientEntry {
|
||||||
conversation.update_avatar();
|
conversation.update_avatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* devel-block(log-client-property-updates) */
|
||||||
group.end();
|
group.end();
|
||||||
this.events.fire("property_update", {
|
/* devel-block-end */
|
||||||
properties: variables.map(e => e.key)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
update_displayed_client_groups() {
|
{
|
||||||
this.tag.find(".container-icons-group").children().remove();
|
let properties = {};
|
||||||
|
for(const property of variables)
|
||||||
for(let id of this.assignedServerGroupIds())
|
properties[property.key] = this.properties[property.key];
|
||||||
this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id));
|
this.events.fire("notify_properties_updated", { updated_properties: properties as any, client_properties: this.properties });
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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[] {
|
assignedServerGroupIds() : number[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
for(let id of this.properties.client_servergroups.split(",")){
|
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 {
|
log_data() : server_log.base.Client {
|
||||||
return {
|
return {
|
||||||
client_unique_id: this.properties.client_unique_identifier,
|
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_resolve = undefined;
|
||||||
this._info_connection_promise_reject = 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 {
|
export class LocalClientEntry extends ClientEntry {
|
||||||
|
@ -1195,64 +981,42 @@ export class LocalClientEntry extends ClientEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeListener(): void {
|
initializeListener(): void {
|
||||||
if(this._listener_initialized)
|
|
||||||
this.tag.off();
|
|
||||||
this._listener_initialized = false; /* could there be a better system */
|
|
||||||
super.initializeListener();
|
super.initializeListener();
|
||||||
this.tag.find(".client-name").addClass("client-name-own");
|
}
|
||||||
|
|
||||||
this.tag.on('dblclick', () => {
|
renameSelf(new_name: string) : Promise<boolean> {
|
||||||
if(Array.isArray(this.channelTree.currently_selected)) { //Multiselect
|
const old_name = this.properties.client_nickname;
|
||||||
return;
|
this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */
|
||||||
}
|
return this.handle.serverConnection.command_helper.updateClient("client_nickname", new_name).then((e) => {
|
||||||
this.openRename();
|
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 {
|
openRename() : void {
|
||||||
this.channelTree.client_mover.enabled = false;
|
const view = this.channelTree.view.current;
|
||||||
|
if(!view) return; //TODO: Fallback input modal
|
||||||
const elm = this.tag.find(".client-name");
|
view.scrollEntryInView(this, () => {
|
||||||
elm.attr("contenteditable", "true");
|
const own_view = this.view.current;
|
||||||
elm.removeClass("client-name-own");
|
if(!own_view) {
|
||||||
elm.css("background-color", "white");
|
return; //TODO: Fallback input modal
|
||||||
elm.focus();
|
|
||||||
this.renaming = true;
|
|
||||||
|
|
||||||
elm.on('keypress', event => {
|
|
||||||
if(event.keyCode == KeyCode.KEY_RETURN) {
|
|
||||||
$(event.target).trigger("focusout");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
elm.on('focusout', event => {
|
own_view.setState({
|
||||||
this.channelTree.client_mover.enabled = true;
|
rename: true,
|
||||||
|
renameInitialName: this.properties.client_nickname
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks";
|
||||||
import {
|
import {
|
||||||
add_server_to_bookmarks,
|
add_server_to_bookmarks,
|
||||||
|
@ -33,7 +33,7 @@ export interface MenuItem {
|
||||||
delete_item(item: MenuItem | HRItem);
|
delete_item(item: MenuItem | HRItem);
|
||||||
items() : (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;
|
label(value?: string) : string;
|
||||||
visible(value?: boolean) : boolean;
|
visible(value?: boolean) : boolean;
|
||||||
disabled(value?: boolean) : boolean;
|
disabled(value?: boolean) : boolean;
|
||||||
|
@ -178,7 +178,7 @@ namespace html {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
icon(klass?: string | Promise<Icon> | Icon): string {
|
icon(klass?: string | LocalIcon): string {
|
||||||
this._label_icon_tag.children().remove();
|
this._label_icon_tag.children().remove();
|
||||||
if(typeof(klass) === "string")
|
if(typeof(klass) === "string")
|
||||||
$.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag);
|
$.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag);
|
||||||
|
@ -288,7 +288,8 @@ export function rebuild_bookmarks() {
|
||||||
} else {
|
} else {
|
||||||
const bookmark = entry as Bookmark;
|
const bookmark = entry as Bookmark;
|
||||||
const item = root.append_item(bookmark.display_name);
|
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));
|
item.click(() => boorkmak_connect(bookmark));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,7 @@ export interface ButtonProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||||
protected default_state(): ButtonState {
|
protected defaultState(): ButtonState {
|
||||||
return {
|
return {
|
||||||
switched: false,
|
switched: false,
|
||||||
dropdownShowed: false,
|
dropdownShowed: false,
|
||||||
|
@ -66,13 +66,13 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseEnter() {
|
private onMouseEnter() {
|
||||||
this.updateState({
|
this.setState({
|
||||||
dropdownShowed: true
|
dropdownShowed: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseLeave() {
|
private onMouseLeave() {
|
||||||
this.updateState({
|
this.setState({
|
||||||
dropdownShowed: false
|
dropdownShowed: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,6 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||||
const new_state = !(this.state.switched || this.props.switched);
|
const new_state = !(this.state.switched || this.props.switched);
|
||||||
const result = this.props.onToggle?.call(undefined, new_state);
|
const result = this.props.onToggle?.call(undefined, new_state);
|
||||||
if(this.props.autoSwitch)
|
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 * as React from "react";
|
||||||
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
import {IconRenderer} from "tc-shared/ui/react-elements/Icon";
|
import {IconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
|
import {LocalIcon} from "tc-shared/FileManager";
|
||||||
const cssStyle = require("./button.scss");
|
const cssStyle = require("./button.scss");
|
||||||
|
|
||||||
export interface DropdownEntryProperties {
|
export interface DropdownEntryProperties {
|
||||||
icon?: string | JQuery<HTMLDivElement>;
|
icon?: string | LocalIcon;
|
||||||
text: JSX.Element | string;
|
text: JSX.Element | string;
|
||||||
|
|
||||||
onClick?: (event) => void;
|
onClick?: (event) => void;
|
||||||
|
@ -12,7 +13,7 @@ export interface DropdownEntryProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
|
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
|
||||||
protected default_state() { return {}; }
|
protected defaultState() { return {}; }
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if(this.props.children) {
|
if(this.props.children) {
|
||||||
|
@ -41,7 +42,7 @@ export interface DropdownContainerProperties { }
|
||||||
export interface DropdownContainerState { }
|
export interface DropdownContainerState { }
|
||||||
|
|
||||||
export class DropdownContainer extends ReactComponentBase<DropdownContainerProperties, DropdownContainerState> {
|
export class DropdownContainer extends ReactComponentBase<DropdownContainerProperties, DropdownContainerState> {
|
||||||
protected default_state() {
|
protected defaultState() {
|
||||||
return { };
|
return { };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ html:root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--menu-bar-background);
|
background: var(--menu-bar-background);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
/* tmp fix for ultra small devices */
|
/* tmp fix for ultra small devices */
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
DirectoryBookmark,
|
DirectoryBookmark,
|
||||||
find_bookmark
|
find_bookmark
|
||||||
} from "tc-shared/bookmarks";
|
} 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 * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||||
import {createInputModal} from "tc-shared/ui/elements/Modal";
|
import {createInputModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {default_recorder} from "tc-shared/voice/RecorderProfile";
|
import {default_recorder} from "tc-shared/voice/RecorderProfile";
|
||||||
|
@ -32,7 +32,7 @@ export interface ConnectionState {
|
||||||
|
|
||||||
@ReactEventHandler(obj => obj.props.event_registry)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<InternalControlBarEvents> }, ConnectionState> {
|
class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<InternalControlBarEvents> }, ConnectionState> {
|
||||||
protected default_state(): ConnectionState {
|
protected defaultState(): ConnectionState {
|
||||||
return {
|
return {
|
||||||
connected: false,
|
connected: false,
|
||||||
connectedAnywhere: false
|
connectedAnywhere: false
|
||||||
|
@ -84,7 +84,7 @@ class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_re
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_connect_state")
|
@EventHandler<InternalControlBarEvents>("update_connect_state")
|
||||||
private handleStateUpdate(state: ConnectionState) {
|
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();
|
this.button_ref = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected default_state() {
|
protected defaultState() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Inter
|
||||||
private renderBookmark(bookmark: Bookmark) {
|
private renderBookmark(bookmark: Bookmark) {
|
||||||
return (
|
return (
|
||||||
<DropdownEntry key={bookmark.unique_id}
|
<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}
|
text={bookmark.display_name}
|
||||||
onClick={BookmarkButton.onBookmarkClick.bind(undefined, bookmark.unique_id)}
|
onClick={BookmarkButton.onBookmarkClick.bind(undefined, bookmark.unique_id)}
|
||||||
onContextMenu={this.onBookmarkContextMenu.bind(this, 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;
|
const bookmark = find_bookmark(bookmark_id) as Bookmark;
|
||||||
if(!bookmark) return;
|
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, {
|
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
name: tr("Connect"),
|
name: tr("Connect"),
|
||||||
|
@ -159,7 +159,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Inter
|
||||||
callback: () => boorkmak_connect(bookmark, true),
|
callback: () => boorkmak_connect(bookmark, true),
|
||||||
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
|
visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION)
|
||||||
}, contextmenu.Entry.CLOSE(() => {
|
}, 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)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, AwayState> {
|
class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, AwayState> {
|
||||||
protected default_state(): AwayState {
|
protected defaultState(): AwayState {
|
||||||
return {
|
return {
|
||||||
away: false,
|
away: false,
|
||||||
awayAnywhere: false,
|
awayAnywhere: false,
|
||||||
|
@ -226,7 +226,7 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalC
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_away_state")
|
@EventHandler<InternalControlBarEvents>("update_away_state")
|
||||||
private handleStateUpdate(state: AwayState) {
|
private handleStateUpdate(state: AwayState) {
|
||||||
this.updateState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ export interface ChannelSubscribeState {
|
||||||
|
|
||||||
@ReactEventHandler(obj => obj.props.event_registry)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, ChannelSubscribeState> {
|
class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, ChannelSubscribeState> {
|
||||||
protected default_state(): ChannelSubscribeState {
|
protected defaultState(): ChannelSubscribeState {
|
||||||
return { subscribeEnabled: false };
|
return { subscribeEnabled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,7 +247,7 @@ class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Regist
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_subscribe_state")
|
@EventHandler<InternalControlBarEvents>("update_subscribe_state")
|
||||||
private handleStateUpdate(state: ChannelSubscribeState) {
|
private handleStateUpdate(state: ChannelSubscribeState) {
|
||||||
this.updateState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,7 +258,7 @@ export interface MicrophoneState {
|
||||||
|
|
||||||
@ReactEventHandler(obj => obj.props.event_registry)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, MicrophoneState> {
|
class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, MicrophoneState> {
|
||||||
protected default_state(): MicrophoneState {
|
protected defaultState(): MicrophoneState {
|
||||||
return {
|
return {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
muted: false
|
muted: false
|
||||||
|
@ -278,7 +278,7 @@ class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<Int
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_microphone_state")
|
@EventHandler<InternalControlBarEvents>("update_microphone_state")
|
||||||
private handleStateUpdate(state: MicrophoneState) {
|
private handleStateUpdate(state: MicrophoneState) {
|
||||||
this.updateState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,7 +288,7 @@ export interface SpeakerState {
|
||||||
|
|
||||||
@ReactEventHandler(obj => obj.props.event_registry)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, SpeakerState> {
|
class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, SpeakerState> {
|
||||||
protected default_state(): SpeakerState {
|
protected defaultState(): SpeakerState {
|
||||||
return {
|
return {
|
||||||
muted: false
|
muted: false
|
||||||
};
|
};
|
||||||
|
@ -304,7 +304,7 @@ class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<Intern
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_speaker_state")
|
@EventHandler<InternalControlBarEvents>("update_speaker_state")
|
||||||
private handleStateUpdate(state: SpeakerState) {
|
private handleStateUpdate(state: SpeakerState) {
|
||||||
this.updateState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,7 +314,7 @@ export interface QueryState {
|
||||||
|
|
||||||
@ReactEventHandler(obj => obj.props.event_registry)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class QueryButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, QueryState> {
|
class QueryButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, QueryState> {
|
||||||
protected default_state() {
|
protected defaultState() {
|
||||||
return {
|
return {
|
||||||
queryShown: false
|
queryShown: false
|
||||||
};
|
};
|
||||||
|
@ -340,7 +340,7 @@ class QueryButton extends ReactComponentBase<{ event_registry: Registry<Internal
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_query_state")
|
@EventHandler<InternalControlBarEvents>("update_query_state")
|
||||||
private handleStateUpdate(state: QueryState) {
|
private handleStateUpdate(state: QueryState) {
|
||||||
this.updateState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,7 +352,7 @@ export interface HostButtonState {
|
||||||
|
|
||||||
@ReactEventHandler(obj => obj.props.event_registry)
|
@ReactEventHandler(obj => obj.props.event_registry)
|
||||||
class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, HostButtonState> {
|
class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, HostButtonState> {
|
||||||
protected default_state() {
|
protected defaultState() {
|
||||||
return {
|
return {
|
||||||
url: undefined,
|
url: undefined,
|
||||||
target_url: undefined
|
target_url: undefined
|
||||||
|
@ -382,7 +382,7 @@ class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalC
|
||||||
|
|
||||||
@EventHandler<InternalControlBarEvents>("update_host_button")
|
@EventHandler<InternalControlBarEvents>("update_host_button")
|
||||||
private handleStateUpdate(state: HostButtonState) {
|
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();
|
const events = target.events();
|
||||||
events.off("notify_state_updated", this.connection_handler_callbacks.notify_state_updated);
|
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);
|
events.off("notify_connection_state_changed", this.connection_handler_callbacks.notify_connection_state_changed);
|
||||||
|
//FIXME: Add the host button here!
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerConnectionHandlerEvents(target: ConnectionHandler) {
|
private registerConnectionHandlerEvents(target: ConnectionHandler) {
|
||||||
|
@ -455,7 +456,6 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
console.error(server_connections.events());
|
|
||||||
server_connections.events().on("notify_active_handler_changed", this.connection_manager_callbacks.active_handler_changed);
|
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() });
|
this.event_registry.fire("set_connection_handler", { handler: server_connections.active_connection() });
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,8 +140,12 @@ export class Conversation {
|
||||||
this._first_unread_message = undefined;
|
this._first_unread_message = undefined;
|
||||||
|
|
||||||
const ctree = this.handle.handle.handle.channelTree;
|
const ctree = this.handle.handle.handle.channelTree;
|
||||||
if(ctree && ctree.tag_tree())
|
if(ctree && ctree.tag_tree()) {
|
||||||
ctree.tag_tree().find(".marker-text-unread[conversation='" + this.channel_id + "']").addClass("hidden");
|
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();
|
this._first_unread_message_pointer.html_element.detach();
|
||||||
}
|
}
|
||||||
|
@ -276,6 +280,9 @@ export class Conversation {
|
||||||
return; /* we already have that message */
|
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) {
|
if(!spliced && this._last_messages.length < this._view_max_messages) {
|
||||||
this._last_messages.push(message);
|
this._last_messages.push(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
|
import {Frame, FrameContent} from "tc-shared/ui/frames/chat_frame";
|
||||||
import * as events from "tc-shared/events";
|
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 {voice} from "tc-shared/connection/ConnectionBase";
|
||||||
import PlayerState = voice.PlayerState;
|
import PlayerState = voice.PlayerState;
|
||||||
import {LogCategory} from "tc-shared/log";
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
@ -328,9 +328,9 @@ export class MusicInfo {
|
||||||
});
|
});
|
||||||
|
|
||||||
/* bot property listener */
|
/* bot property listener */
|
||||||
const callback_property = event => this.events.fire("bot_property_update", { properties: event.properties });
|
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 => this.events.fire("player_time_update", event);
|
const callback_time_update = (event: ClientEvents["music_status_update"]) => this.events.fire("player_time_update", event, true);
|
||||||
const callback_song_change = event => this.events.fire("player_song_change", event);
|
const callback_song_change = (event: ClientEvents["music_song_change"]) => this.events.fire("player_song_change", event, true);
|
||||||
this.events.on("bot_change", event => {
|
this.events.on("bot_change", event => {
|
||||||
if(event.old) {
|
if(event.old) {
|
||||||
event.old.events.off(callback_property);
|
event.old.events.off(callback_property);
|
||||||
|
@ -339,7 +339,7 @@ export class MusicInfo {
|
||||||
event.old.events.disconnect_all(this.events);
|
event.old.events.disconnect_all(this.events);
|
||||||
}
|
}
|
||||||
if(event.new) {
|
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_status_update", callback_time_update);
|
||||||
event.new.events.on("music_song_change", callback_song_change);
|
event.new.events.on("music_song_change", callback_song_change);
|
||||||
|
@ -736,7 +736,7 @@ export class MusicInfo {
|
||||||
this._current_bot.updateClientVariables(true).catch(error => {
|
this._current_bot.updateClientVariables(true).catch(error => {
|
||||||
log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
||||||
}).then(() => {
|
}).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 */
|
this.playlist_subscribe(false); /* we're allowed to see the playlist */
|
||||||
if(!songs) {
|
if(!songs) {
|
||||||
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
||||||
|
|
|
@ -543,7 +543,7 @@ export class PrivateConveration {
|
||||||
} else {
|
} else {
|
||||||
const ctree = this.handle.handle.handle.channelTree;
|
const ctree = this.handle.handle.handle.channelTree;
|
||||||
if(ctree && ctree.tag_tree() && this.client_id)
|
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) {
|
if(this._spacer_unread_message) {
|
||||||
this._destroy_view_entry(this._spacer_unread_message.tag_unread);
|
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 {
|
import {
|
||||||
Bookmark,
|
Bookmark,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
BookmarkType, boorkmak_connect, create_bookmark, create_bookmark_directory,
|
BookmarkType,
|
||||||
|
boorkmak_connect,
|
||||||
|
create_bookmark,
|
||||||
|
create_bookmark_directory,
|
||||||
delete_bookmark,
|
delete_bookmark,
|
||||||
DirectoryBookmark,
|
DirectoryBookmark,
|
||||||
save_bookmark
|
save_bookmark
|
||||||
} from "tc-shared/bookmarks";
|
} from "tc-shared/bookmarks";
|
||||||
import {connection_log, Regex} from "tc-shared/ui/modal/ModalConnect";
|
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 {profiles} from "tc-shared/profiles/ConnectionProfile";
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {Settings, settings} from "tc-shared/settings";
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
import * as log 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 * as i18nc from "tc-shared/i18n/country";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import * as top_menu from "../frames/MenuBar";
|
import * as top_menu from "../frames/MenuBar";
|
||||||
|
@ -107,8 +110,11 @@ export function spawnBookmarkModal() {
|
||||||
input_server_address.val(address);
|
input_server_address.val(address);
|
||||||
|
|
||||||
let profile = input_connect_profile.find("option[value='" + entry.connect_profile + "']");
|
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 = input_connect_profile.find("option[value=default]");
|
||||||
|
}
|
||||||
profile.prop("selected", true);
|
profile.prop("selected", true);
|
||||||
|
|
||||||
input_server_password.val(entry.server_properties.server_password_hash || entry.server_properties.server_password ? "WolverinDEV" : "");
|
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;
|
const bookmark = entry as Bookmark;
|
||||||
container.append(
|
container.append(
|
||||||
bookmark.last_icon_id ?
|
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")
|
$.spawn("div").addClass("icon-container icon_em")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -287,6 +293,7 @@ export function spawnBookmarkModal() {
|
||||||
if(event.type === "change" && valid) {
|
if(event.type === "change" && valid) {
|
||||||
selected_bookmark.display_name = name;
|
selected_bookmark.display_name = name;
|
||||||
label_bookmark_name.text(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_address = address;
|
||||||
entry.server_properties.server_port = 9987;
|
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)));
|
label_server_address.text(entry.server_properties.server_address + (entry.server_properties.server_port == 9987 ? "" : (" " + entry.server_properties.server_port)));
|
||||||
update_connect_info();
|
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 => {
|
input_connect_profile.on('change', event => {
|
||||||
const id = input_connect_profile.val() as string;
|
const id = input_connect_profile.val() as string;
|
||||||
const profile = profiles().find(e => e.id === id);
|
const profile = profiles().find(e => e.id === id);
|
||||||
if(profile) {
|
if(profile) {
|
||||||
(selected_bookmark as Bookmark).connect_profile = id;
|
(selected_bookmark as Bookmark).connect_profile = id;
|
||||||
|
save_bookmark(selected_bookmark);
|
||||||
} else {
|
} else {
|
||||||
log.warn(LogCategory.GENERAL, tr("Failed to change connect profile for profile %s to %s"), selected_bookmark.unique_id, id);
|
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 {createModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {ConnectionProfile, default_profile, find_profile, profiles} from "tc-shared/profiles/ConnectionProfile";
|
import {ConnectionProfile, default_profile, find_profile, profiles} from "tc-shared/profiles/ConnectionProfile";
|
||||||
import {KeyCode} from "tc-shared/PPTListener";
|
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 * as i18nc from "tc-shared/i18n/country";
|
||||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
|
@ -15,7 +15,10 @@ export namespace connection_log {
|
||||||
//TODO: Save password data
|
//TODO: Save password data
|
||||||
export type ConnectionData = {
|
export type ConnectionData = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
icon_id: number;
|
icon_id: number;
|
||||||
|
server_unique_id: string;
|
||||||
|
|
||||||
country: string;
|
country: string;
|
||||||
clients_online: number;
|
clients_online: number;
|
||||||
clients_total: number;
|
clients_total: number;
|
||||||
|
@ -45,6 +48,7 @@ export namespace connection_log {
|
||||||
country: 'unknown',
|
country: 'unknown',
|
||||||
name: 'Unknown',
|
name: 'Unknown',
|
||||||
icon_id: 0,
|
icon_id: 0,
|
||||||
|
server_unique_id: "unknown",
|
||||||
total_connection: 0,
|
total_connection: 0,
|
||||||
|
|
||||||
flag_password: false,
|
flag_password: false,
|
||||||
|
@ -289,7 +293,7 @@ export function spawnConnectModal(options: {
|
||||||
})
|
})
|
||||||
).append(
|
).append(
|
||||||
$.spawn("div").addClass("column name").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)
|
$.spawn("a").text(entry.name)
|
||||||
])
|
])
|
||||||
).append(
|
).append(
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||||
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||||
import PermissionType from "tc-shared/permission/PermissionType";
|
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 {LogCategory} from "tc-shared/log";
|
||||||
import * as log from "tc-shared/log";
|
import * as log from "tc-shared/log";
|
||||||
import {
|
import {
|
||||||
|
@ -712,7 +712,7 @@ export class HTMLPermissionEditor extends AbstractPermissionEditor {
|
||||||
|
|
||||||
let resolve: Promise<JQuery<HTMLDivElement>>;
|
let resolve: Promise<JQuery<HTMLDivElement>>;
|
||||||
if(icon_id >= 0 && icon_id <= 1000)
|
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
|
else
|
||||||
resolve = this.icon_resolver(permission ? permission.get_value() : 0).then(e => $(e));
|
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 permission_editor: AbstractPermissionEditor = (() => {
|
||||||
const editor = new HTMLPermissionEditor();
|
const editor = new HTMLPermissionEditor();
|
||||||
editor.initialize(connection.permissions.groupedPermissions());
|
editor.initialize(connection.permissions.groupedPermissions());
|
||||||
editor.icon_resolver = id => connection.fileManager.icons.resolve_icon(id).then(async icon => {
|
editor.icon_resolver = async id => {
|
||||||
if(!icon)
|
const icon = connection.fileManager.icons.load_icon(id);
|
||||||
return undefined;
|
await icon.await_loading();
|
||||||
|
|
||||||
const tag = document.createElement("img");
|
const tag = document.createElement("img");
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
tag.onerror = reject;
|
tag.onerror = reject;
|
||||||
tag.onload = resolve;
|
tag.onload = resolve;
|
||||||
tag.src = icon.url;
|
tag.src = icon.loaded_url || "nope";
|
||||||
});
|
});
|
||||||
return tag;
|
return tag;
|
||||||
});
|
};
|
||||||
editor.icon_selector = current_icon => new Promise<number>(resolve => {
|
editor.icon_selector = current_icon => new Promise<number>(resolve => {
|
||||||
spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon);
|
spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon);
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,7 +61,7 @@ interface KeyActionEntryProperties {
|
||||||
|
|
||||||
@ReactEventHandler(e => e.props.eventRegistry)
|
@ReactEventHandler(e => e.props.eventRegistry)
|
||||||
class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyActionEntryState> {
|
class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyActionEntryState> {
|
||||||
protected default_state() : KeyActionEntryState {
|
protected defaultState() : KeyActionEntryState {
|
||||||
return {
|
return {
|
||||||
assignedKey: undefined,
|
assignedKey: undefined,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
@ -133,7 +133,7 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
||||||
|
|
||||||
@EventHandler<KeyMapEvents>("set_selected_action")
|
@EventHandler<KeyMapEvents>("set_selected_action")
|
||||||
private handleSelectedChange(event: KeyMapEvents["set_selected_action"]) {
|
private handleSelectedChange(event: KeyMapEvents["set_selected_action"]) {
|
||||||
this.updateState({
|
this.setState({
|
||||||
selected: this.props.action === event.action
|
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.action !== this.props.action) return;
|
||||||
if(event.query_type !== "general") return;
|
if(event.query_type !== "general") return;
|
||||||
|
|
||||||
this.updateState({
|
this.setState({
|
||||||
state: "loading"
|
state: "loading"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -153,12 +153,12 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
||||||
if(event.action !== this.props.action) return;
|
if(event.action !== this.props.action) return;
|
||||||
|
|
||||||
if(event.status === "success") {
|
if(event.status === "success") {
|
||||||
this.updateState({
|
this.setState({
|
||||||
state: "loaded",
|
state: "loaded",
|
||||||
assignedKey: event.key
|
assignedKey: event.key
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.updateState({
|
this.setState({
|
||||||
state: "error",
|
state: "error",
|
||||||
error: event.status === "timeout" ? tr("query timeout") : event.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"]) {
|
private handleSetKeymap(event: KeyMapEvents["set_keymap"]) {
|
||||||
if(event.action !== this.props.action) return;
|
if(event.action !== this.props.action) return;
|
||||||
|
|
||||||
this.updateState({ state: "applying" });
|
this.setState({ state: "applying" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler<KeyMapEvents>("set_keymap_result")
|
@EventHandler<KeyMapEvents>("set_keymap_result")
|
||||||
|
@ -177,12 +177,12 @@ class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyAct
|
||||||
if(event.action !== this.props.action) return;
|
if(event.action !== this.props.action) return;
|
||||||
|
|
||||||
if(event.status === "success") {
|
if(event.status === "success") {
|
||||||
this.updateState({
|
this.setState({
|
||||||
state: "loaded",
|
state: "loaded",
|
||||||
assignedKey: event.key
|
assignedKey: event.key
|
||||||
});
|
});
|
||||||
} else {
|
} 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));
|
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 }> {
|
class KeyActionGroup extends ReactComponentBase<KeyActionGroupProperties, { collapsed: boolean }> {
|
||||||
protected default_state(): { collapsed: boolean } {
|
protected defaultState(): { collapsed: boolean } {
|
||||||
return { collapsed: false }
|
return { collapsed: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ class KeyActionGroup extends ReactComponentBase<KeyActionGroupProperties, { coll
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleCollapsed() {
|
private toggleCollapsed() {
|
||||||
this.updateState({
|
this.setState({
|
||||||
collapsed: !this.state.collapsed
|
collapsed: !this.state.collapsed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,7 @@ interface KeyActionListProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyActionList extends ReactComponentBase<KeyActionListProperties, {}> {
|
class KeyActionList extends ReactComponentBase<KeyActionListProperties, {}> {
|
||||||
protected default_state(): {} {
|
protected defaultState(): {} {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ interface ButtonBarState {
|
||||||
|
|
||||||
@ReactEventHandler(e => e.props.event_registry)
|
@ReactEventHandler(e => e.props.event_registry)
|
||||||
class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEvents> }, ButtonBarState> {
|
class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEvents> }, ButtonBarState> {
|
||||||
protected default_state(): ButtonBarState {
|
protected defaultState(): ButtonBarState {
|
||||||
return {
|
return {
|
||||||
active_action: undefined,
|
active_action: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -271,7 +271,7 @@ class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEven
|
||||||
|
|
||||||
@EventHandler<KeyMapEvents>("set_selected_action")
|
@EventHandler<KeyMapEvents>("set_selected_action")
|
||||||
private handleSetSelectedAction(event: KeyMapEvents["set_selected_action"]) {
|
private handleSetSelectedAction(event: KeyMapEvents["set_selected_action"]) {
|
||||||
this.updateState({
|
this.setState({
|
||||||
active_action: event.action,
|
active_action: event.action,
|
||||||
loading: true
|
loading: true
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -281,7 +281,7 @@ class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEven
|
||||||
|
|
||||||
@EventHandler<KeyMapEvents>("query_keymap_result")
|
@EventHandler<KeyMapEvents>("query_keymap_result")
|
||||||
private handleQueryKeymapResult(event: KeyMapEvents["query_keymap_result"]) {
|
private handleQueryKeymapResult(event: KeyMapEvents["query_keymap_result"]) {
|
||||||
this.updateState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
has_key: event.status === "success" && !!event.key
|
has_key: event.status === "success" && !!event.key
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface ButtonState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||||
protected default_state(): ButtonState {
|
protected defaultState(): ButtonState {
|
||||||
return {
|
return {
|
||||||
disabled: undefined
|
disabled: undefined
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,35 +1,63 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {LocalIcon} from "tc-shared/FileManager";
|
||||||
|
|
||||||
export interface IconProperties {
|
export interface IconProperties {
|
||||||
icon: string | JQuery<HTMLDivElement>;
|
icon: string | LocalIcon;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IconRenderer extends React.Component<IconProperties, {}> {
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
if(typeof this.props.icon === "object")
|
this.callback_state_update = () => {
|
||||||
this.icon_ref = React.createRef();
|
const icon = this.props.icon;
|
||||||
|
if(icon.status !== "destroyed")
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if(!this.props.icon)
|
const icon = this.props.icon;
|
||||||
return <div className={"icon-container icon-empty"} />;
|
if(!icon || icon.status === "empty" || icon.status === "destroyed")
|
||||||
else if(typeof this.props.icon === "string")
|
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||||
return <div className={"icon " + this.props.icon} />;
|
else if(icon.status === "loaded") {
|
||||||
|
if(icon.icon_id >= 0 && icon.icon_id <= 1000) {
|
||||||
|
if(icon.icon_id === 0)
|
||||||
return <div ref={this.icon_ref} />;
|
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 {
|
componentDidMount(): void {
|
||||||
if(this.icon_ref)
|
this.props.icon?.status_change_callbacks.push(this.callback_state_update);
|
||||||
$(this.icon_ref.current).replaceWith(this.props.icon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
if(this.icon_ref)
|
this.props.icon?.status_change_callbacks.remove(this.callback_state_update);
|
||||||
$(this.icon_ref.current).empty();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,22 +1,101 @@
|
||||||
import * as React from "react";
|
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> {
|
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) {
|
constructor(props: Properties) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.batch_component_id = -1;
|
||||||
|
this.batch_component_force_id = -1;
|
||||||
|
|
||||||
this.state = this.default_state();
|
this.state = this.defaultState();
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected initialize() { }
|
protected initialize() { }
|
||||||
protected abstract default_state() : State;
|
protected defaultState() : State { return {} as State; }
|
||||||
|
|
||||||
updateState(updates: {[key in keyof State]?: State[key]}, callback?: () => void) {
|
setState<K extends keyof State>(
|
||||||
if(Object.keys(updates).findIndex(e => updates[e] !== this.state[e]) === -1) {
|
state: ((prevState: Readonly<State>, props: Readonly<Properties>) => (Pick<State, K> | State | null)) | (Pick<State, K> | State | null),
|
||||||
if(callback) setTimeout(callback, 0);
|
callback?: () => void
|
||||||
return; /* no state has been changed */
|
): 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)[]) {
|
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;
|
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 {connection_log} from "tc-shared/ui/modal/ModalConnect";
|
||||||
import * as top_menu from "./frames/MenuBar";
|
import * as top_menu from "./frames/MenuBar";
|
||||||
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
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 {
|
export class ServerProperties {
|
||||||
virtualserver_host: string = "";
|
virtualserver_host: string = "";
|
||||||
|
@ -122,11 +126,21 @@ export interface ServerAddress {
|
||||||
port: number;
|
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;
|
remote_address: ServerAddress;
|
||||||
channelTree: ChannelTree;
|
channelTree: ChannelTree;
|
||||||
properties: ServerProperties;
|
properties: ServerProperties;
|
||||||
|
|
||||||
|
readonly events: Registry<ServerEvents>;
|
||||||
|
readonly view: React.Ref<ServerEntryView>;
|
||||||
|
|
||||||
private info_request_promise: Promise<void> = undefined;
|
private info_request_promise: Promise<void> = undefined;
|
||||||
private info_request_promise_resolve: any = undefined;
|
private info_request_promise_resolve: any = undefined;
|
||||||
private info_request_promise_reject: any = undefined;
|
private info_request_promise_reject: any = undefined;
|
||||||
|
@ -138,56 +152,22 @@ export class ServerEntry {
|
||||||
|
|
||||||
lastInfoRequest: number = 0;
|
lastInfoRequest: number = 0;
|
||||||
nextInfoRequest: number = 0;
|
nextInfoRequest: number = 0;
|
||||||
private _htmlTag: JQuery<HTMLElement>;
|
|
||||||
private _destroyed = false;
|
private _destroyed = false;
|
||||||
|
|
||||||
constructor(tree, name, address: ServerAddress) {
|
constructor(tree, name, address: ServerAddress) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = new Registry<ServerEvents>();
|
||||||
|
this.view = React.createRef();
|
||||||
|
|
||||||
this.properties = new ServerProperties();
|
this.properties = new ServerProperties();
|
||||||
this.channelTree = tree;
|
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;
|
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() {
|
destroy() {
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
if(this._htmlTag) {
|
|
||||||
this._htmlTag.remove();
|
|
||||||
this._htmlTag = undefined;
|
|
||||||
}
|
|
||||||
this.info_request_promise = undefined;
|
this.info_request_promise = undefined;
|
||||||
this.info_request_promise_resolve = undefined;
|
this.info_request_promise_resolve = undefined;
|
||||||
this.info_request_promise_reject = undefined;
|
this.info_request_promise_reject = undefined;
|
||||||
|
@ -196,33 +176,22 @@ export class ServerEntry {
|
||||||
this.remote_address = undefined;
|
this.remote_address = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeListener(){
|
protected onSelect(singleSelect: boolean) {
|
||||||
this._htmlTag.on('click' ,() => {
|
super.onSelect(singleSelect);
|
||||||
this.channelTree.onSelect(this);
|
if(!singleSelect) return;
|
||||||
this.updateProperties(); /* just prepare to show some server info */
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||||
this.htmlTag.on("contextmenu", (event) => {
|
this.channelTree.client.side_bar.channel_conversations().set_current_channel(0);
|
||||||
event.preventDefault();
|
this.channelTree.client.side_bar.show_channel_conversations();
|
||||||
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); });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
|
contextMenuItems() : contextmenu.MenuEntry[] {
|
||||||
let trigger_close = true;
|
return [
|
||||||
contextmenu.spawn_context_menu(x, y, {
|
{
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
name: tr("Show server info"),
|
name: tr("Show server info"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
trigger_close = false;
|
|
||||||
openServerInfo(this);
|
openServerInfo(this);
|
||||||
},
|
},
|
||||||
icon_class: "client-about"
|
icon_class: "client-about"
|
||||||
|
@ -277,7 +246,28 @@ export class ServerEntry {
|
||||||
visible: false, //TODO: Enable again as soon the new design is finished
|
visible: false, //TODO: Enable again as soon the new design is finished
|
||||||
callback: () => spawnAvatarList(this.channelTree.client)
|
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);
|
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) {
|
for(let variable of variables) {
|
||||||
JSON.map_field_to(this.properties, variable.value, variable.key);
|
JSON.map_field_to(this.properties, variable.value, variable.key);
|
||||||
|
|
||||||
if(variable.key == "virtualserver_name") {
|
if(variable.key == "virtualserver_name") {
|
||||||
this.htmlTag.find(".name").text(variable.value);
|
|
||||||
this.channelTree.client.tag_connection_handler.find(".server-name").text(variable.value);
|
this.channelTree.client.tag_connection_handler.find(".server-name").text(variable.value);
|
||||||
server_connections.update_ui();
|
server_connections.update_ui();
|
||||||
} else if(variable.key == "virtualserver_icon_id") {
|
} else if(variable.key == "virtualserver_icon_id") {
|
||||||
|
@ -308,30 +297,38 @@ export class ServerEntry {
|
||||||
* ATTENTION: This is required!
|
* ATTENTION: This is required!
|
||||||
*/
|
*/
|
||||||
this.properties.virtualserver_icon_id = variable.value as any >>> 0;
|
this.properties.virtualserver_icon_id = variable.value as any >>> 0;
|
||||||
|
update_bookmarks = true;
|
||||||
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"));
|
|
||||||
} else if(variable.key.indexOf('hostbanner') != -1) {
|
} else if(variable.key.indexOf('hostbanner') != -1) {
|
||||||
update_bannner = true;
|
update_bannner = true;
|
||||||
} else if(variable.key.indexOf('hostbutton') != -1) {
|
} else if(variable.key.indexOf('hostbutton') != -1) {
|
||||||
update_button = true;
|
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)
|
if(update_bannner)
|
||||||
this.channelTree.client.hostbanner.update();
|
this.channelTree.client.hostbanner.update();
|
||||||
|
|
||||||
if(update_button)
|
if(update_button)
|
||||||
control_bar_instance()?.events().fire("server_updated", { handler: this.channelTree.client, category: "hostbanner" });
|
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,
|
flag_password: this.properties.virtualserver_flag_password,
|
||||||
name: this.properties.virtualserver_name,
|
name: this.properties.virtualserver_name,
|
||||||
icon_id: this.properties.virtualserver_icon_id,
|
icon_id: this.properties.virtualserver_icon_id,
|
||||||
|
server_unique_id: this.properties.virtualserver_unique_identifier,
|
||||||
|
|
||||||
password_hash: undefined /* we've here no clue */
|
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;
|
return this.properties.virtualserver_uptime + (new Date().getTime() - this.lastInfoRequest) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
set flag_text_unread(flag: boolean) {
|
reset() {
|
||||||
this._htmlTag.find(".marker-text-unread").toggleClass("hidden", !flag);
|
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;
|
if(has_export) return nodes;
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyntaxKind.PlusEqualsToken
|
|
@ -16,6 +16,7 @@
|
||||||
"webpack/ManifestPlugin.ts",
|
"webpack/ManifestPlugin.ts",
|
||||||
"webpack/EJSGenerator.ts",
|
"webpack/EJSGenerator.ts",
|
||||||
"webpack/WatLoader.ts",
|
"webpack/WatLoader.ts",
|
||||||
|
"webpack/DevelBlocks.ts",
|
||||||
|
|
||||||
"file.ts"
|
"file.ts"
|
||||||
],
|
],
|
||||||
|
|
|
@ -54,19 +54,22 @@ export class CodecWrapperWorker extends BasicCodec {
|
||||||
|
|
||||||
async initialise() : Promise<Boolean> {
|
async initialise() : Promise<Boolean> {
|
||||||
if(this._initialized) return;
|
if(this._initialized) return;
|
||||||
|
if(this._initialize_promise)
|
||||||
|
return await this._initialize_promise;
|
||||||
|
|
||||||
this._initialize_promise = this.spawn_worker().then(() => this.execute("initialise", {
|
this._initialize_promise = this.spawn_worker().then(() => this.execute("initialise", {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
channelCount: this.channelCount,
|
channelCount: this.channelCount,
|
||||||
})).then(result => {
|
})).then(result => {
|
||||||
if(result.success)
|
if(result.success) {
|
||||||
|
this._initialized = true;
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
log.error(LogCategory.VOICE, tr("Failed to initialize codec %s: %s"), CodecType[this.type], result.error);
|
log.error(LogCategory.VOICE, tr("Failed to initialize codec %s: %s"), CodecType[this.type], result.error);
|
||||||
return Promise.reject(result.error);
|
return Promise.reject(result.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._initialized = true;
|
|
||||||
await this._initialize_promise;
|
await this._initialize_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +84,8 @@ export class CodecWrapperWorker extends BasicCodec {
|
||||||
}
|
}
|
||||||
|
|
||||||
async decode(data: Uint8Array): Promise<AudioBuffer> {
|
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 });
|
const result = await this.execute("decodeSamples", { data: data, length: data.length });
|
||||||
if(result.timings.downstream > 5 || result.timings.upstream > 5 || result.timings.handle > 5)
|
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);
|
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> {
|
async encode(data: AudioBuffer) : Promise<Uint8Array> {
|
||||||
|
if(!this.initialized()) throw "codec not initialized/initialize failed";
|
||||||
|
|
||||||
let buffer = new Float32Array(this.channelCount * data.length);
|
let buffer = new Float32Array(this.channelCount * data.length);
|
||||||
for (let offset = 0; offset < data.length; offset++) {
|
for (let offset = 0; offset < data.length; offset++) {
|
||||||
for (let channel = 0; channel < this.channelCount; channel++)
|
for (let channel = 0; channel < this.channelCount; channel++)
|
||||||
|
|
|
@ -345,9 +345,11 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(json["type"] === "command") {
|
if(json["type"] === "command") {
|
||||||
|
/* devel-block(log-networking-commands) */
|
||||||
let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]);
|
let group = log.group(log.LogType.DEBUG, LogCategory.NETWORKING, tr("Handling command '%s'"), json["command"]);
|
||||||
group.log(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();
|
group.group(log.LogType.TRACE, tr("Json:")).collapsed(true).log("%o", json).end();
|
||||||
|
/* devel-block-end */
|
||||||
|
|
||||||
this._command_boss.invoke_handle({
|
this._command_boss.invoke_handle({
|
||||||
command: json["command"],
|
command: json["command"],
|
||||||
|
@ -361,7 +363,9 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
if(this._voice_connection)
|
if(this._voice_connection)
|
||||||
this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */
|
this._voice_connection.start_rtc_session(); /* FIXME: Move it to a handler boss and not here! */
|
||||||
}
|
}
|
||||||
|
/* devel-block(log-networking-commands) */
|
||||||
group.end();
|
group.end();
|
||||||
|
/* devel-block-end */
|
||||||
} else if(json["type"] === "WebRTC") {
|
} else if(json["type"] === "WebRTC") {
|
||||||
if(this._voice_connection)
|
if(this._voice_connection)
|
||||||
this._voice_connection.handleControlPacket(json);
|
this._voice_connection.handleControlPacket(json);
|
||||||
|
|
|
@ -77,10 +77,7 @@ export namespace codec {
|
||||||
this.entries[index].instance.initialise().then((flag) => {
|
this.entries[index].instance.initialise().then((flag) => {
|
||||||
//TODO test success flag
|
//TODO test success flag
|
||||||
this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject);
|
this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject);
|
||||||
}).catch(error => {
|
}).catch(reject);
|
||||||
log.error(LogCategory.VOICE, tr("Could not initialize codec!\nError: %o"), error);
|
|
||||||
reject(typeof(error) === 'string' ? error : tr("Could not initialize codec!"));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else if(this.entries[index].owner == 0) {
|
} else if(this.entries[index].owner == 0) {
|
||||||
|
@ -388,8 +385,9 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
let config: RTCConfiguration = {};
|
let config: RTCConfiguration = {};
|
||||||
config.iceServers = [];
|
config.iceServers = [];
|
||||||
config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
|
config.iceServers.push({ urls: 'stun:stun.l.google.com:19302' });
|
||||||
|
//config.iceServers.push({ urls: "stun:stun.teaspeak.de:3478" });
|
||||||
this.rtcPeerConnection = new RTCPeerConnection(config);
|
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 = this.rtcPeerConnection.createDataChannel('main', dataChannelConfig);
|
||||||
this.dataChannel.onmessage = this.on_data_channel_message.bind(this);
|
this.dataChannel.onmessage = this.on_data_channel_message.bind(this);
|
||||||
|
@ -401,6 +399,8 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
sdpConstraints.offerToReceiveVideo = false;
|
sdpConstraints.offerToReceiveVideo = false;
|
||||||
sdpConstraints.voiceActivityDetection = true;
|
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);
|
this.rtcPeerConnection.onicecandidate = this.on_local_ice_candidate.bind(this);
|
||||||
if(this.local_audio_stream) { //May a typecheck?
|
if(this.local_audio_stream) { //May a typecheck?
|
||||||
this.rtcPeerConnection.addStream(this.local_audio_stream.stream);
|
this.rtcPeerConnection.addStream(this.local_audio_stream.stream);
|
||||||
|
@ -431,8 +431,26 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
this.connection.client.update_voice_status(undefined);
|
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_use_cache: boolean = true;
|
||||||
private _ice_cache: any[] = [];
|
private _ice_cache: RTCIceCandidate[] = [];
|
||||||
handleControlPacket(json) {
|
handleControlPacket(json) {
|
||||||
if(json["request"] === "answer") {
|
if(json["request"] === "answer") {
|
||||||
const session_description = new RTCSessionDescription(json["msg"]);
|
const session_description = new RTCSessionDescription(json["msg"]);
|
||||||
|
@ -440,24 +458,20 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
this.rtcPeerConnection.setRemoteDescription(session_description).then(() => {
|
this.rtcPeerConnection.setRemoteDescription(session_description).then(() => {
|
||||||
log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length);
|
log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length);
|
||||||
this._ice_use_cache = false;
|
this._ice_use_cache = false;
|
||||||
for(let msg of this._ice_cache) {
|
|
||||||
this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg)).catch(error => {
|
for(let candidate of this._ice_cache)
|
||||||
log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %s: %o"), msg, error);
|
this.registerRemoteICECandidate(candidate);
|
||||||
});
|
|
||||||
}
|
|
||||||
this._ice_cache = [];
|
this._ice_cache = [];
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
log.info(LogCategory.VOICE, tr("Failed to apply remote description: %o"), error); //FIXME error handling!
|
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) {
|
if(!this._ice_use_cache) {
|
||||||
log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), json["msg"]);
|
this.registerRemoteICECandidate(candidate);
|
||||||
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);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
log.info(LogCategory.VOICE, tr("Cache remote ice! (%o)"), json["msg"]);
|
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") {
|
} else if(json["request"] == "status") {
|
||||||
if(json["state"] == "failed") {
|
if(json["state"] == "failed") {
|
||||||
|
@ -472,22 +486,25 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
}
|
}
|
||||||
//TODO handle fail specially when its not allowed to reconnect
|
//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) {
|
private on_local_ice_candidate(event: RTCPeerConnectionIceEvent) {
|
||||||
if (event) {
|
if (event) {
|
||||||
//if(event.candidate && event.candidate.protocol !== "udp")
|
if(event.candidate && event.candidate.protocol !== "tcp")
|
||||||
// return;
|
return;
|
||||||
|
|
||||||
log.info(LogCategory.VOICE, tr("Gathered local ice candidate %o."), event.candidate);
|
|
||||||
if(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({
|
this.connection.sendData(JSON.stringify({
|
||||||
type: 'WebRTC',
|
type: 'WebRTC',
|
||||||
request: "ice",
|
request: "ice",
|
||||||
msg: event.candidate,
|
msg: event.candidate,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
log.info(LogCategory.VOICE, tr("Local ICE candidate gathering finish."));
|
||||||
this.connection.sendData(JSON.stringify({
|
this.connection.sendData(JSON.stringify({
|
||||||
type: 'WebRTC',
|
type: 'WebRTC',
|
||||||
request: "ice_finish"
|
request: "ice_finish"
|
||||||
|
|
|
@ -64,6 +64,9 @@ async function handle_message(command: string, data: any) : Promise<string | obj
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
case "encodeSamples":
|
case "encodeSamples":
|
||||||
|
if(!codec_instance)
|
||||||
|
return "codec not initialized/initialize failed";
|
||||||
|
|
||||||
let encodeArray = new Float32Array(data.length);
|
let encodeArray = new Float32Array(data.length);
|
||||||
for(let index = 0; index < encodeArray.length; index++)
|
for(let index = 0; index < encodeArray.length; index++)
|
||||||
encodeArray[index] = data.data[index];
|
encodeArray[index] = data.data[index];
|
||||||
|
@ -74,6 +77,9 @@ async function handle_message(command: string, data: any) : Promise<string | obj
|
||||||
else
|
else
|
||||||
return { data: encodeResult, length: encodeResult.length };
|
return { data: encodeResult, length: encodeResult.length };
|
||||||
case "decodeSamples":
|
case "decodeSamples":
|
||||||
|
if(!codec_instance)
|
||||||
|
return "codec not initialized/initialize failed";
|
||||||
|
|
||||||
let decodeArray = new Uint8Array(data.length);
|
let decodeArray = new Uint8Array(data.length);
|
||||||
for(let index = 0; index < decodeArray.length; index++)
|
for(let index = 0; index < decodeArray.length; index++)
|
||||||
decodeArray[index] = data.data[index];
|
decodeArray[index] = data.data[index];
|
||||||
|
|
|
@ -111,7 +111,10 @@ export const config = async (target: "web" | "client") => { return {
|
||||||
{
|
{
|
||||||
loader: 'css-loader',
|
loader: 'css-loader',
|
||||||
options: {
|
options: {
|
||||||
modules: true,
|
modules: {
|
||||||
|
mode: "local",
|
||||||
|
localIdentName: isDevelopment ? "[path][name]__[local]--[hash:base64:5]" : "[hash:base64]",
|
||||||
|
},
|
||||||
sourceMap: isDevelopment
|
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 webpack from "webpack";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs-extra";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
|
@ -79,6 +79,8 @@ class ManifestGenerator {
|
||||||
});
|
});
|
||||||
|
|
||||||
compiler.hooks.done.tap(this.constructor.name, () => {
|
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));
|
fs.writeFileSync(this.options.file || "manifest.json", JSON.stringify(this.manifest_content));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue