Develop (#35)
* added support for windows * Added country flags * Added double touch * Added country names * Added a lot of new things * A lot of changes * Removed UA-Parser * Fixed some small errors * Updated changelogcanary
parent
9818385a29
commit
4d983dc36a
21
ChangeLog.md
21
ChangeLog.md
|
@ -1,4 +1,25 @@
|
|||
# Changelog:
|
||||
* **17.03.19**
|
||||
- Using VAD by default instead of PPT
|
||||
- Improved mobile experience:
|
||||
- Double touch join channel
|
||||
- Removed the info bar for devices smaller than 500px
|
||||
- Added country flags and names
|
||||
- Added favicon, which change when you're recording
|
||||
- Fixed double cache loading
|
||||
- Fixed modal sizing scroll bug
|
||||
- Added a channel subscribe all button
|
||||
- Added individual channel subscribe settings
|
||||
- Improved chat switch performance
|
||||
- Added a chat message URL finder
|
||||
- Escape URL detection with `!<url>`
|
||||
- Improved chat experience
|
||||
- Displaying offline chats as offline
|
||||
- Notify when user closes the chat
|
||||
- Notify when user disconnect/reconnects
|
||||
- Preloading hostbanners to prevent flickering
|
||||
- Fixed empty channel and server kick messages
|
||||
|
||||
* **17.02.19**
|
||||
- Removed WebAssembly as dependency (Now working with MS Edge as well (but without audio))
|
||||
- Improved channel tree performance
|
||||
|
|
196
files.php
196
files.php
|
@ -258,9 +258,97 @@
|
|||
],
|
||||
];
|
||||
|
||||
function list_dir($base_dir, $match = null, $depth = -1, &$results = array(), $dir = "") {
|
||||
function systemify_path($path) {
|
||||
return str_replace("/", DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
|
||||
function join_path(...$paths) {
|
||||
$result_path = "";
|
||||
foreach ($paths as $path) {
|
||||
if(strlen($result_path) > 0)
|
||||
$result_path .= DIRECTORY_SEPARATOR . $path;
|
||||
else
|
||||
$result_path = $path;
|
||||
}
|
||||
|
||||
return $result_path;
|
||||
}
|
||||
|
||||
function create_directories(&$error, $path, $dry_run = false) {
|
||||
if(strpos(PHP_OS, "Linux") !== false) {
|
||||
$command = "mkdir -p " . $path;
|
||||
} else if(strpos(PHP_OS, "WINNT") !== false) {
|
||||
$command = "mkdir " . $path; /* default path tree */
|
||||
} else {
|
||||
$error = "unsupported system";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo $command . PHP_EOL;
|
||||
if(!$dry_run) {
|
||||
exec($command, $error, $state);
|
||||
if($state) {
|
||||
$error = "Command execution results in " . $state . ": " . implode(' ', $error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function delete_directories(&$error, $path, $dry_run = false) {
|
||||
if(strpos(PHP_OS, "Linux") !== false) {
|
||||
$command = "rm -r " . $path;
|
||||
} else if(strpos(PHP_OS, "WINNT") !== false) {
|
||||
$command = "rm -r " . $path;
|
||||
} else {
|
||||
$error = "unsupported system";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo $command . PHP_EOL;
|
||||
if(!$dry_run) {
|
||||
$state = 0;
|
||||
exec($command, $output, $state);
|
||||
|
||||
if($state !== 0) {
|
||||
$error = "Command execution results in " . $state . ": " . implode(' ', $output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function create_link(&$error, $source, $target, $dry_run = false) {
|
||||
if(strpos(PHP_OS, "Linux") !== false) {
|
||||
$command = "ln -s " . $source . " " . $target;
|
||||
} else if(strpos(PHP_OS, "WINNT") !== false) {
|
||||
$command = "mklink " . (is_dir($target) ? "/D " : "") . " " . $target . " " . $source;
|
||||
} else {
|
||||
$error = "unsupported system";
|
||||
return false;
|
||||
}
|
||||
|
||||
echo $command . PHP_EOL;
|
||||
if(!$dry_run) {
|
||||
$state = 0;
|
||||
exec($command, $output, $state);
|
||||
|
||||
if($state !== 0) {
|
||||
$error = "Command execution results in " . $state . ": " . implode(' ', $output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function list_dir($base_dir, $match = null, $depth = -1, &$results = array(), $dir = "") {
|
||||
if($depth == 0) return $results;
|
||||
|
||||
if(!is_dir($base_dir . $dir)) {
|
||||
echo "Skipping directory " . $base_dir . $dir . PHP_EOL;
|
||||
return $results;
|
||||
}
|
||||
$files = scandir($base_dir . $dir);
|
||||
|
||||
foreach($files as $key => $value){
|
||||
|
@ -279,12 +367,14 @@
|
|||
class AppFile {
|
||||
public $type;
|
||||
public $name;
|
||||
public $path;
|
||||
public $local_path;
|
||||
|
||||
public $target_path; /* relative path to the target file viewed from the file root */
|
||||
public $local_path; /* absolute path to local file */
|
||||
|
||||
public $hash;
|
||||
}
|
||||
|
||||
function find_files($flag = 0b11, $local_path_prefix = "./", $type = "dev", $args = []) { //TODO Use cache here!
|
||||
function find_files($flag = 0b11, $local_path_prefix = "." . DIRECTORY_SEPARATOR, $type = "dev", $args = []) { //TODO Use cache here!
|
||||
global $APP_FILE_LIST;
|
||||
$result = [];
|
||||
|
||||
|
@ -303,23 +393,22 @@
|
|||
if(!$valid)
|
||||
continue;
|
||||
}
|
||||
$entries = list_dir($local_path_prefix . $entry["local-path"], $entry["search-pattern"], isset($entry["search-depth"]) ? $entry["search-depth"] : -1);
|
||||
$entries = list_dir(
|
||||
systemify_path($local_path_prefix . $entry["local-path"]),
|
||||
$entry["search-pattern"],
|
||||
isset($entry["search-depth"]) ? $entry["search-depth"] : -1
|
||||
);
|
||||
foreach ($entries as $f_entry) {
|
||||
if(isset($entry["search-exclude"]) && preg_match($entry["search-exclude"], $f_entry)) continue;
|
||||
$file = new AppFile;
|
||||
|
||||
$idx_sep = strrpos($f_entry, DIRECTORY_SEPARATOR);
|
||||
$file->path = "./" . $entry["path"] . "/";
|
||||
if($idx_sep > 0) {
|
||||
$file->name = substr($f_entry, strrpos($f_entry, DIRECTORY_SEPARATOR) + 1);
|
||||
$file->path = $file->path . substr($f_entry, 0, strrpos($f_entry, DIRECTORY_SEPARATOR));
|
||||
} else {
|
||||
$file->name = $f_entry;
|
||||
}
|
||||
$f_info = pathinfo($f_entry);
|
||||
$file->target_path = systemify_path($entry["path"]) . DIRECTORY_SEPARATOR . $f_info["dirname"] . DIRECTORY_SEPARATOR;
|
||||
$file->local_path = getcwd() . DIRECTORY_SEPARATOR . systemify_path($entry["local-path"]) . DIRECTORY_SEPARATOR . $f_info["dirname"] . DIRECTORY_SEPARATOR;
|
||||
|
||||
$file->local_path = $local_path_prefix . $entry["local-path"] . DIRECTORY_SEPARATOR . $f_entry;
|
||||
$file->name = $f_info["basename"];
|
||||
$file->type = $entry["type"];
|
||||
$file->hash = sha1_file($file->local_path);
|
||||
$file->hash = sha1_file($file->local_path . DIRECTORY_SEPARATOR . $file->name);
|
||||
|
||||
if(strlen($file->hash) > 0) {
|
||||
foreach ($result as $e)
|
||||
|
@ -334,10 +423,17 @@
|
|||
}
|
||||
|
||||
if(isset($_SERVER["argv"])) { //Executed by command line
|
||||
if(strpos(PHP_OS, "Linux") == -1) {
|
||||
error_log("Invalid operating system! Help tool only available under linux!");
|
||||
exit(1);
|
||||
}
|
||||
$supported = false;
|
||||
if(strpos(PHP_OS, "Linux") !== false) {
|
||||
$supported = true;
|
||||
} else if(strpos(PHP_OS, "WIN") !== false) {
|
||||
$supported = true;
|
||||
}
|
||||
if(!$supported) {
|
||||
error_log("Invalid operating system (" . PHP_OS . ")! Help tool only available under linux!");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if(count($_SERVER["argv"]) < 2) {
|
||||
error_log("Invalid parameters!");
|
||||
goto help;
|
||||
|
@ -358,10 +454,10 @@
|
|||
if($_SERVER["argv"][3] == "dev" || $_SERVER["argv"][3] == "development") {
|
||||
if ($_SERVER["argv"][2] == "web") {
|
||||
$flagset = 0b01;
|
||||
$environment = "web/environment/development";
|
||||
$environment = join_path("web", "environment", "development");
|
||||
} else if ($_SERVER["argv"][2] == "client") {
|
||||
$flagset = 0b10;
|
||||
$environment = "client-api/environment/ui-files/raw";
|
||||
$environment = join_path("client-api", "environment", "ui-files", "raw");
|
||||
} else {
|
||||
error_log("Invalid type!");
|
||||
goto help;
|
||||
|
@ -370,10 +466,10 @@
|
|||
$type = "rel";
|
||||
if ($_SERVER["argv"][2] == "web") {
|
||||
$flagset = 0b01;
|
||||
$environment = "web/environment/release";
|
||||
$environment = join_path("web", "environment", "release");
|
||||
} else if ($_SERVER["argv"][2] == "client") {
|
||||
$flagset = 0b10;
|
||||
$environment = "client-api/environment/ui-files/raw";
|
||||
$environment = join_path("client-api", "environment", "ui-files", "raw");
|
||||
} else {
|
||||
error_log("Invalid type!");
|
||||
goto help;
|
||||
|
@ -385,37 +481,29 @@
|
|||
|
||||
{
|
||||
if(!$dry_run) {
|
||||
exec($command = "rm -r " . $environment, $output, $state);
|
||||
exec($command = "mkdir -p " . $environment, $output, $state); if($state) goto handle_error;
|
||||
if(delete_directories($error, $environment) === false)
|
||||
goto handle_error;
|
||||
|
||||
if(create_directories($error, $environment) === false)
|
||||
goto handle_error;
|
||||
}
|
||||
|
||||
$files = find_files($flagset, "./", $type, array_slice($_SERVER["argv"], 4));
|
||||
$files = find_files($flagset, "." . DIRECTORY_SEPARATOR, $type, array_slice($_SERVER["argv"], 4));
|
||||
$original_path = realpath(".");
|
||||
if(!chdir($environment)) {
|
||||
error_log("Failed to enter directory " . $environment . "!");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
foreach($files as $file) {
|
||||
if(!$dry_run && !is_dir($file->path)) {
|
||||
exec($command = "mkdir -p " . $file->path, $output, $state);
|
||||
if($state) goto handle_error;
|
||||
/** @var AppFile $file */
|
||||
foreach($files as $file) {
|
||||
if(!$dry_run && !is_dir($file->target_path) && strlen($file->target_path) > 0) {
|
||||
if(create_directories($error, $file->target_path, $dry_run) === false)
|
||||
goto handle_error;
|
||||
}
|
||||
|
||||
$parent_base = substr_count(realpath($file->path), DIRECTORY_SEPARATOR) - substr_count(realpath('.'), DIRECTORY_SEPARATOR);
|
||||
$parent_file = substr_count(realpath("."), DIRECTORY_SEPARATOR) - substr_count($original_path, DIRECTORY_SEPARATOR); //Current to parent
|
||||
$parent = $parent_base + $parent_file;
|
||||
|
||||
$path = "";
|
||||
for($index = 0; $index < $parent; $index++)
|
||||
$path = $path . "../";
|
||||
|
||||
$command = "ln -s " . $path . $file->local_path . " " . $file->path;
|
||||
if(!$dry_run) {
|
||||
exec($command, $output, $state);
|
||||
if($state) goto handle_error;
|
||||
}
|
||||
echo $command . PHP_EOL;
|
||||
if(create_link($output, $file->local_path . $file->name, $file->target_path . $file->name, $dry_run) === false)
|
||||
goto handle_error;
|
||||
}
|
||||
if(!chdir($original_path)) {
|
||||
error_log("Failed to reset directory!");
|
||||
|
@ -425,18 +513,20 @@
|
|||
}
|
||||
|
||||
if(!$dry_run) {
|
||||
exec("./scripts/git_index.sh sort-tag", $output, $state);
|
||||
exec("." . DIRECTORY_SEPARATOR . "scripts" . DIRECTORY_SEPARATOR . "git_index.sh sort-tag", $output, $state);
|
||||
file_put_contents($environment . DIRECTORY_SEPARATOR . "version", $output);
|
||||
|
||||
if ($_SERVER["argv"][2] == "client") {
|
||||
if(!chdir("client-api/environment")) {
|
||||
if(!chdir("client-api" . DIRECTORY_SEPARATOR . "environment")) {
|
||||
error_log("Failed to enter directory client-api/environment!");
|
||||
exit(1);
|
||||
}
|
||||
if(!is_dir("versions/beta"))
|
||||
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
|
||||
if(!is_dir("versions/stable"))
|
||||
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
|
||||
if(!is_dir("versions" . DIRECTORY_SEPARATOR . "beta")) {
|
||||
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
|
||||
}
|
||||
if(!is_dir("versions/stable")) {
|
||||
exec($command = "mkdir -p versions/beta", $output, $state); if($state) goto handle_error;
|
||||
}
|
||||
|
||||
exec($command = "ln -s ../api.php ./", $output, $state); $state = 0; //Dont handle an error here!
|
||||
if($state) goto handle_error;
|
||||
|
@ -445,10 +535,8 @@
|
|||
|
||||
exit(0);
|
||||
handle_error:
|
||||
error_log("Failed to execute command '" . $command . "'!");
|
||||
error_log("Command returned code " . $state . ". Output: " . PHP_EOL);
|
||||
foreach ($output as $line)
|
||||
error_log($line);
|
||||
error_log("Command execution failed!");
|
||||
error_log("Error message: " . $error);
|
||||
exit(1);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,8 @@
|
|||
"directories": {},
|
||||
"scripts": {
|
||||
"compile-sass": "sass --update .:.",
|
||||
"build-worker": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
|
||||
"build-worker-codec": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
|
||||
"build-worker-pow": "tsc -p shared/js/workers/tsconfig_worker_pow.json",
|
||||
"dtsgen": "node tools/dtsgen/index.js",
|
||||
"trgen": "node tools/trgen/index.js",
|
||||
"ttsc": "ttsc",
|
||||
|
|
|
@ -28,9 +28,14 @@ if [[ $? -ne 0 ]]; then
|
|||
fi
|
||||
|
||||
echo "Generating web workers"
|
||||
npm run build-worker
|
||||
npm run build-worker-codec
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web workers"
|
||||
echo "Failed to build web worker codec"
|
||||
exit 1
|
||||
fi
|
||||
npm run build-worker-pow
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Failed to build web worker pow"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# Setup the develop environment on windows
|
||||
## 1.0 Requirements
|
||||
The following tools or applications are required to develop the web client:
|
||||
- [1.1 IDE](#11-ide)
|
||||
- [1.2 XAMPP (apache & php)](#12-xampp-with-a-web-server-and-php)
|
||||
- [1.3 NPM](#13-npm)
|
||||
- [1.4 Git bash](#14-git-bash)
|
||||
|
||||
### 1.1 IDE
|
||||
For developing TeaWeb you require and IDE.
|
||||
Preferable is PHPStorm from Jetbrains because the've already build in compiling on changes.
|
||||
Else you've to run the compiler to compile the TS or SCSS files to js e.g. css files.
|
||||
|
||||
### 1.2 XAMPP with a web server and PHP
|
||||
You require PHP (grater than 5) to setup and manage the project files.
|
||||
PHP is required for the index page as well.
|
||||
The web server is required for browsing your final environment and watch your result.
|
||||
The final environment will be found at `web/environemnt/development/`. More information about
|
||||
the file structure could be found [here (TODO: link me!)]().
|
||||
|
||||
### 1.3 NPM
|
||||
NPM min 6.X is required to develop this project.
|
||||
With NPM you could easily download all required dependencies by just typing `npm install`.
|
||||
IMPORTANT: NPM must be available within the PATH environment variable!
|
||||
|
||||
### 1.4 Git bash
|
||||
For using the `.sh` build scripts you require Git Bash.
|
||||
A minimum of 4.2 is recommend, but in general every version should work.
|
||||
|
||||
## 2.0 Development environment setup
|
||||
### 2.1 Native code (codecs) (Not required)
|
||||
If you dont want to develop the codec part or something related to the native
|
||||
webassembly part of TeaWeb you could skip this step and follow the steps in [2.1-b](#21-b-skip-native-code-setup)
|
||||
|
||||
### 2.1-b Skip native code setup
|
|
@ -37,6 +37,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
cursor: pointer;
|
||||
margin-left: 0;
|
||||
|
||||
.server_type {
|
||||
|
@ -134,6 +135,10 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.icon_no_sound {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.container-clients {
|
||||
|
@ -193,7 +198,15 @@
|
|||
}
|
||||
|
||||
/* all icons related to basic_icons */
|
||||
.clicon {width:16px;height:16px;background:url('../../img/ts/basic_icons.png') no-repeat;background-size: 16px 608px;}
|
||||
.clicon {
|
||||
width:16px;
|
||||
height:16px;
|
||||
background:url('../../img/ts/basic_icons.png') no-repeat;
|
||||
background-size: 16px 608px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.host {background-position: 0 -448px}
|
||||
|
||||
|
|
|
@ -47,13 +47,11 @@ $background:lightgray;
|
|||
|
||||
.button-dropdown {
|
||||
.buttons {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: 100%;
|
||||
grid-gap: 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.button {
|
||||
margin-right: 0px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.button-dropdown {
|
||||
|
@ -83,6 +81,7 @@ $background:lightgray;
|
|||
background-color: rgba(0,0,0,0.4);
|
||||
border-color: rgba(255, 255, 255, .75);
|
||||
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||
border-left: 2px solid rgba(255, 255, 255, .75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +102,11 @@ $background:lightgray;
|
|||
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
|
@ -131,8 +135,8 @@ $background:lightgray;
|
|||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dropdown.displayed {
|
||||
&:hover.displayed {
|
||||
.dropdown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
.select_info_table { }
|
||||
.select_info_table tr { }
|
||||
.select_info_table tr td { }
|
||||
.select_info_table {
|
||||
tr {
|
||||
td {
|
||||
&:nth-child(1) {
|
||||
font-weight: bold;
|
||||
padding-right: 5px;
|
||||
//min-width: max(35%, 20px);
|
||||
}
|
||||
|
||||
.select_info_table tr td:nth-child(1) {
|
||||
font-weight: bold;
|
||||
padding-right: 5px;
|
||||
min-width: 20%;
|
||||
&:nth-child(2) {
|
||||
//min-width: max(75%, 40px);
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select_server {
|
||||
|
@ -17,21 +24,18 @@
|
|||
|
||||
.button-update {
|
||||
width: 100%;
|
||||
height: 23px;
|
||||
|
||||
&:disabled {
|
||||
color: red;
|
||||
pointer-events: none;
|
||||
}
|
||||
&:not(:disabled) {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
|
||||
.hostbanner {
|
||||
overflow: hidden;
|
||||
|
@ -39,23 +43,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
<div id="select_info" class="select_info" style="width: 100%; max-width: 100%">
|
||||
<div class="container-banner"></div>
|
||||
<div class="container-info"></div>
|
||||
</div>
|
||||
*/
|
||||
.select_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
width: 100%;
|
||||
|
||||
> .close {
|
||||
z-index: 500;
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container-banner {
|
||||
position: relative;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 2;
|
||||
max-height: 25%;
|
||||
|
@ -74,9 +82,29 @@
|
|||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
}
|
||||
.image-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
div {
|
||||
background-position: center;
|
||||
|
||||
&.hostbanner-mode-0 { }
|
||||
|
||||
&.hostbanner-mode-1 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.hostbanner-mode-2 {
|
||||
background-size: contain!important;
|
||||
width:100%;
|
||||
height:100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,4 +133,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-browser-info {
|
||||
vertical-align: bottom;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: gray;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -228,8 +228,14 @@ footer .container {
|
|||
}
|
||||
|
||||
$separator_thickness: 4px;
|
||||
$small_device: 650px;
|
||||
$animation_length: .5s;
|
||||
|
||||
.app {
|
||||
min-width: 350px;
|
||||
|
||||
.container-app-main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
@ -301,8 +307,78 @@ $separator_thickness: 4px;
|
|||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
|
||||
.hide-small {
|
||||
opacity: 1;
|
||||
transition: opacity $animation_length linear;
|
||||
}
|
||||
|
||||
.show-small {
|
||||
display: none;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity $animation_length linear;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $small_device) {
|
||||
.app-container {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 25px;
|
||||
top: 0;
|
||||
|
||||
transition: all $animation_length linear;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app {
|
||||
.container-app-main {
|
||||
.container-info {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
width: 100%!important; /* override the seperator property */
|
||||
height: 100%;
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
&.shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.select_info {
|
||||
> .close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-channel-chat + .container-seperator {
|
||||
display: none;
|
||||
animation: fadeout $animation_length linear;
|
||||
}
|
||||
|
||||
.container-channel-chat {
|
||||
width: 100%!important; /* override the seperator property */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hide-small {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity $animation_length linear;
|
||||
}
|
||||
|
||||
.show-small {
|
||||
display: block!important;
|
||||
|
||||
opacity: 1!important;
|
||||
transition: opacity $animation_length linear;
|
||||
}
|
||||
}
|
||||
.container-seperator {
|
||||
background: lightgray;
|
||||
flex-grow: 0;
|
||||
|
@ -328,6 +404,7 @@ $separator_thickness: 4px;
|
|||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
@ -354,8 +431,6 @@ $separator_thickness: 4px;
|
|||
}
|
||||
|
||||
html, body {
|
||||
min-height: 500px;
|
||||
min-width: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -365,13 +440,24 @@ body {
|
|||
}
|
||||
|
||||
.icon-playlist-manage {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
&.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background-position: -5px -5px;
|
||||
background-size: 25px;
|
||||
}
|
||||
|
||||
&.icon_x32 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
background-position: -11px -9px;
|
||||
background-size: 50px;
|
||||
}
|
||||
|
||||
display: inline-block;
|
||||
background: url('../../img/music/playlist.svg') no-repeat;
|
||||
background-position: -11px -9px;
|
||||
background-size: 50px;
|
||||
}
|
||||
|
||||
x-content {
|
||||
|
|
|
@ -35,6 +35,20 @@
|
|||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.event-message { /* special formated messages */
|
||||
&.event-partner-disconnect {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&.event-partner-connect {
|
||||
color: green;
|
||||
}
|
||||
|
||||
&.event-partner-closed {
|
||||
color: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,11 +76,9 @@
|
|||
cursor: pointer;
|
||||
height: 18px;
|
||||
|
||||
&.active {
|
||||
background: #11111111;
|
||||
}
|
||||
|
||||
.btn_close {
|
||||
display: none;
|
||||
|
||||
float: none;
|
||||
margin-right: -5px;
|
||||
margin-left: 8px;
|
||||
|
@ -78,9 +90,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
.name, .chatIcon {
|
||||
.name, .chat-type {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.closeable {
|
||||
.btn_close {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #11111111;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
.name {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
&.unread {
|
||||
.name {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,9 @@
|
|||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
background: url('../../../img/client_icon_sprite.svg'), url('../../img/client_icon_sprite.svg') no-repeat;
|
||||
}
|
||||
|
||||
|
@ -1028,7 +1031,7 @@
|
|||
}
|
||||
.icon_x32.client-refresh {
|
||||
background-position: calc(-224px * 2) calc(-256px * 2);
|
||||
}pe the key you wish
|
||||
}
|
||||
.icon_x32.client-register {
|
||||
background-position: calc(-256px * 2) calc(-256px * 2);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
x-tab { display:none }
|
||||
x-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 2px;
|
||||
|
@ -18,15 +15,19 @@ x-content {
|
|||
.tab .tab-content {
|
||||
min-height: 200px;
|
||||
|
||||
border-color: #6f6f6f;
|
||||
border-radius: 0px 2px 2px 2px;
|
||||
border-style: solid;
|
||||
overflow-y: auto;
|
||||
border-radius: 0 2px 2px 2px;
|
||||
border: solid #6f6f6f;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
padding: 2px;
|
||||
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
x-content {
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -39,7 +40,7 @@ x-content {
|
|||
*/
|
||||
|
||||
.tab .tab-header {
|
||||
font-family: Arial;
|
||||
font-family: Arial, serif;
|
||||
font-size: 12px;
|
||||
/*white-space: pre;*/
|
||||
line-height: 1;
|
||||
|
@ -64,14 +65,10 @@ x-content {
|
|||
.tab .tab-header .entry {
|
||||
background: #5f5f5f5f;
|
||||
display: inline-block;
|
||||
border: #6f6f6f;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border: 1px solid #6f6f6f;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
vertical-align: middle;
|
||||
padding: 2px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding: 2px 5px;
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="TeaSpeak Web Client, connect to any TeaSpeak server without installing anything." />
|
||||
<link rel="icon" href="img/favicon/teacup.png">
|
||||
|
||||
<?php
|
||||
if(!$WEB_CLIENT) {
|
||||
|
@ -189,9 +190,9 @@
|
|||
|
||||
<footer style="<?php echo $footer_style; ?>">
|
||||
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;">
|
||||
<div style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
|
||||
<div style="align-self: center;">TeaSpeak Web client (<?php echo $version; ?>) by WolverinDEV</div>
|
||||
<div style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
|
||||
<div class="hide-small" style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
|
||||
<div style="align-self: center;">TeaSpeak Web (<?php echo $version; ?>) by WolverinDEV</div>
|
||||
<div class="hide-small" style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
|
||||
</div>
|
||||
</footer>
|
||||
</html>
|
|
@ -6,7 +6,6 @@
|
|||
<title>TeaSpeak-Web client templates</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- main frame TODO tr -->
|
||||
<script class="jsrender-template" id="tmpl_main" type="text/html">
|
||||
<div class="app-container">
|
||||
<div class="app">
|
||||
|
@ -38,7 +37,7 @@
|
|||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
|
||||
<div class="hide-small button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
|
||||
<div class="buttons">
|
||||
<div class="button icon_x32 client-away btn_away_toggle"></div>
|
||||
<div class="button-dropdown">
|
||||
|
@ -50,15 +49,36 @@
|
|||
<div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button btn_mute_input">
|
||||
<div class="hide-small button btn_mute_input">
|
||||
<div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
|
||||
</div>
|
||||
<div class="button btn_mute_output">
|
||||
<div class="hide-small button btn_mute_output">
|
||||
<div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="button-dropdown btn_token" title="{{tr 'Use token' /}}">
|
||||
<div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
|
||||
<div class="buttons">
|
||||
<div class="button button-display icon_x32 client-music"></div>
|
||||
<div class="button-dropdown">
|
||||
<div class="arrow down"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<div class="btn_mute_input" title="{{tr 'Mute/unmute microphone' /}}">
|
||||
<div class="icon client-input_muted"></div>
|
||||
<a>{{tr "Mute/unmute microphone" /}}</a>
|
||||
</div>
|
||||
<div class="btn_mute_output" title="{{tr 'Mute/unmute headphones' /}}">
|
||||
<div class="icon client-output_muted"></div>
|
||||
<a>{{tr "Mute/unmute headphones" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="button button-subscribe-mode">
|
||||
<div class="icon_x32" title="{{tr 'Toggle channel subscribe mode' /}}"></div>
|
||||
</div>
|
||||
<div class="hide-small button-dropdown btn_token" title="{{tr 'Use token' /}}">
|
||||
<div class="buttons">
|
||||
<div class="button icon_x32 client-token btn_token_use"></div>
|
||||
<div class="button-dropdown">
|
||||
|
@ -72,13 +92,37 @@
|
|||
</div>
|
||||
|
||||
<div style="width: 100%"></div>
|
||||
<div class="button button-playlist-manage" title="{{tr 'Playlists' /}}">
|
||||
<div class="icon-playlist-manage"></div>
|
||||
|
||||
<div class="show-small button-dropdown dropdown-servertools" title="{{tr 'Server tools' /}}">
|
||||
<div class="buttons">
|
||||
<div class="button button-display icon_x32 client-virtualserver_edit"></div>
|
||||
<div class="button-dropdown">
|
||||
<div class="arrow down"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown right">
|
||||
<div class="button-playlist-manage" title="{{tr 'Playlists' /}}">
|
||||
<div class="icon icon-playlist-manage"></div>
|
||||
<a>{{tr "Playlists" /}}</a>
|
||||
</div>
|
||||
<div class="btn_banlist" title="{{tr 'Banlist' /}}">
|
||||
<div class="icon client-ban_list"></div>
|
||||
<a>{{tr "Banlist" /}}</a>
|
||||
</div>
|
||||
<div class="btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
||||
<div class="icon client-permission_overview"></div>
|
||||
<a>{{tr "View/edit permissions" /}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button btn_banlist" title="{{tr 'Banlist' /}}">
|
||||
|
||||
<div class="hide-small button button-playlist-manage" title="{{tr 'Playlists' /}}">
|
||||
<div class="icon_x32 icon-playlist-manage"></div>
|
||||
</div>
|
||||
<div class="hide-small button btn_banlist" title="{{tr 'Banlist' /}}">
|
||||
<div class="icon_x32 client-ban_list"></div>
|
||||
</div>
|
||||
<div class="button btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
||||
<div class="hide-small button btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
||||
<div class="icon_x32 client-permission_overview"></div>
|
||||
</div>
|
||||
|
||||
|
@ -133,8 +177,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="container-seperator vertical" seperator-id="seperator-main-info"></div>
|
||||
<div class="main_container container-info">
|
||||
<div id="select_info" class="select_info" style="width: 100%; max-width: 100%">
|
||||
<div id="select_info" class="main_container container-info">
|
||||
<div class="select_info" style="width: 100%; max-width: 100%">
|
||||
<button type="button" class="close" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div class="container-banner"></div>
|
||||
<!-- <div class="container-seperator horizontal" seperator-id="seperator-hostbanner-info"></div> -->
|
||||
<div class="container-select-info"></div>
|
||||
|
@ -1029,7 +1076,8 @@
|
|||
|
||||
<div class="group_box">
|
||||
<div class="header">{{tr "Microphone" /}}</div>
|
||||
<div class="content settings-microphone">
|
||||
<div class="content settings-microphone {{if !voice_available}}disabled{{/if}}">
|
||||
{{if voice_available}}
|
||||
<div class="form-row settings-device settings-device-microphone">
|
||||
<div class="form-group settings-device-select">
|
||||
<label for="select-settings-microphone-device" class="bmd-label-static">{{tr "Device:" /}}</label>
|
||||
|
@ -1088,6 +1136,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div>{{tr "Voice had been disabled" /}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="group_box">
|
||||
|
@ -2057,7 +2108,10 @@
|
|||
<table class="select_info_table">
|
||||
<tr>
|
||||
<td>{{tr "Name:" /}}</td>
|
||||
<td><node key="client_name"/></td>
|
||||
<td style="display: flex; flex-direction: row">
|
||||
<div style="margin-right: 3px" class="country flag-{{*:(data.property_client_country || 'xx').toLowerCase()}}" title="{{*:i18n.country_name(data.property_client_country || 'XX')}}"></div>
|
||||
<node key="client_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
{{if property_client_description.length > 0}}
|
||||
<tr>
|
||||
|
@ -2068,7 +2122,14 @@
|
|||
{{if !client_is_query}}
|
||||
<tr>
|
||||
<td>{{tr "Version:"/}}</td>
|
||||
<td><a title="{{>property_client_version}}">{{*: data.property_client_version.split(" ")[0]; }}</a> on {{>property_client_platform}}</td>
|
||||
<td>
|
||||
<a title="{{>property_client_version}}">{{*: data.property_client_version.split(" ")[0]; }}</a>
|
||||
{{if client_is_web && false}} <!-- we cant show any browser info because every browser claims to be any browser as well -->
|
||||
<div class="icon client-message_info button-browser-info" title="{{tr 'Browser info' /}}"></div>
|
||||
{{/if}}
|
||||
on
|
||||
<a>{{>property_client_platform}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
<tr>
|
||||
|
@ -2242,26 +2303,11 @@
|
|||
</script>
|
||||
<script class="jsrender-template" id="tmpl_selected_hostbanner" type="text/html">
|
||||
<div class="hostbanner">
|
||||
<a href="{{:property_virtualserver_hostbanner_url}}" target="_blank" style="display: flex; flex-direction: row; justify-content: center; height: 100%">
|
||||
|
||||
<div style="
|
||||
background:center no-repeat url(
|
||||
{{:property_virtualserver_hostbanner_gfx_url}}{{:cache_tag}}
|
||||
);
|
||||
|
||||
background-position: center;
|
||||
{{if property_virtualserver_hostbanner_mode == 0}}
|
||||
|
||||
{{else property_virtualserver_hostbanner_mode == 1}}
|
||||
width: 100%; height: auto;
|
||||
{{else property_virtualserver_hostbanner_mode == 2}}
|
||||
background-size:contain;
|
||||
width:100%;
|
||||
height:100%
|
||||
{{/if}}
|
||||
"
|
||||
|
||||
alt="{{tr "Host banner"/}}"
|
||||
<a class="image-container" href="{{:property_virtualserver_hostbanner_url}}" target="_blank">
|
||||
<div
|
||||
style="background: center no-repeat url({{:hostbanner_gfx_url}})"
|
||||
alt="{{tr 'Host banner'/}}"
|
||||
class="hostbanner-image hostbanner-mode-{{:property_virtualserver_hostbanner_mode}}"
|
||||
></div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -2312,7 +2358,7 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<button class="button-update btn_update">{{tr "Update info"/}}</button>
|
||||
<button class="button-update btn btn-success">{{tr "Update info"/}}</button>
|
||||
</div>
|
||||
</script>
|
||||
<script class="jsrender-template" id="tmpl_selected_channel" type="text/html">
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 75 KiB |
|
@ -1,6 +1,10 @@
|
|||
/// <reference path="client.ts" />
|
||||
/// <reference path="connection/ConnectionBase.ts" />
|
||||
|
||||
/*
|
||||
FIXME: Dont use item storage with base64! Use the larger cache API and drop IE support!
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage#Browser_compatibility
|
||||
*/
|
||||
class FileEntry {
|
||||
name: string;
|
||||
datetime: number;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import LogType = log.LogType;
|
||||
|
||||
enum ChatType {
|
||||
GENERAL,
|
||||
SERVER,
|
||||
|
@ -65,12 +67,11 @@ namespace MessageHelper {
|
|||
}
|
||||
|
||||
if(objects.length < number)
|
||||
console.warn(tr("Message to format contains invalid index (%o)"), number);
|
||||
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
|
||||
|
||||
result.push(...formatElement(objects[number]));
|
||||
found = found + 1 + offset;
|
||||
begin = found + 1;
|
||||
console.log(tr("Offset: %d Number: %d"), offset, number);
|
||||
} while(found++);
|
||||
|
||||
return result;
|
||||
|
@ -93,7 +94,7 @@ namespace MessageHelper {
|
|||
});
|
||||
|
||||
if(result.error) {
|
||||
console.log("BBCode parse error: %o", result.errorQueue);
|
||||
log.error(LogCategory.GENERAL, tr("BBCode parse error: %o"), result.errorQueue);
|
||||
return formatElement(message);
|
||||
}
|
||||
|
||||
|
@ -104,7 +105,7 @@ namespace MessageHelper {
|
|||
class ChatMessage {
|
||||
date: Date;
|
||||
message: JQuery[];
|
||||
private _htmlTag: JQuery<HTMLElement>;
|
||||
private _html_tag: JQuery<HTMLElement>;
|
||||
|
||||
constructor(message: JQuery[]) {
|
||||
this.date = new Date();
|
||||
|
@ -117,8 +118,8 @@ class ChatMessage {
|
|||
return str;
|
||||
}
|
||||
|
||||
get htmlTag() {
|
||||
if(this._htmlTag) return this._htmlTag;
|
||||
get html_tag() {
|
||||
if(this._html_tag) return this._html_tag;
|
||||
|
||||
let tag = $.spawn("div");
|
||||
tag.addClass("message");
|
||||
|
@ -128,26 +129,30 @@ class ChatMessage {
|
|||
dateTag.css("margin-right", "4px");
|
||||
dateTag.css("color", "dodgerblue");
|
||||
|
||||
this._htmlTag = tag;
|
||||
this._html_tag = tag;
|
||||
tag.append(dateTag);
|
||||
this.message.forEach(e => e.appendTo(tag));
|
||||
tag.hide();
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatEntry {
|
||||
handle: ChatBox;
|
||||
readonly handle: ChatBox;
|
||||
type: ChatType;
|
||||
key: string;
|
||||
history: ChatMessage[];
|
||||
|
||||
owner_unique_id?: string;
|
||||
|
||||
private _name: string;
|
||||
private _htmlTag: any;
|
||||
private _closeable: boolean;
|
||||
private _unread : boolean;
|
||||
private _html_tag: any;
|
||||
|
||||
private _flag_closeable: boolean = true;
|
||||
private _flag_unread : boolean = false;
|
||||
private _flag_offline: boolean = false;
|
||||
|
||||
onMessageSend: (text: string) => void;
|
||||
onClose: () => boolean;
|
||||
onClose: () => boolean = () => true;
|
||||
|
||||
constructor(handle, type : ChatType, key) {
|
||||
this.handle = handle;
|
||||
|
@ -155,8 +160,6 @@ class ChatEntry {
|
|||
this.key = key;
|
||||
this._name = key;
|
||||
this.history = [];
|
||||
|
||||
this.onClose = function () { return true; }
|
||||
}
|
||||
|
||||
appendError(message: string, ...args) {
|
||||
|
@ -173,7 +176,7 @@ class ChatEntry {
|
|||
this.history.push(entry);
|
||||
while(this.history.length > 100) {
|
||||
let elm = this.history.pop_front();
|
||||
elm.htmlTag.animate({opacity: 0}, 200, function () {
|
||||
elm.html_tag.animate({opacity: 0}, 200, function () {
|
||||
$(this).detach();
|
||||
});
|
||||
}
|
||||
|
@ -181,66 +184,75 @@ class ChatEntry {
|
|||
let box = $(this.handle.htmlTag).find(".messages");
|
||||
let mbox = $(this.handle.htmlTag).find(".message_box");
|
||||
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
|
||||
mbox.append(entry.htmlTag);
|
||||
entry.htmlTag.show().css("opacity", "0").animate({opacity: 1}, 100);
|
||||
mbox.append(entry.html_tag);
|
||||
entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100);
|
||||
if(bottom) box.scrollTop(mbox.height());
|
||||
} else {
|
||||
this.unread = true;
|
||||
this.flag_unread = true;
|
||||
}
|
||||
}
|
||||
|
||||
displayHistory() {
|
||||
this.unread = false;
|
||||
let box = $(this.handle.htmlTag).find(".messages");
|
||||
let mbox = $(this.handle.htmlTag).find(".message_box");
|
||||
this.flag_unread = false;
|
||||
let box = this.handle.htmlTag.find(".messages");
|
||||
let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */
|
||||
mbox.empty();
|
||||
|
||||
for(let e of this.history) {
|
||||
mbox.append(e.htmlTag);
|
||||
if(e.htmlTag.is(":hidden")) e.htmlTag.show();
|
||||
mbox.append(e.html_tag);
|
||||
/* TODO Is this really totally useless?
|
||||
Because its at least a performance bottleneck because is(...) recalculates the page style
|
||||
if(e.htmlTag.is(":hidden"))
|
||||
e.htmlTag.show();
|
||||
*/
|
||||
}
|
||||
|
||||
mbox.appendTo(box);
|
||||
box.scrollTop(mbox.height());
|
||||
}
|
||||
|
||||
get htmlTag() {
|
||||
if(this._htmlTag) return this._htmlTag;
|
||||
get html_tag() {
|
||||
if(this._html_tag)
|
||||
return this._html_tag;
|
||||
|
||||
let tag = $.spawn("div");
|
||||
tag.addClass("chat");
|
||||
if(this._flag_unread)
|
||||
tag.addClass('unread');
|
||||
if(this._flag_offline)
|
||||
tag.addClass('offline');
|
||||
if(this._flag_closeable)
|
||||
tag.addClass('closeable');
|
||||
|
||||
tag.append("<div class=\"chatIcon icon " + this.chatIcon() + "\"></div>");
|
||||
tag.append("<a class='name'>" + this._name + "</a>");
|
||||
tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon()));
|
||||
tag.append($.spawn("a").addClass("name").text(this._name));
|
||||
|
||||
let closeTag = $.spawn("div");
|
||||
closeTag.addClass("btn_close icon client-tab_close_button");
|
||||
if(!this._closeable) closeTag.hide();
|
||||
tag.append(closeTag);
|
||||
let tag_close = $.spawn("div");
|
||||
tag_close.addClass("btn_close icon client-tab_close_button");
|
||||
if(!this._flag_closeable) tag_close.hide();
|
||||
tag.append(tag_close);
|
||||
|
||||
const _this = this;
|
||||
tag.click(function () {
|
||||
_this.handle.activeChat = _this;
|
||||
});
|
||||
tag.on("contextmenu", function (e) {
|
||||
tag.click(() => { this.handle.activeChat = this; });
|
||||
tag.on("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let actions = [];
|
||||
let actions: ContextMenuEntry[] = [];
|
||||
actions.push({
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "",
|
||||
name: tr("Clear"),
|
||||
callback: () => {
|
||||
_this.history = [];
|
||||
_this.displayHistory();
|
||||
this.history = [];
|
||||
this.displayHistory();
|
||||
}
|
||||
});
|
||||
if(_this.closeable) {
|
||||
if(this.flag_closeable) {
|
||||
actions.push({
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-tab_close_button",
|
||||
name: tr("Close"),
|
||||
callback: () => {
|
||||
chat.deleteChat(_this);
|
||||
chat.deleteChat(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -251,18 +263,20 @@ class ChatEntry {
|
|||
name: tr("Close all private tabs"),
|
||||
callback: () => {
|
||||
//TODO Implement this?
|
||||
}
|
||||
},
|
||||
visible: false
|
||||
});
|
||||
spawn_context_menu(e.pageX, e.pageY, ...actions);
|
||||
});
|
||||
|
||||
closeTag.click(function () {
|
||||
if($.isFunction(_this.onClose) && !_this.onClose()) return;
|
||||
_this.handle.deleteChat(_this);
|
||||
tag_close.click(() => {
|
||||
if($.isFunction(this.onClose) && !this.onClose())
|
||||
return;
|
||||
|
||||
this.handle.deleteChat(this);
|
||||
});
|
||||
|
||||
this._htmlTag = tag;
|
||||
return tag;
|
||||
return this._html_tag = tag;
|
||||
}
|
||||
|
||||
focus() {
|
||||
|
@ -271,33 +285,37 @@ class ChatEntry {
|
|||
}
|
||||
|
||||
set name(newName : string) {
|
||||
console.log(tr("Change name!"));
|
||||
this._name = newName;
|
||||
this.htmlTag.find(".name").text(this._name);
|
||||
this.html_tag.find(".name").text(this._name);
|
||||
}
|
||||
|
||||
set closeable(flag : boolean) {
|
||||
if(this._closeable == flag) return;
|
||||
set flag_closeable(flag : boolean) {
|
||||
if(this._flag_closeable == flag) return;
|
||||
|
||||
this._closeable = flag;
|
||||
console.log(tr("Set closeable: ") + this._closeable);
|
||||
if(flag) this.htmlTag.find(".btn_close").show();
|
||||
else this.htmlTag.find(".btn_close").hide();
|
||||
this._flag_closeable = flag;
|
||||
|
||||
this.html_tag.toggleClass('closeable', flag);
|
||||
}
|
||||
|
||||
set unread(flag : boolean) {
|
||||
if(this._unread == flag) return;
|
||||
this._unread = flag;
|
||||
this.htmlTag.find(".chatIcon").attr("class", "chatIcon icon " + this.chatIcon());
|
||||
if(flag) {
|
||||
this.htmlTag.find(".name").css("color", "blue");
|
||||
} else {
|
||||
this.htmlTag.find(".name").css("color", "black");
|
||||
}
|
||||
set flag_unread(flag : boolean) {
|
||||
if(this._flag_unread == flag) return;
|
||||
this._flag_unread = flag;
|
||||
this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon());
|
||||
this.html_tag.toggleClass('unread', flag);
|
||||
}
|
||||
|
||||
private chatIcon() : string {
|
||||
if(this._unread) {
|
||||
get flag_offline() { return this._flag_offline; }
|
||||
|
||||
set flag_offline(flag: boolean) {
|
||||
if(flag == this._flag_offline)
|
||||
return;
|
||||
|
||||
this._flag_offline = flag;
|
||||
this.html_tag.toggleClass('offline', flag);
|
||||
}
|
||||
|
||||
private chat_icon() : string {
|
||||
if(this._flag_unread) {
|
||||
switch (this.type) {
|
||||
case ChatType.CLIENT:
|
||||
return "client-new_chat";
|
||||
|
@ -319,6 +337,10 @@ class ChatEntry {
|
|||
|
||||
|
||||
class ChatBox {
|
||||
//https://regex101.com/r/YQbfcX/2
|
||||
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
||||
static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
||||
|
||||
htmlTag: JQuery;
|
||||
chats: ChatEntry[];
|
||||
private _activeChat: ChatEntry;
|
||||
|
@ -359,10 +381,11 @@ class ChatBox {
|
|||
return;
|
||||
|
||||
chat.serverChat().appendMessage(tr("Failed to send text message."));
|
||||
console.error(tr("Failed to send server text message: %o"), error);
|
||||
log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
|
||||
});
|
||||
};
|
||||
this.serverChat().name = tr("Server chat");
|
||||
this.serverChat().flag_closeable = false;
|
||||
|
||||
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
||||
if(!globalClient.serverConnection) {
|
||||
|
@ -372,10 +395,11 @@ class ChatBox {
|
|||
|
||||
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => {
|
||||
chat.channelChat().appendMessage(tr("Failed to send text message."));
|
||||
console.error(tr("Failed to send channel text message: %o"), error);
|
||||
log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
|
||||
});
|
||||
};
|
||||
this.channelChat().name = tr("Channel chat");
|
||||
this.channelChat().flag_closeable = false;
|
||||
|
||||
globalClient.permissions.initializedListener.push(flag => {
|
||||
if(flag) this.activeChat0(this._activeChat);
|
||||
|
@ -385,11 +409,15 @@ class ChatBox {
|
|||
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
|
||||
let chat = new ChatEntry(this, type, key);
|
||||
this.chats.push(chat);
|
||||
this.htmlTag.find(".chats").append(chat.htmlTag);
|
||||
this.htmlTag.find(".chats").append(chat.html_tag);
|
||||
if(!this._activeChat) this.activeChat = chat;
|
||||
return chat;
|
||||
}
|
||||
|
||||
open_chats() : ChatEntry[] {
|
||||
return this.chats;
|
||||
}
|
||||
|
||||
findChat(key : string) : ChatEntry {
|
||||
for(let e of this.chats)
|
||||
if(e.key == key) return e;
|
||||
|
@ -398,7 +426,7 @@ class ChatBox {
|
|||
|
||||
deleteChat(chat : ChatEntry) {
|
||||
this.chats.remove(chat);
|
||||
chat.htmlTag.detach();
|
||||
chat.html_tag.detach();
|
||||
if(this._activeChat === chat) {
|
||||
if(this.chats.length > 0)
|
||||
this.activeChat = this.chats.last();
|
||||
|
@ -414,8 +442,38 @@ class ChatBox {
|
|||
this._input_message.val("");
|
||||
this._input_message.trigger("input");
|
||||
|
||||
/* preprocessing text */
|
||||
const words = text.split(/[ \n]/);
|
||||
for(let index = 0; index < words.length; index++) {
|
||||
const flag_escaped = words[index].startsWith('!');
|
||||
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
||||
|
||||
_try:
|
||||
try {
|
||||
const url = new URL(unescaped);
|
||||
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
||||
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||
break _try;
|
||||
if(flag_escaped)
|
||||
words[index] = unescaped;
|
||||
else {
|
||||
text = undefined;
|
||||
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
||||
}
|
||||
} catch(e) { /* word isn't an url */ }
|
||||
|
||||
if(unescaped.match(ChatBox.URL_REGEX)) {
|
||||
if(flag_escaped)
|
||||
words[index] = unescaped;
|
||||
else {
|
||||
text = undefined;
|
||||
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
|
||||
this._activeChat.onMessageSend(text);
|
||||
this._activeChat.onMessageSend(text || words.join(" "));
|
||||
}
|
||||
|
||||
set activeChat(chat : ChatEntry) {
|
||||
|
@ -427,27 +485,27 @@ class ChatBox {
|
|||
private activeChat0(chat: ChatEntry) {
|
||||
this._activeChat = chat;
|
||||
for(let e of this.chats)
|
||||
e.htmlTag.removeClass("active");
|
||||
e.html_tag.removeClass("active");
|
||||
|
||||
let flagAllowSend = false;
|
||||
let disable_input = !chat;
|
||||
if(this._activeChat) {
|
||||
this._activeChat.htmlTag.addClass("active");
|
||||
this._activeChat.html_tag.addClass("active");
|
||||
this._activeChat.displayHistory();
|
||||
|
||||
if(globalClient && globalClient.permissions && globalClient.permissions.initialized())
|
||||
if(!disable_input && globalClient && globalClient.permissions && globalClient.permissions.initialized())
|
||||
switch (this._activeChat.type) {
|
||||
case ChatType.CLIENT:
|
||||
flagAllowSend = true;
|
||||
disable_input = false;
|
||||
break;
|
||||
case ChatType.SERVER:
|
||||
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
|
||||
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
|
||||
break;
|
||||
case ChatType.CHANNEL:
|
||||
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
|
||||
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._input_message.prop("disabled", !flagAllowSend);
|
||||
this._input_message.prop("disabled", disable_input);
|
||||
}
|
||||
|
||||
get activeChat(){ return this._activeChat; }
|
||||
|
|
|
@ -50,7 +50,7 @@ enum ViewReasonId {
|
|||
class TSClient {
|
||||
channelTree: ChannelTree;
|
||||
serverConnection: connection.ServerConnection;
|
||||
voiceConnection: VoiceConnection;
|
||||
voiceConnection: VoiceConnection | undefined;
|
||||
fileManager: FileManager;
|
||||
selectInfo: InfoBar;
|
||||
permissions: PermissionManager;
|
||||
|
@ -69,10 +69,12 @@ class TSClient {
|
|||
this.fileManager = new FileManager(this);
|
||||
this.permissions = new PermissionManager(this);
|
||||
this.groups = new GroupManager(this);
|
||||
this.voiceConnection = new VoiceConnection(this);
|
||||
this._ownEntry = new LocalClientEntry(this);
|
||||
this.controlBar = new ControlBar(this, $("#control_bar"));
|
||||
this.channelTree.registerClient(this._ownEntry);
|
||||
|
||||
if(!settings.static_global(Settings.KEY_DISABLE_VOICE, false))
|
||||
this.voiceConnection = new VoiceConnection(this);
|
||||
}
|
||||
|
||||
setup() {
|
||||
|
@ -114,7 +116,7 @@ class TSClient {
|
|||
|
||||
|
||||
getClient() : LocalClientEntry { return this._ownEntry; }
|
||||
getClientId() { return this._clientId; } //TODO here
|
||||
getClientId() { return this._clientId; }
|
||||
|
||||
set clientId(id: number) {
|
||||
this._clientId = id;
|
||||
|
@ -136,11 +138,14 @@ class TSClient {
|
|||
this.channelTree.registerClient(this._ownEntry);
|
||||
settings.setServer(this.channelTree.server);
|
||||
this.permissions.requestPermissionList();
|
||||
this.serverConnection.send_command("channelsubscribeall");
|
||||
if(this.groups.serverGroups.length == 0)
|
||||
this.groups.requestGroups();
|
||||
this.controlBar.updateProperties();
|
||||
if(!this.voiceConnection.current_encoding_supported())
|
||||
if(this.controlBar.channel_subscribe_all)
|
||||
this.channelTree.subscribe_all_channels();
|
||||
else
|
||||
this.channelTree.unsubscribe_all_channels();
|
||||
if(this.voiceConnection && !this.voiceConnection.current_encoding_supported())
|
||||
createErrorModal(tr("Codec encode type not supported!"), tr("Codec encode type " + VoiceConnectionType[this.voiceConnection.type] + " not supported by this browser!<br>Choose another one!")).open(); //TODO tr
|
||||
}
|
||||
|
||||
|
@ -270,7 +275,8 @@ class TSClient {
|
|||
}
|
||||
|
||||
this.channelTree.reset();
|
||||
this.voiceConnection.dropSession();
|
||||
if(this.voiceConnection)
|
||||
this.voiceConnection.dropSession();
|
||||
if(this.serverConnection) this.serverConnection.disconnect();
|
||||
this.controlBar.update_connection_state();
|
||||
this.selectInfo.setCurrentSelected(null);
|
||||
|
@ -292,7 +298,7 @@ class TSClient {
|
|||
this._reconnect_timer = setTimeout(() => {
|
||||
this._reconnect_timer = undefined;
|
||||
chat.serverChat().appendMessage(tr("Reconnecting..."));
|
||||
console.log(tr("Reconnecting..."));
|
||||
log.info(LogCategory.NETWORKING, tr("Reconnecting..."))
|
||||
this.startConnection(server_address.host + ":" + server_address.port, profile, name, password ? { password: password, hashed: true} : undefined);
|
||||
this._reconnect_attempt = true;
|
||||
}, 5000);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
namespace connection {
|
||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
constructor(connection: AbstractServerConnection) {
|
||||
|
@ -26,6 +27,7 @@ namespace connection {
|
|||
this["notifychannelmoved"] = this.handleNotifyChannelMoved;
|
||||
this["notifychanneledited"] = this.handleNotifyChannelEdited;
|
||||
this["notifytextmessage"] = this.handleNotifyTextMessage;
|
||||
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
|
||||
this["notifyclientupdated"] = this.handleNotifyClientUpdated;
|
||||
this["notifyserveredited"] = this.handleNotifyServerEdited;
|
||||
this["notifyserverupdated"] = this.handleNotifyServerUpdated;
|
||||
|
@ -37,6 +39,9 @@ namespace connection {
|
|||
this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd;
|
||||
this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove;
|
||||
this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged;
|
||||
|
||||
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
|
||||
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
|
||||
}
|
||||
|
||||
handle_command(command: ServerCommand) : boolean {
|
||||
|
@ -81,8 +86,12 @@ namespace connection {
|
|||
|
||||
handleCommandServerInit(json){
|
||||
//We could setup the voice channel
|
||||
console.log(tr("Setting up voice"));
|
||||
this.connection.client.voiceConnection.createSession();
|
||||
if( this.connection.client.voiceConnection) {
|
||||
console.log(tr("Setting up voice"));
|
||||
this.connection.client.voiceConnection.createSession();
|
||||
} else {
|
||||
console.log(tr("Skipping voice setup (No voice bridge available)"));
|
||||
}
|
||||
|
||||
|
||||
json = json[0]; //Only one bulk
|
||||
|
@ -285,6 +294,26 @@ namespace connection {
|
|||
|
||||
client.updateVariables(...updates);
|
||||
|
||||
{
|
||||
let client_chat = client.chat(false);
|
||||
if(!client_chat) {
|
||||
for(const c of chat.open_chats()) {
|
||||
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) {
|
||||
client_chat = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(client_chat) {
|
||||
client_chat.appendMessage(
|
||||
"{0}", true,
|
||||
$.spawn("div")
|
||||
.addClass("event-message event-partner-connect")
|
||||
.text(tr("Your chat partner has reconnected"))
|
||||
);
|
||||
client_chat.flag_offline = false;
|
||||
}
|
||||
}
|
||||
if(client instanceof LocalClientEntry)
|
||||
this.connection.client.controlBar.updateVoice();
|
||||
}
|
||||
|
@ -364,6 +393,19 @@ namespace connection {
|
|||
} else {
|
||||
console.error(tr("Unknown client left reason!"));
|
||||
}
|
||||
|
||||
{
|
||||
const chat = client.chat(false);
|
||||
if(chat) {
|
||||
chat.flag_offline = true;
|
||||
chat.appendMessage(
|
||||
"{0}", true,
|
||||
$.spawn("div")
|
||||
.addClass("event-message event-partner-disconnect")
|
||||
.text(tr("Your chat partner has disconnected"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tree.deleteClient(client);
|
||||
|
@ -499,7 +541,6 @@ namespace connection {
|
|||
handleNotifyTextMessage(json) {
|
||||
json = json[0]; //Only one bulk
|
||||
|
||||
//TODO chat format?
|
||||
let mode = json["targetmode"];
|
||||
if(mode == 1){
|
||||
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]);
|
||||
|
@ -530,6 +571,38 @@ namespace connection {
|
|||
}
|
||||
}
|
||||
|
||||
handleNotifyClientChatClosed(json) {
|
||||
json = json[0]; //Only one bulk
|
||||
|
||||
//Chat partner has closed the conversation
|
||||
|
||||
//clid: "6"
|
||||
//cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY="
|
||||
|
||||
const client = this.connection.client.channelTree.findClient(json["clid"]);
|
||||
if(!client) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client"));
|
||||
return;
|
||||
}
|
||||
if(client.properties.client_unique_identifier !== json["cluid"]) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but unique ids dosn't match. (expected %o, received %o)"), client.properties.client_unique_identifier, json["cluid"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = client.chat(false);
|
||||
if(!chat) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
|
||||
return;
|
||||
}
|
||||
chat.flag_offline = true;
|
||||
chat.appendMessage(
|
||||
"{0}", true,
|
||||
$.spawn("div")
|
||||
.addClass("event-message event-partner-closed")
|
||||
.text(tr("Your chat partner has close the conversation"))
|
||||
);
|
||||
}
|
||||
|
||||
handleNotifyClientUpdated(json) {
|
||||
json = json[0]; //Only one bulk
|
||||
|
||||
|
@ -645,5 +718,31 @@ namespace connection {
|
|||
sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
handleNotifyChannelSubscribed(json) {
|
||||
for(const entry of json) {
|
||||
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
|
||||
if(!channel) {
|
||||
console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']);
|
||||
continue;
|
||||
}
|
||||
|
||||
channel.flag_subscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleNotifyChannelUnsubscribed(json) {
|
||||
for(const entry of json) {
|
||||
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
|
||||
if(!channel) {
|
||||
console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']);
|
||||
continue;
|
||||
}
|
||||
|
||||
channel.flag_subscribed = false;
|
||||
for(const client of channel.clients(false))
|
||||
this.connection.client.channelTree.deleteClient(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,20 +28,37 @@ namespace connection {
|
|||
abstract disconnect(reason?: string) : Promise<void>;
|
||||
|
||||
abstract support_voice() : boolean;
|
||||
abstract voice_connection() : AbstractVoiceConnection | undefined;
|
||||
abstract voice_connection() : voice.AbstractVoiceConnection | undefined;
|
||||
|
||||
abstract command_handler_boss() : AbstractCommandHandlerBoss;
|
||||
abstract send_command(command: string, data?: any | any[], options?: CommandOptions) : Promise<CommandResult>;
|
||||
}
|
||||
|
||||
export abstract class AbstractVoiceConnection {
|
||||
readonly connection: AbstractServerConnection;
|
||||
export namespace voice {
|
||||
export interface VoiceClient {
|
||||
client_id: number;
|
||||
|
||||
protected constructor(connection: AbstractServerConnection) {
|
||||
this.connection = connection;
|
||||
callback_playback: () => any;
|
||||
callback_timeout: () => any;
|
||||
callback_stopped: () => any;
|
||||
|
||||
get_volume() : number;
|
||||
set_volume(volume: number) : Promise<void>;
|
||||
}
|
||||
|
||||
abstract connected() : boolean;
|
||||
export abstract class AbstractVoiceConnection {
|
||||
readonly connection: AbstractServerConnection;
|
||||
|
||||
protected constructor(connection: AbstractServerConnection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
abstract connected() : boolean;
|
||||
|
||||
abstract register_client(client_id: number) : VoiceClient;
|
||||
abstract availible_clients() : VoiceClient[];
|
||||
abstract unregister_client(client: VoiceClient) : Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerCommand {
|
||||
|
|
|
@ -202,7 +202,12 @@ namespace connection {
|
|||
arguments: json["data"]
|
||||
});
|
||||
group.end();
|
||||
} else if(json["type"] === "WebRTC") this.client.voiceConnection.handleControlPacket(json);
|
||||
} else if(json["type"] === "WebRTC") {
|
||||
if(this.client.voiceConnection)
|
||||
this.client.voiceConnection.handleControlPacket(json);
|
||||
else
|
||||
console.log(tr("Dropping WebRTC command packet, because we havent a bridge."))
|
||||
}
|
||||
else {
|
||||
console.log(tr("Unknown command type %o"), json["type"]);
|
||||
}
|
||||
|
@ -233,7 +238,7 @@ namespace connection {
|
|||
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
|
||||
if(!this._socket || !this.connected()) {
|
||||
console.warn(tr("Tried to send a command without a valid connection."));
|
||||
return;
|
||||
return Promise.reject(tr("not connected"));
|
||||
}
|
||||
|
||||
const options: CommandOptions = {};
|
||||
|
@ -241,6 +246,8 @@ namespace connection {
|
|||
Object.assign(options, _options);
|
||||
|
||||
data = $.isArray(data) ? data : [data || {}];
|
||||
if(data.length == 0) /* we require min one arg to append return_code */
|
||||
data.push({});
|
||||
|
||||
const _this = this;
|
||||
let result = new Promise<CommandResult>((resolve, failed) => {
|
||||
|
@ -299,7 +306,7 @@ namespace connection {
|
|||
return false;
|
||||
}
|
||||
|
||||
voice_connection(): connection.AbstractVoiceConnection | undefined {
|
||||
voice_connection(): connection.voice.AbstractVoiceConnection | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,8 +52,9 @@ interface ContextMenuEntry {
|
|||
name: (() => string) | string;
|
||||
icon?: (() => string) | string | JQuery;
|
||||
disabled?: boolean;
|
||||
invalidPermission?: boolean;
|
||||
visible?: boolean;
|
||||
|
||||
invalidPermission?: boolean;
|
||||
sub_menu?: ContextMenuEntry[];
|
||||
}
|
||||
|
||||
|
@ -96,8 +97,11 @@ function generate_tag(entry: ContextMenuEntry) : JQuery {
|
|||
if(entry.disabled || entry.invalidPermission) tag.addClass("disabled");
|
||||
else {
|
||||
let menu = $.spawn("div").addClass("sub-menu").addClass("context-menu");
|
||||
for(let e of entry.sub_menu)
|
||||
for(const e of entry.sub_menu) {
|
||||
if(typeof(entry.visible) === 'boolean' && !entry.visible)
|
||||
continue;
|
||||
menu.append(generate_tag(e));
|
||||
}
|
||||
menu.appendTo(tag);
|
||||
}
|
||||
return tag;
|
||||
|
@ -111,7 +115,10 @@ function spawn_context_menu(x, y, ...entries: ContextMenuEntry[]) {
|
|||
|
||||
contextMenuCloseFn = undefined;
|
||||
|
||||
for(let entry of entries){
|
||||
for(const entry of entries){
|
||||
if(typeof(entry.visible) === 'boolean' && !entry.visible)
|
||||
continue;
|
||||
|
||||
if(entry.type == MenuEntryType.CLOSE) {
|
||||
contextMenuCloseFn = entry.callback;
|
||||
} else
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -53,7 +53,7 @@ namespace loader {
|
|||
DONE
|
||||
}
|
||||
|
||||
export let allow_cached_files: boolean = false;
|
||||
export let cache_tag: string | undefined;
|
||||
let current_stage: Stage = Stage.INITIALIZING;
|
||||
const tasks: {[key:number]:Task[]} = {};
|
||||
|
||||
|
@ -206,7 +206,7 @@ namespace loader {
|
|||
|
||||
document.getElementById("scripts").appendChild(tag);
|
||||
|
||||
tag.src = path + (allow_cached_files ? "" : "?_ts=" + Date.now());
|
||||
tag.src = path + (cache_tag || "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ namespace loader {
|
|||
};
|
||||
|
||||
document.getElementById("style").appendChild(tag);
|
||||
tag.href = path + (allow_cached_files ? "" : "?_ts=" + Date.now());
|
||||
tag.href = path + (cache_tag || "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -480,6 +480,7 @@ const loader_javascript = {
|
|||
//Load general API's
|
||||
"js/proto.js",
|
||||
"js/i18n/localize.js",
|
||||
"js/i18n/country.js",
|
||||
"js/log.js",
|
||||
|
||||
"js/sound/Sounds.js",
|
||||
|
@ -631,6 +632,7 @@ const loader_style = {
|
|||
"css/static/ts/tab.css",
|
||||
"css/static/ts/chat.css",
|
||||
"css/static/ts/icons.css",
|
||||
"css/static/ts/country.css",
|
||||
"css/static/general.css",
|
||||
"css/static/modals.css",
|
||||
"css/static/modal-bookmarks.css",
|
||||
|
@ -662,7 +664,7 @@ const loader_style = {
|
|||
|
||||
async function load_templates() {
|
||||
try {
|
||||
const response = await $.ajax("templates.html" + (loader.allow_cached_files ? "" : "?_ts" + Date.now()));
|
||||
const response = await $.ajax("templates.html" + (loader.cache_tag || ""));
|
||||
|
||||
let node = document.createElement("html");
|
||||
node.innerHTML = response;
|
||||
|
@ -706,12 +708,11 @@ async function check_updates() {
|
|||
|
||||
if(!app_version) {
|
||||
/* TODO add warning */
|
||||
loader.allow_cached_files = false;
|
||||
loader.cache_tag = "?_ts=" + Date.now();
|
||||
return;
|
||||
}
|
||||
const cached_version = localStorage.getItem("cached_version");
|
||||
if(!cached_version || cached_version != app_version) {
|
||||
loader.allow_cached_files = false;
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
priority: 0,
|
||||
name: "cached version updater",
|
||||
|
@ -719,11 +720,8 @@ async function check_updates() {
|
|||
localStorage.setItem("cached_version", app_version);
|
||||
}
|
||||
});
|
||||
/* loading screen */
|
||||
return;
|
||||
}
|
||||
|
||||
loader.allow_cached_files = true;
|
||||
loader.cache_tag = "?_version=" + app_version;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
|
|
@ -7,7 +7,8 @@ enum LogCategory {
|
|||
GENERAL,
|
||||
NETWORKING,
|
||||
VOICE,
|
||||
I18N
|
||||
I18N,
|
||||
IDENTITIES
|
||||
}
|
||||
|
||||
namespace log {
|
||||
|
@ -21,14 +22,15 @@ namespace log {
|
|||
|
||||
let category_mapping = new Map<number, string>([
|
||||
[LogCategory.CHANNEL, "Channel "],
|
||||
[LogCategory.CLIENT, "Channel "],
|
||||
[LogCategory.CHANNEL_PROPERTIES, "Client "],
|
||||
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
|
||||
[LogCategory.CLIENT, "Client "],
|
||||
[LogCategory.SERVER, "Server "],
|
||||
[LogCategory.PERMISSIONS, "Permission "],
|
||||
[LogCategory.GENERAL, "General "],
|
||||
[LogCategory.NETWORKING, "Network "],
|
||||
[LogCategory.VOICE, "Voice "],
|
||||
[LogCategory.I18N, "I18N "]
|
||||
[LogCategory.I18N, "I18N "],
|
||||
[LogCategory.IDENTITIES, "IDENTITIES "]
|
||||
]);
|
||||
|
||||
export let enabled_mapping = new Map<number, boolean>([
|
||||
|
@ -40,7 +42,8 @@ namespace log {
|
|||
[LogCategory.GENERAL, true],
|
||||
[LogCategory.NETWORKING, true],
|
||||
[LogCategory.VOICE, true],
|
||||
[LogCategory.I18N, false]
|
||||
[LogCategory.I18N, false],
|
||||
[LogCategory.IDENTITIES, true]
|
||||
]);
|
||||
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
|
@ -109,10 +112,16 @@ namespace log {
|
|||
name = "[%s] " + name;
|
||||
optionalParams.unshift(category_mapping.get(category));
|
||||
|
||||
return new Group(level, category, name, optionalParams);
|
||||
return new Group(GroupMode.PREFIX, level, category, name, optionalParams);
|
||||
}
|
||||
|
||||
enum GroupMode {
|
||||
NATIVE,
|
||||
PREFIX
|
||||
}
|
||||
|
||||
export class Group {
|
||||
readonly mode: GroupMode;
|
||||
readonly level: LogType;
|
||||
readonly category: LogCategory;
|
||||
readonly enabled: boolean;
|
||||
|
@ -123,9 +132,11 @@ namespace log {
|
|||
private readonly optionalParams: any[][];
|
||||
private _collapsed: boolean = true;
|
||||
private initialized = false;
|
||||
private _log_prefix: string;
|
||||
|
||||
constructor(level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
|
||||
constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
|
||||
this.level = level;
|
||||
this.mode = mode;
|
||||
this.category = category;
|
||||
this.name = name;
|
||||
this.optionalParams = optionalParams;
|
||||
|
@ -133,7 +144,7 @@ namespace log {
|
|||
}
|
||||
|
||||
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
|
||||
return new Group(level, this.category, name, optionalParams, this);
|
||||
return new Group(this.mode, level, this.category, name, optionalParams, this);
|
||||
}
|
||||
|
||||
collapsed(flag: boolean = true) : this {
|
||||
|
@ -146,19 +157,43 @@ namespace log {
|
|||
return this;
|
||||
|
||||
if(!this.initialized) {
|
||||
if(this._collapsed && console.groupCollapsed)
|
||||
console.groupCollapsed(this.name, ...this.optionalParams);
|
||||
else
|
||||
console.group(this.name, ...this.optionalParams);
|
||||
if(this.mode == GroupMode.NATIVE) {
|
||||
if(this._collapsed && console.groupCollapsed)
|
||||
console.groupCollapsed(this.name, ...this.optionalParams);
|
||||
else
|
||||
console.group(this.name, ...this.optionalParams);
|
||||
} else {
|
||||
this._log_prefix = " ";
|
||||
let parent = this.owner;
|
||||
while(parent) {
|
||||
if(parent.mode == GroupMode.PREFIX)
|
||||
this._log_prefix = this._log_prefix + parent._log_prefix;
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
logDirect(this.level, message, ...optionalParams);
|
||||
if(this.mode == GroupMode.NATIVE)
|
||||
logDirect(this.level, message, ...optionalParams);
|
||||
else
|
||||
logDirect(this.level, this._log_prefix + message, ...optionalParams);
|
||||
return this;
|
||||
}
|
||||
|
||||
end() {
|
||||
if(this.initialized)
|
||||
console.groupEnd();
|
||||
if(this.initialized) {
|
||||
if(this.mode == GroupMode.NATIVE)
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
get prefix() : string {
|
||||
return this._log_prefix;
|
||||
}
|
||||
|
||||
set prefix(prefix: string) {
|
||||
this._log_prefix = prefix;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -238,15 +238,15 @@ function main() {
|
|||
chat = new ChatBox($("#chat"));
|
||||
globalClient.setup();
|
||||
|
||||
if(settings.static("connect_default", false) && settings.static("connect_address", "")) {
|
||||
const profile_uuid = settings.static("connect_profile") as string;
|
||||
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
|
||||
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
|
||||
console.log("UUID: %s", profile_uuid);
|
||||
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
|
||||
const address = settings.static("connect_address", "");
|
||||
const username = settings.static("connect_username", "Another TeaSpeak user");
|
||||
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
|
||||
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
|
||||
|
||||
const password = settings.static("connect_password", "");
|
||||
const password_hashed = settings.static("connect_password_hashed", false);
|
||||
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
|
||||
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
|
||||
|
||||
if(profile && profile.valid()) {
|
||||
globalClient.startConnection(address, profile, username, password.length > 0 ? {
|
||||
|
@ -270,6 +270,7 @@ function main() {
|
|||
clearTimeout(_resize_timeout);
|
||||
_resize_timeout = setTimeout(() => {
|
||||
globalClient.channelTree.handle_resized();
|
||||
globalClient.selectInfo.handle_resize();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
|
|
|
@ -20,8 +20,7 @@ namespace profiles.identities {
|
|||
authentication_method: this.identity.type(),
|
||||
client_nickname: this.identity.name()
|
||||
}).catch(error => {
|
||||
console.error(tr("Failed to initialize name based handshake. Error: %o"), error);
|
||||
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error);
|
||||
if(error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
this.trigger_fail("failed to execute begin (" + error + ")");
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace profiles.identities {
|
|||
authentication_method: this.identity.type(),
|
||||
data: this.identity.data_json()
|
||||
}).catch(error => {
|
||||
console.error(tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
|
||||
|
||||
if(error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
|
@ -32,7 +32,7 @@ namespace profiles.identities {
|
|||
this.connection.send_command("handshakeindentityproof", {
|
||||
proof: this.identity.data_sign()
|
||||
}).catch(error => {
|
||||
console.error(tr("Failed to proof the identity. Error: %o"), error);
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
|
||||
|
||||
if(error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
|
|
|
@ -214,7 +214,7 @@ namespace profiles.identities {
|
|||
authentication_method: this.identity.type(),
|
||||
publicKey: this.identity.public_key
|
||||
}).catch(error => {
|
||||
console.error(tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
|
||||
|
||||
if(error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
|
@ -230,7 +230,7 @@ namespace profiles.identities {
|
|||
|
||||
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
|
||||
this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => {
|
||||
console.error(tr("Failed to proof the identity. Error: %o"), error);
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
|
||||
|
||||
if(error instanceof CommandResult)
|
||||
error = error.extra_message || error.message;
|
||||
|
@ -281,7 +281,7 @@ namespace profiles.identities {
|
|||
resolve();
|
||||
};
|
||||
this._worker.onerror = event => {
|
||||
console.error("POW Worker error %o", event);
|
||||
log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event);
|
||||
clearTimeout(timeout_id);
|
||||
reject("Failed to load worker (" + event.message + ")");
|
||||
};
|
||||
|
@ -394,7 +394,7 @@ namespace profiles.identities {
|
|||
};
|
||||
});
|
||||
} catch(error) {
|
||||
console.warn("Failed to finalize POW worker! (%o)", error);
|
||||
log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error);
|
||||
}
|
||||
|
||||
this._worker.terminate();
|
||||
|
@ -402,7 +402,7 @@ namespace profiles.identities {
|
|||
}
|
||||
|
||||
private handle_message(message: any) {
|
||||
console.log("Received message: %o", message);
|
||||
log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,7 +412,7 @@ namespace profiles.identities {
|
|||
try {
|
||||
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
||||
} catch(e) {
|
||||
console.error(tr("Could not generate a new key: %o"), e);
|
||||
log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e);
|
||||
throw "Failed to generate keypair";
|
||||
}
|
||||
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
|
||||
|
@ -483,7 +483,7 @@ namespace profiles.identities {
|
|||
|
||||
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
|
||||
this.initialize().catch(error => {
|
||||
console.error("Failed to initialize TeaSpeakIdentity (%s)", error);
|
||||
log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error);
|
||||
this._initialized = false;
|
||||
});
|
||||
}
|
||||
|
@ -633,7 +633,7 @@ namespace profiles.identities {
|
|||
try {
|
||||
await Promise.all(initialize_promise);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
log.error(LogCategory.IDENTITIES, error);
|
||||
throw "failed to initialize";
|
||||
}
|
||||
}
|
||||
|
@ -688,7 +688,7 @@ namespace profiles.identities {
|
|||
if(worker.current_level() > best_level) {
|
||||
this.hash_number = worker.current_hash();
|
||||
|
||||
console.log("Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
|
||||
log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
|
||||
best_level = worker.current_level();
|
||||
if(callback_level)
|
||||
callback_level(best_level);
|
||||
|
@ -712,7 +712,7 @@ namespace profiles.identities {
|
|||
}).catch(error => {
|
||||
worker_promise.remove(p);
|
||||
|
||||
console.warn("POW worker error %o", error);
|
||||
log.warn(LogCategory.IDENTITIES, "POW worker error %o", error);
|
||||
reject(error);
|
||||
|
||||
return Promise.resolve();
|
||||
|
@ -736,7 +736,7 @@ namespace profiles.identities {
|
|||
try {
|
||||
await Promise.all(finalize_promise);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
log.error(LogCategory.IDENTITIES, error);
|
||||
throw "failed to finalize";
|
||||
}
|
||||
}
|
||||
|
@ -761,14 +761,14 @@ namespace profiles.identities {
|
|||
try {
|
||||
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
log.error(LogCategory.IDENTITIES, error);
|
||||
throw "failed to create crypto sign key";
|
||||
}
|
||||
|
||||
try {
|
||||
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
log.error(LogCategory.IDENTITIES, error);
|
||||
throw "failed to create crypto key";
|
||||
}
|
||||
|
||||
|
@ -776,7 +776,7 @@ namespace profiles.identities {
|
|||
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
|
||||
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
log.error(LogCategory.IDENTITIES, error);
|
||||
throw "failed to calculate unique id";
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,15 @@ if(typeof(customElements) !== "undefined") {
|
|||
}
|
||||
}
|
||||
|
||||
/* T = value type */
|
||||
interface SettingsKey<T> {
|
||||
key: string;
|
||||
|
||||
fallback_keys?: string | string[];
|
||||
fallback_imports?: {[key: string]:(value: string) => T};
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class StaticSettings {
|
||||
private static _instance: StaticSettings;
|
||||
static get instance() : StaticSettings {
|
||||
|
@ -20,12 +29,14 @@ class StaticSettings {
|
|||
return this._instance;
|
||||
}
|
||||
|
||||
protected static transformStO?<T>(input?: string, _default?: T) : T {
|
||||
protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
|
||||
default_type = default_type || typeof _default;
|
||||
|
||||
if (typeof input === "undefined") return _default;
|
||||
if (typeof _default === "string") return input as any;
|
||||
else if (typeof _default === "number") return parseInt(input) as any;
|
||||
else if (typeof _default === "boolean") return (input == "1" || input == "true") as any;
|
||||
else if (typeof _default === "undefined") return input as any;
|
||||
if (default_type === "string") return input as any;
|
||||
else if (default_type === "number") return parseInt(input) as any;
|
||||
else if (default_type === "boolean") return (input == "1" || input == "true") as any;
|
||||
else if (default_type === "undefined") return input as any;
|
||||
return JSON.parse(input) as any;
|
||||
}
|
||||
|
||||
|
@ -37,6 +48,35 @@ class StaticSettings {
|
|||
return JSON.stringify(input);
|
||||
}
|
||||
|
||||
protected static resolveKey<T>(key: SettingsKey<T>, _default: T, resolver: (key: string) => string | boolean, default_type?: string) : T {
|
||||
let value = resolver(key.key);
|
||||
if(!value) {
|
||||
/* trying fallbacks */
|
||||
for(const fallback of key.fallback_keys || []) {
|
||||
value = resolver(fallback);
|
||||
if(typeof(value) === "string") {
|
||||
/* fallback key succeeded */
|
||||
const importer = (key.fallback_imports || {})[fallback];
|
||||
if(importer)
|
||||
return importer(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(typeof(value) !== 'string')
|
||||
return _default;
|
||||
|
||||
return StaticSettings.transformStO(value as string, _default, default_type);
|
||||
}
|
||||
|
||||
protected static keyify<T>(key: string | SettingsKey<T>) : SettingsKey<T> {
|
||||
if(typeof(key) === "string")
|
||||
return {key: key};
|
||||
if(typeof(key) === "object" && key.key)
|
||||
return key;
|
||||
throw "key is not a key";
|
||||
}
|
||||
|
||||
protected _handle: StaticSettings;
|
||||
protected _staticPropsTag: JQuery;
|
||||
|
||||
|
@ -59,25 +99,98 @@ class StaticSettings {
|
|||
});
|
||||
}
|
||||
|
||||
static?<T>(key: string, _default?: T) : T {
|
||||
if(this._handle) return this._handle.static<T>(key, _default);
|
||||
let result = this._staticPropsTag.find("[key='" + key + "']");
|
||||
return StaticSettings.transformStO(result.length > 0 ? decodeURIComponent(result.last().attr("value")) : undefined, _default);
|
||||
static?<T>(key: string | SettingsKey<T>, _default?: T, default_type?: string) : T {
|
||||
if(this._handle) return this._handle.static<T>(key, _default, default_type);
|
||||
|
||||
key = StaticSettings.keyify(key);
|
||||
return StaticSettings.resolveKey(key, _default, key => {
|
||||
let result = this._staticPropsTag.find("[key='" + key + "']");
|
||||
if(result.length > 0)
|
||||
return decodeURIComponent(result.last().attr('value'));
|
||||
return false;
|
||||
}, default_type);
|
||||
}
|
||||
|
||||
deleteStatic(key: string) {
|
||||
deleteStatic<T>(key: string | SettingsKey<T>) {
|
||||
if(this._handle) {
|
||||
this._handle.deleteStatic(key);
|
||||
this._handle.deleteStatic<T>(key);
|
||||
return;
|
||||
}
|
||||
let result = this._staticPropsTag.find("[key='" + key + "']");
|
||||
|
||||
key = StaticSettings.keyify(key);
|
||||
let result = this._staticPropsTag.find("[key='" + key.key + "']");
|
||||
if(result.length != 0) result.detach();
|
||||
}
|
||||
}
|
||||
|
||||
class Settings extends StaticSettings {
|
||||
static readonly KEY_DISABLE_CONTEXT_MENU = "disableContextMenu";
|
||||
static readonly KEY_DISABLE_UNLOAD_DIALOG = "disableUnloadDialog";
|
||||
static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
|
||||
key: 'disableContextMenu',
|
||||
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier'
|
||||
};
|
||||
static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
|
||||
key: 'disableUnloadDialog',
|
||||
description: 'Disables the unload popup on side closing'
|
||||
};
|
||||
static readonly KEY_DISABLE_VOICE: SettingsKey<boolean> = {
|
||||
key: 'disableVoice',
|
||||
description: 'Disables the voice bridge. If disabled, the audio and codec workers aren\'t required anymore'
|
||||
};
|
||||
|
||||
/* Control bar */
|
||||
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = {
|
||||
key: 'mute_input'
|
||||
};
|
||||
static readonly KEY_CONTROL_MUTE_OUTPUT: SettingsKey<boolean> = {
|
||||
key: 'mute_output'
|
||||
};
|
||||
static readonly KEY_CONTROL_SHOW_QUERIES: SettingsKey<boolean> = {
|
||||
key: 'show_server_queries'
|
||||
};
|
||||
static readonly KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL: SettingsKey<boolean> = {
|
||||
key: 'channel_subscribe_all'
|
||||
};
|
||||
|
||||
/* Connect parameters */
|
||||
static readonly KEY_FLAG_CONNECT_DEFAULT: SettingsKey<boolean> = {
|
||||
key: 'connect_default'
|
||||
};
|
||||
static readonly KEY_CONNECT_ADDRESS: SettingsKey<string> = {
|
||||
key: 'connect_address'
|
||||
};
|
||||
static readonly KEY_CONNECT_PROFILE: SettingsKey<string> = {
|
||||
key: 'connect_profile'
|
||||
};
|
||||
static readonly KEY_CONNECT_USERNAME: SettingsKey<string> = {
|
||||
key: 'connect_username'
|
||||
};
|
||||
static readonly KEY_CONNECT_PASSWORD: SettingsKey<string> = {
|
||||
key: 'connect_password'
|
||||
};
|
||||
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
|
||||
key: 'connect_password_hashed'
|
||||
};
|
||||
|
||||
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey<ChannelSubscribeMode> = channel => {
|
||||
return {
|
||||
key: 'channel_subscribe_mode_' + channel.getChannelId()
|
||||
}
|
||||
};
|
||||
|
||||
static readonly KEYS = (() => {
|
||||
const result = [];
|
||||
|
||||
for(const key in Settings) {
|
||||
if(!key.toUpperCase().startsWith("KEY_"))
|
||||
continue;
|
||||
if(key.toUpperCase() == "KEYS")
|
||||
continue;
|
||||
|
||||
result.push(key);
|
||||
}
|
||||
|
||||
return result;
|
||||
})();
|
||||
|
||||
private static readonly UPDATE_DIRECT: boolean = true;
|
||||
private cacheGlobal = {};
|
||||
|
@ -96,37 +209,41 @@ class Settings extends StaticSettings {
|
|||
}, 5 * 1000);
|
||||
}
|
||||
|
||||
static_global?<T>(key: string, _default?: T) : T {
|
||||
let _static = this.static<string>(key);
|
||||
if(_static) return StaticSettings.transformStO(_static, _default);
|
||||
static_global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
const default_object = { seed: Math.random() } as any;
|
||||
let _static = this.static(key, default_object, typeof _default);
|
||||
if(_static !== default_object) return StaticSettings.transformStO(_static, _default);
|
||||
return this.global<T>(key, _default);
|
||||
}
|
||||
|
||||
global?<T>(key: string, _default?: T) : T {
|
||||
let result = this.cacheGlobal[key];
|
||||
return StaticSettings.transformStO(result, _default);
|
||||
global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]);
|
||||
}
|
||||
|
||||
server?<T>(key: string, _default?: T) : T {
|
||||
let result = this.cacheServer[key];
|
||||
return StaticSettings.transformStO(result, _default);
|
||||
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
|
||||
}
|
||||
|
||||
changeGlobal<T>(key: string, value?: T){
|
||||
if(this.cacheGlobal[key] == value) return;
|
||||
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
|
||||
key = Settings.keyify(key);
|
||||
|
||||
|
||||
if(this.cacheGlobal[key.key] == value) return;
|
||||
|
||||
this.updated = true;
|
||||
this.cacheGlobal[key] = StaticSettings.transformOtS(value);
|
||||
this.cacheGlobal[key.key] = StaticSettings.transformOtS(value);
|
||||
|
||||
if(Settings.UPDATE_DIRECT)
|
||||
this.save();
|
||||
}
|
||||
|
||||
changeServer<T>(key: string, value?: T) {
|
||||
if(this.cacheServer[key] == value) return;
|
||||
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
||||
key = Settings.keyify(key);
|
||||
|
||||
if(this.cacheServer[key.key] == value) return;
|
||||
|
||||
this.updated = true;
|
||||
this.cacheServer[key] = StaticSettings.transformOtS(value);
|
||||
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
||||
|
||||
if(Settings.UPDATE_DIRECT)
|
||||
this.save();
|
||||
|
|
|
@ -286,7 +286,7 @@ namespace sound {
|
|||
try {
|
||||
console.log(tr("Decoding data"));
|
||||
context.decodeAudioData(buffer, result => {
|
||||
console.log(tr("Got decoded data"));
|
||||
log.info(LogCategory.VOICE, tr("Got decoded data"));
|
||||
file.cached = result;
|
||||
play(sound, options);
|
||||
}, error => {
|
||||
|
|
|
@ -14,6 +14,12 @@ namespace ChannelType {
|
|||
}
|
||||
}
|
||||
|
||||
enum ChannelSubscribeMode {
|
||||
SUBSCRIBED,
|
||||
UNSUBSCRIBED,
|
||||
INHERITED
|
||||
}
|
||||
|
||||
class ChannelProperties {
|
||||
channel_order: number = 0;
|
||||
channel_name: string = "";
|
||||
|
@ -71,6 +77,9 @@ class ChannelEntry {
|
|||
private _cached_channel_description_promise_resolve: any = undefined;
|
||||
private _cached_channel_description_promise_reject: any = undefined;
|
||||
|
||||
private _flag_subscribed: boolean;
|
||||
private _subscribe_mode: ChannelSubscribeMode;
|
||||
|
||||
constructor(channelId, channelName, parent = null) {
|
||||
this.properties = new ChannelProperties();
|
||||
this.channelId = channelId;
|
||||
|
@ -359,17 +368,35 @@ class ChannelEntry {
|
|||
}
|
||||
|
||||
initializeListener() {
|
||||
const _this = this;
|
||||
this.channelTag().click(function () {
|
||||
_this.channelTree.onSelect(_this);
|
||||
});
|
||||
this.channelTag().dblclick(() => {
|
||||
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();
|
||||
|
@ -378,9 +405,9 @@ class ChannelEntry {
|
|||
return;
|
||||
}
|
||||
|
||||
_this.channelTree.onSelect(_this, true);
|
||||
_this.showContextMenu(event.pageX, event.pageY, () => {
|
||||
_this.channelTree.onSelect(undefined, true);
|
||||
this.channelTree.onSelect(this, true);
|
||||
this.showContextMenu(event.pageX, event.pageY, () => {
|
||||
this.channelTree.onSelect(undefined, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -424,13 +451,49 @@ class ChannelEntry {
|
|||
flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_TEMPORARY).granted(1);
|
||||
}
|
||||
|
||||
let trigger_close = true;
|
||||
spawn_context_menu(x, y, {
|
||||
type: MenuEntryType.ENTRY,
|
||||
name: tr("Show channel info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.selectInfo.open_popover()
|
||||
},
|
||||
icon: "client-about",
|
||||
visible: this.channelTree.client.selectInfo.is_popover()
|
||||
}, {
|
||||
type: MenuEntryType.HR,
|
||||
visible: this.channelTree.client.selectInfo.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-channel_switch",
|
||||
name: tr("<b>Switch to channel</b>"),
|
||||
callback: () => this.joinChannel()
|
||||
},
|
||||
MenuEntry.HR(),
|
||||
{
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-subscribe_to_channel",
|
||||
name: tr("<b>Subscribe to channel</b>"),
|
||||
callback: () => this.subscribe(),
|
||||
visible: !this.flag_subscribed
|
||||
},
|
||||
{
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-channel_unsubscribed",
|
||||
name: tr("<b>Unsubscribe from channel</b>"),
|
||||
callback: () => this.unsubscribe(),
|
||||
visible: this.flag_subscribed
|
||||
},
|
||||
{
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-subscribe_mode",
|
||||
name: tr("<b>Use inherited subscribe mode</b>"),
|
||||
callback: () => this.unsubscribe(true),
|
||||
visible: this.subscribe_mode != ChannelSubscribeMode.INHERITED
|
||||
},
|
||||
MenuEntry.HR(),
|
||||
{
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-channel_edit",
|
||||
|
@ -507,7 +570,7 @@ class ChannelEntry {
|
|||
invalidPermission: !channelCreate,
|
||||
callback: () => this.channelTree.spawnCreateChannel()
|
||||
},
|
||||
MenuEntry.CLOSE(on_close)
|
||||
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -621,7 +684,10 @@ class ChannelEntry {
|
|||
}
|
||||
} 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.channelTree.client.voiceConnection.codecSupported(this.properties.channel_codec) ? $.fn.hide : $.fn.show).apply(this.channelTag().find(".icons .icon_no_sound"));
|
||||
this.channelTag().find(".icons .icon_no_sound").toggle(!(
|
||||
this.channelTree.client.voiceConnection &&
|
||||
this.channelTree.client.voiceConnection.codecSupported(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")
|
||||
|
@ -646,6 +712,7 @@ class ChannelEntry {
|
|||
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");
|
||||
|
||||
|
@ -660,7 +727,7 @@ class ChannelEntry {
|
|||
else
|
||||
type = "green";
|
||||
|
||||
tag.addClass("client-channel_" + type + "_subscribed");
|
||||
tag.addClass("client-channel_" + type + (this._flag_subscribed ? "_subscribed" : ""));
|
||||
}
|
||||
|
||||
generate_bbcode() {
|
||||
|
@ -705,6 +772,66 @@ class ChannelEntry {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
async subscribe() : Promise<void> {
|
||||
if(this.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
|
||||
return;
|
||||
|
||||
this.subscribe_mode = ChannelSubscribeMode.SUBSCRIBED;
|
||||
|
||||
const connection = this.channelTree.client.getServerConnection();
|
||||
if(!this.flag_subscribed && connection)
|
||||
await connection.send_command('channelsubscribe', {
|
||||
'cid': this.getChannelId()
|
||||
});
|
||||
else
|
||||
this.flag_subscribed = false;
|
||||
}
|
||||
|
||||
async unsubscribe(inherited_subscription_mode?: boolean) : Promise<void> {
|
||||
const connection = this.channelTree.client.getServerConnection();
|
||||
let unsubscribe: boolean;
|
||||
|
||||
if(inherited_subscription_mode) {
|
||||
this.subscribe_mode = ChannelSubscribeMode.INHERITED;
|
||||
unsubscribe = this.flag_subscribed && !this.channelTree.client.controlBar.channel_subscribe_all;
|
||||
} else {
|
||||
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
|
||||
unsubscribe = this.flag_subscribed;
|
||||
}
|
||||
|
||||
if(unsubscribe) {
|
||||
if(connection)
|
||||
await connection.send_command('channelunsubscribe', {
|
||||
'cid': this.getChannelId()
|
||||
});
|
||||
else
|
||||
this.flag_subscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
get flag_subscribed() : boolean {
|
||||
return this._flag_subscribed;
|
||||
}
|
||||
set flag_subscribed(flag: boolean) {
|
||||
if(this._flag_subscribed == flag)
|
||||
return;
|
||||
|
||||
this._flag_subscribed = flag;
|
||||
this.updateChannelTypeIcon();
|
||||
}
|
||||
|
||||
get subscribe_mode() : ChannelSubscribeMode {
|
||||
return typeof(this._subscribe_mode) !== 'undefined' ? this._subscribe_mode : (this._subscribe_mode = settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), ChannelSubscribeMode.INHERITED));
|
||||
}
|
||||
|
||||
set subscribe_mode(mode: ChannelSubscribeMode) {
|
||||
if(this.subscribe_mode == mode)
|
||||
return;
|
||||
|
||||
this._subscribe_mode = mode;
|
||||
settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
|
||||
}
|
||||
}
|
||||
|
||||
//Global functions
|
||||
|
|
|
@ -266,28 +266,40 @@ class ClientEntry {
|
|||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
|
||||
const _this = this;
|
||||
|
||||
let trigger_close = true;
|
||||
spawn_context_menu(x, y,
|
||||
{
|
||||
type: MenuEntryType.ENTRY,
|
||||
name: tr("Show client info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.selectInfo.open_popover()
|
||||
},
|
||||
icon: "client-about",
|
||||
visible: this.channelTree.client.selectInfo.is_popover()
|
||||
}, {
|
||||
type: MenuEntryType.HR,
|
||||
visible: this.channelTree.client.selectInfo.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-change_nickname",
|
||||
name: tr("<b>Open text chat</b>"),
|
||||
callback: function () {
|
||||
chat.activeChat = _this.chat(true);
|
||||
callback: () => {
|
||||
chat.activeChat = this.chat(true);
|
||||
chat.focus();
|
||||
}
|
||||
}, {
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-poke",
|
||||
name: tr("Poke client"),
|
||||
callback: function () {
|
||||
callback: () => {
|
||||
createInputModal(tr("Poke client"), tr("Poke message:<br>"), text => true, result => {
|
||||
if(typeof(result) === "string") {
|
||||
//TODO tr
|
||||
console.log("Poking client " + _this.clientNickName() + " with message " + result);
|
||||
_this.channelTree.client.serverConnection.send_command("clientpoke", {
|
||||
clid: _this.clientId(),
|
||||
console.log("Poking client " + this.clientNickName() + " with message " + result);
|
||||
this.channelTree.client.serverConnection.send_command("clientpoke", {
|
||||
clid: this.clientId(),
|
||||
msg: result
|
||||
});
|
||||
|
||||
|
@ -298,13 +310,13 @@ class ClientEntry {
|
|||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-edit",
|
||||
name: tr("Change description"),
|
||||
callback: function () {
|
||||
callback: () => {
|
||||
createInputModal(tr("Change client description"), tr("New description:<br>"), text => true, result => {
|
||||
if(typeof(result) === "string") {
|
||||
//TODO tr
|
||||
console.log("Changing " + _this.clientNickName() + "'s description to " + result);
|
||||
_this.channelTree.client.serverConnection.send_command("clientedit", {
|
||||
clid: _this.clientId(),
|
||||
console.log("Changing " + this.clientNickName() + "'s description to " + result);
|
||||
this.channelTree.client.serverConnection.send_command("clientedit", {
|
||||
clid: this.clientId(),
|
||||
client_description: result
|
||||
});
|
||||
|
||||
|
@ -330,11 +342,11 @@ class ClientEntry {
|
|||
name: tr("Kick client from channel"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => {
|
||||
if(result) {
|
||||
if(typeof(result) !== 'boolean' || result) {
|
||||
//TODO tr
|
||||
console.log("Kicking client " + _this.clientNickName() + " from channel with reason " + result);
|
||||
_this.channelTree.client.serverConnection.send_command("clientkick", {
|
||||
clid: _this.clientId(),
|
||||
console.log("Kicking client " + this.clientNickName() + " from channel with reason " + result);
|
||||
this.channelTree.client.serverConnection.send_command("clientkick", {
|
||||
clid: this.clientId(),
|
||||
reasonid: ViewReasonId.VREASON_CHANNEL_KICK,
|
||||
reasonmsg: result
|
||||
});
|
||||
|
@ -348,11 +360,11 @@ class ClientEntry {
|
|||
name: tr("Kick client fom server"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Kick client from server"), tr("Kick reason:<br>"), text => true, result => {
|
||||
if(result) {
|
||||
if(typeof(result) !== 'boolean' || result) {
|
||||
//TODO tr
|
||||
console.log("Kicking client " + _this.clientNickName() + " from server with reason " + result);
|
||||
_this.channelTree.client.serverConnection.send_command("clientkick", {
|
||||
clid: _this.clientId(),
|
||||
console.log("Kicking client " + this.clientNickName() + " from server with reason " + result);
|
||||
this.channelTree.client.serverConnection.send_command("clientkick", {
|
||||
clid: this.clientId(),
|
||||
reasonid: ViewReasonId.VREASON_SERVER_KICK,
|
||||
reasonmsg: result
|
||||
});
|
||||
|
@ -411,7 +423,7 @@ class ClientEntry {
|
|||
});
|
||||
}
|
||||
},
|
||||
MenuEntry.CLOSE(on_close)
|
||||
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -661,19 +673,21 @@ class ClientEntry {
|
|||
chat(create: boolean = false) : ChatEntry {
|
||||
let chatName = "client_" + this.clientUid() + ":" + this.clientId();
|
||||
let c = chat.findChat(chatName);
|
||||
if((!c) && create) {
|
||||
if(!c && create) {
|
||||
c = chat.createChat(chatName);
|
||||
c.closeable = true;
|
||||
c.flag_closeable = true;
|
||||
c.name = this.clientNickName();
|
||||
c.owner_unique_id = this.properties.client_unique_identifier;
|
||||
|
||||
const _this = this;
|
||||
c.onMessageSend = function (text: string) {
|
||||
_this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, _this);
|
||||
c.onMessageSend = text => {
|
||||
this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this);
|
||||
};
|
||||
|
||||
c.onClose = function () : boolean {
|
||||
//TODO check online?
|
||||
_this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": _this.clientId()});
|
||||
c.onClose = () => {
|
||||
if(!c.flag_offline)
|
||||
this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": this.clientId()}, {process_result: false}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to notify chat participant (%o) that the chat has been closed. Error: %o"), this, error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -908,8 +922,22 @@ class MusicClientEntry extends ClientEntry {
|
|||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined): void {
|
||||
let trigger_close = true;
|
||||
spawn_context_menu(x, y,
|
||||
{
|
||||
type: MenuEntryType.ENTRY,
|
||||
name: tr("Show bot info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.selectInfo.open_popover()
|
||||
},
|
||||
icon: "client-about",
|
||||
visible: this.channelTree.client.selectInfo.is_popover()
|
||||
}, {
|
||||
type: MenuEntryType.HR,
|
||||
visible: this.channelTree.client.selectInfo.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
name: tr("<b>Change bot name</b>"),
|
||||
icon: "client-change_nickname",
|
||||
disabled: false,
|
||||
|
@ -1011,7 +1039,7 @@ class MusicClientEntry extends ClientEntry {
|
|||
name: tr("Kick client from channel"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Kick client from channel"), tr("Kick reason:<br>"), text => true, result => {
|
||||
if(result) {
|
||||
if(typeof(result) !== 'boolean' || result) {
|
||||
console.log(tr("Kicking client %o from channel with reason %o"), this.clientNickName(), result);
|
||||
this.channelTree.client.serverConnection.send_command("clientkick", {
|
||||
clid: this.clientId(),
|
||||
|
@ -1073,7 +1101,7 @@ class MusicClientEntry extends ClientEntry {
|
|||
},
|
||||
type: MenuEntryType.ENTRY
|
||||
},
|
||||
MenuEntry.CLOSE(on_close)
|
||||
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ class ControlBar {
|
|||
private _away: boolean;
|
||||
private _query_visible: boolean;
|
||||
private _awayMessage: string;
|
||||
private _channel_subscribe_all: boolean;
|
||||
|
||||
private codec_supported: boolean = false;
|
||||
private support_playback: boolean = false;
|
||||
|
@ -40,39 +41,43 @@ class ControlBar {
|
|||
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.bind(this));
|
||||
this.htmlTag.find(".btn_permissions").on('click', this.onPermission.bind(this));
|
||||
this.htmlTag.find(".btn_banlist").on('click', this.onBanlist.bind(this));
|
||||
this.htmlTag.find(".button-subscribe-mode").on('click', this.on_toggle_channel_subscribe_all.bind(this));
|
||||
this.htmlTag.find(".button-playlist-manage").on('click', this.on_playlist_manage.bind(this));
|
||||
|
||||
let dropdownify = (tag: JQuery) => {
|
||||
tag.find(".button-dropdown").on('click', () => {
|
||||
tag.addClass("displayed");
|
||||
}).hover(() => {
|
||||
console.log("Add");
|
||||
tag.addClass("displayed");
|
||||
}, () => {
|
||||
if(tag.find(".dropdown:hover").length > 0)
|
||||
return;
|
||||
console.log("Removed");
|
||||
tag.removeClass("displayed");
|
||||
});
|
||||
tag.on('mouseleave', () => {
|
||||
tag.removeClass("displayed");
|
||||
});
|
||||
};
|
||||
{
|
||||
let tokens = this.htmlTag.find(".btn_token");
|
||||
tokens.find(".button-dropdown").on('click', () => {
|
||||
tokens.find(".dropdown").addClass("displayed");
|
||||
});
|
||||
tokens.on('mouseleave', () => {
|
||||
tokens.find(".dropdown").removeClass("displayed");
|
||||
});
|
||||
dropdownify(tokens);
|
||||
|
||||
tokens.find(".btn_token_use").on('click', this.on_token_use.bind(this));
|
||||
tokens.find(".btn_token_list").on('click', this.on_token_list.bind(this));
|
||||
}
|
||||
{
|
||||
let away = this.htmlTag.find(".btn_away");
|
||||
away.find(".button-dropdown").on('click', () => {
|
||||
away.find(".dropdown").addClass("displayed");
|
||||
});
|
||||
away.on('mouseleave', () => {
|
||||
away.find(".dropdown").removeClass("displayed");
|
||||
});
|
||||
dropdownify(away);
|
||||
|
||||
away.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
|
||||
away.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
|
||||
}
|
||||
{
|
||||
let bookmark = this.htmlTag.find(".btn_bookmark");
|
||||
bookmark.find(".button-dropdown").on('click', () => {
|
||||
bookmark.find("> .dropdown").addClass("displayed");
|
||||
});
|
||||
bookmark.on('mouseleave', () => {
|
||||
bookmark.find("> .dropdown").removeClass("displayed");
|
||||
});
|
||||
dropdownify(bookmark);
|
||||
|
||||
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
|
||||
bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
|
||||
|
||||
|
@ -81,22 +86,30 @@ class ControlBar {
|
|||
}
|
||||
{
|
||||
let query = this.htmlTag.find(".btn_query");
|
||||
query.find(".button-dropdown").on('click', () => {
|
||||
query.find(".dropdown").addClass("displayed");
|
||||
});
|
||||
query.on('mouseleave', () => {
|
||||
query.find(".dropdown").removeClass("displayed");
|
||||
});
|
||||
dropdownify(query);
|
||||
|
||||
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
|
||||
query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
|
||||
query.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
|
||||
}
|
||||
|
||||
/* Mobile dropdowns */
|
||||
{
|
||||
const dropdown = this.htmlTag.find(".dropdown-audio");
|
||||
dropdownify(dropdown);
|
||||
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
|
||||
}
|
||||
{
|
||||
const dropdown = this.htmlTag.find(".dropdown-servertools");
|
||||
dropdownify(dropdown);
|
||||
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
|
||||
}
|
||||
|
||||
//Need an initialise
|
||||
this.muteInput = settings.static_global("mute_input", false);
|
||||
this.muteOutput = settings.static_global("mute_output", false);
|
||||
this.query_visible = settings.static_global("show_server_queries", false);
|
||||
this.muteInput = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false);
|
||||
this.muteOutput = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false);
|
||||
this.query_visible = settings.static_global(Settings.KEY_CONTROL_SHOW_QUERIES, false);
|
||||
this.channel_subscribe_all = settings.static_global(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, true);
|
||||
}
|
||||
|
||||
|
||||
|
@ -125,22 +138,20 @@ class ControlBar {
|
|||
this._muteInput = flag;
|
||||
|
||||
let tag = this.htmlTag.find(".btn_mute_input");
|
||||
if(flag) {
|
||||
if(!tag.hasClass("activated"))
|
||||
tag.addClass("activated");
|
||||
tag.find(".icon_x32").attr("class", "icon_x32 client-input_muted");
|
||||
} else {
|
||||
if(tag.hasClass("activated"))
|
||||
tag.removeClass("activated");
|
||||
tag.find(".icon_x32").attr("class", "icon_x32 client-capture");
|
||||
}
|
||||
const tag_icon = tag.find(".icon_x32, .icon");
|
||||
|
||||
tag.toggleClass('activated', flag)
|
||||
|
||||
tag_icon
|
||||
.toggleClass('client-input_muted', flag)
|
||||
.toggleClass('client-capture', !flag);
|
||||
|
||||
|
||||
if(this.handle.serverConnection.connected)
|
||||
if(this.handle.serverConnection.connected())
|
||||
this.handle.serverConnection.send_command("clientupdate", {
|
||||
client_input_muted: this._muteInput
|
||||
});
|
||||
settings.changeGlobal("mute_input", this._muteInput);
|
||||
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this._muteInput);
|
||||
this.updateMicrophoneRecordState();
|
||||
}
|
||||
|
||||
|
@ -150,22 +161,21 @@ class ControlBar {
|
|||
if(this._muteOutput == flag) return;
|
||||
this._muteOutput = flag;
|
||||
|
||||
let tag = this.htmlTag.find(".btn_mute_output");
|
||||
if(flag) {
|
||||
if(!tag.hasClass("activated"))
|
||||
tag.addClass("activated");
|
||||
tag.find(".icon_x32").attr("class", "icon_x32 client-output_muted");
|
||||
} else {
|
||||
if(tag.hasClass("activated"))
|
||||
tag.removeClass("activated");
|
||||
tag.find(".icon_x32").attr("class", "icon_x32 client-volume");
|
||||
}
|
||||
|
||||
if(this.handle.serverConnection.connected)
|
||||
let tag = this.htmlTag.find(".btn_mute_output");
|
||||
const tag_icon = tag.find(".icon_x32, .icon");
|
||||
|
||||
tag.toggleClass('activated', flag)
|
||||
|
||||
tag_icon
|
||||
.toggleClass('client-output_muted', flag)
|
||||
.toggleClass('client-volume', !flag);
|
||||
|
||||
if(this.handle.serverConnection.connected())
|
||||
this.handle.serverConnection.send_command("clientupdate", {
|
||||
client_output_muted: this._muteOutput
|
||||
});
|
||||
settings.changeGlobal("mute_output", this._muteOutput);
|
||||
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this._muteOutput);
|
||||
this.updateMicrophoneRecordState();
|
||||
}
|
||||
|
||||
|
@ -196,7 +206,8 @@ class ControlBar {
|
|||
|
||||
private updateMicrophoneRecordState() {
|
||||
let enabled = !this._muteInput && !this._muteOutput && !this._away;
|
||||
this.handle.voiceConnection.voiceRecorder.update(enabled);
|
||||
if(this.handle.voiceConnection)
|
||||
this.handle.voiceConnection.voiceRecorder.update(enabled);
|
||||
}
|
||||
|
||||
updateProperties() {
|
||||
|
@ -212,12 +223,13 @@ class ControlBar {
|
|||
}
|
||||
|
||||
updateVoice(targetChannel?: ChannelEntry) {
|
||||
if(!targetChannel) targetChannel = this.handle.getClient().currentChannel();
|
||||
if(!targetChannel)
|
||||
targetChannel = this.handle.getClient().currentChannel();
|
||||
let client = this.handle.getClient();
|
||||
|
||||
this.codec_supported = targetChannel ? this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec) : true;
|
||||
this.support_record = this.handle.voiceConnection.voice_send_support();
|
||||
this.support_playback = this.handle.voiceConnection.voice_playback_support();
|
||||
this.codec_supported = targetChannel ? this.handle.voiceConnection && this.handle.voiceConnection.codecSupported(targetChannel.properties.channel_codec) : true;
|
||||
this.support_record = this.handle.voiceConnection && this.handle.voiceConnection.voice_send_support();
|
||||
this.support_playback = this.handle.voiceConnection && this.handle.voiceConnection.voice_playback_support();
|
||||
|
||||
this.htmlTag.find(".btn_mute_input").prop("disabled", !this.codec_supported|| !this.support_playback || !this.support_record);
|
||||
this.htmlTag.find(".btn_mute_output").prop("disabled", !this.codec_supported || !this.support_playback);
|
||||
|
@ -421,7 +433,7 @@ class ControlBar {
|
|||
if(this._query_visible == flag) return;
|
||||
|
||||
this._query_visible = flag;
|
||||
settings.changeGlobal("show_server_queries", flag);
|
||||
settings.changeGlobal(Settings.KEY_CONTROL_SHOW_QUERIES, flag);
|
||||
this.update_query_visibility_button();
|
||||
this.handle.channelTree.toggle_server_queries(flag);
|
||||
}
|
||||
|
@ -432,12 +444,7 @@ class ControlBar {
|
|||
}
|
||||
|
||||
private update_query_visibility_button() {
|
||||
let tag = this.htmlTag.find(".btn_query_toggle");
|
||||
if(this._query_visible) {
|
||||
tag.addClass("activated");
|
||||
} else {
|
||||
tag.removeClass("activated");
|
||||
}
|
||||
this.htmlTag.find(".btn_query_toggle").toggleClass('activated', this._query_visible);
|
||||
}
|
||||
|
||||
private on_query_create() {
|
||||
|
@ -464,4 +471,33 @@ class ControlBar {
|
|||
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
|
||||
}
|
||||
}
|
||||
|
||||
get channel_subscribe_all() : boolean {
|
||||
return this._channel_subscribe_all;
|
||||
}
|
||||
|
||||
set channel_subscribe_all(flag: boolean) {
|
||||
if(this._channel_subscribe_all == flag)
|
||||
return;
|
||||
|
||||
this._channel_subscribe_all = flag;
|
||||
|
||||
this.htmlTag
|
||||
.find(".button-subscribe-mode")
|
||||
.toggleClass('activated', this._channel_subscribe_all)
|
||||
.find('.icon_x32')
|
||||
.toggleClass('client-unsubscribe_from_all_channels', !this._channel_subscribe_all)
|
||||
.toggleClass('client-subscribe_to_all_channels', this._channel_subscribe_all);
|
||||
|
||||
settings.changeGlobal(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, flag);
|
||||
|
||||
if(flag)
|
||||
this.handle.channelTree.subscribe_all_channels();
|
||||
else
|
||||
this.handle.channelTree.unsubscribe_all_channels();
|
||||
}
|
||||
|
||||
private on_toggle_channel_subscribe_all() {
|
||||
this.channel_subscribe_all = !this.channel_subscribe_all;
|
||||
}
|
||||
}
|
|
@ -53,7 +53,8 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
readonly handle: TSClient;
|
||||
|
||||
private current_selected?: AvailableTypes;
|
||||
private _htmlTag: JQuery<HTMLElement>;
|
||||
private _tag: JQuery<HTMLElement>;
|
||||
private _tag_content: JQuery<HTMLElement>;
|
||||
private _tag_info: JQuery<HTMLElement>;
|
||||
private _tag_banner: JQuery<HTMLElement>;
|
||||
|
||||
|
@ -63,9 +64,10 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
|
||||
constructor(client: TSClient, htmlTag: JQuery<HTMLElement>) {
|
||||
this.handle = client;
|
||||
this._htmlTag = htmlTag;
|
||||
this._tag_info = htmlTag.find(".container-select-info");
|
||||
this._tag_banner = htmlTag.find(".container-banner");
|
||||
this._tag = htmlTag;
|
||||
this._tag_content = htmlTag.find("> .select_info");
|
||||
this._tag_info = this._tag_content.find(".container-select-info");
|
||||
this._tag_banner = this._tag_content.find(".container-banner");
|
||||
|
||||
this.managers.push(new MusicInfoManager());
|
||||
this.managers.push(new ClientInfoManager());
|
||||
|
@ -73,10 +75,24 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
this.managers.push(new ServerInfoManager());
|
||||
|
||||
this.banner_manager = new Hostbanner(client, this._tag_banner);
|
||||
|
||||
this._tag.find("button.close").on('click', () => {
|
||||
this._tag.toggleClass('shown', false);
|
||||
});
|
||||
}
|
||||
|
||||
handle_resize() {
|
||||
/* test if the popover isn't a popover anymore */
|
||||
if(this._tag.hasClass('shown')) {
|
||||
this._tag.removeClass('shown');
|
||||
if(this.is_popover())
|
||||
this._tag.addClass('shown');
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentSelected(entry: AvailableTypes) {
|
||||
if(this.current_selected == entry) return;
|
||||
|
||||
if(this._current_manager) {
|
||||
(this._current_manager as InfoManager<AvailableTypes>).finalizeFrame(this.current_selected, this._tag_info);
|
||||
this._current_manager = null;
|
||||
|
@ -112,7 +128,20 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
|||
|
||||
current_manager() { return this._current_manager; }
|
||||
|
||||
html_tag() { return this._htmlTag; }
|
||||
html_tag() { return this._tag_content; }
|
||||
|
||||
is_popover() : boolean {
|
||||
return !this._tag.is(':visible') || this._tag.hasClass('shown');
|
||||
}
|
||||
|
||||
open_popover() {
|
||||
this._tag.toggleClass('shown', true);
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
Image: typeof HTMLImageElement;
|
||||
HTMLImageElement: typeof HTMLImageElement;
|
||||
}
|
||||
|
||||
class Hostbanner {
|
||||
|
@ -136,9 +165,20 @@ class Hostbanner {
|
|||
|
||||
if(tag) {
|
||||
tag.then(element => {
|
||||
this.html_tag.empty();
|
||||
const children = this.html_tag.children();
|
||||
this.html_tag.append(element).removeClass("disabled");
|
||||
|
||||
/* allow the new image be loaded from cache URL */
|
||||
{
|
||||
children
|
||||
.css('z-index', '2')
|
||||
.css('position', 'absolute')
|
||||
.css('height', '100%')
|
||||
.css('width', '100%');
|
||||
setTimeout(() => {
|
||||
children.detach();
|
||||
}, 250);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.warn(tr("Failed to load hostbanner: %o"), error);
|
||||
this.html_tag.empty().addClass("disabled");
|
||||
|
@ -159,44 +199,63 @@ class Hostbanner {
|
|||
for(let key in server.properties)
|
||||
properties["property_" + key] = server.properties[key];
|
||||
|
||||
properties["hostbanner_gfx_url"] = server.properties.virtualserver_hostbanner_gfx_url;
|
||||
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
|
||||
const update_interval = Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60);
|
||||
const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
|
||||
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
|
||||
try {
|
||||
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
|
||||
if(url.search.length == 0)
|
||||
properties["cache_tag"] = "?_ts=" + update_timestamp;
|
||||
properties["hostbanner_gfx_url"] += "?_ts=" + update_timestamp;
|
||||
else
|
||||
properties["cache_tag"] = "&_ts=" + update_timestamp;
|
||||
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
|
||||
} catch(error) {
|
||||
console.warn(tr("Failed to parse banner URL: %o"), error);
|
||||
properties["cache_tag"] = "&_ts=" + update_timestamp;
|
||||
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
|
||||
}
|
||||
|
||||
this.updater = setTimeout(() => this.update(), update_interval * 1000);
|
||||
} else {
|
||||
properties["cache_tag"] = "";
|
||||
}
|
||||
|
||||
|
||||
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
|
||||
console.debug(tr("Hostbanner has been loaded"));
|
||||
return Promise.resolve(rendered);
|
||||
/*
|
||||
const image = rendered.find("img");
|
||||
return new Promise<JQuery<HTMLElement>>((resolve, reject) => {
|
||||
const node_image = image[0] as HTMLImageElement;
|
||||
node_image.onload = () => {
|
||||
console.debug(tr("Hostbanner has been loaded"));
|
||||
if(server.properties.virtualserver_hostbanner_gfx_interval > 0)
|
||||
this.updater = setTimeout(() => this.update(), Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60) * 1000);
|
||||
resolve(rendered);
|
||||
};
|
||||
node_image.onerror = event => {
|
||||
reject(event);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
if(window.fetch) {
|
||||
return (async () => {
|
||||
const start = Date.now();
|
||||
|
||||
const tag_image = rendered.find(".hostbanner-image");
|
||||
|
||||
_fetch:
|
||||
try {
|
||||
const result = await fetch(properties["hostbanner_gfx_url"]);
|
||||
|
||||
if(!result.ok) {
|
||||
if(result.type === 'opaque' || result.type === 'opaqueredirect') {
|
||||
log.warn(LogCategory.SERVER, tr("Could not load hostbanner because 'Access-Control-Allow-Origin' isnt valid!"));
|
||||
break _fetch;
|
||||
}
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(await result.blob());
|
||||
tag_image.css('background-image', 'url(' + url + ')');
|
||||
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
|
||||
|
||||
if(URL.revokeObjectURL) {
|
||||
setTimeout(() => {
|
||||
log.debug(LogCategory.SERVER, tr("Revoked hostbanner url %s"), url);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 10000);
|
||||
}
|
||||
} catch(error) {
|
||||
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
|
||||
}
|
||||
return rendered;
|
||||
})();
|
||||
} else {
|
||||
console.debug(tr("Hostbanner has been loaded"));
|
||||
return Promise.resolve(rendered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,6 +292,7 @@ class ClientInfoManager extends InfoManager<ClientEntry> {
|
|||
properties["client_onlinetime"] = formatDate(client.calculateOnlineTime());
|
||||
properties["sound_volume"] = client.audioController.volume * 100;
|
||||
properties["client_is_query"] = client.properties.client_type == ClientType.CLIENT_QUERY;
|
||||
properties["client_is_web"] = client.properties.client_type_exact == ClientType.CLIENT_WEB;
|
||||
|
||||
properties["group_server"] = [];
|
||||
for(let groupId of client.assignedServerGroupIds()) {
|
||||
|
@ -299,8 +359,12 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
|
|||
|
||||
|
||||
{
|
||||
let requestUpdate = rendered.find(".btn_update");
|
||||
requestUpdate.prop("disabled", !server.shouldUpdateProperties());
|
||||
const disabled = !server.shouldUpdateProperties();
|
||||
let requestUpdate = rendered.find(".button-update");
|
||||
requestUpdate
|
||||
.prop("disabled", disabled)
|
||||
.toggleClass('btn-success', !disabled)
|
||||
.toggleClass('btn-danger', disabled);
|
||||
|
||||
requestUpdate.click(() => {
|
||||
server.updateProperties();
|
||||
|
@ -308,7 +372,10 @@ class ServerInfoManager extends InfoManager<ServerEntry> {
|
|||
});
|
||||
|
||||
this.registerTimer(setTimeout(function () {
|
||||
requestUpdate.prop("disabled", false);
|
||||
requestUpdate
|
||||
.prop("disabled", false)
|
||||
.toggleClass('btn-success', true)
|
||||
.toggleClass('btn-danger', false);
|
||||
}, server.nextInfoRequest - Date.now()));
|
||||
}
|
||||
|
||||
|
|
|
@ -31,11 +31,11 @@ namespace Modals {
|
|||
input_nickname.attr("placeholder", "");
|
||||
|
||||
let address = input_address.val().toString();
|
||||
settings.changeGlobal("connect_address", address);
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
|
||||
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
|
||||
|
||||
let nickname = input_nickname.val().toString();
|
||||
settings.changeGlobal("connect_name", nickname);
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
|
||||
let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32;
|
||||
|
||||
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
|
||||
|
@ -48,8 +48,8 @@ namespace Modals {
|
|||
}
|
||||
};
|
||||
|
||||
input_nickname.val(settings.static_global("connect_name", undefined));
|
||||
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global("connect_address", defaultHost.url));
|
||||
input_nickname.val(settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined));
|
||||
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
|
||||
input_address
|
||||
.on("keyup", () => updateFields())
|
||||
.on('keydown', event => {
|
||||
|
@ -150,7 +150,7 @@ namespace Modals {
|
|||
},
|
||||
|
||||
width: '70%',
|
||||
//closeable: false
|
||||
//flag_closeable: false
|
||||
});
|
||||
connectModal.open();
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -135,7 +135,21 @@ class ServerEntry {
|
|||
}
|
||||
|
||||
spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) {
|
||||
let trigger_close = true;
|
||||
spawn_context_menu(x, y, {
|
||||
type: MenuEntryType.ENTRY,
|
||||
name: tr("Show server info"),
|
||||
callback: () => {
|
||||
trigger_close = false;
|
||||
this.channelTree.client.selectInfo.open_popover()
|
||||
},
|
||||
icon: "client-about",
|
||||
visible: this.channelTree.client.selectInfo.is_popover()
|
||||
}, {
|
||||
type: MenuEntryType.HR,
|
||||
visible: this.channelTree.client.selectInfo.is_popover(),
|
||||
name: ''
|
||||
}, {
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon: "client-virtualserver_edit",
|
||||
name: tr("Edit"),
|
||||
|
@ -162,7 +176,7 @@ class ServerEntry {
|
|||
createInfoModal(tr("Buddy invite URL"), tr("Your buddy invite URL:<br>") + url + tr("<bt>This has been copied to your clipboard.")).open();
|
||||
}
|
||||
},
|
||||
MenuEntry.CLOSE(on_close)
|
||||
MenuEntry.CLOSE(() => (trigger_close ? on_close : () => {})())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -755,4 +755,46 @@ class ChannelTree {
|
|||
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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -40,11 +40,14 @@ var TabFunctions = {
|
|||
let content = $.spawn("div");
|
||||
content.addClass("tab-content");
|
||||
|
||||
content.append($.spawn("div").addClass("height-watcher"));
|
||||
|
||||
let silentContent = $.spawn("div");
|
||||
silentContent.addClass("tab-content-invisible");
|
||||
|
||||
/* add some kind of min height */
|
||||
const update_height = () => {
|
||||
const height_watcher = tag.find("> .tab-content .height-watcher");
|
||||
const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content");
|
||||
console.error(entries);
|
||||
let max_height = 0;
|
||||
|
@ -56,13 +59,7 @@ var TabFunctions = {
|
|||
max_height = height;
|
||||
});
|
||||
|
||||
console.error("HIGHT: " + max_height);
|
||||
entries.each((_, _e) => {
|
||||
const entry = $(_e);
|
||||
entry.animate({
|
||||
'min-height': max_height + "px"
|
||||
}, 250);
|
||||
})
|
||||
height_watcher.css('min-height', max_height + "px");
|
||||
};
|
||||
|
||||
template.find("x-entry").each( (_, _entry) => {
|
||||
|
|
|
@ -144,6 +144,7 @@ class VoiceConnection {
|
|||
private vpacketId: number = 0;
|
||||
private chunkVPacketId: number = 0;
|
||||
private send_task: NodeJS.Timer;
|
||||
private _tag_favicon: JQuery;
|
||||
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
@ -171,6 +172,7 @@ class VoiceConnection {
|
|||
});
|
||||
|
||||
this.send_task = setInterval(this.sendNextVoicePacket.bind(this), 20);
|
||||
this._tag_favicon = $("head link[rel='icon']");
|
||||
}
|
||||
|
||||
native_encoding_supported() : boolean {
|
||||
|
@ -463,6 +465,8 @@ class VoiceConnection {
|
|||
|
||||
if(this.dataChannel)
|
||||
this.sendVoicePacket(new Uint8Array(0), this.current_channel_codec()); //TODO Use channel codec!
|
||||
|
||||
this._tag_favicon.attr('href', "img/favicon/teacup.png");
|
||||
}
|
||||
|
||||
private handleVoiceStarted() {
|
||||
|
@ -470,5 +474,6 @@ class VoiceConnection {
|
|||
|
||||
if(this.client && this.client.getClient())
|
||||
this.client.getClient().speaking = true;
|
||||
this._tag_favicon.attr('href', "img/favicon/speaking.png");
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
declare namespace WebAssembly {
|
||||
export function instantiateStreaming(stream: Promise<Response>, imports?: any) : Promise<ResultObject>;
|
||||
}
|
||||
declare function postMessage(message: any): void;
|
||||
|
||||
const prefix = "[POWWorker] ";
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 9a1c31f27fcac129fa3503c2c1d2096c126d3fd2
|
||||
Subproject commit 23f9aca6b6dc1ffccd20d6da04953776a1882f2b
|
|
@ -0,0 +1 @@
|
|||
C:/Users/WolverinDEV/TeaSpeak/TeaWeb/vendor/jquery/jquery.min.js
|
File diff suppressed because one or more lines are too long
|
@ -1,17 +1,29 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
|
||||
min-height: 250px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
bottom: 40px;
|
||||
top: 10px;
|
||||
|
||||
transition: all .5s linear;
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
||||
display: flex; flex-direction: column; resize: both;
|
||||
|
|
Loading…
Reference in New Issue