* 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 changelog
canary
WolverinDEV 2019-03-17 12:44:11 +01:00 committed by GitHub
parent 9818385a29
commit 4d983dc36a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 4425 additions and 895 deletions

View File

@ -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
View File

@ -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);
}
}

View File

@ -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",

View File

@ -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

35
setup_windows.md Normal file
View File

@ -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

View File

@ -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}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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

View File

@ -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;

View File

@ -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; }

View File

@ -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);

View File

@ -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);
}
}
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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

1504
shared/js/i18n/country.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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 {

View File

@ -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;
}
}
}

View File

@ -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);
});

View File

@ -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 + ")");

View File

@ -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;

View File

@ -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";
}

View File

@ -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();

View File

@ -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 => {

View File

@ -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

View File

@ -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 : () => {})())
);
}

View File

@ -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;
}
}

View File

@ -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()));
}

View File

@ -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

View File

@ -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 : () => {})())
);
}

View File

@ -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);
});
}
}

View File

@ -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) => {

View File

@ -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");
}
}

View File

@ -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] ";

2
vendor/bbcode vendored

@ -1 +1 @@
Subproject commit 9a1c31f27fcac129fa3503c2c1d2096c126d3fd2
Subproject commit 23f9aca6b6dc1ffccd20d6da04953776a1882f2b

1
vendor/jqueryjquery.min.js vendored Symbolic link
View File

@ -0,0 +1 @@
C:/Users/WolverinDEV/TeaSpeak/TeaWeb/vendor/jquery/jquery.min.js

File diff suppressed because one or more lines are too long

View File

@ -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;