diff --git a/files.php b/files.php index 92983355..97b7dd72 100644 --- a/files.php +++ b/files.php @@ -9,6 +9,24 @@ "path" => "./", "local-path" => "./shared/html/" ], + + [ /* javascript loader */ + "type" => "js", + "search-pattern" => "/.*\.js$/", + "build-target" => "dev", + + "path" => "loader/", + "local-path" => "./shared/loader/" + ], + [ /* javascript loader for releases */ + "type" => "js", + "search-pattern" => "/.*loader_[\S]+.min.js$/", + "build-target" => "rel", + + "path" => "loader/", + "local-path" => "./shared/generated/" + ], + [ /* shared javascript files (WebRTC adapter) */ "type" => "js", "search-pattern" => "/.*\.js$/", @@ -17,6 +35,7 @@ "path" => "adapter/", "local-path" => "./shared/adapter/" ], + [ /* shared javascript files (development mode only) */ "type" => "js", "search-pattern" => "/.*\.js$/", @@ -36,6 +55,7 @@ "local-path" => "./shared/js/", "req-parm" => ["--mappings"] ], + [ /* shared generated worker codec */ "type" => "js", "search-pattern" => "/(WorkerPOW.js)$/", @@ -231,16 +251,6 @@ "path" => "js/", "local-path" => "./web/generated/" ], - [ /* Add the shared generated files. Exclude the shared file because we're including it already */ - "web-only" => true, - "type" => "js", - "search-pattern" => "/.*\.js$/", - "search-exclude" => "/shared\.js(.map)?$/", - "build-target" => "rel", - - "path" => "js/", - "local-path" => "./shared/generated/" - ], [ /* web css files */ "web-only" => true, "type" => "css", diff --git a/package.json b/package.json index 1069de96..2598dbfd 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "ttsc": "ttsc", "csso": "csso", "rebuild-structure-web-dev": "php files.php generate web dev", - "minify-web-rel-file": "minify web/generated/client.js --outFile web/generated/client.min.js --evaluate --removeDebugger --undefinedToVoid --mangle.keepClassName --deadcode.keepFnArgs" + "minify-web-rel-file": "terser --compress --mangle --ecma 6 --keep_classnames --keep_fnames --output" }, "author": "TeaSpeak (WolverinDEV)", "license": "ISC", @@ -25,15 +25,15 @@ "@types/node": "^12.7.2", "@types/sha256": "^0.2.0", "@types/websocket": "0.0.40", - "babel-minify": "^0.5.1", + "clean-css": "^4.2.1", "csso-cli": "^2.0.2", "gulp": "^4.0.2", "sass": "^1.22.10", "sha256": "^0.2.0", + "terser": "^4.2.1", "ttypescript": "^1.5.7", "typescript": "^3.5.3", - "wat2wasm": "^1.0.2", - "clean-css": "^4.2.1" + "wat2wasm": "^1.0.2" }, "repository": { "type": "git", diff --git a/scripts/build_declarations.sh b/scripts/build_declarations.sh index 747d5b21..9e97fc26 100755 --- a/scripts/build_declarations.sh +++ b/scripts/build_declarations.sh @@ -61,4 +61,7 @@ fi #Last but not least the client imports generate_link shared/declarations/exports.d.ts web/declarations/imports_shared.d.ts -generate_link shared/declarations/exports.d.ts client/declarations/imports_shared.d.ts \ No newline at end of file +generate_link shared/declarations/exports_loader_app.d.ts web/declarations/imports_shared_loader.d.ts + +generate_link shared/declarations/exports.d.ts client/declarations/imports_shared.d.ts +generate_link shared/declarations/exports_loader_app.d.ts client/declarations/imports_shared_loader.d.ts \ No newline at end of file diff --git a/shared/css/generate_packed.sh b/shared/css/generate_packed.sh new file mode 100755 index 00000000..ec0ba838 --- /dev/null +++ b/shared/css/generate_packed.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +cd $(dirname $0) +#find css/static/ -name '*.css' -exec cat {} \; | npm run csso -- --output `pwd`/generated/static/base.css + +#File order +files=( + "css/static/main-layout.css" + "css/static/helptag.css" + "css/static/scroll.css" + "css/static/channel-tree.css" + "css/static/ts/tab.css" + "css/static/ts/chat.css" + "css/static/ts/icons.css" + "css/static/ts/icons_em.css" + "css/static/ts/country.css" + "css/static/general.css" + "css/static/modal.css" + "css/static/modals.css" + "css/static/modal-about.css" + "css/static/modal-avatar.css" + "css/static/modal-icons.css" + "css/static/modal-bookmarks.css" + "css/static/modal-connect.css" + "css/static/modal-channel.css" + "css/static/modal-query.css" + "css/static/modal-invite.css" + "css/static/modal-playlist.css" + "css/static/modal-banlist.css" + "css/static/modal-bancreate.css" + "css/static/modal-clientinfo.css" + "css/static/modal-serverinfo.css" + "css/static/modal-identity.css" + "css/static/modal-settings.css" + "css/static/modal-poke.css" + "css/static/modal-server.css" + "css/static/modal-keyselect.css" + "css/static/modal-permissions.css" + "css/static/modal-group-assignment.css" + "css/static/music/info_plate.css" + "css/static/frame/SelectInfo.css" + "css/static/control_bar.css" + "css/static/context_menu.css" + "css/static/frame-chat.css" + "css/static/connection_handlers.css" + "css/static/server-log.css" + "css/static/htmltags.css" + "css/static/hostbanner.css" + "css/static/menu-bar.css" +) + +target_file=`pwd`/../generated/static/base.css +echo "/* Auto generated merged CSS file */" > ${target_file} +for file in "${files[@]}"; do + if [[ ${file} =~ css/* ]]; then + file="./${file:4}" + fi + cat ${file} >> ${target_file} +done + +cat ${target_file} | npm run csso -- --output `pwd`/../generated/static/base.css \ No newline at end of file diff --git a/shared/css/loader/loader.scss b/shared/css/loader/loader.scss index 34e70e0e..f9ec5b67 100644 --- a/shared/css/loader/loader.scss +++ b/shared/css/loader/loader.scss @@ -1,300 +1,362 @@ -$thickness : 5px; -$duration : 2500; -$delay : $duration/6; +$thickness: 5px; +$duration: 2500; +$delay: $duration/6; $background: #222222; -@mixin polka($size, $dot, $base, $accent){ - background: $base; - background-image: radial-gradient($accent $dot, transparent 0); - background-size:$size $size; - background-position: 0 -2.5px; +@mixin polka($size, $dot, $base, $accent) { + background: $base; + background-image: radial-gradient($accent $dot, transparent 0); + background-size: $size $size; + background-position: 0 -2.5px; } .loader { - margin: 0; + margin: 0; - display: block; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; + display: block; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; - z-index: 900; - text-align: center; + z-index: 900; + text-align: center; } .loader .half { - position: fixed; - background: #222222; - top: 0; - bottom: 0; - width: 50%; - height: 100%; + position: fixed; + background: #222222; + top: 0; + bottom: 0; + width: 50%; + height: 100%; } .loader .half.right { - right: 0; + right: 0; } + .loader .half.left { - left: 0; + left: 0; } .bookshelf_wrapper { - position: relative; - top: 40%; - left: 50%; - transform: translate(-50%, -50%); + position: relative; + top: 40%; + left: 50%; + transform: translate(-50%, -50%); } .books_list { - margin: 0 auto; - width: 300px; - padding: 0; + margin: 0 auto; + width: 300px; + padding: 0; } .book_item { - position: absolute; - top: -120px; - box-sizing: border-box; - list-style: none; - width: 40px; - height: 120px; - opacity: 0; - background-color: #1e6cc7; - border: $thickness solid white; - transform-origin: bottom left; - transform: translateX(300px); - animation: travel #{$duration}ms linear infinite; + position: absolute; + top: -120px; + box-sizing: border-box; + list-style: none; + width: 40px; + height: 120px; + opacity: 0; + background-color: #1e6cc7; + border: $thickness solid white; + transform-origin: bottom left; + transform: translateX(300px); + animation: travel #{$duration}ms linear infinite; - &.first { - top: -140px; - height: 140px; + &.first { + top: -140px; + height: 140px; - &:before, - &:after { - content:''; - position: absolute; - top: 10px; - left: 0; - width: 100%; - height: $thickness; - background-color: white; - } + &:before, + &:after { + content: ''; + position: absolute; + top: 10px; + left: 0; + width: 100%; + height: $thickness; + background-color: white; + } - &:after { - top: initial; - bottom: 10px; - } - } + &:after { + top: initial; + bottom: 10px; + } + } - &.second, - &.fifth { - &:before, - &:after { - box-sizing: border-box; - content:''; - position: absolute; - top: 10px; - left: 0; - width: 100%; - height: $thickness*3.5; - border-top: $thickness solid white; - border-bottom: $thickness solid white; - } + &.second, + &.fifth { + &:before, + &:after { + box-sizing: border-box; + content: ''; + position: absolute; + top: 10px; + left: 0; + width: 100%; + height: $thickness*3.5; + border-top: $thickness solid white; + border-bottom: $thickness solid white; + } - &:after { - top: initial; - bottom: 10px; - } - } + &:after { + top: initial; + bottom: 10px; + } + } - &.third { - &:before, - &:after { - box-sizing: border-box; - content:''; - position: absolute; - top: 10px; - left: 9px; - width: 12px; - height: 12px; - border-radius: 50%; - border: $thickness solid white; - } + &.third { + &:before, + &:after { + box-sizing: border-box; + content: ''; + position: absolute; + top: 10px; + left: 9px; + width: 12px; + height: 12px; + border-radius: 50%; + border: $thickness solid white; + } - &:after { - top: initial; - bottom: 10px; - } - } + &:after { + top: initial; + bottom: 10px; + } + } - &.fourth { - top: -130px; - height: 130px; + &.fourth { + top: -130px; + height: 130px; - &:before { - box-sizing: border-box; - content:''; - position: absolute; - top: 46px; - left: 0; - width: 100%; - height: $thickness*3.5; - border-top: $thickness solid white; - border-bottom: $thickness solid white; - } - } + &:before { + box-sizing: border-box; + content: ''; + position: absolute; + top: 46px; + left: 0; + width: 100%; + height: $thickness*3.5; + border-top: $thickness solid white; + border-bottom: $thickness solid white; + } + } - &.fifth { - top: -100px; - height: 100px; - } + &.fifth { + top: -100px; + height: 100px; + } - &.sixth { - top: -140px; - height: 140px; + &.sixth { + top: -140px; + height: 140px; - &:before { - box-sizing: border-box; - content:''; - position: absolute; - bottom: 31px; - left: 0px; - width: 100%; - height: $thickness; - background-color: white; - } + &:before { + box-sizing: border-box; + content: ''; + position: absolute; + bottom: 31px; + left: 0px; + width: 100%; + height: $thickness; + background-color: white; + } - &:after { - box-sizing: border-box; - content:''; - position: absolute; - bottom: 10px; - left: 9px; - width: 12px; - height: 12px; - border-radius: 50%; - border: $thickness solid white; - } - } + &:after { + box-sizing: border-box; + content: ''; + position: absolute; + bottom: 10px; + left: 9px; + width: 12px; + height: 12px; + border-radius: 50%; + border: $thickness solid white; + } + } - &:nth-child(2) { - animation-delay: #{$delay*1}ms; - } + &:nth-child(2) { + animation-delay: #{$delay*1}ms; + } - &:nth-child(3) { - animation-delay: #{$delay*2}ms; - } + &:nth-child(3) { + animation-delay: #{$delay*2}ms; + } - &:nth-child(4) { - animation-delay: #{$delay*3}ms; - } + &:nth-child(4) { + animation-delay: #{$delay*3}ms; + } - &:nth-child(5) { - animation-delay: #{$delay*4}ms; - } + &:nth-child(5) { + animation-delay: #{$delay*4}ms; + } - &:nth-child(6) { - animation-delay: #{$delay*5}ms; - } + &:nth-child(6) { + animation-delay: #{$delay*5}ms; + } } .shelf { - width: 300px; - height: $thickness; - margin: 0 auto; - background-color: white; - position: relative; + width: 300px; + height: $thickness; + margin: 0 auto; + background-color: white; + position: relative; - &:before, - &:after { - content:''; - position : absolute; - width: 100%; - height: 100%; - @include polka(10px, 30%, $background, rgba(255,255,255,0.5)); - top: 200%; - left: 5%; - animation: move #{$duration/10}ms linear infinite; - } + &:before, + &:after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + @include polka(10px, 30%, $background, rgba(255, 255, 255, 0.5)); + top: 200%; + left: 5%; + animation: move #{$duration/10}ms linear infinite; + } - &:after { - top: 400%; - left: 7.5%; - } + &:after { + top: 400%; + left: 7.5%; + } } @keyframes move { - from { - background-position-x: 0; - } + from { + background-position-x: 0; + } - to { - background-position-x: 10px; - } + to { + background-position-x: 10px; + } } @keyframes travel { - 0% { - opacity: 0; - transform: translateX(300px) rotateZ(0deg) scaleY(1); - } + 0% { + opacity: 0; + transform: translateX(300px) rotateZ(0deg) scaleY(1); + } - 6.5% { - transform: translateX(279.5px) rotateZ(0deg) scaleY(1.1); - } + 6.5% { + transform: translateX(279.5px) rotateZ(0deg) scaleY(1.1); + } - 8.8% { - transform: translateX(273.6px) rotateZ(0deg) scaleY(1); - } + 8.8% { + transform: translateX(273.6px) rotateZ(0deg) scaleY(1); + } - 10% { - opacity: 1; - transform: translateX(270px) rotateZ(0deg); - } + 10% { + opacity: 1; + transform: translateX(270px) rotateZ(0deg); + } - 17.6% { - transform: translateX(247.2px) rotateZ(-30deg); - } + 17.6% { + transform: translateX(247.2px) rotateZ(-30deg); + } - 45% { - transform: translateX(165px) rotateZ(-30deg); - } + 45% { + transform: translateX(165px) rotateZ(-30deg); + } - 49.5% { - transform: translateX(151.5px) rotateZ(-45deg); - } + 49.5% { + transform: translateX(151.5px) rotateZ(-45deg); + } - 61.5% { - transform: translateX(115.5px) rotateZ(-45deg); - } + 61.5% { + transform: translateX(115.5px) rotateZ(-45deg); + } - 67% { - transform: translateX(99px) rotateZ(-60deg); - } + 67% { + transform: translateX(99px) rotateZ(-60deg); + } - 76% { - transform: translateX(72px) rotateZ(-60deg); - } + 76% { + transform: translateX(72px) rotateZ(-60deg); + } - 83.5% { - opacity: 1; - transform: translateX(49.5px) rotateZ(-90deg); - } + 83.5% { + opacity: 1; + transform: translateX(49.5px) rotateZ(-90deg); + } - 90% { - opacity: 0; - } + 90% { + opacity: 0; + } - 100% { - opacity: 0; - transform: translateX(0px) rotateZ(-90deg); - } + 100% { + opacity: 0; + transform: translateX(0px) rotateZ(-90deg); + } +} + +/* Automated loader timeout */ +$loader_timeout: 2.5s; +.loader:not(.started) { + &, & > .half, & > .bookshelf_wrapper { + -moz-animation: _loader_hide 0s ease-in $loader_timeout forwards; + -webkit-animation: _loader_hide 0s ease-in $loader_timeout forwards; + -o-animation: _loader_hide 0s ease-in $loader_timeout forwards; + animation: _loader_hide 0s ease-in $loader_timeout forwards; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; + } +} + +.loader:not(.started) + #critical-load { + display: block !important; + opacity: 0; + + -moz-animation: _loader_show 0s ease-in $loader_timeout forwards; + -webkit-animation: _loader_show 0s ease-in $loader_timeout forwards; + -o-animation: _loader_show 0s ease-in $loader_timeout forwards; + animation: _loader_show 0s ease-in $loader_timeout forwards; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; + + .error::before { + content: 'Failed to startup app loader!'; + } + + .detail::before { + content: 'Lookup the console for more details'; + } +} + +@keyframes _loader_hide { + to { + width: 0; + height: 0; + overflow: hidden; + } +} + +@-webkit-keyframes _loader_hide { + to { + width: 0; + height: 0; + visibility: hidden; + } +} + +@keyframes _loader_show { + to { + opacity: 1; + } +} + +@-webkit-keyframes _loader_show { + to { + opacity: 1; + } } \ No newline at end of file diff --git a/shared/css/static/frame-chat.scss b/shared/css/static/frame-chat.scss index f91e468a..10a60d86 100644 --- a/shared/css/static/frame-chat.scss +++ b/shared/css/static/frame-chat.scss @@ -51,11 +51,21 @@ $client_info_avatar_size: 10em; &.right { text-align: right; + + &.mode-client_info { + max-width: calc(50% - #{$client_info_avatar_size / 2}); + margin-left: calc(#{$client_info_avatar_size / 2}); + } } &.left { text-align: left; padding-right: 10px; + + &.mode-client_info { + max-width: calc(50% - #{$client_info_avatar_size / 2}); + margin-right: calc(#{$client_info_avatar_size} / 2); + } } .title, .value, .small-value { @@ -167,11 +177,6 @@ $client_info_avatar_size: 10em; } @include transition(background-color $button_hover_animation_time ease-in-out); } - - &.mode-client_info { - min-width: 50%; - margin-right: calc(#{$client_info_avatar_size} / 2); - } } } } @@ -507,6 +512,21 @@ $client_info_avatar_size: 10em; flex-direction: column; min-height: 2em; + + .container-typing { + font-size: .85em; + padding-left: .6em; + line-height: 1; + color: hsla(0, 0%, 30%, 1); + + opacity: 1; + + &.hidden { + opacity: 0; + } + + @include transition($button_hover_animation_time ease-in-out); + } } } @@ -1201,8 +1221,6 @@ $client_info_avatar_size: 10em; } } - - .button-close { font-size: 4em; @@ -1243,6 +1261,32 @@ $client_info_avatar_size: 10em; transform: rotate(-45deg); } } + + .button-more { + flex-grow: 0; + flex-shrink: 0; + + height: 1.5em; + font-size: 1.25em; + + text-align: center; + + color: #999999; + cursor: pointer; + + margin-left: -5px; + margin-right: -5px; + + background-color: #2d2d2d; + + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; + + &:hover { + background-color: #393939; + } + @include transition($button_hover_animation_time ease-in-out); + } } .container-private-conversations, .container-channel-chat { diff --git a/shared/css/static/hostbanner.scss b/shared/css/static/hostbanner.scss index ebedfe4b..18e7db2a 100644 --- a/shared/css/static/hostbanner.scss +++ b/shared/css/static/hostbanner.scss @@ -1,6 +1,6 @@ @import "./mixin.scss"; -#hostbanner { +.hostbanner { .container-hostbanner { position: relative; diff --git a/shared/css/static/modal-clientinfo.scss b/shared/css/static/modal-clientinfo.scss new file mode 100644 index 00000000..6d281273 --- /dev/null +++ b/shared/css/static/modal-clientinfo.scss @@ -0,0 +1,618 @@ +@import "mixin"; +@import "properties"; + +.modal-body.modal-client-info { + padding: 0!important; + + $avatar_size: 12em; + .head { + flex-shrink: 0; + flex-grow: 0; + + z-index: 1; + + height: 7em; + background-color: #212125; + + .status-row { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + .status-entry { + font-size: 1.5em; + + margin: .25em; + + width: 1em; + height: 1em; + } + } + + .container-away-message { + $offset_left: (.25em) * 1.5 /* 1.5 is the font size of the icons */; + + position: relative; + margin-left: $offset_left; + margin-top: .25em; + + background-color: #1c1c1c; + border: 1px solid #161515; + border-radius: 3px; + + max-width: calc(50% - #{$avatar_size / 2 + $offset_left + 1em}); /* do actual 1em space to the avatar */ + max-height: 4em; /* else it will overflow the header */ + + display: flex; + flex-direction: column; + justify-content: start; + + width: max-content; + padding: .15em; + + overflow: hidden; + + //A verry long away message, because I want to tell a story. There was a child.... + a { + font-size: .85em; + } + + &:hover { + max-height: 200em; + } + + @include transition(.5s ease-in-out); + } + } + + .body { + flex-shrink: 1; + flex-grow: 0; + + //TODO: Min height here! + + display: flex; + flex-direction: column; + justify-content: stretch; + + background-color: #2f2f35; + + .container-avatar { + z-index: 2; /* overlay the header */ + + flex-grow: 0; + flex-shrink: 0; + + position: relative; + display: inline-block; + margin: calc(#{$avatar_size} / -2) 0.75em 0.5em 0.5em; + align-self: center; + + .avatar { + height: $avatar_size; + width: $avatar_size; + + border-radius: 50%; + overflow: hidden; + } + } + + .container-name { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: center; + + .htmltag-client { + text-align: center; + font-size: 1.5em; + color: #cccccc; + font-weight: bold; + } + } + + .container-description { + flex-grow: 0; + flex-shrink: 0; + + padding-right: calc(10em / 2); + padding-left: calc(10em / 2); + + text-align: center; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .client-description { + color: #6f6f6f; + max-width: 100%; + flex-shrink: 1; + flex-grow: 1; + overflow-wrap: break-word; + } + } + + + .container-categories { + margin-top: 1em; + + display: flex; + flex-direction: column; + justify-content: stretch; + + min-height: 14em; + + .categories { + height: 2.5em; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding-left: 2.5em; + padding-right: 2.5em; + + border-bottom: 1px solid #1d1d1d; + + .entry { + padding: .5em; + + text-align: center; + + flex-grow: 1; + flex-shrink: 1; + + cursor: pointer; + + &:hover { + color: #b6c4d6; + } + + &.selected { + border-bottom: 3px solid #245184; + margin-bottom: -2px; + + color: #245184; + } + + @include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time); + } + } + + .bodies { + position: relative; + + flex-shrink: 1; + flex-grow: 1; + + display: flex; + justify-content: stretch; + + padding-left: .5em; + padding-right: .5em; + + min-height: 10em; + height: 21em; /* body size 20 + .5 padding */ + + .container-tooltip { + flex-shrink: 0; + flex-grow: 0; + + font-size: .8em; /* shrink the tip a bit */ + + position: relative; + width: 1.6em; + margin-left: .5em; + + display: flex; + flex-direction: column; + justify-content: center; + + img { + height: 1em; + width: 1em; + + align-self: center; + font-size: 1.2em; + } + + .tooltip { + display: none; + } + } + + .body { + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + padding: .5em; + + display: flex; + justify-content: stretch; + + overflow: auto; /* else the tooltip will trigger the scrollbar */ + @include chat-scrollbar-vertical(); + + &.hidden { + display: none; + } + + &.container-basic { + flex-direction: row; + + .spacer { + flex-grow: 0; + flex-shrink: 0; + width: 1em; + } + + .left, .right { + height: 20em; + width: calc(50% - .5em); /* the spacer in the middle thats why -.5 em */ + + flex-grow: 1; + flex-shrink: 1; + + + border-radius: .2em; + border: 1px solid #1f2122; + background-color: #28292b; + padding: .5em; + + .property { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + .title, .value { + display: flex; + flex-direction: row; + justify-content: stretch; + + white-space: nowrap; + overflow: hidden; + + > * { + flex-shrink: 0; + flex-grow: 0; + + align-self: center; + } + + a { + flex-shrink: 1; + + overflow: hidden; + text-overflow: ellipsis; + } + } + + .title { + color: #254d7b; + text-transform: uppercase; + } + + .value { + color: #bdbdbd; + + a, a:visited { + color: #bdbdbd!important; + } + + .button { + width: 1.6em; + height: 1.6em; + + display: flex; + flex-direction: column; + justify-content: space-around; + + cursor: pointer; + opacity: .5; + + > div { + align-self: center; + } + + &:hover { + opacity: 1; + } + + @include transition($button_hover_animation_time ease-in-out); + } + + .country { + margin-right: .25em; + } + } + + &:not(:first-of-type) { + margin-top: .5em; + } + + &.property-unique-id, &.property-ip { + .value { + justify-content: space-between; + } + } + + &.property-version { + .a-on { + flex-shrink: 0; + flex-grow: 0; + margin-left: .25em; + margin-right: .25em; + } + } + } + } + } + + &.container-packets { + flex-direction: row; + + .spacer { + flex-grow: 0; + flex-shrink: 0; + width: 1em; + } + + .left, .right { + height: 20em; + width: calc(50% - .5em); /* the spacer in the middle thats why -.5 em */ + + flex-grow: 1; + flex-shrink: 1; + + + border-radius: .2em; + border: 1px solid #1f2122; + background-color: #28292b; + padding: .5em; + + .statistic { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + .title, .upstream, .downstream { + display: flex; + flex-direction: row; + justify-content: stretch; + + white-space: nowrap; + overflow: hidden; + + > * { + flex-shrink: 0; + flex-grow: 0; + + align-self: center; + } + + a { + flex-shrink: 1; + + overflow: hidden; + text-overflow: ellipsis; + } + } + + .title { + color: #254d7b; + text-transform: uppercase; + } + + .upstream, .downstream { + padding-top: .25em; + + + display: flex; + flex-direction: row; + justify-content: space-between; + + > a { + align-self: center; + } + } + + .upstream { + color: #fd3913; + } + + .downstream { + color: #0e8afd; + } + + &:not(:first-of-type) { + margin-top: .5em; + } + } + } + } + + &.container-groups { + flex-direction: row; + + .spacer { + flex-grow: 0; + flex-shrink: 0; + width: 1em; + } + + .left, .right { + height: 20em; + width: calc(50% - .5em); /* the spacer in the middle thats why -.5 em */ + + flex-grow: 1; + flex-shrink: 1; + + .title { + align-self: center; + color: #254d7b; + text-transform: uppercase; + } + + .container { + margin-top: .5em; + } + } + + .left { + display: flex; + flex-direction: column; + justify-content: stretch; + + .container { + border-radius: .2em .2em 0 0; + border: 1px solid #1f2122; + border-bottom: 0; + + padding: 0!important; + background-color: #28292b; + + flex-grow: 1; + flex-shrink: 1; + overflow-y: auto; + + min-height: 4em; + position: relative; + + @include chat-scrollbar-vertical(); + + .entries { + flex-grow: 1; + flex-shrink: 1; + + min-height: 4em; + + .entry { + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 1.6em; + padding-left: .5em; + padding-right: .5em; + + &:hover { + background-color: #232425; + } + + > * { + align-self: center; + } + + .icon-container { + margin-right: .25em; + } + + .name { + flex-grow: 1; + flex-shrink: 1; + + min-width: 1em; + line-height: normal; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .button-delete { + height: 1.3em; + width: 1.3em; + + cursor: pointer; + border-radius: .2em; + + &:hover { + background-color: #2c2d2e; + } + + display: flex; + flex-direction: row; + justify-content: space-around; + + > div { + align-self: center; + } + + @include transition($button_hover_animation_time ease-in-out); + } + } + } + + .container-default-groups { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; + + a { + align-self: center; + font-size: 1.25em; + color: hsla(0, 0%, 30%, 1); + } + } + } + + .buttons { + flex-grow: 0; + flex-shrink: 0; + + border-radius: 0 0 .2em .2em; + border: 1px solid #1f2122; + background-color: #28292b; + + padding: .5em; + + display: flex; + flex-direction: row; + justify-content: space-around; + + .button { + align-self: center; + } + } + } + + .right { + .container { + padding: 0!important; + + select { + font-size: .8em; + width: 100%; + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/shared/css/static/modal-connect.scss b/shared/css/static/modal-connect.scss index 55bd8693..88dd4810 100644 --- a/shared/css/static/modal-connect.scss +++ b/shared/css/static/modal-connect.scss @@ -259,7 +259,7 @@ align-self: center; .country, .icon-container { align-self: center; - margin-right: 0.1em; + margin-right: 0.25em; } diff --git a/shared/css/static/modal-identity.scss b/shared/css/static/modal-identity.scss new file mode 100644 index 00000000..6f6a0e18 --- /dev/null +++ b/shared/css/static/modal-identity.scss @@ -0,0 +1,188 @@ +@import "mixin"; + +.modal-body.modal-identity-improve { + padding: 0!important; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + .container-tooltip { + flex-grow: 0!important; + flex-shrink: 0!important; + min-width: unset!important; + + position: relative; + width: .8em; + margin-right: .5em; + font-size: .9em; + + display: inline-flex; + flex-direction: column; + justify-content: center; + vertical-align: middle; + margin-bottom: .1em; + + img { + height: 1em; + width: 1em; + + align-self: center; + font-size: 1.2em; + } + + .tooltip { + display: none; + } + } + + .options, .status { + flex-grow: 0; + flex-shrink: 0; + + padding: 1em; + + .title { + color: #387fb5; + } + + .row { + display: flex; + flex-direction: row; + justify-content: stretch; + + div { + flex-grow: 1; + flex-shrink: 1; + min-width: 4em; + + &:not(:first-of-type) { + margin-left: 1em; + } + } + } + } + + .status { + border-top: 3px solid #20262d; + } + + .buttons { + flex-grow: 0; + flex-shrink: 0; + + margin: 0 1em 1em; + + display: flex; + flex-direction: row; + justify-content: flex-end; + + button { + min-width: 8em; + + &:not(:first-of-type) { + margin-left: 1em; + } + } + } +} + +.modal-body.modal-identity-import { + padding: 0!important; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + @include user-select(none); + + .container-status { + display: flex; + flex-direction: row; + justify-content: center; + + margin: 1em 1em -1em; + height: 1.6em; + + overflow: hidden; + + color: #389738; + &.error { + color: #973838; + } + + &.loading { + color: #96903a; + } + + &.hidden { + height: 0; + } + + a { + align-self: center; + } + + @include transition(.2s ease-in-out); + } + + .container-text, .container-file { + display: flex; + flex-direction: row; + justify-content: stretch; + + > label { + flex-shrink: 0; + flex-grow: 0; + + width: 10em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + padding-left: .25em; + align-self: center; + + margin-right: 1em; + + .radio-button { + align-self: center; + margin-right: .5em; + } + } + + > * { + align-self: center; + } + + .form-group { + margin-bottom: 1.75em; + width: 100%; + } + + button { + min-width: 8em; + margin-right: 1em; + } + } + + .footer { + flex-shrink: 0; + flex-grow: 0; + + padding: 1em; + + display: flex; + flex-direction: row; + justify-content: flex-end; + + button { + min-width: 8em; + } + } + + .file-selector { + display: none; + } +} \ No newline at end of file diff --git a/shared/css/static/modal-serverinfo.scss b/shared/css/static/modal-serverinfo.scss new file mode 100644 index 00000000..c874e944 --- /dev/null +++ b/shared/css/static/modal-serverinfo.scss @@ -0,0 +1,229 @@ +@import "mixin"; + +.modal-body.modal-server-info { + padding: 0!important; + width: 55em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + background-color: #2f2f35; + + .container-tooltip { + flex-shrink: 0; + flex-grow: 0; + + position: relative; + width: 1.6em; + margin-left: .5em; + font-size: .9em; + + display: flex; + flex-direction: column; + justify-content: center; + + img { + height: 1em; + width: 1em; + + align-self: center; + font-size: 1.2em; + } + + .tooltip { + display: none; + } + } + + .hostbanner { + flex-grow: 0; + flex-shrink: 0; + + max-height: 9em; + //width: 30em; /* set a default width where we have to grow/shrink */ + + display: flex; + flex-direction: column; + justify-content: stretch; + + .container-hostbanner { + border: none; + border-radius: 0; + //background-color: #261f30; + background-color: hsla(265, 10%, 15%, 1); + } + + &.hidden { + display: none; + } + } + + .group { + flex-grow: 0; + flex-shrink: 0; + + margin: 1em; + padding: .5em; + + border-radius: .2em; + border: 1px solid #1f2122; + + background-color: #28292b; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 10em; + max-height: 10em; + + .container-image { + flex-grow: 0; + flex-shrink: 0; + + max-width: 15em; + max-height: 9em; /* minus one padding */ + + display: flex; + flex-direction: column; + justify-content: center; + + img { + object-fit: contain; + max-height: 100%; + max-width: 100%; + } + + margin-right: 2em; + @include transition(.25s ease-in-out); + } + + .container-properties { + flex-shrink: 1; + flex-grow: 1; + + min-width: 25em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + height: inherit; + + .row { + flex-grow: 0; + flex-shrink: 0; + + height: 1.8em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + .key { + flex-shrink: 0; + flex-grow: 0; + + color: #557edc; + text-transform: uppercase; + align-self: center; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + width: 15em; + } + + .value { + color: #d6d6d7; + align-self: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .country { + display: inline-block; + margin-right: .25em; + } + + &.server-version { + display: flex; + flex-direction: row; + justify-content: flex-start; + } + } + } + + .container-network { + display: flex; + flex-direction: row; + justify-content: center; + + .container-button { + margin-right: 1em; + + flex-shrink: 1; + min-width: 5em; + + display: flex; + flex-direction: column; + justify-content: flex-end; + + button { + height: 2.5em; + width: 12em; + + max-width: 100%; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .right { + flex-grow: 1; + } + } + } + + &.reverse { + flex-direction: row-reverse; + text-align: right; + + .container-image { + margin-right: 0; + margin-left: 2em; + } + + .container-properties { + .row { + flex-direction: row-reverse; + } + } + } + } + + .container-buttons { + margin: 1em; + + display: flex; + flex-direction: row; + justify-content: flex-end; + + button { + min-width: 8em; + } + } +} + +@media all and (max-width: 50em) { + .modal-body.modal-server-info { + .container-image { + margin: 0!important; + max-width: 0!important; + } + } +} \ No newline at end of file diff --git a/shared/css/static/modal.scss b/shared/css/static/modal.scss index ca109ec1..4e5115d2 100644 --- a/shared/css/static/modal.scss +++ b/shared/css/static/modal.scss @@ -119,6 +119,10 @@ flex-shrink: 1; color: #9d9d9e; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } h5 { @@ -461,7 +465,7 @@ box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); &:hover { - background-color: #151515; + background-color: #0a0a0a; } &:disabled { @@ -473,21 +477,36 @@ } } - &.btn-success { + &.btn-success, &.btn-green { border-bottom-width: 2px; border-bottom-color: #389738; } - &.btn-info { + &.btn-info, &.btn-blue { border-bottom-width: 2px; border-bottom-color: #386896; } - &.btn-warning, &.btn-danger { + &.btn-warning, &.btn-danger, &.btn-red { border-bottom-width: 2px; border-bottom-color: #973838; } + &.btn-purple { + border-bottom-width: 2px; + border-bottom-color: #5f3586; + } + + &.btn-brown { + border-bottom-width: 2px; + border-bottom-color: #965238; + } + + &.btn-yellow { + border-bottom-width: 2px; + border-bottom-color: #96903a; + } + @include transition(background-color $button_hover_animation_time ease-in-out); } @@ -595,8 +614,8 @@ } } -/* general ratio button look */ -.ratio-button { +/* general radio button look */ +.ratio-button, .radio-button { $button_size: 1.2em; $mark_size: .6em; @@ -647,14 +666,18 @@ box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); } -label:hover > .ratio-button, .ratio-button:hover { - &.ratio-button, > .ratio-button { +label:hover > .ratio-button, .ratio-button:hover, +label:hover > .radio-button, .radio-button:hover{ + &.ratio-button, > .ratio-button, + &.radio-button, > .radio-button{ background-color: #2c2b2b; } } -label.disabled > .ratio-button, .ratio-button.disabled, .ratio-button:disabled { - &.ratio-button, > .ratio-button { +label.disabled > .ratio-button, .ratio-button.disabled, .ratio-button:disabled, +label.disabled > .radio-button, .radio-button.disabled, .radio-button:disabled { + &.ratio-button, > .ratio-button, + &.radio-button, > .radio-button { pointer-events: none!important; background-color: #1a1919!important; } diff --git a/shared/generate_packed.sh b/shared/generate_packed.sh index 86da58ae..4c1ad88a 100755 --- a/shared/generate_packed.sh +++ b/shared/generate_packed.sh @@ -5,7 +5,7 @@ cd "$BASEDIR" source ../scripts/resolve_commands.sh #Generate the loader definitions first -LOADER_FILE="declarations/exports_loader.d.ts" +LOADER_FILE="declarations/exports_loader_app.d.ts" if [[ -e ${LOADER_FILE} ]]; then rm ${LOADER_FILE} if [[ $? -ne 0 ]]; then @@ -13,10 +13,9 @@ if [[ -e ${LOADER_FILE} ]]; then fi fi -npm run dtsgen -- --config $(pwd)/tsconfig/dtsconfig_loader.json -v +npm run dtsgen -- --config $(pwd)/tsconfig/dtsconfig_loader_app.json -v if [[ ! -e ${LOADER_FILE} ]]; then echo "Failed to generate definitions" - echo "$result" exit 1 fi @@ -26,12 +25,19 @@ if [[ $? -ne 0 ]]; then exit 1 fi -execute_ttsc -p tsconfig/tsconfig_packed_loader.json +execute_ttsc -p tsconfig/tsconfig_packed_loader_app.json if [[ $? -ne 0 ]]; then echo "Failed to generate packed loader file!" exit 1 fi +npm run minify-web-rel-file `pwd`/generated/loader_app.min.js `pwd`/generated/loader_app.js +if [[ $? -ne 0 ]]; then + echo "Failed to minimize packed loader file!" + exit 1 +fi + + execute_ttsc -p tsconfig/tsconfig_packed.json if [[ $? -ne 0 ]]; then echo "Failed to generate packed file!" @@ -49,7 +55,7 @@ if [[ ! -d generated/static/ ]]; then fi # Create packed CSS file -find css/static/ -name '*.css' -exec cat {} \; | npm run csso -- --output `pwd`/generated/static/base.css +./css/generate_packed.sh echo "Packed file generated!" exit 0 \ No newline at end of file diff --git a/shared/html/index.php b/shared/html/index.php index 6cd78a9e..0ee6701f 100644 --- a/shared/html/index.php +++ b/shared/html/index.php @@ -93,7 +93,9 @@
- + + +
@@ -113,7 +115,7 @@ -
+
@@ -133,12 +135,12 @@
-

Ooops, we encountered some trouble while loading important files!

+

- +
@@ -155,8 +157,29 @@ + + + + +
@@ -275,7 +280,10 @@
-
+
+
{{tr "Partner is typing..." /}}
+ +
@@ -469,6 +477,7 @@
+
{{tr "Full Info" /}}
@@ -2747,75 +2756,99 @@ + + + + \ No newline at end of file diff --git a/shared/img/bookmark_background.png b/shared/img/bookmark_background.png new file mode 100644 index 00000000..4176b3b9 Binary files /dev/null and b/shared/img/bookmark_background.png differ diff --git a/shared/img/serveredit_1.png b/shared/img/serveredit_1.png new file mode 100644 index 00000000..e0061c97 Binary files /dev/null and b/shared/img/serveredit_1.png differ diff --git a/shared/img/serveredit_2.png b/shared/img/serveredit_2.png new file mode 100644 index 00000000..0acb2290 Binary files /dev/null and b/shared/img/serveredit_2.png differ diff --git a/shared/img/serveredit_3.png b/shared/img/serveredit_3.png new file mode 100644 index 00000000..548dc652 Binary files /dev/null and b/shared/img/serveredit_3.png differ diff --git a/shared/js/BrowserIPC.ts b/shared/js/BrowserIPC.ts index 5ce38483..dde0b7cc 100644 --- a/shared/js/BrowserIPC.ts +++ b/shared/js/BrowserIPC.ts @@ -61,7 +61,7 @@ namespace bipc { abstract send_message(type: string, data: any, target?: string); protected handle_message(message: BroadcastMessage) { - console.log("Received: %o", message); + log.trace(LogCategory.IPC, tr("Received message %o"), message); if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) { if(message.type == "process-query") { @@ -327,10 +327,10 @@ namespace bipc { protected register_method(method: (...args: any[]) => Promise | string) { let method_name: string; if(typeof method === "function") { - console.log("Proxy method: %o", method.name); + log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method.name); method_name = method.name; } else { - console.log("Proxy method: %o", method); + log.debug(LogCategory.IPC, tr("Registering method proxy for %s"), method); method_name = method; } @@ -415,11 +415,11 @@ namespace bipc { } try { - console.log(tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments); + log.info(LogCategory.IPC, tr("Invoking method %s with arguments: %o"), data.method_name, data.arguments); const promise = this[data.method_name](...data.arguments); promise.then(result => { - console.log(tr("Result: %o"), result); + log.info(LogCategory.IPC, tr("Result: %o"), result); this._send_result(data.promise_id, true, result); }).catch(error => { this._send_result(data.promise_id, false, error); diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index eccacee4..a7eca860 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -2,7 +2,6 @@ /// /// /// -/// /// /// /// @@ -90,7 +89,6 @@ class ConnectionHandler { groups: GroupManager; side_bar: chat.Frame; - select_info: InfoBar; settings: ServerSettings; sound: sound.SoundManager; @@ -126,7 +124,6 @@ class ConnectionHandler { this.settings = new ServerSettings(); this.log = new log.ServerLog(this); - this.select_info = new InfoBar(this); this.channelTree = new ChannelTree(this); this.side_bar = new chat.Frame(this); this.sound = new sound.SoundManager(this); @@ -192,7 +189,7 @@ class ConnectionHandler { server_address.port = 9987; } } - console.log(tr("Start connection to %s:%d"), server_address.host, server_address.port); + log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port); this.log.log(log.server.Type.CONNECTION_BEGIN, { address: { server_hostname: server_address.host, @@ -210,7 +207,7 @@ class ConnectionHandler { password: password } } catch(error) { - console.error(tr("Failed to hash connect password: %o"), error); + log.error(LogCategory.CLIENT, tr("Failed to hash connect password: %o"), error); createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!
") + error).open(); } } @@ -278,7 +275,7 @@ class ConnectionHandler { * LISTENER */ onConnected() { - console.log("Client connected!"); + log.info(LogCategory.CLIENT, tr("Client connected")); this.permissions.requestPermissionList(); if(this.groups.serverGroups.length == 0) this.groups.requestGroups(); @@ -416,7 +413,7 @@ class ConnectionHandler { this.sound.play(Sound.CONNECTION_DISCONNECTED); break; case DisconnectReason.DNS_FAILED: - console.error(tr("Failed to resolve hostname: %o"), data); + log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data); this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, { message: data as any }); @@ -428,7 +425,7 @@ class ConnectionHandler { this.log.log(log.server.Type.CONNECTION_FAILED, {}); break; } - console.error(tr("Could not connect to remote host! Error: %o"), data); + log.error(LogCategory.CLIENT, tr("Could not connect to remote host! Error: %o"), data); if(native_client) { createErrorModal( @@ -452,7 +449,7 @@ class ConnectionHandler { break; case DisconnectReason.HANDSHAKE_FAILED: //TODO sound - console.error(tr("Failed to process handshake: %o"), data); + log.error(LogCategory.CLIENT, tr("Failed to process handshake: %o"), data); createErrorModal( tr("Could not connect"), tr("Failed to process handshake: ") + data as string @@ -476,7 +473,7 @@ class ConnectionHandler { auto_reconnect = false; break; case DisconnectReason.CONNECTION_CLOSED: - console.error(tr("Lost connection to remote server!")); + log.error(LogCategory.CLIENT, tr("Lost connection to remote server!")); createErrorModal( tr("Connection closed"), tr("The connection was closed by remote host") @@ -486,7 +483,7 @@ class ConnectionHandler { auto_reconnect = true; break; case DisconnectReason.CONNECTION_PING_TIMEOUT: - console.error(tr("Connection ping timeout")); + log.error(LogCategory.CLIENT, tr("Connection ping timeout")); this.sound.play(Sound.CONNECTION_DISCONNECTED_TIMEOUT); createErrorModal( tr("Connection lost"), @@ -561,9 +558,8 @@ class ConnectionHandler { this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse break; default: - console.error(tr("Got uncaught disconnect!")); - console.error(tr("Type: %o Data:"), type); - console.error(data); + log.error(LogCategory.CLIENT, tr("Got uncaught disconnect!")); + log.error(LogCategory.CLIENT, tr("Type: %o Data: %o"), type, data); break; } @@ -574,18 +570,17 @@ class ConnectionHandler { if(control_bar.current_connection_handler() == this) control_bar.update_connection_state(); - this.select_info.setCurrentSelected(null); this.side_bar.private_conversations().clear_client_ids(); this.hostbanner.update(); if(auto_reconnect) { if(!this.serverConnection) { - console.log(tr("Allowed to auto reconnect but cant reconnect because we dont have any information left...")); + log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left...")); return; } this.log.log(log.server.Type.RECONNECT_SCHEDULED, {timeout: 50000}); - console.log(tr("Allowed to auto reconnect. Reconnecting in 5000ms")); + log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms")); const server_address = this.serverConnection.remote_address(); const profile = this.serverConnection.handshake_handler().profile; @@ -695,11 +690,11 @@ class ConnectionHandler { if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) { vconnection.voice_recorder().input.start().then(result => { if(result != audio.recorder.InputStartResult.EOK) { - console.warn(tr("Failed to start microphone input (%s)."), result); + log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), result); createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), result)).open(); } }).catch(error => { - console.warn(tr("Failed to start microphone input (%s)."), error); + log.warn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error); createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open(); }); } @@ -751,14 +746,13 @@ class ConnectionHandler { resize_elements() { this.channelTree.handle_resized(); - this.select_info.handle_resize(); this.invoke_resized_on_activate = false; } acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean) { const vconnection = this.serverConnection.voice_connection(); (vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(error => { - console.error(tr("Failed to acquire recorder (%o)"), error); + log.warn(LogCategory.VOICE, tr("Failed to acquire recorder (%o)"), error); }).then(() => { this.update_voice_status(undefined); }); @@ -785,7 +779,7 @@ class ConnectionHandler { if(typeof(data) === "undefined") return; if(data === null) { - console.log(tr("Deleting existing avatar")); + log.info(LogCategory.CLIENT, tr("Deleting existing avatar")); this.serverConnection.send_command('ftdeletefile', { name: "/avatar_", /* delete own avatar */ path: "", @@ -793,7 +787,7 @@ class ConnectionHandler { }).then(() => { createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open(); }).catch(error => { - console.error(tr("Failed to reset avatar flag: %o"), error); + log.error(LogCategory.GENERAL, tr("Failed to reset avatar flag: %o"), error); let message; if(error instanceof CommandResult) @@ -804,7 +798,7 @@ class ConnectionHandler { return; }); } else { - console.log(tr("Uploading new avatar")); + log.info(LogCategory.CLIENT, tr("Uploading new avatar")); (async () => { let key: transfer.UploadKey; try { @@ -817,7 +811,7 @@ class ConnectionHandler { channel_password: undefined }); } catch(error) { - console.error(tr("Failed to initialize avatar upload: %o"), error); + log.error(LogCategory.GENERAL, tr("Failed to initialize avatar upload: %o"), error); let message; if(error instanceof CommandResult) { //TODO: Resolve permission name @@ -837,7 +831,7 @@ class ConnectionHandler { try { await transfer.spawn_upload_transfer(key).put_data(data); } catch(error) { - console.error(tr("Failed to upload avatar: %o"), error); + log.error(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error); let message; if(typeof(error) === "string") @@ -853,7 +847,7 @@ class ConnectionHandler { client_flag_avatar: guid() }); } catch(error) { - console.error(tr("Failed to update avatar flag: %o"), error); + log.error(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error); let message; if(error instanceof CommandResult) @@ -889,9 +883,6 @@ class ConnectionHandler { this.side_bar && this.side_bar.destroy(); this.side_bar = undefined; - this.select_info && this.select_info.destroy(); - this.select_info = undefined; - this.log && this.log.destroy(); this.log = undefined; diff --git a/shared/js/FileManager.ts b/shared/js/FileManager.ts index 0be06317..bd40114d 100644 --- a/shared/js/FileManager.ts +++ b/shared/js/FileManager.ts @@ -235,7 +235,7 @@ class FileManager extends connection.AbstractCommandHandler { } if(!entry) { - console.error(tr("Invalid file list entry. Path: %s"), json[0]["path"]); + log.error(LogCategory.CLIENT, tr("Invalid file list entry. Path: %s"), json[0]["path"]); return; } for(let e of (json as Array)) { @@ -258,7 +258,7 @@ class FileManager extends connection.AbstractCommandHandler { } if(!entry) { - console.error(tr("Invalid file list entry finish. Path: "), json[0]["path"]); + log.error(LogCategory.CLIENT, tr("Invalid file list entry finish. Path: "), json[0]["path"]); return; } entry.callback(entry.entries); @@ -659,7 +659,7 @@ class IconManager { try { download_key = await this.create_icon_download(id); } catch(error) { - console.error(tr("Could not request download for icon %d: %o"), id, error); + log.error(LogCategory.CLIENT, tr("Could not request download for icon %d: %o"), id, error); throw "Failed to request icon"; } @@ -668,7 +668,7 @@ class IconManager { try { response = await downloader.request_file(); } catch(error) { - console.error(tr("Could not download icon %d: %o"), id, error); + log.error(LogCategory.CLIENT, tr("Could not download icon %d: %o"), id, error); throw "failed to download icon"; } @@ -713,7 +713,7 @@ class IconManager { return result; throw "load result is empty"; } catch(error) { - console.error(tr("Icon download failed of icon %d: %o"), id, error); + log.error(LogCategory.CLIENT, tr("Icon download failed of icon %d: %o"), id, error); } throw "icon not found"; @@ -759,7 +759,7 @@ class IconManager { }; if(icon instanceof Promise) { icon.then(_apply).catch(error => { - console.error(tr("Could not load icon. Reason: %s"), error); + log.error(LogCategory.CLIENT, tr("Could not load icon. Reason: %s"), error); icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon"); }); } else { @@ -858,7 +858,7 @@ class AvatarManager { } create_avatar_download(client_avatar_id: string) : Promise { - console.log(tr("Downloading avatar %s"), client_avatar_id); + log.debug(LogCategory.GENERAL, "Requesting download for avatar %s", client_avatar_id); return this.handle.download_file("", "/avatar_" + client_avatar_id); } @@ -868,7 +868,7 @@ class AvatarManager { try { download_key = await this.create_avatar_download(client_avatar_id); } catch(error) { - console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error); + log.error(LogCategory.GENERAL, tr("Could not request download for avatar %s: %o"), client_avatar_id, error); throw "failed to request avatar download"; } @@ -877,7 +877,7 @@ class AvatarManager { try { response = await downloader.request_file(); } catch(error) { - console.error(tr("Could not download avatar %s: %o"), client_avatar_id, error); + log.error(LogCategory.GENERAL, tr("Could not download avatar %s: %o"), client_avatar_id, error); throw "failed to download avatar"; } @@ -915,7 +915,7 @@ class AvatarManager { if(_cached.avatar_id === avatar_id) return; /* cache is up2date */ - console.log(tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id); + log.info(LogCategory.GENERAL, tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id); delete this._cached_avatars[client_avatar_id]; AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => { log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error); @@ -961,7 +961,7 @@ class AvatarManager { try { avatar = await this.resolved_cached(client_avatar_id, avatar_id); } catch(error) { - console.error(error); + log.error(LogCategory.CLIENT, error); } if(!avatar) @@ -984,7 +984,7 @@ class AvatarManager { }); }); })().catch(reason => { - console.error(tr("Could not load avatar for id %s. Reason: %s"), client_avatar_id, reason); + log.error(LogCategory.CLIENT, tr("Could not load avatar for id %s. Reason: %s"), client_avatar_id, reason); //TODO Broken image loader_image.addClass("icon client-warning").attr("tag", tr("Could not load avatar ") + client_avatar_id); }) @@ -1049,7 +1049,7 @@ class AvatarManager { if(avatar_id) { if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */ const cache: Avatar = this._cached_avatars[avatar_id]; - console.log("[AVATAR] Using cached avatar. ID: %o | Version: %o (Cached: %o)", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id); + log.debug(LogCategory.GENERAL, tr("Using cached avatar. ID: %o | Version: %o (Cached: %o)"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id); if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) { const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'}); return container.append(image); @@ -1063,13 +1063,13 @@ class AvatarManager { let avatar: Avatar; let loaded_image = this.generate_default_image(); - console.log("[AVATAR] Resolving avatar. ID: %o | Version: %o", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + log.debug(LogCategory.GENERAL, tr("Resolving avatar. ID: %o | Version: %o"), avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); try { //TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart try { avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); } catch(error) { - console.error(tr("Failed to use cached avatar: %o"), error); + log.error(LogCategory.GENERAL, tr("Failed to use cached avatar: %o"), error); } if(!avatar) diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index d2bff591..dda6729d 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -94,7 +94,7 @@ namespace bookmarks { try { bookmarks = JSON.parse(bookmark_json) || {} as BookmarkConfig; } catch(error) { - console.error(tr("Failed to load bookmarks: %o"), error); + log.error(LogCategory.BOOKMARKS, tr("Failed to load bookmarks: %o"), error); bookmarks = {} as any; } diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index 36f5885b..89c4eec6 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -26,6 +26,8 @@ namespace connection { this["notifychannelshow"] = this.handleCommandChannelShow; this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo; + this["notifyconnectioninfo"] = this.handleNotifyConnectionInfo; + this["notifycliententerview"] = this.handleCommandClientEnterView; this["notifyclientleftview"] = this.handleCommandClientLeftView; this["notifyclientmoved"] = this.handleNotifyClientMoved; @@ -33,6 +35,7 @@ namespace connection { this["notifychannelmoved"] = this.handleNotifyChannelMoved; this["notifychanneledited"] = this.handleNotifyChannelEdited; this["notifytextmessage"] = this.handleNotifyTextMessage; + this["notifyclientchatcomposing"] = this.notifyClientChatComposing; this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; this["notifyclientupdated"] = this.handleNotifyClientUpdated; this["notifyserveredited"] = this.handleNotifyServerEdited; @@ -78,8 +81,7 @@ namespace connection { } else if(typeof(ex) === "string") { this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex}); } else { - console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex)); - console.error(ex); + log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); } } @@ -110,7 +112,7 @@ namespace connection { let code : string = json["return_code"]; if(!code || code.length == 0) { - console.log(tr("Invalid return code! (%o)"), json); + log.warn(LogCategory.NETWORKING, tr("Invalid return code! (%o)"), json); return; } let retListeners = this.connection["_retListener"]; @@ -130,9 +132,9 @@ namespace connection { handleCommandServerInit(json){ //We could setup the voice channel if(this.connection.support_voice()) { - console.log(tr("Setting up voice")); + log.debug(LogCategory.NETWORKING, tr("Setting up voice")); } else { - console.log(tr("Skipping voice setup (No voice bridge available)")); + log.debug(LogCategory.NETWORKING, tr("Skipping voice setup (No voice bridge available)")); } @@ -222,11 +224,32 @@ namespace connection { /* everything is a number, so lets parse it */ for(const key of Object.keys(json)) - json[key] = parseInt(json[key]); + json[key] = parseFloat(json[key]); this.connection_handler.channelTree.server.set_connection_info(json); } + handleNotifyConnectionInfo(json) { + json = json[0]; + + const object = new ClientConnectionInfo(); + /* everything is a number (except ip), so lets parse it */ + for(const key of Object.keys(json)) { + if(key === "connection_client_ip") + object[key] = json[key]; + else + object[key] = parseFloat(json[key]); + } + + const client = this.connection_handler.channelTree.findClient(parseInt(json["clid"])); + if(!client) { + log.warn(LogCategory.NETWORKING, tr("Received client connection info for unknown client (%o)"), json["clid"]); + return; + } + + client.set_connection_info(object); + } + private createChannelFromJson(json, ignoreOrder: boolean = false) { let tree = this.connection.client.channelTree; @@ -236,14 +259,14 @@ namespace connection { let prev = tree.findChannel(json["channel_order"]); if(!prev && json["channel_order"] != 0) { if(!ignoreOrder) { - console.error(tr("Invalid channel order id!")); + log.error(LogCategory.NETWORKING, tr("Invalid channel order id!")); return; } } let parent = tree.findChannel(json["cpid"]); if(!parent && json["cpid"] != 0) { - console.error(tr("Invalid channel parent")); + log.error(LogCategory.NETWORKING, tr("Invalid channel parent")); return; } tree.moveChannel(channel, prev, parent); //TODO test if channel exists! @@ -275,7 +298,7 @@ namespace connection { handleCommandChannelList(json) { this.connection.client.channelTree.hide_channel_tree(); /* dont perform channel inserts on the dom to prevent style recalculations */ - console.log(tr("Got %d new channels"), json.length); + log.debug(LogCategory.NETWORKING, tr("Got %d new channels"), json.length); for(let index = 0; index < json.length; index++) this.createChannelFromJson(json[index], true); } @@ -297,12 +320,12 @@ namespace connection { let tree = this.connection.client.channelTree; const conversations = this.connection.client.side_bar.channel_conversations(); - console.log(tr("Got %d channel deletions"), json.length); + log.info(LogCategory.NETWORKING, tr("Got %d channel deletions"), json.length); for(let index = 0; index < json.length; index++) { conversations.delete_conversation(parseInt(json[index]["cid"])); let channel = tree.findChannel(json[index]["cid"]); if(!channel) { - console.error(tr("Invalid channel onDelete (Unknown channel)")); + log.error(LogCategory.NETWORKING, tr("Invalid channel onDelete (Unknown channel)")); continue; } tree.deleteChannel(channel); @@ -313,12 +336,12 @@ namespace connection { let tree = this.connection.client.channelTree; const conversations = this.connection.client.side_bar.channel_conversations(); - console.log(tr("Got %d channel hides"), json.length); + log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length); for(let index = 0; index < json.length; index++) { conversations.delete_conversation(parseInt(json[index]["cid"])); let channel = tree.findChannel(json[index]["cid"]); if(!channel) { - console.error(tr("Invalid channel on hide (Unknown channel)")); + log.error(LogCategory.NETWORKING, tr("Invalid channel on hide (Unknown channel)")); continue; } tree.deleteChannel(channel); @@ -443,7 +466,7 @@ namespace connection { let tree = this.connection.client.channelTree; let client = tree.findClient(entry["clid"]); if(!client) { - console.error(tr("Unknown client left!")); + log.error(LogCategory.NETWORKING, tr("Unknown client left!")); return 0; } if(client == this.connection.client.getClient()) { @@ -500,7 +523,7 @@ namespace connection { if(channel_from == own_channel) this.connection_handler.sound.play(Sound.USER_LEFT_TIMEOUT); } else { - console.error(tr("Unknown client left reason!")); + log.error(LogCategory.NETWORKING, tr("Unknown client left reason!")); } if(!channel_to) { @@ -531,16 +554,16 @@ namespace connection { let channel_from = tree.findChannel(json["cfid"]); if(!client) { - console.error(tr("Unknown client move (Client)!")); + log.error(LogCategory.NETWORKING, tr("Unknown client move (Client)!")); return 0; } if(!channel_to) { - console.error(tr("Unknown client move (Channel to)!")); + log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel to)!")); return 0; } if(!channel_from) //Not critical - console.error(tr("Unknown client move (Channel from)!")); + log.error(LogCategory.NETWORKING, tr("Unknown client move (Channel from)!")); let self = client instanceof LocalClientEntry; let current_clients: ClientEntry[]; @@ -626,25 +649,23 @@ namespace connection { handleNotifyChannelMoved(json) { json = json[0]; //Only one bulk - for(let key in json) - console.log("Key: " + key + " Value: " + json[key]); let tree = this.connection.client.channelTree; let channel = tree.findChannel(json["cid"]); if(!channel) { - console.error(tr("Unknown channel move (Channel)!")); + log.error(LogCategory.NETWORKING, tr("Unknown channel move (Channel)!")); return 0; } let prev = tree.findChannel(json["order"]); if(!prev && json["order"] != 0) { - console.error(tr("Unknown channel move (prev)!")); + log.error(LogCategory.NETWORKING, tr("Unknown channel move (prev)!")); return 0; } let parent = tree.findChannel(json["cpid"]); if(!parent && json["cpid"] != 0) { - console.error(tr("Unknown channel move (parent)!")); + log.error(LogCategory.NETWORKING, tr("Unknown channel move (parent)!")); return 0; } @@ -657,7 +678,7 @@ namespace connection { let tree = this.connection.client.channelTree; let channel = tree.findChannel(json["cid"]); if(!channel) { - console.error(tr("Unknown channel edit (Channel)!")); + log.error(LogCategory.NETWORKING, tr("Unknown channel edit (Channel)!")); return 0; } @@ -686,7 +707,7 @@ namespace connection { const target_own = target_client_id === this.connection.client.getClientId(); if(target_own && target_client_id === json["invokerid"]) { - console.error(tr("Received conversation message from invalid client id. Data: %o", json)); + log.error(LogCategory.NETWORKING, tr("Received conversation message from invalid client id. Data: %o", json)); return; } @@ -700,7 +721,7 @@ namespace connection { attach: target_own }); if(!conversation) { - console.error(tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); + log.error(LogCategory.NETWORKING, tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); return; } @@ -764,6 +785,24 @@ namespace connection { } } + notifyClientChatComposing(json) { + json = json[0]; + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: parseInt(json["clid"]), + unique_id: json["cluid"], + name: undefined + }, { + create: false, + attach: false + }); + if(!conversation) + return; + + conversation.trigger_typing(); + } + handleNotifyClientChatClosed(json) { json = json[0]; //Only one bulk @@ -793,7 +832,7 @@ namespace connection { let client = this.connection.client.channelTree.findClient(json["clid"]); if(!client) { - console.error(tr("Tried to update an non existing client")); + log.error(LogCategory.NETWORKING, tr("Tried to update an non existing client")); return; } @@ -806,8 +845,6 @@ namespace connection { updates.push({key: key, value: json[key]}); } client.updateVariables(...updates); - if(this.connection.client.select_info.currentSelected == client) - this.connection.client.select_info.update(); } handleNotifyServerEdited(json) { @@ -826,8 +863,6 @@ namespace connection { updates.push({key: key, value: json[key]}); } this.connection.client.channelTree.server.updateVariables(false, ...updates); - if(this.connection.client.select_info.currentSelected == this.connection.client.channelTree.server) - this.connection.client.select_info.update(); } handleNotifyServerUpdated(json) { @@ -846,9 +881,6 @@ namespace connection { updates.push({key: key, value: json[key]}); } this.connection.client.channelTree.server.updateVariables(true, ...updates); - let info = this.connection.client.select_info; - if(info.currentSelected instanceof ServerEntry) - info.update(); } handleNotifyMusicPlayerInfo(json) { diff --git a/shared/js/load.ts b/shared/js/load.ts deleted file mode 100644 index fa241745..00000000 --- a/shared/js/load.ts +++ /dev/null @@ -1,1152 +0,0 @@ -namespace app { - export enum Type { - UNKNOWN, - CLIENT_RELEASE, - CLIENT_DEBUG, - WEB_DEBUG, - WEB_RELEASE - } - export let type: Type = Type.UNKNOWN; - - export function is_web() { - return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG; - } - - let _ui_version; - export function ui_version() { - if(typeof(_ui_version) !== "string") { - const version_node = document.getElementById("app_version"); - if(!version_node) return undefined; - - const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; - if(!version) return undefined; - - return (_ui_version = version); - } - return _ui_version; - } -} - -namespace loader { - export type Task = { - name: string, - priority: number, /* tasks with the same priority will be executed in sync */ - function: () => Promise - }; - - export enum Stage { - /* - loading loader required files (incl this) - */ - INITIALIZING, - /* - setting up the loading process - */ - SETUP, - /* - loading all style sheet files - */ - STYLE, - /* - loading all javascript files - */ - JAVASCRIPT, - /* - loading all template files - */ - TEMPLATES, - /* - initializing static/global stuff - */ - JAVASCRIPT_INITIALIZING, - /* - finalizing load process - */ - FINALIZING, - /* - invoking main task - */ - LOADED, - - DONE - } - - export let cache_tag: string | undefined; - let current_stage: Stage = undefined; - const tasks: {[key:number]:Task[]} = {}; - - export function finished() { - return current_stage == Stage.DONE; - } - - export function register_task(stage: Stage, task: Task) { - if(current_stage > stage) { - console.warn("Register loading task, but it had already been finished. Executing task anyways!"); - task.function().catch(error => { - console.error("Failed to execute delayed loader task!"); - console.log(" - %s: %o", task.name, error); - - displayCriticalError(error); - }); - return; - } - - const task_array = tasks[stage] || []; - task_array.push(task); - tasks[stage] = task_array.sort((a, b) => a.priority - b.priority); - } - - export async function execute() { - const load_begin = Date.now(); - - let begin: number = Date.now(); - let end: number; - while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") { - - let current_tasks: Task[] = []; - while((tasks[current_stage] || []).length > 0) { - if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) { - current_tasks.push(tasks[current_stage].pop()); - } else break; - } - - const errors: { - error: any, - task: Task - }[] = []; - - const promises: Promise[] = []; - for(const task of current_tasks) { - try { - console.debug("Executing loader %s (%d)", task.name, task.priority); - promises.push(task.function().catch(error => { - errors.push({ - task: task, - error: error - }); - return Promise.resolve(); - })); - } catch(error) { - errors.push({ - task: task, - error: error - }); - } - } - - await Promise.all([...promises]); - - if(errors.length > 0) { - console.groupEnd(); - console.error("Failed to execute loader. The following tasks failed (%d):", errors.length); - for(const error of errors) - console.error(" - %s: %o", error.task.name, error.error); - - throw "failed to process step " + Stage[current_stage]; - } - - if(current_tasks.length == 0) { - if(typeof(current_stage) === "undefined") { - current_stage = -1; - console.debug("[loader] Booting app"); - } else if(current_stage < Stage.INITIALIZING) { - console.groupEnd(); - console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin); - } else { - console.groupEnd(); - console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin); - } - - begin = end; - current_stage += 1; - - if(current_stage != Stage.DONE) - console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); - } - } - - /* cleanup */ - { - _script_promises = {}; - } - console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); - } - - type DependSource = { - url: string; - depends: string[]; - } - type SourcePath = string | DependSource | string[]; - - function script_name(path: SourcePath) { - if(Array.isArray(path)) { - let buffer = ""; - let _or = " or "; - for(let entry of path) - buffer += _or + script_name(entry); - return buffer.slice(_or.length); - } else if(typeof(path) === "string") - return "" + path + ""; - else - return "" + path.url + ""; - } - - class SyntaxError { - source: any; - - constructor(source: any) { - this.source = source; - } - } - - let _script_promises: {[key: string]: Promise} = {}; - export async function load_script(path: SourcePath) : Promise { - if(Array.isArray(path)) { //We have some fallback - return load_script(path[0]).catch(error => { - if(error instanceof SyntaxError) - return Promise.reject(error.source); - - if(path.length > 1) - return load_script(path.slice(1)); - - return Promise.reject(error); - }); - } else { - const source = typeof(path) === "string" ? {url: path, depends: []} : path; - if(source.url.length == 0) return Promise.resolve(); - - return _script_promises[source.url] = (async () => { - /* await depends */ - for(const depend of source.depends) { - if(!_script_promises[depend]) - throw "Missing dependency " + depend; - await _script_promises[depend]; - } - - const tag: HTMLScriptElement = document.createElement("script"); - - await new Promise((resolve, reject) => { - let error = false; - const error_handler = (event: ErrorEvent) => { - if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error - console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); - window.removeEventListener('error', error_handler as any); - - reject(new SyntaxError(event.error)); - event.preventDefault(); - error = true; - } - }; - window.addEventListener('error', error_handler as any); - - const cleanup = () => { - tag.onerror = undefined; - tag.onload = undefined; - - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); - }; - const timeout_handle = setTimeout(() => { - cleanup(); - reject("timeout"); - }, 5000); - tag.type = "application/javascript"; - tag.async = true; - tag.defer = true; - tag.onerror = error => { - cleanup(); - tag.remove(); - reject(error); - }; - tag.onload = () => { - cleanup(); - - console.debug("Script %o loaded", path); - setTimeout(resolve, 100); - }; - - document.getElementById("scripts").appendChild(tag); - - tag.src = source.url + (cache_tag || ""); - }); - })(); - } - } - - export async function load_scripts(paths: SourcePath[]) : Promise { - const promises: Promise[] = []; - const errors: { - script: SourcePath, - error: any - }[] = []; - - for(const script of paths) - promises.push(load_script(script).catch(error => { - errors.push({ - script: script, - error: error - }); - return Promise.resolve(); - })); - - await Promise.all([...promises]); - - if(errors.length > 0) { - console.error("Failed to load the following scripts:"); - for(const script of errors) - console.log(" - %o: %o", script.script, script.error); - - displayCriticalError("Failed to load script " + script_name(errors[0].script) + "
" + "View the browser console for more information!"); - throw "failed to load script " + script_name(errors[0].script); - } - } - - export async function load_style(path: SourcePath) : Promise { - if(Array.isArray(path)) { //We have some fallback - return load_style(path[0]).catch(error => { - if(error instanceof SyntaxError) - return Promise.reject(error.source); - - if(path.length > 1) - return load_script(path.slice(1)); - - return Promise.reject(error); - }); - } else { - if(!path) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const tag: HTMLLinkElement = document.createElement("link"); - - let error = false; - const error_handler = (event: ErrorEvent) => { - console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); - if(event.filename == tag.href) { //FIXME! - window.removeEventListener('error', error_handler as any); - - reject(new SyntaxError(event.error)); - event.preventDefault(); - error = true; - } - }; - window.addEventListener('error', error_handler as any); - - tag.type = "text/css"; - tag.rel = "stylesheet"; - - const cleanup = () => { - tag.onerror = undefined; - tag.onload = undefined; - - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); - }; - - const timeout_handle = setTimeout(() => { - cleanup(); - reject("timeout"); - }, 5000); - - tag.onerror = error => { - cleanup(); - tag.remove(); - console.error("File load error for file %s: %o", path, error); - reject("failed to load file " + path); - }; - tag.onload = () => { - cleanup(); - { - const css: CSSStyleSheet = tag.sheet as CSSStyleSheet; - const rules = css.cssRules; - const rules_remove: number[] = []; - const rules_add: string[] = []; - - for(let index = 0; index < rules.length; index++) { - const rule = rules.item(index); - let rule_text = rule.cssText; - - if(rule.cssText.indexOf("%%base_path%%") != -1) { - rules_remove.push(index); - rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname)); - } - } - - for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) { - if(css.removeRule) - css.removeRule(index); - else - css.deleteRule(index); - } - for(const rule of rules_add) - css.insertRule(rule, rules_remove[0]); - } - - console.debug("Style sheet %o loaded", path); - setTimeout(resolve, 100); - }; - - document.getElementById("style").appendChild(tag); - tag.href = path + (cache_tag || ""); - }); - } - } - - export async function load_styles(paths: SourcePath[]) : Promise { - const promises: Promise[] = []; - const errors: { - sheet: SourcePath, - error: any - }[] = []; - - for(const sheet of paths) - promises.push(load_style(sheet).catch(error => { - errors.push({ - sheet: sheet, - error: error - }); - return Promise.resolve(); - })); - - await Promise.all([...promises]); - - if(errors.length > 0) { - console.error("Failed to load the following style sheet:"); - for(const sheet of errors) - console.log(" - %o: %o", sheet.sheet, sheet.error); - - displayCriticalError("Failed to load style sheet " + script_name(errors[0].sheet) + "
" + "View the browser console for more information!"); - throw "failed to load style sheet " + script_name(errors[0].sheet); - } - } -} - -/* define that here */ -let _critical_triggered = false; -const display_critical_load = (message: string, error?: string) => { - if(_critical_triggered) return; /* only show the first error */ - _critical_triggered = true; - - let tag = document.getElementById("critical-load"); - - let detail = tag.getElementsByClassName("detail")[0]; - detail.innerHTML = message; - - if(error) { - const error_tags = tag.getElementsByClassName("error"); - error_tags[0].innerHTML = error; - } - - //error-message - tag.style.display = "block"; - _fadeout_warned = true; /* we know that JQuery hasn't been loaded, else this function would be replaced by something else */ -}; - -const loader_impl_display_critical_error = message => { - if(typeof(createErrorModal) !== 'undefined' && typeof((window).ModalFunctions) !== 'undefined') { - createErrorModal("A critical error occurred while loading the page!", message, {closeable: false}).open(); - } else { - display_critical_load(message); - } - fadeoutLoader(); -}; - -interface Window { - impl_display_critical_error: (_: string) => any; -} - -if(!window.impl_display_critical_error) { /* default impl */ - window.impl_display_critical_error = loader_impl_display_critical_error; -} -function displayCriticalError(message: string) { - if(window.impl_display_critical_error) - window.impl_display_critical_error(message); - else - loader_impl_display_critical_error(message); -} - -/* all javascript loaders */ -const loader_javascript = { - detect_type: async () => { - if(window.require) { - const request = new Request("js/proto.js"); - let file_path = request.url; - if(!file_path.startsWith("file://")) - throw "Invalid file path (" + file_path + ")"; - file_path = file_path.substring(process.platform === "win32" ? 8 : 7); - - const fs = require('fs'); - if(fs.existsSync(file_path)) { - app.type = app.Type.CLIENT_DEBUG; - } else { - app.type = app.Type.CLIENT_RELEASE; - } - } else { - /* test if js/proto.js is available. If so we're in debug mode */ - const request = new XMLHttpRequest(); - request.open('GET', 'js/proto.js', true); - - await new Promise((resolve, reject) => { - request.onreadystatechange = () => { - if (request.readyState === 4){ - if (request.status === 404) { - app.type = app.Type.WEB_RELEASE; - } else { - app.type = app.Type.WEB_DEBUG; - } - resolve(); - } - }; - request.onerror = () => { - reject("Failed to detect app type"); - }; - request.send(); - }); - } - }, - load_scripts: async () => { - /* - if(window.require !== undefined) { - console.log("Loading node specific things"); - const remote = require('electron').remote; - module.paths.push(remote.app.getAppPath() + "/node_modules"); - module.paths.push(remote.app.getAppPath() + "/app"); - module.paths.push(remote.getGlobal("browser-root") + "js/"); - window.$ = require("assets/jquery.min.js"); - require("native/loader_adapter.js"); - } - */ - - if(!window.require) { - await loader.load_script(["vendor/jquery/jquery.min.js"]); - } else { - /* - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "forum sync", - priority: 10, - function: async () => { - forum.sync_main(); - } - }); - */ - } - await loader.load_script(["vendor/DOMPurify/purify.min.js"]); - - /* bootstrap material design and libs */ - //await loader.load_script(["vendor/popper/popper.js"]); - - //depends on popper - //await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]); - - /* - loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "materialize body", - priority: 10, - function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); } - }); - */ - - await loader.load_script("vendor/jsrender/jsrender.min.js"); - await loader.load_scripts([ - ["vendor/xbbcode/src/parser.js"], - ["vendor/moment/moment.js"], - ["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */ - ["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */ - ["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */ - ["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"] - ]); - await loader.load_scripts([ - ["vendor/emoji-picker/src/jquery.lsxemojipicker.js"] - ]); - - if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "scripts release", - priority: 20, - function: loader_javascript.load_release - }); - } else { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "scripts debug", - priority: 20, - function: loader_javascript.load_scripts_debug - }); - } - }, - load_scripts_debug: async () => { - /* test if we're loading as TeaClient or WebClient */ - if(!window.require) { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "javascript web", - priority: 10, - function: loader_javascript.load_scripts_debug_web - }); - } else { - loader.register_task(loader.Stage.JAVASCRIPT, { - name: "javascript client", - priority: 10, - function: loader_javascript.load_scripts_debug_client - }); - } - - /* load some extends classes */ - await loader.load_scripts([ - ["js/connection/ConnectionBase.js"] - ]); - - /* load the main app */ - await loader.load_scripts([ - //Load general API's - "js/proto.js", - "js/i18n/localize.js", - "js/i18n/country.js", - "js/log.js", - - "js/sound/Sounds.js", - - "js/utils/helpers.js", - - "js/crypto/sha.js", - "js/crypto/hex.js", - "js/crypto/asn1.js", - "js/crypto/crc32.js", - - //load the profiles - "js/profiles/ConnectionProfile.js", - "js/profiles/Identity.js", - "js/profiles/identities/teaspeak-forum.js", - - //Basic UI elements - "js/ui/elements/context_divider.js", - "js/ui/elements/context_menu.js", - "js/ui/elements/modal.js", - "js/ui/elements/tab.js", - "js/ui/elements/slider.js", - "js/ui/elements/tooltip.js", - - //Load UI - "js/ui/modal/ModalAbout.js", - "js/ui/modal/ModalAvatar.js", - "js/ui/modal/ModalAvatarList.js", - "js/ui/modal/ModalQuery.js", - "js/ui/modal/ModalQueryManage.js", - "js/ui/modal/ModalPlaylistList.js", - "js/ui/modal/ModalPlaylistEdit.js", - "js/ui/modal/ModalBookmarks.js", - "js/ui/modal/ModalConnect.js", - "js/ui/modal/ModalSettings.js", - "js/ui/modal/ModalCreateChannel.js", - "js/ui/modal/ModalServerEdit.js", - "js/ui/modal/ModalChangeVolume.js", - "js/ui/modal/ModalBanClient.js", - "js/ui/modal/ModalIconSelect.js", - "js/ui/modal/ModalInvite.js", - "js/ui/modal/ModalIdentity.js", - "js/ui/modal/ModalBanCreate.js", - "js/ui/modal/ModalBanList.js", - "js/ui/modal/ModalYesNo.js", - "js/ui/modal/ModalPoke.js", - "js/ui/modal/ModalKeySelect.js", - "js/ui/modal/ModalGroupAssignment.js", - "js/ui/modal/permission/ModalPermissionEdit.js", - {url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, - {url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, - - "js/ui/channel.js", - "js/ui/client.js", - "js/ui/server.js", - "js/ui/view.js", - "js/ui/client_move.js", - "js/ui/htmltags.js", - - "js/ui/frames/SelectedItemInfo.js", - "js/ui/frames/ControlBar.js", - "js/ui/frames/chat.js", - "js/ui/frames/chat_frame.js", - "js/ui/frames/connection_handlers.js", - "js/ui/frames/server_log.js", - "js/ui/frames/hostbanner.js", - "js/ui/frames/MenuBar.js", - - //Load permissions - "js/permission/PermissionManager.js", - "js/permission/GroupManager.js", - - //Load audio - "js/voice/RecorderBase.js", - "js/voice/RecorderProfile.js", - - //Load general stuff - "js/settings.js", - "js/bookmarks.js", - "js/FileManager.js", - "js/ConnectionHandler.js", - "js/BrowserIPC.js", - "js/dns.js", - - //Connection - "js/connection/CommandHandler.js", - "js/connection/CommandHelper.js", - "js/connection/HandshakeHandler.js", - "js/connection/ServerConnectionDeclaration.js", - - "js/stats.js", - "js/PPTListener.js", - - "js/profiles/identities/NameIdentity.js", //Depends on Identity - "js/profiles/identities/TeaForumIdentity.js", //Depends on Identity - "js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity - ]); - - await loader.load_script("js/main.js"); - }, - load_scripts_debug_web: async () => { - await loader.load_scripts([ - ["js/audio/AudioPlayer.js"], - ["js/audio/WebCodec.js"], - ["js/WebPPTListener.js"], - - "js/voice/AudioResampler.js", - "js/voice/JavascriptRecorder.js", - "js/voice/VoiceHandler.js", - "js/voice/VoiceClient.js", - - //Connection - "js/connection/ServerConnection.js", - - //Load codec - "js/codec/Codec.js", - "js/codec/BasicCodec.js", - {url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]}, - ]); - }, - load_scripts_debug_client: async () => { - await loader.load_scripts([ - ]); - }, - - load_release: async () => { - console.log("Load for release!"); - - await loader.load_scripts([ - //Load general API's - ["js/client.min.js", "js/client.js"] - ]); - } -}; - -const loader_webassembly = { - test_webassembly: async () => { - /* We dont required WebAssembly anymore for fundamental functions, only for auto decoding - if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") { - console.log(navigator.browserSpecs); - if (navigator.browserSpecs.name == 'Safari') { - if (parseInt(navigator.browserSpecs.version) < 11) { - displayCriticalError("You require Safari 11 or higher to use the web client!
Safari " + navigator.browserSpecs.version + " does not support WebAssambly!"); - return; - } - } - else { - // Do something for all other browsers. - } - displayCriticalError("You require WebAssembly for TeaSpeak-Web!"); - throw "Missing web assembly"; - } - */ - } -}; - -const loader_style = { - load_style: async () => { - await loader.load_styles([ - "vendor/xbbcode/src/xbbcode.css" - ]); - await loader.load_styles([ - "vendor/emoji-picker/src/jquery.lsxemojipicker.css" - ]); - await loader.load_styles([ - ["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */ - ]); - - if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) { - await loader_style.load_style_debug(); - } else { - await loader_style.load_style_release(); - } - }, - - load_style_debug: async () => { - await loader.load_styles([ - "css/static/main.css", - "css/static/main-layout.css", - "css/static/helptag.css", - "css/static/scroll.css", - "css/static/channel-tree.css", - "css/static/ts/tab.css", - "css/static/ts/chat.css", - "css/static/ts/icons.css", - "css/static/ts/icons_em.css", - "css/static/ts/country.css", - "css/static/general.css", - "css/static/modal.css", - "css/static/modals.css", - "css/static/modal-about.css", - "css/static/modal-avatar.css", - "css/static/modal-icons.css", - "css/static/modal-bookmarks.css", - "css/static/modal-connect.css", - "css/static/modal-channel.css", - "css/static/modal-query.css", - "css/static/modal-invite.css", - "css/static/modal-playlist.css", - "css/static/modal-banlist.css", - "css/static/modal-bancreate.css", - "css/static/modal-settings.css", - "css/static/modal-poke.css", - "css/static/modal-server.css", - "css/static/modal-keyselect.css", - "css/static/modal-permissions.css", - "css/static/modal-group-assignment.css", - "css/static/music/info_plate.css", - "css/static/frame/SelectInfo.css", - "css/static/control_bar.css", - "css/static/context_menu.css", - "css/static/frame-chat.css", - "css/static/connection_handlers.css", - "css/static/server-log.css", - "css/static/htmltags.css", - "css/static/hostbanner.css", - "css/static/menu-bar.css" - ]); - }, - - load_style_release: async () => { - await loader.load_styles([ - "css/static/base.css", - "css/static/main.css", - ]); - } -}; - -async function load_templates() { - try { - const response = await $.ajax("templates.html" + (loader.cache_tag || "")); - - let node = document.createElement("html"); - node.innerHTML = response; - let tags: HTMLCollection; - if(node.getElementsByTagName("body").length > 0) - tags = node.getElementsByTagName("body")[0].children; - else - tags = node.children; - - let root = document.getElementById("templates"); - if(!root) { - displayCriticalError("Failed to find template tag!"); - return; - } - while(tags.length > 0){ - let tag = tags.item(0); - root.appendChild(tag); - - } - } catch(error) { - displayCriticalError("Failed to find template tag!"); - throw "template error"; - } -} - -/* test if all files shall be load from cache or fetch again */ -async function check_updates() { - const app_version = (() => { - const version = app.ui_version(); - - if(!version || version == "unknown" || version.replace(/0+/, "").length == 0) - return undefined; - - return version; - })(); - console.log("Found current app version: %o", app_version); - - if(!app_version) { - /* TODO add warning */ - loader.cache_tag = "?_ts=" + Date.now(); - return; - } - const cached_version = localStorage.getItem("cached_version"); - if(!cached_version || cached_version != app_version) { - loader.register_task(loader.Stage.LOADED, { - priority: 0, - name: "cached version updater", - function: async () => { - localStorage.setItem("cached_version", app_version); - } - }); - } - loader.cache_tag = "?_version=" + app_version; -} - -interface Window { - $: JQuery; -} - -//FUN: loader_ignore_age=0&loader_default_duration=1500&loader_default_age=5000 -let _fadeout_warned = false; -function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = undefined) { - if(typeof($) === "undefined") { - if(!_fadeout_warned) - console.warn("Could not fadeout loader screen. Missing jquery functions."); - _fadeout_warned = true; - return; - } - - let settingsDefined = typeof(StaticSettings) !== "undefined"; - if(!duration) { - if(settingsDefined) - duration = StaticSettings.instance.static("loader_default_duration", 750); - else duration = 750; - } - if(!minAge) { - if(settingsDefined) - minAge = StaticSettings.instance.static("loader_default_age", 1750); - else minAge = 750; - } - if(!ignoreAge) { - if(settingsDefined) - ignoreAge = StaticSettings.instance.static("loader_ignore_age", false); - else ignoreAge = false; - } - - /* - let age = Date.now() - app.appLoaded; - if(age < minAge && !ignoreAge) { - setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age); - return; - } - */ - - $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration); - $(".loader .half").animate({width: 0}, duration, () => { - $(".loader").detach(); - }); -} - -/* register tasks */ -loader.register_task(loader.Stage.INITIALIZING, { - name: "safari fix", - function: async () => { - /* safari remove "fix" */ - if(Element.prototype.remove === undefined) - Object.defineProperty(Element.prototype, "remove", { - enumerable: false, - configurable: false, - writable: false, - value: function(){ - this.parentElement.removeChild(this); - } - }); - }, - priority: 50 -}); - -loader.register_task(loader.Stage.INITIALIZING, { - name: "Browser detection", - function: async () => { - navigator.browserSpecs = (function(){ - let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - if(/trident/i.test(M[1])){ - tem = /\brv[ :]+(\d+)/g.exec(ua) || []; - return {name:'IE',version:(tem[1] || '')}; - } - if(M[1]=== 'Chrome'){ - tem = ua.match(/\b(OPR|Edge)\/(\d+)/); - if(tem != null) return {name:tem[1].replace('OPR', 'Opera'),version:tem[2]}; - } - M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?']; - if((tem = ua.match(/version\/(\d+)/i))!= null) - M.splice(1, 1, tem[1]); - return {name:M[0], version:M[1]}; - })(); - - console.log("Resolved browser specs: %o", navigator.browserSpecs); //Object { name: "Firefox", version: "42" } - }, - priority: 30 -}); - -loader.register_task(loader.Stage.INITIALIZING, { - name: "secure tester", - function: async () => { - /* we need https or localhost to use some things like the storage API */ - if(typeof isSecureContext === "undefined") - (window)["isSecureContext"] = location.protocol !== 'https:' && location.hostname !== 'localhost'; - - if(!isSecureContext) { - display_critical_load("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!"); - throw "App requires a secure context!" - } - }, - priority: 20 -}); - -loader.register_task(loader.Stage.INITIALIZING, { - name: "webassembly tester", - function: loader_webassembly.test_webassembly, - priority: 20 -}); - -loader.register_task(loader.Stage.INITIALIZING, { - name: "app type test", - function: loader_javascript.detect_type, - priority: 20 -}); - -loader.register_task(loader.Stage.INITIALIZING, { - name: "update tester", - priority: 60, - function: check_updates -}); - -loader.register_task(loader.Stage.JAVASCRIPT, { - name: "javascript", - function: loader_javascript.load_scripts, - priority: 10 -}); - -loader.register_task(loader.Stage.STYLE, { - name: "style", - function: loader_style.load_style, - priority: 10 -}); - -loader.register_task(loader.Stage.TEMPLATES, { - name: "templates", - function: load_templates, - priority: 10 -}); - -loader.register_task(loader.Stage.LOADED, { - name: "loaded handler", - function: async () => { - fadeoutLoader(); - }, - priority: 10 -}); - -loader.register_task(loader.Stage.LOADED, { - name: "error task", - function: async () => { - if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) { - display_critical_load("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good."); - throw "The tea is cold!"; - } - }, - priority: 20 -}); -loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { - name: "lsx emoji picker setup", - function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}), - priority: 10 -}); - - -window["Module"] = window["Module"] || {}; -/* TeaClient */ -if(window.require) { - const path = require("path"); - const remote = require('electron').remote; - module.paths.push(path.join(remote.app.getAppPath(), "/modules")); - module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js")); - - const connector = require("renderer"); - console.log(connector); - - loader.register_task(loader.Stage.INITIALIZING, { - name: "teaclient initialize", - function: connector.initialize, - priority: 40 - }); -} else { - const hello_world = () => { - const print_security = () => { - { - const css = [ - "display: block", - "text-align: center", - "font-size: 42px", - "font-weight: bold", - "-webkit-text-stroke: 2px black", - "color: red" - ].join(";"); - console.log("%c ", "font-size: 100px;"); - console.log("%cSecurity warning:", css); - } - { - const css = [ - "display: block", - "text-align: center", - "font-size: 18px", - "font-weight: bold" - ].join(";"); - - console.log("%cPasting anything in here could give attackers access to your data.", css); - console.log("%cUnless you understand exactly what you are doing, close this window and stay safe.", css); - console.log("%c ", "font-size: 100px;"); - } - }; - - /* print the hello world */ - { - const css = [ - "display: block", - "text-align: center", - "font-size: 72px", - "font-weight: bold", - "-webkit-text-stroke: 2px black", - "color: #18BC9C" - ].join(";"); - console.log("%cHey, hold on!", css); - } - { - const css = [ - "display: block", - "text-align: center", - "font-size: 26px", - "font-weight: bold" - ].join(";"); - - const css_2 = [ - "display: block", - "text-align: center", - "font-size: 26px", - "font-weight: bold", - "color: blue" - ].join(";"); - - const display_detect = /./; - display_detect.toString = function() { print_security(); return ""; } - - console.log("%cLovely to see you using and debugging the TeaSpeak Web client.", css); - console.log("%cIf you have some good ideas or already done some incredible changes,", css); - console.log("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2); - console.log("%c ", display_detect); - } - }; - - try { /* lets try to print it as VM code :)*/ - let hello_world_code = hello_world.toString(); - hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8); - hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}")); - hello_world_code = hello_world_code.replace(/(?!"\S*) {2,}(?!\S*")/g, " ").replace(/[\n\r]/g, ""); - eval(hello_world_code); - } catch(e) { - hello_world(); - } -} - -loader.execute().then(() => { - console.log("app successfully loaded!"); -}).catch(error => { - displayCriticalError("failed to load app!
Please lookup the browser console for more details"); - /* console.error("Failed to load app!\nError: %o", error); */ //Error should be already printed by the loader -}); \ No newline at end of file diff --git a/shared/js/log.ts b/shared/js/log.ts index 1ca4c3a0..b6f50290 100644 --- a/shared/js/log.ts +++ b/shared/js/log.ts @@ -2,14 +2,17 @@ enum LogCategory { CHANNEL, CHANNEL_PROPERTIES, /* separating channel and channel properties because on channel init logging is a big bottleneck */ CLIENT, + BOOKMARKS, SERVER, PERMISSIONS, GENERAL, NETWORKING, VOICE, + AUDIO, I18N, IPC, - IDENTITIES + IDENTITIES, + STATISTICS } namespace log { @@ -26,13 +29,16 @@ namespace log { [LogCategory.CHANNEL_PROPERTIES, "Channel "], [LogCategory.CLIENT, "Client "], [LogCategory.SERVER, "Server "], + [LogCategory.BOOKMARKS, "Bookmark "], [LogCategory.PERMISSIONS, "Permission "], [LogCategory.GENERAL, "General "], [LogCategory.NETWORKING, "Network "], [LogCategory.VOICE, "Voice "], + [LogCategory.AUDIO, "Audio "], [LogCategory.I18N, "I18N "], - [LogCategory.IDENTITIES, "IDENTITIES "], - [LogCategory.IPC, "IPC "] + [LogCategory.IDENTITIES, "Identities "], + [LogCategory.IPC, "IPC "], + [LogCategory.STATISTICS, "Statistics "] ]); export let enabled_mapping = new Map([ @@ -40,13 +46,25 @@ namespace log { [LogCategory.CHANNEL_PROPERTIES, false], [LogCategory.CLIENT, true], [LogCategory.SERVER, true], + [LogCategory.BOOKMARKS, true], [LogCategory.PERMISSIONS, true], [LogCategory.GENERAL, true], [LogCategory.NETWORKING, true], [LogCategory.VOICE, true], + [LogCategory.AUDIO, true], [LogCategory.I18N, false], [LogCategory.IDENTITIES, true], - [LogCategory.IPC, true] + [LogCategory.IPC, true], + [LogCategory.STATISTICS, true] + ]); + + //Values will be overridden by initialize() + export let level_mapping = new Map([ + [LogType.TRACE, true], + [LogType.DEBUG, true], + [LogType.INFO, true], + [LogType.WARNING, true], + [LogType.ERROR, true] ]); enum GroupMode { @@ -55,22 +73,36 @@ namespace log { } const group_mode: GroupMode = GroupMode.PREFIX; - loader.register_task(loader.Stage.LOADED, { + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "log enabled initialisation", function: async () => initialize(), - priority: 10 + priority: 150 }); - //Example: ?log.i18n.enabled=0 + //Category Example: ?log.i18n.enabled=0 + //Level Example A: ?log.level.trace.enabled=0 + //Level Example B: ?log.level=0 export function initialize() { for(const category of Object.keys(LogCategory).map(e => parseInt(e))) { if(isNaN(category)) continue; - const category_name = LogCategory[category]; - enabled_mapping[category] = settings.static_global("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category)); + const category_name = LogCategory[category].toLowerCase(); + enabled_mapping.set(category, settings.static_global("log." + category_name.toLowerCase() + ".enabled", enabled_mapping.get(category))); + } + + const base_level = settings.static_global("log.level", app.type === app.Type.CLIENT_DEBUG || app.type === app.Type.WEB_DEBUG ? LogType.TRACE : LogType.INFO); + + for(const level of Object.keys(LogType).map(e => parseInt(e))) { + if(isNaN(level)) continue; + + const level_name = LogType[level].toLowerCase(); + level_mapping.set(level, settings.static_global("log." + level_name + ".enabled", level >= base_level)); } } function logDirect(type: LogType, message: string, ...optionalParams: any[]) { + if(!level_mapping.get(type)) + return; + switch (type) { case LogType.TRACE: case LogType.DEBUG: @@ -86,11 +118,10 @@ namespace log { console.error(message, ...optionalParams); break; } - //console.log("This is %cMy stylish message", "color: yellow; font-style: italic; background-color: blue;padding: 2px"); } export function log(type: LogType, category: LogCategory, message: string, ...optionalParams: any[]) { - if(!enabled_mapping[category]) return; + if(!enabled_mapping.get(category)) return; optionalParams.unshift(category_mapping.get(category)); message = "[%s] " + message; @@ -124,13 +155,15 @@ namespace log { return new Group(group_mode, level, category, name, optionalParams); } - export function table(title: string, arguments: any) { + export function table(level: LogType, category: LogCategory, title: string, arguments: any) { if(group_mode == GroupMode.NATIVE) { console.groupCollapsed(title); console.table(arguments); console.groupEnd(); } else { - console.log("Snipped table %s", title); + if(!enabled_mapping.get(category) || !level_mapping.get(level)) + return; + logDirect(level, tr("Snipped table \"%s\""), title); } } @@ -154,7 +187,7 @@ namespace log { this.category = category; this.name = name; this.optionalParams = optionalParams; - this.enabled = enabled_mapping[category]; + this.enabled = enabled_mapping.get(category); } group(level: LogType, name: string, ...optionalParams: any[]) : Group { @@ -190,8 +223,9 @@ namespace log { } if(this.mode == GroupMode.NATIVE) logDirect(this.level, message, ...optionalParams); - else - logDirect(this.level, this._log_prefix + message, ...optionalParams); + else { + logDirect(this.level, "[%s] " + this._log_prefix + message, category_mapping.get(this.category), ...optionalParams); + } return this; } diff --git a/shared/js/main.ts b/shared/js/main.ts index 71c88608..aa01cb5a 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -84,11 +84,11 @@ function setup_close() { declare function moment(...arguments) : any; function setup_jsrender() : boolean { if(!js_render) { - displayCriticalError("Missing jsrender extension!"); + loader.critical_error("Missing jsrender extension!"); return false; } if(!js_render.views) { - displayCriticalError("Missing jsrender viewer extension!"); + loader.critical_error("Missing jsrender viewer extension!"); return false; } js_render.views.settings.allowCode(true); @@ -108,10 +108,10 @@ function setup_jsrender() : boolean { }); $(".jsrender-template").each((idx, _entry) => { - if(!js_render.templates(_entry.id, _entry.innerHTML)) { //, _entry.innerHTML - console.error("Failed to cache template " + _entry.id + " for js render!"); + if(!js_render.templates(_entry.id, _entry.innerHTML)) { + log.error(LogCategory.GENERAL, tr("Failed to setup cache for js renderer template %s!"), _entry.id); } else - console.debug("Successfully loaded jsrender template " + _entry.id); + log.info(LogCategory.GENERAL, tr("Successfully loaded jsrender template %s"), _entry.id); }); return true; } @@ -123,7 +123,7 @@ async function initialize() { await i18n.initialize(); } catch(error) { console.error(tr("Failed to initialized the translation system!\nError: %o"), error); - displayCriticalError("Failed to setup the translation system"); + loader.critical_error("Failed to setup the translation system"); return; } @@ -131,13 +131,6 @@ async function initialize() { } async function initialize_app() { - const display_load_error = message => { - if(typeof(display_critical_load) !== "undefined") - display_critical_load(message); - else - displayCriticalError(message); - }; - try { //Initialize main template const main = $("#tmpl_main").renderTag({ multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), @@ -146,8 +139,8 @@ async function initialize_app() { $("body").append(main); } catch(error) { - console.error(error); - display_load_error(tr("Failed to setup main page!")); + log.error(LogCategory.GENERAL, error); + loader.critical_error(tr("Failed to setup main page!")); return; } @@ -158,7 +151,7 @@ async function initialize_app() { if(audio.player.set_master_volume) audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100); else - console.warn("Client does not support audio.player.set_master_volume()... May client is too old?"); + log.warn(LogCategory.GENERAL, tr("Client does not support audio.player.set_master_volume()... May client is too old?")); if(audio.recorder.device_refresh_available()) await audio.recorder.refresh_devices(); @@ -166,7 +159,7 @@ async function initialize_app() { await default_recorder.initialize(); sound.initialize().then(() => { - console.log(tr("Sounds initialitzed")); + log.info(LogCategory.AUDIO, tr("Sounds initialized")); }); sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100); @@ -175,8 +168,8 @@ async function initialize_app() { try { await ppt.initialize(); } catch(error) { - console.error(tr("Failed to initialize ppt!\nError: %o"), error); - displayCriticalError(tr("Failed to initialize ppt!")); + log.error(LogCategory.GENERAL, tr("Failed to initialize ppt!\nError: %o"), error); + loader.critical_error(tr("Failed to initialize ppt!")); return; } @@ -274,15 +267,15 @@ class TestProxy extends bipc.MethodProxy { } protected on_connected() { - console.log("Test proxy connected"); + log.info(LogCategory.IPC, "Test proxy connected"); } protected on_disconnected() { - console.log("Test proxy disconnected"); + log.info(LogCategory.IPC, "Test proxy disconnected"); } private async say_hello() : Promise { - console.log("Hello World"); + log.info(LogCategory.IPC, "Hello World"); } private async add_slave(a: number, b: number) : Promise { @@ -363,7 +356,7 @@ function main() { volatile_collection_only: false }); stats.register_user_count_listener(status => { - console.log("Received user count update: %o", status); + log.info(LogCategory.STATISTICS, tr("Received user count update: %o"), status); }); (window).test_upload = (message?: string) => { @@ -377,7 +370,6 @@ function main() { name: '/HelloWorld.txt', path: '' }).then(key => { - console.log("Got key: %o", key); const upload = new RequestFileUpload(key); const buffer = new Uint8Array(message.length); @@ -396,7 +388,6 @@ function main() { 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(Settings.KEY_CONNECT_ADDRESS, ""); const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user"); @@ -431,9 +422,10 @@ function main() { }); */ + // Modals.openServerInfo(connection.channelTree.server); //Modals.createServerModal(connection.channelTree.server, properties => Promise.resolve()); }, 1000); - //Modals.spawnSettingsModal("audio-sounds"); + Modals.spawnSettingsModal("identity-profiles"); //Modals.spawnKeySelect(console.log); } @@ -454,7 +446,7 @@ const task_teaweb_starter: loader.Task = { console.error(ex.stack); if(ex instanceof ReferenceError || ex instanceof TypeError) ex = ex.name + ": " + ex.message; - displayCriticalError("Failed to invoke main function:
" + ex); + loader.critical_error("Failed to invoke main function:
" + ex); } }, priority: 10 @@ -519,7 +511,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { if(!setup_jsrender()) throw "invalid load"; } catch (error) { - displayCriticalError(tr("Failed to setup jsrender")); + loader.critical_error(tr("Failed to setup jsrender")); console.error(tr("Failed to load jsrender! %o"), error); return; } @@ -543,7 +535,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { if(ex instanceof ReferenceError || ex instanceof TypeError) ex = ex.name + ": " + ex.message; - displayCriticalError("Failed to boot app function:
" + ex); + loader.critical_error("Failed to boot app function:
" + ex); } }, priority: 1000 diff --git a/shared/js/permission/GroupManager.ts b/shared/js/permission/GroupManager.ts index c6cec295..29eb7fb2 100644 --- a/shared/js/permission/GroupManager.ts +++ b/shared/js/permission/GroupManager.ts @@ -51,7 +51,6 @@ class Group { if(key == "iconid") { this.properties.iconid = (new Uint32Array([this.properties.iconid]))[0]; - console.log("Icon id " + this.properties.iconid); this.handle.handle.channelTree.clientsByGroup(this).forEach(client => { client.updateGroupIcon(this); }); @@ -139,7 +138,7 @@ class GroupManager extends connection.AbstractCommandHandler { if(json[0]["sgid"]) target = GroupTarget.SERVER; else if(json[0]["cgid"]) target = GroupTarget.CHANNEL; else { - console.error(tr("Could not resolve group target! => %o"), json[0]); + log.error(LogCategory.CLIENT, tr("Could not resolve group target! => %o"), json[0]); return; } @@ -155,8 +154,7 @@ class GroupManager extends connection.AbstractCommandHandler { case 1: type = GroupType.NORMAL; break; case 2: type = GroupType.QUERY; break; default: - //TODO tr - console.error("Invalid group type: " + groupData["type"] + " for group " + groupData["name"]); + log.error(LogCategory.CLIENT, tr("Invalid group type: %o for group %s"), groupData["type"],groupData["name"]); continue; } @@ -180,7 +178,6 @@ class GroupManager extends connection.AbstractCommandHandler { this.channelGroups.push(group); } - console.log("Got " + json.length + " new " + target + " groups:"); for(const client of this.handle.channelTree.clients) client.update_displayed_client_groups(); } diff --git a/shared/js/permission/PermissionManager.ts b/shared/js/permission/PermissionManager.ts index 638982ae..6159bfee 100644 --- a/shared/js/permission/PermissionManager.ts +++ b/shared/js/permission/PermissionManager.ts @@ -1,6 +1,8 @@ /// /// +import LogType = log.LogType; + enum PermissionType { B_SERVERINSTANCE_HELP_VIEW = "b_serverinstance_help_view", /* Permission ID: 1 */ B_SERVERINSTANCE_VERSION_VIEW = "b_serverinstance_version_view", /* Permission ID: 2 */ @@ -601,7 +603,7 @@ class PermissionManager extends connection.AbstractCommandHandler { "description": perm.description }); } - log.table("Permission list", table_entries); + log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Permission list", table_entries); group.end(); log.info(LogCategory.PERMISSIONS, tr("Got %i permissions"), this.permissionList.length); @@ -658,7 +660,7 @@ class PermissionManager extends connection.AbstractCommandHandler { }); } - log.table("Needed client permissions", table_entries); + log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Needed client permissions", table_entries); group.end(); log.debug(LogCategory.PERMISSIONS, tr("Dropping %o needed permissions and added %o permissions."), copy.length, addcount); diff --git a/shared/js/sound/Sounds.ts b/shared/js/sound/Sounds.ts index bb55d1bf..d74f51bc 100644 --- a/shared/js/sound/Sounds.ts +++ b/shared/js/sound/Sounds.ts @@ -201,7 +201,7 @@ namespace sound { manager = new SoundManager(undefined); audio.player.on_ready(reinitialisize_audio); - return new Promise(resolve => { + return new Promise((resolve, reject) => { $.ajax({ url: "audio/speech/mapping.json", success: response => { @@ -211,9 +211,9 @@ namespace sound { register_sound(entry.key, "speech/" + entry.file); resolve(); }, - error: () => { - console.log("error!"); - console.dir(...arguments); + error: error => { + log.error(LogCategory.AUDIO, "error: %o", error); + reject(); }, timeout: 5000, async: true, @@ -254,19 +254,17 @@ namespace sound { if(context.decodeAudioData) { if(!file.cached) { const decode_data = buffer => { - console.log(buffer); try { - console.log(tr("Decoding data")); + log.info(LogCategory.AUDIO, tr("Decoding data")); context.decodeAudioData(buffer, result => { file.cached = result; }, error => { - console.error(tr("Failed to decode audio data for %o"), sound); - console.error(error); + log.error(LogCategory.AUDIO, tr("Failed to decode audio data for %o: %o"), sound, error); file.not_supported = true; file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again! }) } catch (error) { - console.error(error); + log.error(LogCategory.AUDIO, error); file.not_supported = true; file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again! } @@ -288,15 +286,15 @@ namespace sound { if (xhr.status != 200) throw "invalid response code (" + xhr.status + ")"; - console.log(tr("Decoding data")); + log.debug(LogCategory.AUDIO, tr("Decoding data")); try { file.cached = await context.decodeAudioData(xhr.response); } catch(error) { - console.error(error); + log.error(LogCategory.AUDIO, error); throw "failed to decode audio data"; } } catch(error) { - console.error(tr("Failed to load audio file %s. Error: %o"), sound, error); + log.error(LogCategory.AUDIO, tr("Failed to load audio file %s. Error: %o"), sound, error); file.not_supported = true; file.not_supported_timeout = Date.now() + 1000 * 60 * 2; //Try in 2min again! } @@ -305,7 +303,7 @@ namespace sound { if(!file.node) { if(!warned) { warned = true; - console.warn(tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!")); + log.warn(LogCategory.AUDIO, tr("Your browser does not support decodeAudioData! Using a node to playback! This bypasses the audio output and volume regulation!")); } const container = $("#sounds"); const node = $.spawn("audio").attr("src", path); @@ -332,7 +330,7 @@ namespace sound { options = options || {}; const volume = get_sound_volume(_sound, options.default_volume); - console.log(tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume); + log.info(LogCategory.AUDIO, tr("Replaying sound %s (Sound volume: %o | Master volume %o)"), _sound, volume, master_volume); if(volume == 0 || master_volume == 0) return; @@ -342,7 +340,7 @@ namespace sound { const context = audio.player.context(); if(!context) { - console.warn(tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound); + log.warn(LogCategory.AUDIO, tr("Tried to replay a sound without an audio context (Sound: %o). Dropping playback"), _sound); return; } @@ -351,7 +349,7 @@ namespace sound { return; if(!options.ignore_overlap && (this._playing_sounds[_sound] > 0) && !sound.overlap_activated()) { - console.log(tr("Dropping requested playback for sound %s because it would overlap."), _sound); + log.info(LogCategory.AUDIO, tr("Dropping requested playback for sound %s because it would overlap."), _sound); return; } @@ -387,18 +385,18 @@ namespace sound { if(options.callback) options.callback(true); }).catch(error => { - console.warn(tr("Sound playback for sound %o resulted in an error: %o"), sound, error); + log.warn(LogCategory.AUDIO, tr("Sound playback for sound %o resulted in an error: %o"), sound, error); if(options.callback) options.callback(false); }); } else { - console.warn(tr("Failed to replay sound %o because of missing handles."), sound); + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because of missing handles."), sound); if(options.callback) options.callback(false); return; } }).catch(error => { - console.warn(tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error); + log.warn(LogCategory.AUDIO, tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error); if(options.callback) options.callback(false); }); diff --git a/shared/js/stats.ts b/shared/js/stats.ts index 417b54db..ce9ca84b 100644 --- a/shared/js/stats.ts +++ b/shared/js/stats.ts @@ -73,7 +73,7 @@ namespace stats { export function initialize(config: Config) { current_config = initialize_config_object(config || {}, DEFAULT_CONFIG); if(current_config.verbose) - console.log(LOG_PREFIX + tr("Initializing statistics with this config: %o"), current_config); + log.info(LogCategory.STATISTICS, tr("Initializing statistics with this config: %o"), current_config); connection.start_connection(); } @@ -110,7 +110,7 @@ namespace stats { if(connection_copy !== connection) return; if(current_config.verbose) - console.log(LOG_PREFIX + tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event); + log.warn(LogCategory.STATISTICS, tr("Lost connection to statistics server (Connection closed). Reason: %o. Event object: %o"), CloseCodes[event.code] || event.code, event); if(event.code != CloseCodes.BANNED) invoke_reconnect(); @@ -120,7 +120,7 @@ namespace stats { if(connection_copy !== connection) return; if(current_config.verbose) - console.log(LOG_PREFIX + tr("Successfully connected to server. Initializing session.")); + log.info(LogCategory.STATISTICS, tr("Successfully connected to server. Initializing session.")); connection_state = ConnectionState.INITIALIZING; initialize_session(); @@ -130,7 +130,7 @@ namespace stats { if(connection_copy !== connection) return; if(current_config.verbose) - console.log(LOG_PREFIX + tr("Received an error. Closing connection. Object: %o"), event); + log.warn(LogCategory.STATISTICS, tr("Received an error. Closing connection. Object: %o"), event); connection.close(CloseCodes.INTERNAL_ERROR); invoke_reconnect(); @@ -141,7 +141,7 @@ namespace stats { if(typeof(event.data) !== 'string') { if(current_config.verbose) - console.warn(LOG_PREFIX + tr("Received an message which isn't a string. Event object: %o"), event); + log.info(LogCategory.STATISTICS, tr("Received an message which isn't a string. Event object: %o"), event); return; } @@ -170,11 +170,11 @@ namespace stats { } if(current_config.verbose) - console.log(LOG_PREFIX + tr("Scheduled reconnect in %dms"), current_config.reconnect_interval); + log.info(LogCategory.STATISTICS, tr("Scheduled reconnect in %dms"), current_config.reconnect_interval); reconnect_timer = setTimeout(() => { if(current_config.verbose) - console.log(LOG_PREFIX + tr("Reconnecting")); + log.info(LogCategory.STATISTICS, tr("Reconnecting")); start_connection(); }, current_config.reconnect_interval); } @@ -212,10 +212,10 @@ namespace stats { if(typeof(handler[type]) === 'function') { if(current_config.verbose) - console.debug(LOG_PREFIX + tr("Handling message of type %s"), type); + log.debug(LogCategory.STATISTICS, tr("Handling message of type %s"), type); handler[type](data); } else if(current_config.verbose) { - console.warn(LOG_PREFIX + tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object); + log.warn(LogCategory.STATISTICS, tr("Received message with an unknown type (%s). Dropping message. Full message: %o"), type, data_object); } } @@ -231,7 +231,7 @@ namespace stats { interface NotifyInitialized {} function handle_notify_initialized(json: NotifyInitialized) { if(current_config.verbose) - console.log(LOG_PREFIX + tr("Session successfully initialized.")); + log.info(LogCategory.STATISTICS, tr("Session successfully initialized.")); connection_state = ConnectionState.CONNECTED; } diff --git a/shared/js/ui/channel.ts b/shared/js/ui/channel.ts index d495b220..0339ff2e 100644 --- a/shared/js/ui/channel.ts +++ b/shared/js/ui/channel.ts @@ -393,8 +393,9 @@ class ChannelEntry { for(let index = 0; index + 1 < clients.length; index++) clients[index].tag.before(clients[index + 1].tag); + log.debug(LogCategory.CHANNEL, tr("Reordered channel clients: %d"), clients.length); for(let client of clients) { - console.log("- %i %s", client.properties.client_talk_power, client.properties.client_nickname); + log.debug(LogCategory.CHANNEL, "- %i %s", client.properties.client_talk_power, client.properties.client_nickname); } } } @@ -624,23 +625,23 @@ class ChannelEntry { } handle_frame_resized() { - this.__updateChannelName(); + if(this._channel_name_formatted === "align-repetitive") + this.__updateChannelName(); } private static NAME_ALIGNMENTS: string[] = ["align-left", "align-center", "align-right", "align-repetitive"]; private __updateChannelName() { this._channel_name_formatted = undefined; - parseType: + parse_type: if(this.parent_channel() == null && this.properties.channel_name.charAt(0) == '[') { let end = this.properties.channel_name.indexOf(']'); - if(end == -1) break parseType; + if(end == -1) break parse_type; let options = this.properties.channel_name.substr(1, end - 1); - if(options.indexOf("spacer") == -1) break parseType; + if(options.indexOf("spacer") == -1) break parse_type; options = options.substr(0, options.indexOf("spacer")); - console.log(tr("Channel options: '%o'"), options); if(options.length == 0) options = "l"; else if(options.length > 1) @@ -661,11 +662,10 @@ class ChannelEntry { break; default: this._channel_name_alignment = undefined; - break parseType; + break parse_type; } this._channel_name_formatted = this.properties.channel_name.substr(end + 1) || ""; - console.log(tr("Got formated channel name: %o"), this._channel_name_formatted); } this._tag_channel.find(".show-channel-normal-only").toggleClass("channel-normal", this._channel_name_formatted === undefined); @@ -696,14 +696,13 @@ class ChannelEntry { index = 63; } while (tag_name.parent().width() >= tag_name.width() && ++index < 64); if(index == 64) - console.warn(LogCategory.CHANNEL, tr("Repeating spacer took too much repeats!")); + log.warn(LogCategory.CHANNEL, tr("Repeating spacer took too much repeats!")); if(lastSuccess.length > 0) { tag_name.text(lastSuccess); } } } } - console.log(tr("Align: %s"), this._channel_name_alignment); } recalculate_repetitive_name() { @@ -722,7 +721,7 @@ class ChannelEntry { value: variable.value, type: typeof (this.properties[variable.key]) }); - log.table("Clannel update properties", entries); + log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Clannel update properties", entries); } let info_update = false; diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index 491bab8f..c22e7dd2 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -25,6 +25,8 @@ class ClientProperties { client_channel_group_id: number = 0; client_lastconnected: number = 0; + client_created: number = 0; + client_totalconnections: number = 0; client_flag_avatar: string = ""; client_icon_id: number = 0; @@ -44,9 +46,68 @@ class ClientProperties { client_teaforo_name: string = ""; client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */ + + /* not updated in view! */ + client_month_bytes_uploaded: number = 0; + client_month_bytes_downloaded: number = 0; + client_total_bytes_uploaded: number = 0; + client_total_bytes_downloaded: number = 0; + client_talk_power: number = 0; } +class ClientConnectionInfo { + connection_bandwidth_received_last_minute_control: number = -1; + connection_bandwidth_received_last_minute_keepalive: number = -1; + connection_bandwidth_received_last_minute_speech: number = -1; + connection_bandwidth_received_last_second_control: number = -1; + connection_bandwidth_received_last_second_keepalive: number = -1; + connection_bandwidth_received_last_second_speech: number = -1; + + connection_bandwidth_sent_last_minute_control: number = -1; + connection_bandwidth_sent_last_minute_keepalive: number = -1; + connection_bandwidth_sent_last_minute_speech: number = -1; + connection_bandwidth_sent_last_second_control: number = -1; + connection_bandwidth_sent_last_second_keepalive: number = -1; + connection_bandwidth_sent_last_second_speech: number = -1; + + connection_bytes_received_control: number = -1; + connection_bytes_received_keepalive: number = -1; + connection_bytes_received_speech: number = -1; + connection_bytes_sent_control: number = -1; + connection_bytes_sent_keepalive: number = -1; + connection_bytes_sent_speech: number = -1; + + connection_packets_received_control: number = -1; + connection_packets_received_keepalive: number = -1; + connection_packets_received_speech: number = -1; + + connection_packets_sent_control: number = -1; + connection_packets_sent_keepalive: number = -1; + connection_packets_sent_speech: number = -1; + + connection_ping: number = -1; + connection_ping_deviation: number = -1; + + connection_server2client_packetloss_control: number = -1; + connection_server2client_packetloss_keepalive: number = -1; + connection_server2client_packetloss_speech: number = -1; + connection_server2client_packetloss_total: number = -1; + + connection_client2server_packetloss_speech: number = -1; + connection_client2server_packetloss_keepalive: number = -1; + connection_client2server_packetloss_control: number = -1; + connection_client2server_packetloss_total: number = -1; + + connection_filetransfer_bandwidth_sent: number = -1; + connection_filetransfer_bandwidth_received: number = -1; + + connection_connected_time: number = -1; + connection_idle_time: number = -1; + connection_client_ip: string | undefined; + connection_client_port: number = -1; +} + class ClientEntry { protected _clientId: number; protected _channel: ChannelEntry; @@ -61,6 +122,14 @@ class ClientEntry { protected _audio_volume: number; protected _audio_muted: boolean; + private _info_variables_promise: Promise; + private _info_variables_promise_timestamp: number; + + private _info_connection_promise: Promise; + private _info_connection_promise_timestamp: number; + private _info_connection_promise_resolve: any; + private _info_connection_promise_reject: any; + channelTree: ChannelTree; constructor(clientId: number, clientName, properties: ClientProperties = new ClientProperties()) { @@ -176,10 +245,6 @@ class ClientEntry { } }); - this.tag.on('click', event => { - console.log("I've been clicked!"); - }); - if(!(this instanceof LocalClientEntry) && !(this instanceof MusicClientEntry)) this.tag.dblclick(event => { if($.isArray(this.channelTree.currently_selected)) { //Multiselect @@ -524,8 +589,7 @@ class ClientEntry { this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); if(this._audio_handle) this._audio_handle.set_volume(volume); - if(this.channelTree.client.select_info.currentSelected == this) - this.channelTree.client.select_info.update(); + //TODO: Update in info }); } }, { @@ -710,7 +774,7 @@ class ClientEntry { value: variable.value, type: typeof (this.properties[variable.key]) }); - log.table("Client update properties", entries); + log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Client update properties", entries); } for(const variable of variables) { @@ -848,11 +912,17 @@ class ClientEntry { } } - updateClientVariables(){ - if(this.lastVariableUpdate == 0 || new Date().getTime() - 10 * 60 * 1000 > this.lastVariableUpdate){ //Cache these only 10 min - this.lastVariableUpdate = new Date().getTime(); - this.channelTree.client.serverConnection.send_command("clientgetvariables", {clid: this.clientId()}); - } + updateClientVariables(force_update?: boolean) : Promise { + if(Date.now() - 10 * 60 * 1000 < this._info_variables_promise_timestamp && this._info_variables_promise && (typeof(force_update) !== "boolean" || force_update)) + return this._info_variables_promise; + + this._info_variables_promise_timestamp = Date.now(); + return (this._info_variables_promise = new Promise((resolve, reject) => { + this.channelTree.client.serverConnection.send_command("clientgetvariables", {clid: this.clientId()}).then(() => resolve()).catch(error => { + this._info_connection_promise_timestamp = 0; /* not succeeded */ + reject(error); + }); + })); } updateClientIcon() { @@ -957,6 +1027,34 @@ class ClientEntry { client_id: this._clientId } } + + /* max 1s ago, so we could update every second */ + request_connection_info() : Promise { + if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise) + return this._info_connection_promise; + + if(this._info_connection_promise_reject) + this._info_connection_promise_resolve("timeout"); + + let _local_reject; /* to ensure we're using the right resolve! */ + this._info_connection_promise = new Promise((resolve, reject) => { + this._info_connection_promise_resolve = resolve; + this._info_connection_promise_reject = reject; + _local_reject = reject; + }); + + this._info_connection_promise_timestamp = Date.now(); + this.channelTree.client.serverConnection.send_command("getconnectioninfo", {clid: this._clientId}).catch(error => _local_reject(error)); + return this._info_connection_promise; + } + + set_connection_info(info: ClientConnectionInfo) { + if(!this._info_connection_promise_resolve) + return; + this._info_connection_promise_resolve(info); + this._info_connection_promise_resolve = undefined; + this._info_connection_promise_reject = undefined; + } } class LocalClientEntry extends ClientEntry { @@ -1243,8 +1341,6 @@ class MusicClientEntry extends ClientEntry { Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => { this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); this._audio_handle.set_volume(volume); - if(this.channelTree.client.select_info.currentSelected == this) - (this.channelTree.client.select_info.current_manager()).update_local_volume(volume); }); } }, @@ -1265,8 +1361,7 @@ class MusicClientEntry extends ClientEntry { clid: this.clientId(), player_volume: value, }).then(() => { - if(this.channelTree.client.select_info.currentSelected == this) - (this.channelTree.client.select_info.current_manager()).update_remote_volume(value); + //TODO: Update in info }); }); } diff --git a/shared/js/ui/client_move.ts b/shared/js/ui/client_move.ts index cee2d9db..5ccfa8a1 100644 --- a/shared/js/ui/client_move.ts +++ b/shared/js/ui/client_move.ts @@ -50,7 +50,7 @@ class ClientMover { this.selected_client = client; this.callback = callback; - console.log(tr("Starting mouse move")); + log.debug(LogCategory.GENERAL, tr("Starting mouse move")); ClientMover.listener_root.on('mouseup', this._bound_finish = this.finish_listener.bind(this)).on('mousemove', this._bound_move = this.move_listener.bind(this)); @@ -112,6 +112,7 @@ class ClientMover { private finish_listener(event) { ClientMover.move_element.hide(); + log.debug(LogCategory.GENERAL, tr("Finishing mouse move")); const channel_id = this.hovered_channel ? parseInt(this.hovered_channel.getAttribute("channel-id")) : 0; ClientMover.listener_root.unbind('mouseleave', this._bound_finish); diff --git a/shared/js/ui/elements/context_divider.ts b/shared/js/ui/elements/context_divider.ts index 6e68484d..282f89a6 100644 --- a/shared/js/ui/elements/context_divider.ts +++ b/shared/js/ui/elements/context_divider.ts @@ -16,9 +16,18 @@ if(!$.fn.dividerfy) { const seperator_id = element.attr("seperator-id"); const vertical = element.hasClass("vertical"); - const apply_view = (property: string, previous: number, next: number) => { - previous_element.css(property, "calc(" + previous + "% - " + (vertical ? element.width() : element.height()) + "px)"); - next_element.css(property, "calc(" +next + "% - " + (vertical ? element.width() : element.height()) + "px)"); + const apply_view = (property: "width" | "height", previous: number, next: number) => { + const value_a = "calc(" + previous + "% - " + (vertical ? element.width() : element.height()) + "px)"; + const value_b = "calc(" + next + "% - " + (vertical ? element.width() : element.height()) + "px)"; + + /* dont cause a reflow here */ + if(property === "height") { + (previous_element[0] as HTMLElement).style.height = value_a; + (next_element[0] as HTMLElement).style.height = value_b; + } else { + (previous_element[0] as HTMLElement).style.width = value_a; + (next_element[0] as HTMLElement).style.width = value_b; + } }; const listener_move = (event: MouseEvent | TouchEvent) => { @@ -105,12 +114,12 @@ if(!$.fn.dividerfy) { try { const config = JSON.parse(settings.global("seperator-settings-" + seperator_id)); if(config) { - console.log("Apply previous changed: %o", config); + log.debug(LogCategory.GENERAL, tr("Applying previous changed sperator settings for %s: %o"), seperator_id, config); apply_view(config.property, config.previous, config.next); } } catch(e) { if(!(e instanceof SyntaxError)) - console.error(e); + log.error(LogCategory.GENERAL, tr("Failed to parse seperator settings for sperator %s: %o"), seperator_id, e); } } }); diff --git a/shared/js/ui/elements/context_menu.ts b/shared/js/ui/elements/context_menu.ts index 1f4562e5..e15ff944 100644 --- a/shared/js/ui/elements/context_menu.ts +++ b/shared/js/ui/elements/context_menu.ts @@ -80,14 +80,15 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { private _global_click_listener: (event) => any; private _context_menu: JQuery; private _close_callbacks: (() => any)[] = []; + private _visible = false; despawn_context_menu() { - let menu = this._context_menu || (this._context_menu = $(".context-menu")); - - if(!menu.is(":visible")) + if(!this._visible) return; + let menu = this._context_menu || (this._context_menu = $(".context-menu")); menu.animate({opacity: 0}, 100, () => menu.css("display", "none")); + this._visible = false; for(const callback of this._close_callbacks) { if(typeof(callback) !== "function") { console.error(tr("Given close callback is not a function!. Callback: %o"), callback); @@ -185,6 +186,8 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { } spawn_context_menu(x: number, y: number, ...entries: contextmenu.MenuEntry[]) { + this._visible = true; + let menu_tag = this._context_menu || (this._context_menu = $(".context-menu")); menu_tag.finish().empty().css("opacity", "0"); diff --git a/shared/js/ui/frames/SelectedItemInfo.ts b/shared/js/ui/frames/SelectedItemInfo.ts deleted file mode 100644 index b06521a4..00000000 --- a/shared/js/ui/frames/SelectedItemInfo.ts +++ /dev/null @@ -1,700 +0,0 @@ -/// -/// - -abstract class InfoManagerBase { - private timers: NodeJS.Timer[] = []; - private intervals: number[] = []; - - protected resetTimers() { - for(let timer of this.timers) - clearTimeout(timer); - this.timers = []; - } - - protected resetIntervals() { - for(let interval of this.intervals) - clearInterval(interval); - this.intervals = []; - } - - protected registerTimer(timer: NodeJS.Timer) { - this.timers.push(timer); - } - - protected registerInterval(interval: T) { - this.intervals.push(interval as number); - } - - abstract available(object: V) : boolean; -} - -abstract class InfoManager extends InfoManagerBase { - protected handle?: InfoBar; - - createFrame<_>(handle: InfoBar<_>, object: T, html_tag: JQuery) { - this.handle = handle as InfoBar; - } - - abstract updateFrame(object: T, html_tag: JQuery); - - finalizeFrame(object: T, frame: JQuery) { - this.resetIntervals(); - this.resetTimers(); - this.handle = undefined; - } - - protected triggerUpdate() { - if(this.handle) - this.handle.update(); - } -} - -class InfoBar { - readonly handle: ConnectionHandler; - - private current_selected?: AvailableTypes; - - private _tag: JQuery; - private readonly _tag_info: JQuery; - private readonly _tag_banner: JQuery; - - private _current_manager: InfoManagerBase = undefined; - private managers: InfoManagerBase[] = []; - - constructor(client: ConnectionHandler) { - this.handle = client; - - this._tag = $("#tmpl_select_info").renderTag(); - this._tag_info = this._tag.find(".container-select-info"); - this._tag_banner = this._tag.find(".container-banner"); - - this.managers.push(new MusicInfoManager()); - this.managers.push(new ClientInfoManager()); - this.managers.push(new ChannelInfoManager()); - this.managers.push(new ServerInfoManager()); - - this._tag.find("button.close").on('click', () => this.close_popover()); - } - - get_tag() : JQuery { - return this._tag; - } - - destroy() { - this._tag && this._tag.remove(); - this._tag = undefined; - - this.managers = undefined; - this._current_manager = undefined; - - this.current_selected = undefined; - } - - handle_resize() { - /* test if the popover isn't a popover anymore */ - if(this._tag.parent().hasClass('shown')) { - this._tag.parent().removeClass('shown'); - if(this.is_popover()) - this._tag.parent().addClass('shown'); - } - } - - setCurrentSelected(entry: AvailableTypes) { - if(this.current_selected == entry) return; - - if(this._current_manager) { - (this._current_manager as InfoManager).finalizeFrame(this.current_selected, this._tag_info); - this._current_manager = null; - this.current_selected = null; - } - this._tag_info.empty(); - - this.current_selected = entry; - for(let manager of this.managers) { - if(manager.available(this.current_selected)) { - this._current_manager = manager; - break; - } - } - - console.log(tr("Using info manager: %o"), this._current_manager); - if(this._current_manager) - (this._current_manager as InfoManager).createFrame(this, this.current_selected, this._tag_info); - } - - get currentSelected() { - return this.current_selected; - } - - update(){ - if(this._current_manager && this.current_selected) - (this._current_manager as InfoManager).updateFrame(this.current_selected, this._tag_info); - } - - current_manager() { return this._current_manager; } - - is_popover() : boolean { - return !this._tag.parent().is(':visible') || this._tag.parent().hasClass('shown'); - } - - open_popover() { - this._tag.parent().toggleClass('shown', true); - } - - close_popover() { - this._tag.parent().toggleClass('shown', false); - } - - rendered_tag() { - return this._tag_info; - } -} - -interface Window { - Image: typeof HTMLImageElement; - HTMLImageElement: typeof HTMLImageElement; -} - -class ClientInfoManager extends InfoManager { - available(object: V): boolean { - return typeof object == "object" && object instanceof ClientEntry; - } - - createFrame<_>(handle: InfoBar<_>, client: ClientEntry, html_tag: JQuery) { - super.createFrame(handle, client, html_tag); - - client.updateClientVariables(); - this.updateFrame(client, html_tag); - } - - updateFrame(client: ClientEntry, html_tag: JQuery) { - this.resetIntervals(); - html_tag.empty(); - - let properties = this.buildProperties(client); - - let rendered = $("#tmpl_selected_client").renderTag(properties); - html_tag.append(rendered); - - this.registerInterval(setInterval(() => { - html_tag.find(".update_onlinetime").text(formatDate(client.calculateOnlineTime())); - }, 1000)); - } - - buildProperties(client: ClientEntry) : any { - let properties: any = {}; - - properties["client_name"] = client.createChatTag()[0]; - properties["client_onlinetime"] = formatDate(client.calculateOnlineTime()); - properties["sound_volume"] = client.get_audio_handle() ? client.get_audio_handle().get_volume() * 100 : -1; - 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()) { - let group = client.channelTree.client.groups.serverGroup(groupId); - if(!group) continue; - - let group_property = {}; - - group_property["group_id"] = groupId; - group_property["group_name"] = group.name; - properties["group_server"].push(group_property); - properties["group_" + groupId + "_icon"] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid); - } - - let group = client.channelTree.client.groups.channelGroup(client.assignedChannelGroup()); - if(group) { - properties["group_channel"] = group.id; - properties["group_" + group.id + "_name"] = group.name; - properties["group_" + group.id + "_icon"] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid); - } - - for(let key in client.properties) - properties["property_" + key] = client.properties[key]; - - if(client.properties.client_teaforo_id > 0) { - properties["teaspeak_forum"] = $.spawn("a") - .attr("href", "//forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) - .attr("target", "_blank") - .text(client.properties.client_teaforo_id); - } - - if(client.properties.client_flag_avatar && client.properties.client_flag_avatar.length > 0) { - properties["client_avatar"] = client.channelTree.client.fileManager.avatars.generate_client_tag(client); - } - return properties; - } -} - -class ServerInfoManager extends InfoManager { - createFrame<_>(handle: InfoBar<_>, server: ServerEntry, html_tag: JQuery) { - super.createFrame(handle, server, html_tag); - - if(server.shouldUpdateProperties()) server.updateProperties(); - this.updateFrame(server, html_tag); - } - - updateFrame(server: ServerEntry, html_tag: JQuery) { - this.resetIntervals(); - html_tag.empty(); - let properties: any = {}; - - properties["server_name"] = $.spawn("a").text(server.properties.virtualserver_name); - properties["server_onlinetime"] = formatDate(server.calculateUptime()); - properties["server_address"] = server.remote_address.host + (server.remote_address.port == 9987 ? "" : ":" + server.remote_address.port); - const peer_address = server.channelTree.client.serverConnection.remote_address(); - properties["server_peer_address"] = peer_address.host + (peer_address.port == 9987 ? "" : ":" + server.remote_address.port); - properties["hidden_clients"] = Math.max(0, server.properties.virtualserver_clientsonline - server.channelTree.clients.length); - - for(let key in server.properties) - properties["property_" + key] = server.properties[key]; - - let rendered = $("#tmpl_selected_server").renderTag([properties]); - - this.registerInterval(setInterval(() => { - html_tag.find(".update_onlinetime").text(formatDate(server.calculateUptime())); - }, 1000)); - - - { - 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(); - this.triggerUpdate(); - }); - - this.registerTimer(setTimeout(function () { - requestUpdate - .prop("disabled", false) - .toggleClass('btn-success', true) - .toggleClass('btn-danger', false); - }, server.nextInfoRequest - Date.now())); - } - - html_tag.append(rendered); - } - - available(object: V): boolean { - return typeof object == "object" && object instanceof ServerEntry; - } -} - -class ChannelInfoManager extends InfoManager { - createFrame<_>(handle: InfoBar<_>, channel: ChannelEntry, html_tag: JQuery) { - super.createFrame(handle, channel, html_tag); - this.updateFrame(channel, html_tag); - } - - updateFrame(channel: ChannelEntry, html_tag: JQuery) { - this.resetIntervals(); - html_tag.empty(); - let properties: any = {}; - - properties["channel_name"] = channel.generate_tag(false); - properties["channel_type"] = ChannelType.normalize(channel.channelType()); - properties["channel_clients"] = channel.channelTree.clientsByChannel(channel).length; - properties["channel_subscribed"] = channel.flag_subscribed; - properties["server_encryption"] = channel.channelTree.server.properties.virtualserver_codec_encryption_mode; - - for(let key in channel.properties) - properties["property_" + key] = channel.properties[key]; - - let tag_channel_description = $.spawn("div"); - properties["bbcode_channel_description"] = tag_channel_description; - - channel.getChannelDescription().then(description => { - const result = xbbcode.parse(description, {}); - /* - if(result.error) { - console.log("BBCode parse error: %o", result.errorQueue); - } - */ - - tag_channel_description.empty() - .append($.spawn("div").html(result.build_html()).contents()) - .css("overflow-y", "auto") - .css("flex-grow", "1"); - }); - - let rendered = $("#tmpl_selected_channel").renderTag([properties]); - html_tag.append(rendered); - } - - available(object: V): boolean { - return typeof object == "object" && object instanceof ChannelEntry; - } -} - -function format_time(time: number) { - let hours: any = 0, minutes: any = 0, seconds: any = 0; - if(time >= 60 * 60) { - hours = Math.floor(time / (60 * 60)); - time -= hours * 60 * 60; - } - if(time >= 60) { - minutes = Math.floor(time / 60); - time -= minutes * 60; - } - seconds = time; - - if(hours > 9) - hours = hours.toString(); - else if(hours > 0) - hours = '0' + hours.toString(); - else hours = ''; - - if(minutes > 9) - minutes = minutes.toString(); - else if(minutes > 0) - minutes = '0' + minutes.toString(); - else - minutes = '00'; - - if(seconds > 9) - seconds = seconds.toString(); - else if(seconds > 0) - seconds = '0' + seconds.toString(); - else - seconds = '00'; - - return (hours ? hours + ":" : "") + minutes + ':' + seconds; -} - -enum MusicPlayerState { - SLEEPING, - LOADING, - - PLAYING, - PAUSED, - STOPPED -} - -class MusicInfoManager extends ClientInfoManager { - single_handler: connection.SingleCommandHandler; - - createFrame<_>(handle: InfoBar<_>, channel: MusicClientEntry, html_tag: JQuery) { - super.createFrame(handle, channel, html_tag); - this.updateFrame(channel, html_tag); - } - - updateFrame(bot: MusicClientEntry, html_tag: JQuery) { - if(this.single_handler) { - this.handle.handle.serverConnection.command_handler_boss().remove_single_handler(this.single_handler); - this.single_handler = undefined; - } - - this.resetIntervals(); - html_tag.empty(); - - let properties = super.buildProperties(bot); - { //Render info frame - if(bot.properties.player_state < MusicPlayerState.PLAYING) { - properties["music_player"] = $("#tmpl_music_frame_empty").renderTag().css("align-self", "center"); - } else { - let frame = $.spawn("div").text(tr("loading...")) as JQuery; - properties["music_player"] = frame; - properties["song_url"] = $.spawn("a").text(tr("loading...")); - - bot.requestPlayerInfo().then(info => { - let timestamp = Date.now(); - - console.log(info); - let _frame = $("#tmpl_music_frame").renderTag({ - song_name: info.player_title ? info.player_title : - info.song_url ? info.song_url : tr("No title or url"), - song_url: info.song_url, - thumbnail: info.song_thumbnail && info.song_thumbnail.length > 0 ? info.song_thumbnail : undefined - }).css("align-self", "center"); - properties["song_url"].text(info.song_url); - - frame.replaceWith(_frame); - frame = _frame; - - /* Play/Pause logic */ - { - let button_play = frame.find(".button_play"); - let button_pause = frame.find(".button_pause"); - let button_stop = frame.find('.button_stop'); - button_play.click(handler => { - if(!button_play.hasClass("active")) { - this.handle.handle.serverConnection.send_command("musicbotplayeraction", { - bot_id: bot.properties.client_database_id, - action: 1 - }).then(updated => this.triggerUpdate()).catch(error => { - createErrorModal(tr("Failed to execute play"), MessageHelper.formatMessage(tr("Failed to execute play.
{}"), error)).open(); - this.triggerUpdate(); - }); - } - button_pause.show(); - button_play.hide(); - }); - button_pause.click(handler => { - if(!button_pause.hasClass("active")) { - this.handle.handle.serverConnection.send_command("musicbotplayeraction", { - bot_id: bot.properties.client_database_id, - action: 2 - }).then(updated => this.triggerUpdate()).catch(error => { - createErrorModal(tr("Failed to execute pause"), MessageHelper.formatMessage(tr("Failed to execute pause.
{}"), error)).open(); - this.triggerUpdate(); - }); - } - button_play.show(); - button_pause.hide(); - }); - button_stop.click(handler => { - this.handle.handle.serverConnection.send_command("musicbotplayeraction", { - bot_id: bot.properties.client_database_id, - action: 0 - }).then(updated => this.triggerUpdate()).catch(error => { - createErrorModal(tr("Failed to execute stop"), MessageHelper.formatMessage(tr("Failed to execute stop.
{}"), error)).open(); - this.triggerUpdate(); - }); - }); - - if(bot.properties.player_state == 2) { - button_play.hide(); - button_pause.show(); - } else if(bot.properties.player_state == 3) { - button_pause.hide(); - button_play.show(); - } else if(bot.properties.player_state == 4) { - button_pause.hide(); - button_play.show(); - } - } - - { /* Button functions */ - _frame.find(".btn-forward").click(() => { - this.handle.handle.serverConnection.send_command("musicbotplayeraction", { - bot_id: bot.properties.client_database_id, - action: 3 - }).then(updated => this.triggerUpdate()).catch(error => { - createErrorModal(tr("Failed to execute forward"), tr("Failed to execute pause.
{}").format(error)).open(); - this.triggerUpdate(); - }); - }); - _frame.find(".btn-rewind").click(() => { - this.handle.handle.serverConnection.send_command("musicbotplayeraction", { - bot_id: bot.properties.client_database_id, - action: 4 - }).then(updated => this.triggerUpdate()).catch(error => { - createErrorModal(tr("Failed to execute rewind"),tr( "Failed to execute pause.
{}").format(error)).open(); - this.triggerUpdate(); - }); - }); - _frame.find(".btn-settings").click(() => { - this.handle.handle.serverConnection.command_helper.request_playlist_list().then(lists => { - for(const entry of lists) { - if(entry.playlist_id == bot.properties.client_playlist_id) { - Modals.spawnPlaylistEdit(bot.channelTree.client, entry); - return; - } - } - createErrorModal(tr("Invalid permissions"), tr("You don't have to see the bots playlist.")).open(); - }).catch(error => { - createErrorModal(tr("Failed to query playlist."), tr("Failed to query playlist info.")).open(); - }); - }); - } - - - /* Required flip card javascript */ - frame.find(".right").mouseenter(() => { - frame.find(".controls-overlay").addClass("flipped"); - }); - frame.find(".right").mouseleave(() => { - frame.find(".controls-overlay").removeClass("flipped"); - }); - - /* Slider */ - frame.find(".timeline .slider").on('mousedown', ev => { - let timeline = frame.find(".timeline"); - let time = frame.find(".time"); - let slider = timeline.find(".slider"); - let slider_old = slider.css("margin-left"); - - let time_max = parseInt(timeline.attr("time-max")); - slider.prop("editing", true); - - let target_timestamp = 0; - let move_handler = (event: MouseEvent) => { - let max = timeline.width(); - let current = event.pageX - timeline.offset().left - slider.width() / 2; - if(current < 0) current = 0; - else if(current > max) current = max; - - target_timestamp = current / max * time_max; - time.text(format_time(Math.floor(target_timestamp / 1000))); - slider.css("margin-left", current / max * 100 + "%"); - }; - - let finish_handler = event => { - console.log("Event (%i | %s): %o", event.button, event.type, event); - if(event.type == "mousedown" && event.button != 2) return; - $(document).unbind("mousemove", move_handler as any); - $(document).unbind("mouseup mouseleave mousedown", finish_handler as any); - - if(event.type != "mousedown") { - slider.prop("editing", false); - slider.prop("edited", true); - - let current_timestamp = info.player_replay_index + Date.now() - timestamp; - this.handle.handle.serverConnection.send_command("musicbotplayeraction", { - bot_id: bot.properties.client_database_id, - action: current_timestamp > target_timestamp ? 6 : 5, - units: current_timestamp < target_timestamp ? target_timestamp - current_timestamp : current_timestamp - target_timestamp - }).then(() => this.triggerUpdate()).catch(error => { - slider.prop("edited", false); - }); - } else { //Restore old - event.preventDefault(); - slider.css("margin-left", slider_old + "%"); - } - }; - - $(document).on('mousemove', move_handler as any); - $(document).on('mouseup mouseleave mousedown', finish_handler as any); - - ev.preventDefault(); - return false; - }); - - { - frame.find(".timeline").attr("time-max", info.player_max_index); - let timeline = frame.find(".timeline"); - let time_bar = timeline.find(".played"); - let buffered_bar = timeline.find(".buffered"); - let slider = timeline.find(".slider"); - - let player_time = _frame.find(".player_time"); - - let update_handler = (played?: number, buffered?: number) => { - let time_index = played || info.player_replay_index + (bot.properties.player_state == 2 ? Date.now() - timestamp : 0); - let buffered_index = buffered || 0; - - time_bar.css("width", time_index / info.player_max_index * 100 + "%"); - buffered_bar.css("width", buffered_index / info.player_max_index * 100 + "%"); - if(!slider.prop("editing") && !slider.prop("edited")) { - player_time.text(format_time(Math.floor(time_index / 1000))); - slider.css("margin-left", time_index / info.player_max_index * 100 + "%"); - } - }; - - let interval = setInterval(update_handler, 1000); - this.registerInterval(interval); - update_handler(); - - /* register subscription */ - this.handle.handle.serverConnection.send_command("musicbotsetsubscription", {bot_id: bot.properties.client_database_id}).catch(error => { - console.error("Failed to subscribe to displayed music bot! Using pseudo timeline"); - }).then(() => { - clearInterval(interval); - }); - - this.single_handler = { - command: "notifymusicstatusupdate", - function: command => { - const json = command.arguments[0]; - update_handler(parseInt(json["player_replay_index"]), parseInt(json["player_buffered_index"])); - - return false; /* do not unregister me! */ - } - }; - - this.handle.handle.serverConnection.command_handler_boss().register_single_handler(this.single_handler); - } - }); - } - - const player = properties["music_player"]; - const player_transformer = $.spawn("div").append(player); - player_transformer.css({ - 'display': 'block', - //'width': "100%", - 'height': '100%' - }); - properties["music_player"] = player_transformer; - } - - let rendered = $("#tmpl_selected_music").renderTag([properties]); - html_tag.append(rendered); - - { - const player = properties["music_player"] as JQuery; - const player_width = 400; //player.width(); - const player_height = 400; //player.height(); - - const parent = player.parent(); - parent.css({ - 'flex-grow': 1, - 'display': 'flex', - 'flex-direction': 'row', - 'justify-content': 'space-around', - }); - - const padding = 14; - const scale_x = Math.min((parent.width() - padding) / player_width, 1.5); - const scale_y = Math.min((parent.height() - padding) / player_height, 1.5); - let scale = Math.min(scale_x, scale_y); - - let translate_x = 50, translate_y = 50; - if(scale_x == scale_y && scale_x == scale) { - //Equal scale - } else if(scale_x == scale) { - //We scale on the x-Axis - //We have to adjust the y-Axis - } else { - //We scale on the y-Axis - //We have to adjust the x-Axis - - } - //1 => 0 | 0 - //1.5 => 0 | 25 - //0.5 => 0 | -25 - //const translate_x = scale_x != scale ? 0 : undefined || 50 - (50 * ((parent.width() - padding) / player_width)); - //const translate_y = scale_y != scale || scale_y > 1 ? 0 : undefined || 50 - (50 * ((parent.height() - padding) / player_height)); - const transform = ("translate(0%, " + (scale * 50 - 50) + "%) scale(" + scale.toPrecision(2) + ")"); - - console.log(tr("Parents: %o | %o"), parent.width(), parent.height()); - console.log(tr("Player: %o | %o"), player_width, player_height); - console.log(tr("Scale: %f => translate: %o | %o"), scale, translate_x, translate_y); - player.css({ - transform: transform - }); - console.log("Transform: " + transform); - } - - this.registerInterval(setInterval(() => { - html_tag.find(".update_onlinetime").text(formatDate(bot.calculateOnlineTime())); - }, 1000)); - } - - update_local_volume(volume: number) { - this.handle.rendered_tag().find(".property-volume-local").text(Math.floor(volume * 100) + "%"); - } - - update_remote_volume(volume: number) { - this.handle.rendered_tag().find(".property-volume-remote").text(Math.floor(volume * 100) + "%") - } - - available(object: V): boolean { - return typeof object == "object" && object instanceof MusicClientEntry; - } - - finalizeFrame(object: ClientEntry, frame: JQuery) { - if(this.single_handler) { - this.handle.handle.serverConnection.command_handler_boss().remove_single_handler(this.single_handler); - this.single_handler = undefined; - } - - super.finalizeFrame(object, frame); - } - -} \ No newline at end of file diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index 4613fec6..177a7a3b 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -5,6 +5,7 @@ enum ChatType { CLIENT } +declare const xbbcode: any; namespace MessageHelper { export function htmlEscape(message: string) : string[] { const div = document.createElement('div'); @@ -90,7 +91,7 @@ namespace MessageHelper { } export function bbcode_chat(message: string) : JQuery[] { - const result: xbbcode.Result = xbbcode.parse(message, { + const result = xbbcode.parse(message, { /* TODO make this configurable and allow IMG */ tag_whitelist: [ "b", "big", @@ -133,6 +134,123 @@ namespace MessageHelper { //return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry)); } + export namespace network { + export const KB = 1024; + export const MB = 1024 * KB; + export const GB = 1024 * MB; + export const TB = 1024 * GB; + + export function format_bytes(value: number, options?: { + time?: string, + unit?: string, + exact?: boolean + }) : string { + options = Object.assign(options || {}, { + time: undefined, + unit: "Bytes", + exact: true + }); + + let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + + let v, unit; + if(value > 2 * TB) { + unit = "TB"; + v = value / TB; + } else if(value > GB) { + unit = "GB"; + v = value / GB; + } else if(value > MB) { + unit = "MB"; + v = value / MB; + } else if(value > KB) { + unit = "KB"; + v = value / KB; + } else { + unit = ""; + v = value; + } + if(unit && options.time) + unit = unit + "/" + options.time; + return (options.exact || !unit ? (points + " " + (options.unit || "")) : "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : ""); + } + } + + export const K = 1000; + export const M = 1000 * K; + export const G = 1000 * M; + export const T = 1000 * G; + export function format_number(value: number, options?: { + time?: string, + unit?: string + }) { + options = Object.assign(options || {}, {}); + + let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + + let v, unit; + if(value > 2 * T) { + unit = "T"; + v = value / T; + } else if(value > G) { + unit = "G"; + v = value / G; + } else if(value > M) { + unit = "M"; + v = value / M; + } else if(value > K) { + unit = "K"; + v = value / K; + } else { + unit = ""; + v = value; + } + if(unit && options.time) + unit = unit + "/" + options.time; + return points + " " + (options.unit || "") + (unit ? (" / " + v.toFixed(2) + " " + unit) : ""); + } + + export const TIME_SECOND = 1000; + export const TIME_MINUTE = 60 * TIME_SECOND; + export const TIME_HOUR = 60 * TIME_MINUTE; + export const TIME_DAY = 24 * TIME_HOUR; + export const TIME_WEEK = 7 * TIME_DAY; + + export function format_time(time: number, default_value: string) { + let result = ""; + if(time > TIME_WEEK) { + const amount = Math.floor(time / TIME_WEEK); + result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week")); + time -= amount * TIME_WEEK; + } + + if(time > TIME_DAY) { + const amount = Math.floor(time / TIME_DAY); + result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day")); + time -= amount * TIME_DAY; + } + + if(time > TIME_HOUR) { + const amount = Math.floor(time / TIME_HOUR); + result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour")); + time -= amount * TIME_HOUR; + } + + if(time > TIME_MINUTE) { + const amount = Math.floor(time / TIME_MINUTE); + result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute")); + time -= amount * TIME_MINUTE; + } + + if(time > TIME_SECOND) { + const amount = Math.floor(time / TIME_SECOND); + result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second")); + time -= amount * TIME_SECOND; + } + + return result.length > 0 ? result.substring(1) : default_value; + } + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "XBBCode code tag init", function: async () => { @@ -141,7 +259,7 @@ namespace MessageHelper { tag: ["code", "icode", "i-code"], content_tags_whitelist: [], - build_html(layer: xbbcode.TagLayer) : string { + build_html(layer) : string { const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code"; const language = (layer.options || "").replace("\"", "'").toLowerCase(); diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/chat_frame.ts index d30107f0..1589ab3b 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/chat_frame.ts @@ -1,7 +1,6 @@ /* the bar on the right with the chats (Channel & Client) */ namespace chat { - /* Fix some declare issues... */ - import instantiate = WebAssembly.instantiate; + import Event = JQuery.Event; declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; @@ -20,6 +19,8 @@ namespace chat { private _value_ping: JQuery; private _ping_updater: number; + private _button_conversation: HTMLElement; + constructor(handle: Frame) { this.handle = handle; this._build_html_tag(); @@ -44,6 +45,20 @@ namespace chat { this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations()); this._value_ping = this._html_tag.find(".value-ping"); this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations()); + this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => { + const selected_client = this.handle.client_info().current_client(); + if(!selected_client) return; + + const conversation = selected_client ? this.handle.private_conversations().find_conversation({ + name: selected_client.properties.client_nickname, + unique_id: selected_client.properties.client_unique_identifier, + client_id: selected_client.clientId() + }, { create: true, attach: true }) : undefined; + if(!conversation) return; + + this.handle.private_conversations().set_selected_conversation(conversation); + this.handle.show_private_conversations(); + })[0]; } update_ping() { @@ -115,7 +130,7 @@ namespace chat { const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0; const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined; - const tag_container = this._html_tag.find(".mode-channel_chat"); + const tag_container = this._html_tag.find(".mode-channel_chat.channel"); const html_tag_title = tag_container.find(".title"); const html_tag = tag_container.find(".value-text-channel"); const html_limit_tag = tag_container.find(".value-text-limit"); @@ -179,11 +194,30 @@ namespace chat { } set_mode(mode: InfoFrameMode) { - if(this._mode === mode) - return; - this._mode = mode; - this._html_tag.find(".mode-based").hide(); - this._html_tag.find(".mode-" + mode).show(); + if(this._mode !== mode) { + this._mode = mode; + this._html_tag.find(".mode-based").hide(); + this._html_tag.find(".mode-" + mode).show(); + } + + if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) { + //Will be called every time a client is shown + const selected_client = this.handle.client_info().current_client(); + const conversation = selected_client ? this.handle.private_conversations().find_conversation({ + name: selected_client.properties.client_nickname, + unique_id: selected_client.properties.client_unique_identifier, + client_id: selected_client.clientId() + }, { create: false, attach: false }) : undefined; + + const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.clientId) ? "visible" : "hidden"; + if(this._button_conversation.style.visibility !== visibility) + this._button_conversation.style.visibility = visibility; + if(conversation) { + this._button_conversation.innerText = tr("Open conversation"); + } else { + this._button_conversation.innerText = tr("Start a conversation"); + } + } } } @@ -195,6 +229,11 @@ namespace chat { private __callback_key_down; private __callback_paste; + private _typing_timeout: number; /* ID when the next callback_typing will be called */ + private _typing_last_event: number; /* timestamp of the last typing event */ + + typing_interval: number = 2000; /* update frequency */ + callback_typing: () => any; callback_text: (text: string) => any; constructor() { @@ -216,15 +255,18 @@ namespace chat { this._html_tag = undefined; this._html_input = undefined; + clearTimeout(this._typing_timeout); + this.__callback_text_changed = undefined; this.__callback_key_down = undefined; this.__callback_paste = undefined; this.callback_text = undefined; + this.callback_typing = undefined; } private _initialize_listener() { - this._html_input.on("cut paste drop key keyup", (event) => this.__callback_text_changed(event)); + this._html_input.on("cut paste drop keydown keyup", (event) => this.__callback_text_changed(event)); this._html_input.on("change", this.__callback_text_changed); this._html_input.on("keydown", this.__callback_key_down); this._html_input.on("paste", this.__callback_paste); @@ -246,7 +288,7 @@ namespace chat { }); } - private _callback_text_changed(event) { + private _callback_text_changed(event: Event) { if(event && event.isDefaultPrevented()) return; @@ -254,6 +296,28 @@ namespace chat { const text = this._html_input[0]; text.style.height = "1em"; text.style.height = text.scrollHeight + 'px'; + + if(!event || (event.type !== "keydown" && event.type !== "keyup" && event.type !== "change")) + return; + + this._typing_last_event = Date.now(); + if(this._typing_timeout) + return; + + const _trigger_typing = (last_time: number) => { + if(this._typing_last_event <= last_time) { + this._typing_timeout = 0; + return; + } + + try { + if(this.callback_typing) + this.callback_typing(); + } finally { + this._typing_timeout = setTimeout(_trigger_typing, this.typing_interval, this._typing_last_event); + } + }; + _trigger_typing(0); /* We def want that*/ } private _text(element: HTMLElement) { @@ -331,8 +395,14 @@ namespace chat { this.callback_text(helpers.preprocess_chat_message(text)); } + if(this._typing_timeout) + clearTimeout(this._typing_timeout); + this._typing_timeout = 1; /* enforce no typing update while sending */ this._html_input.text(""); - setTimeout(() => this.__callback_text_changed()); + setTimeout(() => { + this.__callback_text_changed(); + this._typing_timeout = 0; /* enable text change listener again */ + }); } } @@ -392,7 +462,7 @@ namespace chat { } namespace md2bbc { - type RemarkToken = { + export type RemarkToken = { type: string; tight: boolean; lines: number[]; @@ -764,11 +834,11 @@ test } } - type PrivateConversationViewEntry = { + export type PrivateConversationViewEntry = { html_tag: JQuery; } - type PrivateConversationMessageData = { + export type PrivateConversationMessageData = { message_id: string; message: string; sender: "self" | "partner"; @@ -780,10 +850,10 @@ test timestamp: number; }; - type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { + export type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { time_update_id: number; }; - type PrivateConversationViewSpacer = PrivateConversationViewEntry; + export type PrivateConversationViewSpacer = PrivateConversationViewEntry; export enum PrivateConversationState { OPEN, @@ -791,7 +861,7 @@ test DISCONNECTED, } - type DisplayedMessage = { + export type DisplayedMessage = { timestamp: number; message: PrivateConversationViewMessage | PrivateConversationViewEntry; @@ -817,6 +887,9 @@ test private _state: PrivateConversationState; private _last_message_updater_id: number; + private _last_typing: number = 0; + private _typing_timeout: number = 4000; + private _typing_timeout_task: number; _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ _html_message_container: JQuery; /* only set when this chat is selected! */ @@ -888,6 +961,8 @@ test this._html_entry_tag = undefined; this._message_history = undefined; + if(this._typing_timeout_task) + clearTimeout(this._typing_timeout_task); } private _2d_flat(array: T[][]) : T[] { @@ -943,7 +1018,20 @@ test while(this._message_history.length > 100) this._message_history.pop(); } - this._update_message_timestamp(); + + if(sender.type === "partner") { + clearTimeout(this._typing_timeout_task); + this._typing_timeout_task = 0; + + if(this.typing_active()) { + this._last_typing = 0; + this.typing_expired(); + } else { + this._update_message_timestamp(); + } + } else { + this._update_message_timestamp(); + } if(typeof(save_history) !== "boolean" || save_history) this.save_history(); @@ -1039,6 +1127,15 @@ test private _update_message_timestamp() { if(this._last_message_updater_id) clearTimeout(this._last_message_updater_id); + + if(!this._html_entry_tag) + return; /* we got deleted, not need for updates */ + + if(this.typing_active()) { + this._html_entry_tag.find(".last-message").text(tr("currently typing...")); + return; + } + const last_message = this._message_history[0]; if(!last_message) { this._html_entry_tag.find(".last-message").text(tr("no history")); @@ -1312,9 +1409,10 @@ test if(this._state == state) return; - if(state == PrivateConversationState.DISCONNECTED) + if(state == PrivateConversationState.DISCONNECTED) { this._append_state_change("disconnect"); - else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED) + this.client_id = 0; + } else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED) this._append_state_change("reconnect"); else if(state == PrivateConversationState.CLOSED) this._append_state_change("closed"); @@ -1355,6 +1453,31 @@ test }); } } + + private typing_expired() { + this._update_message_timestamp(); + if(this.handle.current_conversation() === this) + this.handle.update_typing_state(); + } + + trigger_typing() { + let _new = Date.now() - this._last_typing > this._typing_timeout; + this._last_typing = Date.now(); + + if(this._typing_timeout_task) + clearTimeout(this._typing_timeout_task); + + if(_new) + this._update_message_timestamp(); + if(this.handle.current_conversation() === this) + this.handle.update_typing_state(); + + this._typing_timeout_task = setTimeout(() => this.typing_expired(), this._typing_timeout); + } + + typing_active() { + return Date.now() - this._last_typing < this._typing_timeout; + } } export class PrivateConverations { @@ -1365,6 +1488,7 @@ test private _container_conversation: JQuery; private _container_conversation_messages: JQuery; private _container_conversation_list: JQuery; + private _container_typing: JQuery; private _html_no_chats: JQuery; private _conversations: PrivateConveration[] = []; @@ -1378,12 +1502,29 @@ test this._build_html_tag(); this.update_chatbox_state(); + this.update_typing_state(); this._chat_box.callback_text = message => { if(!this._current_conversation) { console.warn(tr("Dropping conversation message because of no active conversation.")); return; } this._current_conversation.call_message(message); + }; + + this._chat_box.callback_typing = () => { + if(!this._current_conversation) { + console.warn(tr("Dropping conversation typing action because of no active conversation.")); + return; + } + + console.log("TYPING!"); + const connection = this.handle.handle.serverConnection; + if(!connection || !connection.connected()) + return; + + connection.send_command("clientchatcomposing", { + clid: this._current_conversation.client_id + }); } } @@ -1408,6 +1549,8 @@ test } + current_conversation() : PrivateConveration | undefined { return this._current_conversation; } + conversations() : PrivateConveration[] { return this._conversations; } create_conversation(client_uid: string, client_name: string, client_id: number) : PrivateConveration { const conv = new PrivateConveration(this, client_uid, client_name, client_id); @@ -1523,8 +1666,14 @@ test this._chat_box.set_enabled(!!this._current_conversation && this._current_conversation.chat_enabled()); } + update_typing_state() { + this._container_typing.toggleClass("hidden", !this._current_conversation || !this._current_conversation.typing_active()); + } + private _build_html_tag() { - this._html_tag = $("#tmpl_frame_chat_private").renderTag().dividerfy(); + this._html_tag = $("#tmpl_frame_chat_private").renderTag({ + chatbox: this._chat_box.html_tag() + }).dividerfy(); this._container_conversation = this._html_tag.find(".conversation"); this._container_conversation.on('click', event => { /* lets think if a user clicks within that field that he has read the messages */ if(this._current_conversation) @@ -1542,10 +1691,10 @@ test else this._current_conversation._scroll_position = this._container_conversation_messages[0].scrollTop; }); - this._container_conversation.find(".chatbox").append(this._chat_box.html_tag()); this._container_conversation_list = this._html_tag.find(".conversation-list"); this._html_no_chats = this._container_conversation_list.find(".no-chats"); + this._container_typing = this._html_tag.find(".container-typing"); } try_input_focus() { @@ -1637,7 +1786,17 @@ test } else { this._scroll_position = this._container_messages[0].scrollTop; } - this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position); + + const will_visible = !!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position; + const is_visible = this._container_new_message[0].classList.contains("shown"); + if(!is_visible && will_visible) + this._container_new_message[0].classList.add("shown"); + + if(is_visible && !will_visible) + this._container_new_message[0].classList.remove("shown"); + + //This causes a Layout recalc (Forced reflow) + //this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position); }); this._view_older_messages = this._generate_view_spacer(tr("Load older messages"), "old"); @@ -2147,6 +2306,12 @@ test this.handle.set_content(this.previous_frame_content); }); + this._html_tag.find(".button-more").on('click', () => { + if(!this._current_client) + return; + + Modals.openClientInfo(this._current_client); + }); this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar()); } @@ -2221,7 +2386,7 @@ test } const volume = this._html_tag.find(".client-local-volume"); - volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1) + "%"); + volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1).toFixed(0) + "%"); } /* teaspeak forum */ @@ -2471,6 +2636,7 @@ test show_client_info(client: ClientEntry) { this._client_info.set_current_client(client); + this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */ if(this._content_type === FrameContent.CLIENT_INFO) return; @@ -2479,7 +2645,6 @@ test this._clear(); this._content_type = FrameContent.CLIENT_INFO; this._container_chat.append(this._client_info.html_tag()); - this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); } set_content(type: FrameContent) { diff --git a/shared/js/ui/frames/connection_handlers.ts b/shared/js/ui/frames/connection_handlers.ts index cab2c10b..08a081e3 100644 --- a/shared/js/ui/frames/connection_handlers.ts +++ b/shared/js/ui/frames/connection_handlers.ts @@ -8,7 +8,6 @@ class ServerConnectionManager { private _container_log_server: JQuery; private _container_channel_tree: JQuery; private _container_hostbanner: JQuery; - private _container_select_info: JQuery; private _container_chat: JQuery; private _tag: JQuery; @@ -34,7 +33,6 @@ class ServerConnectionManager { this._container_log_server = $("#server-log"); this._container_channel_tree = $("#channelTree"); this._container_hostbanner = $("#hostbanner"); - this._container_select_info = $("#select_info"); this._container_chat = $("#chat"); this.set_active_connection_handler(undefined); @@ -75,11 +73,8 @@ class ServerConnectionManager { if(handler && this.connection_handlers.indexOf(handler) == -1) throw "Handler hasn't been registered or is already obsolete!"; - if(this.active_handler) - this.active_handler.select_info.close_popover(); this._tag_connection_entries.find(".active").removeClass("active"); this._container_channel_tree.children().detach(); - this._container_select_info.children().detach(); this._container_chat.children().detach(); this._container_log_server.children().detach(); this._container_hostbanner.children().detach(); @@ -90,7 +85,6 @@ class ServerConnectionManager { this._container_hostbanner.append(handler.hostbanner.html_tag); this._container_channel_tree.append(handler.channelTree.tag_tree()); - this._container_select_info.append(handler.select_info.get_tag()); this._container_chat.append(handler.side_bar.html_tag()); this._container_log_server.append(handler.log.html_tag()); diff --git a/shared/js/ui/frames/hostbanner.ts b/shared/js/ui/frames/hostbanner.ts index 08f5fd84..10b96577 100644 --- a/shared/js/ui/frames/hostbanner.ts +++ b/shared/js/ui/frames/hostbanner.ts @@ -68,20 +68,14 @@ class Hostbanner { this.html_tag.attr('title', server ? server.properties.virtualserver_hostbanner_url : undefined); } - private async generate_tag?() : Promise { - if(!this.client.connected) - return undefined; + public static async generate_tag(banner_url: string | undefined, gfx_interval: number, mode: number) : Promise { + if(!banner_url) return undefined; - const server = this.client.channelTree.server; - if(!server) return undefined; - if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined; - - let banner_url = server.properties.virtualserver_hostbanner_gfx_url; - if(server.properties.virtualserver_hostbanner_gfx_interval > 0) { - const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60); + if(gfx_interval > 0) { + const update_interval = Math.max(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); + const url = new URL(banner_url); if(url.search.length == 0) banner_url += "?_ts=" + update_timestamp; else @@ -90,8 +84,6 @@ class Hostbanner { console.warn(tr("Failed to parse banner URL: %o. Using default '&' append."), error); banner_url += "&_ts=" + update_timestamp; } - - this.updater = setTimeout(() => this.update(), update_interval * 1000); } /* first now load the image */ @@ -102,11 +94,27 @@ class Hostbanner { image_element.src = banner_url; image_element.style.display = 'none'; document.body.append(image_element); - console.log("Loading image!"); + console.log("Loading hostbanner image!"); }); image_element.parentNode.removeChild(image_element); image_element.style.display = 'unset'; - return $.spawn("div").addClass("hostbanner-image-container hostbanner-mode-" + server.properties.virtualserver_hostbanner_mode).append($(image_element)); + return $.spawn("div").addClass("hostbanner-image-container hostbanner-mode-" + mode).append($(image_element)); + } + + private async generate_tag?() : Promise { + if(!this.client.connected) + return undefined; + + const server = this.client.channelTree.server; + if(!server) return undefined; + if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined; + + const timeout = server.properties.virtualserver_hostbanner_gfx_interval; + const tag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode); + if(timeout > 0) + this.updater = setTimeout(() => this.update(), timeout * 1000); + + return tag; } } diff --git a/shared/js/ui/frames/server_log.ts b/shared/js/ui/frames/server_log.ts index efe8e4e8..265e46b1 100644 --- a/shared/js/ui/frames/server_log.ts +++ b/shared/js/ui/frames/server_log.ts @@ -251,8 +251,8 @@ namespace log { "channel_delete": event.ChannelDelete; } - type MessageBuilderOptions = {}; - type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; + export type MessageBuilderOptions = {}; + export type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; export const MessageBuilders: {[key: string]: MessageBuilder} = { "error_custom": (data: event.ErrorCustom, options) => { diff --git a/shared/js/ui/htmltags.ts b/shared/js/ui/htmltags.ts index 59e23d3b..80e2916a 100644 --- a/shared/js/ui/htmltags.ts +++ b/shared/js/ui/htmltags.ts @@ -182,7 +182,7 @@ namespace htmltags { const origin_url = xbbcode.register.find_parser('url'); xbbcode.register.register_parser({ tag: 'url', - build_html_tag_open(layer: xbbcode.TagLayer): string { + build_html_tag_open(layer): string { if(layer.options) { if(layer.options.match(url_channel_regex)) { const groups = url_channel_regex.exec(layer.options); @@ -205,7 +205,7 @@ namespace htmltags { } return origin_url.build_html_tag_open(layer); }, - build_html_tag_close(layer: xbbcode.TagLayer): string { + build_html_tag_close(layer): string { if(layer.options) { if(layer.options.match(url_client_regex)) return ""; diff --git a/shared/js/ui/modal/ModalClientInfo.ts b/shared/js/ui/modal/ModalClientInfo.ts new file mode 100644 index 00000000..d0ea3e4f --- /dev/null +++ b/shared/js/ui/modal/ModalClientInfo.ts @@ -0,0 +1,523 @@ +namespace Modals { + type InfoUpdateCallback = (info: ClientConnectionInfo) => any; + export function openClientInfo(client: ClientEntry) { + let modal: Modal; + let update_callbacks: InfoUpdateCallback[] = []; + + modal = createModal({ + header: tr("Profile Information: ") + client.clientNickName(), + body: () => { + const template = $("#tmpl_client_info").renderTag(); + + /* the tab functionality */ + { + const container_tabs = template.find(".container-categories"); + container_tabs.find(".categories .entry").on('click', event => { + const entry = $(event.target); + + container_tabs.find(".bodies > .body").addClass("hidden"); + container_tabs.find(".categories > .selected").removeClass("selected"); + + entry.addClass("selected"); + container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); + }); + + container_tabs.find(".entry").first().trigger('click'); + } + + apply_static_info(client, template, modal, update_callbacks); + apply_client_status(client, template, modal, update_callbacks); + apply_basic_info(client, template.find(".container-basic"), modal, update_callbacks); + apply_groups(client, template.find(".container-groups"), modal, update_callbacks); + apply_packets(client, template.find(".container-packets"), modal, update_callbacks); + + tooltip(template); + return template.children(); + }, + footer: null + }); + + const updater = setInterval(() => { + client.request_connection_info().then(info => update_callbacks.forEach(e => e(info))); + }, 1000); + + modal.htmlTag.find(".modal-body").addClass("modal-client-info"); + modal.open(); + modal.close_listener.push(() => clearInterval(updater)); + } + + const TIME_SECOND = 1000; + const TIME_MINUTE = 60 * TIME_SECOND; + const TIME_HOUR = 60 * TIME_MINUTE; + const TIME_DAY = 24 * TIME_HOUR; + const TIME_WEEK = 7 * TIME_DAY; + + function format_time(time: number, default_value: string) { + let result = ""; + if(time > TIME_WEEK) { + const amount = Math.floor(time / TIME_WEEK); + result += " " + amount + " " + (amount > 1 ? tr("Weeks") : tr("Week")); + time -= amount * TIME_WEEK; + } + + if(time > TIME_DAY) { + const amount = Math.floor(time / TIME_DAY); + result += " " + amount + " " + (amount > 1 ? tr("Days") : tr("Day")); + time -= amount * TIME_DAY; + } + + if(time > TIME_HOUR) { + const amount = Math.floor(time / TIME_HOUR); + result += " " + amount + " " + (amount > 1 ? tr("Hours") : tr("Hour")); + time -= amount * TIME_HOUR; + } + + if(time > TIME_MINUTE) { + const amount = Math.floor(time / TIME_MINUTE); + result += " " + amount + " " + (amount > 1 ? tr("Minutes") : tr("Minute")); + time -= amount * TIME_MINUTE; + } + + if(time > TIME_SECOND) { + const amount = Math.floor(time / TIME_SECOND); + result += " " + amount + " " + (amount > 1 ? tr("Seconds") : tr("Second")); + time -= amount * TIME_SECOND; + } + + return result.length > 0 ? result.substring(1) : default_value; + } + + function apply_static_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + tag.find(".container-avatar").append( + client.channelTree.client.fileManager.avatars.generate_chat_tag({database_id: client.properties.client_database_id, id: client.clientId()}, client.properties.client_unique_identifier) + ); + + tag.find(".container-name").append( + client.createChatTag() + ); + + tag.find(".client-description").text( + client.properties.client_description + ); + } + + function apply_client_status(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + tag.find(".status-output-disabled").toggle(!client.properties.client_output_hardware); + tag.find(".status-input-disabled").toggle(!client.properties.client_input_hardware); + + tag.find(".status-output-muted").toggle(client.properties.client_output_muted); + tag.find(".status-input-muted").toggle(client.properties.client_input_muted); + + + tag.find(".status-away").toggle(client.properties.client_away); + if(client.properties.client_away_message) { + tag.find(".container-away-message").show().find("a").text(client.properties.client_away_message); + } else { + tag.find(".container-away-message").hide(); + } + } + + function apply_basic_info(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + /* Unique ID */ + { + const container = tag.find(".property-unique-id"); + + container.find(".value a").text(client.clientUid()); + container.find(".value-dbid").text(client.properties.client_database_id); + + container.find(".button-copy").on('click', event => { + copy_to_clipboard(client.clientUid()); + createInfoModal(tr("Unique ID copied"), tr("The unique id has been copied to your clipboard!")).open(); + }); + } + + /* TeaForo */ + { + const container = tag.find(".property-teaforo .value").empty(); + + if(client.properties.client_teaforo_id) { + container.children().remove(); + + let text = client.properties.client_teaforo_name; + if((client.properties.client_teaforo_flags & 0x01) > 0) + text += " (" + tr("Banned") + ")"; + if((client.properties.client_teaforo_flags & 0x02) > 0) + text += " (" + tr("Stuff") + ")"; + if((client.properties.client_teaforo_flags & 0x04) > 0) + text += " (" + tr("Premium") + ")"; + + $.spawn("a") + .attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) + .attr("target", "_blank") + .text(text) + .appendTo(container); + } else { + container.append($.spawn("a").text(tr("Not connected"))); + } + } + + /* Version */ + { + const container = tag.find(".property-version"); + + let version_full = client.properties.client_version; + let version = version_full.substring(0, version_full.indexOf(" ")); + + container.find(".value").empty().append( + $.spawn("a").attr("title", version_full).text(version), + $.spawn("a").addClass("a-on").text("on"), + $.spawn("a").text(client.properties.client_platform) + ); + + const container_timestamp = container.find(".container-tooltip"); + + let timestamp = -1; + version_full.replace(/\[build: ?([0-9]+)]/gmi, (group, ts) => { + timestamp = parseInt(ts); + return ""; + }); + if(timestamp > 0) { + container_timestamp.find(".value-timestamp").text(moment(timestamp * 1000).format('MMMM Do YYYY, h:mm:ss a')); + container_timestamp.show(); + } else { + container_timestamp.hide(); + } + } + + /* Country */ + { + const container = tag.find(".property-country"); + container.find(".value").empty().append( + $.spawn("div").addClass("country flag-" + client.properties.client_country.toLowerCase()), + $.spawn("a").text(i18n.country_name(client.properties.client_country, tr("Unknown"))) + ); + } + + /* IP Address */ + { + const container = tag.find(".property-ip"); + const value = container.find(".value a"); + value.text(tr("loading...")); + + container.find(".button-copy").on('click', event => { + copy_to_clipboard(value.text()); + createInfoModal(tr("Client IP copied"), tr("The client IP has been copied to your clipboard!")).open(); + }); + + callbacks.push(info => { + value.text(info.connection_client_ip ? (info.connection_client_ip + ":" + info.connection_client_port) : tr("Hidden")); + }); + } + + /* first connected */ + { + const container = tag.find(".property-first-connected"); + + container.find(".value a").text(tr("loading...")); + client.updateClientVariables().then(() => { + container.find(".value a").text(moment(client.properties.client_created * 1000).format('MMMM Do YYYY, h:mm:ss a')); + }).catch(error => { + container.find(".value a").text(tr("error")); + }); + } + + /* connect count */ + { + const container = tag.find(".property-connect-count"); + + container.find(".value a").text(tr("loading...")); + client.updateClientVariables().then(() => { + container.find(".value a").text(client.properties.client_totalconnections); + }).catch(error => { + container.find(".value a").text(tr("error")); + }); + } + + /* Online since */ + { + const container = tag.find(".property-online-since"); + + const node = container.find(".value a")[0]; + if(node) { + const update = () => { + node.innerText = format_time(client.calculateOnlineTime() * 1000, tr("0 Seconds")); + }; + + callbacks.push(update); /* keep it in sync with all other updates. Else it looks wired */ + update(); + } + } + + /* Idle time */ + { + const container = tag.find(".property-idle-time"); + const node = container.find(".value a")[0]; + if(node) { + callbacks.push(info => { + node.innerText = format_time(info.connection_idle_time, tr("Currently active")); + }); + node.innerText = tr("loading..."); + } + } + + /* ping */ + { + const container = tag.find(".property-ping"); + const node = container.find(".value a")[0]; + + if(node) { + callbacks.push(info => { + if(info.connection_ping >= 0) + node.innerText = info.connection_ping.toFixed(0) + "ms ± " + info.connection_ping_deviation.toFixed(2) + "ms"; + else if(info.connection_ping == -1 && info.connection_ping_deviation == -1) + node.innerText = tr("Not calculated"); + else + node.innerText = tr("loading..."); + }); + node.innerText = tr("loading..."); + } + } + } + + function apply_groups(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + /* server groups */ + { + const container_entries = tag.find(".entries"); + const container_empty = tag.find(".container-default-groups"); + + const update_groups = () => { + container_entries.empty(); + container_empty.show(); + + for(const group_id of client.assignedServerGroupIds()) { + if(group_id == client.channelTree.server.properties.virtualserver_default_server_group) + continue; + + const group = client.channelTree.client.groups.serverGroup(group_id); + if(!group) continue; //This shall never happen! + + container_empty.hide(); + container_entries.append($.spawn("div").addClass("entry").append( + client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid), + $.spawn("a").addClass("name").text(group.name + " (" + group.id + ")"), + $.spawn("div").addClass("button-delete").append( + $.spawn("div").addClass("icon_em client-delete").attr("title", tr("Delete group")).on('click', event => { + client.channelTree.client.serverConnection.send_command("servergroupdelclient", { + sgid: group.id, + cldbid: client.properties.client_database_id + }).then(result => update_groups()); + }) + ).toggleClass("visible", + client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER).granted(group.requiredMemberRemovePower) || + client.clientId() == client.channelTree.client.getClientId() && client.channelTree.client.permissions.neededPermission(PermissionType.I_SERVER_GROUP_SELF_REMOVE_POWER).granted(group.requiredMemberRemovePower) + ) + )) + } + }; + + tag.find(".button-group-add").on('click', event => { + Modals.createServerGroupAssignmentModal(client, (group, flag) => { + if(flag) { + return client.channelTree.client.serverConnection.send_command("servergroupaddclient", { + sgid: group.id, + cldbid: client.properties.client_database_id + }).then(result => { update_groups(); return true; }); + } else + return client.channelTree.client.serverConnection.send_command("servergroupdelclient", { + sgid: group.id, + cldbid: client.properties.client_database_id + }).then(result => { update_groups(); return true; }); + }); + }); + + update_groups(); + } + } + + function apply_packets(client: ClientEntry, tag: JQuery, modal: Modal, callbacks: InfoUpdateCallback[]) { + + /* Packet Loss */ + { + const container = tag.find(".statistic-packet-loss"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + node_downstream.innerText = info.connection_server2client_packetloss_control < 0 ? tr("Not calculated") : (info.connection_server2client_packetloss_control || 0).toFixed(); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + node_upstream.innerText = info.connection_client2server_packetloss_total < 0 ? tr("Not calculated") : (info.connection_client2server_packetloss_total || 0).toFixed(); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* Packets transmitted */ + { + const container = tag.find(".statistic-transmitted-packets"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + let packets = 0; + packets += info.connection_packets_received_speech > 0 ? info.connection_packets_received_speech : 0; + packets += info.connection_packets_received_control > 0 ? info.connection_packets_received_control : 0; + packets += info.connection_packets_received_keepalive > 0 ? info.connection_packets_received_keepalive : 0; + if(packets == 0 && info.connection_packets_received_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let packets = 0; + packets += info.connection_packets_sent_speech > 0 ? info.connection_packets_sent_speech : 0; + packets += info.connection_packets_sent_control > 0 ? info.connection_packets_sent_control : 0; + packets += info.connection_packets_sent_keepalive > 0 ? info.connection_packets_sent_keepalive : 0; + if(packets == 0 && info.connection_packets_sent_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = MessageHelper.format_number(packets, {unit: "Packets"}); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* Bytes transmitted */ + { + const container = tag.find(".statistic-transmitted-bytes"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bytes_received_speech > 0 ? info.connection_bytes_received_speech : 0; + bytes += info.connection_bytes_received_control > 0 ? info.connection_bytes_received_control : 0; + bytes += info.connection_bytes_received_keepalive > 0 ? info.connection_bytes_received_keepalive : 0; + if(bytes == 0 && info.connection_bytes_received_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = MessageHelper.network.format_bytes(bytes); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bytes_sent_speech > 0 ? info.connection_bytes_sent_speech : 0; + bytes += info.connection_bytes_sent_control > 0 ? info.connection_bytes_sent_control : 0; + bytes += info.connection_bytes_sent_keepalive > 0 ? info.connection_bytes_sent_keepalive : 0; + if(bytes == 0 && info.connection_bytes_sent_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = MessageHelper.network.format_bytes(bytes); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* Bandwidth second */ + { + const container = tag.find(".statistic-bandwidth-second"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_received_last_second_speech > 0 ? info.connection_bandwidth_received_last_second_speech : 0; + bytes += info.connection_bandwidth_received_last_second_control > 0 ? info.connection_bandwidth_received_last_second_control : 0; + bytes += info.connection_bandwidth_received_last_second_keepalive > 0 ? info.connection_bandwidth_received_last_second_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_received_last_second_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_sent_last_second_speech > 0 ? info.connection_bandwidth_sent_last_second_speech : 0; + bytes += info.connection_bandwidth_sent_last_second_control > 0 ? info.connection_bandwidth_sent_last_second_control : 0; + bytes += info.connection_bandwidth_sent_last_second_keepalive > 0 ? info.connection_bandwidth_sent_last_second_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_sent_last_second_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* Bandwidth minute */ + { + const container = tag.find(".statistic-bandwidth-minute"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_received_last_minute_speech > 0 ? info.connection_bandwidth_received_last_minute_speech : 0; + bytes += info.connection_bandwidth_received_last_minute_control > 0 ? info.connection_bandwidth_received_last_minute_control : 0; + bytes += info.connection_bandwidth_received_last_minute_keepalive > 0 ? info.connection_bandwidth_received_last_minute_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_received_last_minute_keepalive == -1) + node_downstream.innerText = tr("Not calculated"); + else + node_downstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + callbacks.push(info => { + let bytes = 0; + bytes += info.connection_bandwidth_sent_last_minute_speech > 0 ? info.connection_bandwidth_sent_last_minute_speech : 0; + bytes += info.connection_bandwidth_sent_last_minute_control > 0 ? info.connection_bandwidth_sent_last_minute_control : 0; + bytes += info.connection_bandwidth_sent_last_minute_keepalive > 0 ? info.connection_bandwidth_sent_last_minute_keepalive : 0; + if(bytes == 0 && info.connection_bandwidth_sent_last_minute_keepalive == -1) + node_upstream.innerText = tr("Not calculated"); + else + node_upstream.innerText = MessageHelper.network.format_bytes(bytes, {time: "s"}); + }); + node_upstream.innerText = tr("loading..."); + } + } + + /* quota */ + { + const container = tag.find(".statistic-quota"); + const node_downstream = container.find(".downstream .value")[0]; + const node_upstream = container.find(".upstream .value")[0]; + + if(node_downstream) { + client.updateClientVariables().then(info => { + //TODO: Test for own client info and if so then show the max quota (needed permission) + node_downstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_downloaded, {exact: false}); + }); + node_downstream.innerText = tr("loading..."); + } + + if(node_upstream) { + client.updateClientVariables().then(info => { + //TODO: Test for own client info and if so then show the max quota (needed permission) + node_upstream.innerText = MessageHelper.network.format_bytes(client.properties.client_month_bytes_uploaded, {exact: false}); + }); + node_upstream.innerText = tr("loading..."); + } + } + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalIdentity.ts b/shared/js/ui/modal/ModalIdentity.ts index beaafba5..1c5f9296 100644 --- a/shared/js/ui/modal/ModalIdentity.ts +++ b/shared/js/ui/modal/ModalIdentity.ts @@ -1,12 +1,14 @@ namespace Modals { - export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity): Modal { + export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity, name: string): Modal { let modal: Modal; let elapsed_timer: NodeJS.Timer; modal = createModal({ header: tr("Improve identity"), body: () => { - let template = $("#tmpl_settings-teamspeak_improve").renderTag(); + let template = $("#tmpl_settings-teamspeak_improve").renderTag({ + identity_name: name + }); template = $.spawn("div").append(template); let active; @@ -103,11 +105,14 @@ namespace Modals { }).catch(error => { input_current_level.val("error: " + error); }); - return template; + tooltip(template); + return template.children(); }, footer: undefined, width: 750 }); + + modal.htmlTag.find(".modal-body").addClass("modal-identity-improve modal-green"); modal.close_listener.push(() => modal.htmlTag.find(".button-close").trigger('click')); modal.open(); return modal; @@ -115,78 +120,104 @@ namespace Modals { export function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal { let modal: Modal; - let loaded_identity: profiles.identities.TeaSpeakIdentity; + let selected_type: string; + let identities: {[key: string]: profiles.identities.TeaSpeakIdentity} = {}; modal = createModal({ header: tr("Import identity"), body: () => { let template = $("#tmpl_settings-teamspeak_import").renderTag(); - template = $.spawn("div").append(template); - - template.find(".button-load-file").on('click', event => template.find(".input-file").trigger('click')); const button_import = template.find(".button-import"); - const set_error = message => { - template.find(".success").hide(); - if (message) { - template.find(".error").text(message).show(); - button_import.prop("disabled", true); - } else - template.find(".error").hide(); + const button_file_select = template.find(".button-load-file"); + + const container_status = template.find(".container-status"); + const input_text = template.find(".input-identity-text"); + const input_file = template.find(".file-selector"); + + const set_status = (message: string | undefined, type: "error" | "loading" | "success") => { + container_status.toggleClass("hidden", !message); + if(message) { + container_status.toggleClass("error", type === "error"); + container_status.toggleClass("loading", type === "loading"); + container_status.find("a").text(message); + } }; + button_file_select.on('click', event => input_file.trigger('click')); + + template.find("input[name='type']").on('change', event => { + const type = (event.target as HTMLInputElement).value; + + button_file_select.prop("disabled", type !== "file"); + input_text.prop("disabled", type !== "text"); + + selected_type = type; + button_import.prop("disabled", !identities[type]); + }); + template.find("input[name='type'][value='file']").prop("checked", true).trigger("change"); + const import_identity = (data: string, ini: boolean) => { + set_status(tr("Parsing identity"), "loading"); profiles.identities.TeaSpeakIdentity.import_ts(data, ini).then(identity => { - loaded_identity = identity; - set_error(""); + identities[selected_type] = identity; + set_status("Identity parsed successfully.", "success"); button_import.prop("disabled", false); template.find(".success").show(); }).catch(error => { - set_error("Failed to load identity: " + error); + set_status(tr("Failed to parse identity: ") + error, "error"); }); }; - { /* file select button */ - template.find(".input-file").on('change', event => { - const element = event.target as HTMLInputElement; - const file_reader = new FileReader(); + /* file select button */ + input_file.on('change', event => { + const element = event.target as HTMLInputElement; + const file_reader = new FileReader(); - file_reader.onload = function () { - import_identity(file_reader.result as string, true); - }; + set_status(tr("Loading file"), "loading"); + file_reader.onload = function () { + import_identity(file_reader.result as string, true); + }; - file_reader.onerror = ev => { - console.error(tr("Failed to read give identity file: %o"), ev); - set_error(tr("Failed to read file!")); - return; - }; + file_reader.onerror = ev => { + console.error(tr("Failed to read give identity file: %o"), ev); + set_status(tr("Failed to read the identity file."), "error"); + return; + }; - if (element.files && element.files.length > 0) - file_reader.readAsText(element.files[0]); - }); - } + if (element.files && element.files.length > 0) + file_reader.readAsText(element.files[0]); + }); - { /* text input */ - template.find(".button-load-text").on('click', event => { - createInputModal("Import identity from text", "Please paste your idenity bellow
", text => text.length > 0 && text.indexOf('V') != -1, result => { - if (result) - import_identity(result as string, false); - }).open(); - }); - } + input_text.on('change keyup', event => { + const text = input_text.val() as string; + if(!text) { + set_status("", "success"); + return; + } + + if(text.indexOf('V') == -1) { + set_status(tr("Invalid identity string"), "error"); + return; + } + + import_identity(text, false); + }); button_import.on('click', event => { modal.close(); - callback(loaded_identity); + callback(identities[selected_type]); }); - set_error(""); + set_status("", "success"); button_import.prop("disabled", true); - return template; + return template.children(); }, footer: undefined, width: 750 }); + + modal.htmlTag.find(".modal-body").addClass("modal-identity-import modal-green"); modal.open(); return modal; } diff --git a/shared/js/ui/modal/ModalPoke.ts b/shared/js/ui/modal/ModalPoke.ts index 2132d6ad..d4769a2c 100644 --- a/shared/js/ui/modal/ModalPoke.ts +++ b/shared/js/ui/modal/ModalPoke.ts @@ -79,7 +79,7 @@ namespace Modals { } } - type PokeInvoker = { + export type PokeInvoker = { name: string, id: number, unique_id: string diff --git a/shared/js/ui/modal/ModalServerEdit.ts b/shared/js/ui/modal/ModalServerEdit.ts index dc76ed36..089f1522 100644 --- a/shared/js/ui/modal/ModalServerEdit.ts +++ b/shared/js/ui/modal/ModalServerEdit.ts @@ -327,42 +327,14 @@ namespace Modals { } } - const format_unit = (value: number) => { - const KB = 1024; - const MB = 1024 * KB; - const GB = 1024 * MB; - const TB = 1024 * GB; - - let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); - - let v, unit; - if(value > 2 * TB) { - unit = "TB"; - v = value / TB; - } else if(value > GB) { - unit = "GB"; - v = value / GB; - } else if(value > MB) { - unit = "MB"; - v = value / MB; - } else if(value > KB) { - unit = "KB"; - v = value / KB; - } else { - unit = ""; - v = value; - } - return points + " Bytes" + (unit ? (" / " + v.toFixed(2) + " " + unit) : ""); - }; - /* quota info */ { server.updateProperties().then(() => { - tag.find(".value.virtualserver_month_bytes_downloaded").text(format_unit(server.properties.virtualserver_month_bytes_downloaded)); - tag.find(".value.virtualserver_month_bytes_uploaded").text(format_unit(server.properties.virtualserver_month_bytes_uploaded)); + tag.find(".value.virtualserver_month_bytes_downloaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_month_bytes_downloaded)); + tag.find(".value.virtualserver_month_bytes_uploaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_month_bytes_uploaded)); - tag.find(".value.virtualserver_total_bytes_downloaded").text(format_unit(server.properties.virtualserver_total_bytes_downloaded)); - tag.find(".value.virtualserver_total_bytes_uploaded").text(format_unit(server.properties.virtualserver_total_bytes_uploaded)); + tag.find(".value.virtualserver_total_bytes_downloaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_total_bytes_downloaded)); + tag.find(".value.virtualserver_total_bytes_uploaded").text(MessageHelper.network.format_bytes(server.properties.virtualserver_total_bytes_uploaded)); }); } @@ -381,14 +353,14 @@ namespace Modals { server.request_connection_info().then(info => { if(info.connection_filetransfer_bytes_sent_month && month_bytes_downloaded) - month_bytes_downloaded.innerText = format_unit(info.connection_filetransfer_bytes_sent_month); + month_bytes_downloaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_sent_month); if(info.connection_filetransfer_bytes_received_month && month_bytes_uploaded) - month_bytes_uploaded.innerText = format_unit(info.connection_filetransfer_bytes_received_month); + month_bytes_uploaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_received_month); if(info.connection_filetransfer_bytes_sent_total && total_bytes_downloaded) - total_bytes_downloaded.innerText = format_unit(info.connection_filetransfer_bytes_sent_total); + total_bytes_downloaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_sent_total); if(info.connection_filetransfer_bytes_received_total && total_bytes_uploaded) - total_bytes_uploaded.innerText = format_unit(info.connection_filetransfer_bytes_received_total); + total_bytes_uploaded.innerText = MessageHelper.network.format_bytes(info.connection_filetransfer_bytes_received_total); }); }, 1000); modal.close_listener.push(() => clearInterval(id)); diff --git a/shared/js/ui/modal/ModalServerInfo.ts b/shared/js/ui/modal/ModalServerInfo.ts new file mode 100644 index 00000000..aae9ba13 --- /dev/null +++ b/shared/js/ui/modal/ModalServerInfo.ts @@ -0,0 +1,195 @@ +namespace Modals { + type InfoUpdateCallback = (info: ServerConnectionInfo | boolean) => any; + export function openServerInfo(server: ServerEntry) { + let modal: Modal; + let update_callbacks: InfoUpdateCallback[] = []; + + modal = createModal({ + header: tr("Server Information: ") + server.properties.virtualserver_name, + body: () => { + const template = $("#tmpl_server_info").renderTag(); + + apply_hostbanner(server, template.find(".container-top")); + apply_category_1(server, template, update_callbacks); + apply_category_2(server, template, update_callbacks); + apply_category_3(server, template, update_callbacks); + + tooltip(template); + return template.children(); + }, + footer: null, + min_width: "25em" + }); + + const updater = setInterval(() => { + server.request_connection_info().then(info => update_callbacks.forEach(e => e(info))).catch(error => update_callbacks.forEach(e => e(false))); + }, 1000); + + + modal.htmlTag.find(".button-close").on('click', event => modal.close()); + modal.htmlTag.find(".button-show-bandwidth").on('click', event => { + //TODO! + }); + + modal.htmlTag.find(".modal-body").addClass("modal-server-info"); + modal.open(); + modal.close_listener.push(() => clearInterval(updater)); + } + + function apply_hostbanner(server: ServerEntry, tag: JQuery) { + let container: JQuery; + tag.empty().append( + container = $.spawn("div").addClass("container-hostbanner") + ).addClass("hidden"); + + const htag = Hostbanner.generate_tag(server.properties.virtualserver_hostbanner_gfx_url, server.properties.virtualserver_hostbanner_gfx_interval, server.properties.virtualserver_hostbanner_mode); + htag.then(t => { + if(!t) return; + + tag.removeClass("hidden"); + container.append(t); + }); + } + + function apply_category_1(server: ServerEntry, tag: JQuery, update_callbacks: InfoUpdateCallback[]) { + /* server name */ + { + const container = tag.find(".server-name"); + container.text(server.properties.virtualserver_name); + } + + /* server region */ + { + const container = tag.find(".server-region").empty(); + container.append( + $.spawn("div").addClass("country flag-" + server.properties.virtualserver_country_code.toLowerCase()), + $.spawn("a").text(i18n.country_name(server.properties.virtualserver_country_code, tr("Global"))) + ); + } + + /* slots */ + { + const container = tag.find(".server-slots"); + + let text = server.properties.virtualserver_clientsonline + "/" + server.properties.virtualserver_maxclients; + if(server.properties.virtualserver_queryclientsonline) + text += " +" + (server.properties.virtualserver_queryclientsonline > 1 ? + server.properties.virtualserver_queryclientsonline + " " + tr("Queries") : + server.properties.virtualserver_queryclientsonline + " " + tr("Query")); + if(server.properties.virtualserver_reserved_slots) + text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")"; + + container.text(text); + } + + /* first run */ + { + const container = tag.find(".server-first-run"); + + container.text( + server.properties.virtualserver_created > 0 ? + moment(server.properties.virtualserver_created * 1000).format('MMMM Do YYYY, h:mm:ss a') : + tr("Unknown") + ); + } + + /* uptime */ + { + const container = tag.find(".server-uptime"); + const update = () => container.text(MessageHelper.format_time(server.calculateUptime() * 1000, tr("just started"))); + update_callbacks.push(update); + update(); + } + } + + function apply_category_2(server: ServerEntry, tag: JQuery, update_callbacks: InfoUpdateCallback[]) { + /* ip */ + { + const container = tag.find(".server-ip"); + container.text(server.remote_address.host + (server.remote_address.port == 9987 ? "" : (":" + server.remote_address.port))) + } + + /* version */ + { + const container = tag.find(".server-version"); + + let timestamp = -1; + const version = (server.properties.virtualserver_version || "unknwon").replace(/ ?\[build: ?([0-9]+)]/gmi, (group, ts) => { + timestamp = parseInt(ts); + return ""; + }); + + container.find("a").text(version); + container.find(".container-tooltip").toggle(timestamp > 0).find(".tooltip a").text( + moment(timestamp * 1000).format('[Build timestamp:] YYYY-MM-DD HH:mm Z') + ); + } + + /* platform */ + { + const container = tag.find(".server-platform"); + container.text(server.properties.virtualserver_platform); + } + + /* ping */ + { + const container = tag.find(".server-ping"); + container.text(tr("calculating...")); + update_callbacks.push(data => { + if(typeof(data) === "boolean") + container.text(tr("No Permissions")); + else + container.text(data.connection_ping.toFixed(0) + " " + "ms"); + }); + } + + /* packet loss */ + { + const container = tag.find(".server-packet-loss"); + container.text(tr("calculating...")); + update_callbacks.push(data => { + if(typeof(data) === "boolean") + container.text(tr("No Permissions")); + else + container.text(data.connection_packetloss_total.toFixed(2) + "%"); + }); + } + } + + function apply_category_3(server: ServerEntry, tag: JQuery, update_callbacks: InfoUpdateCallback[]) { + /* unique id */ + { + const container = tag.find(".server-unique-id"); + container.text(server.properties.virtualserver_unique_identifier || tr("Unknown")); + } + + /* voice encryption */ + { + const container = tag.find(".server-voice-encryption"); + if(server.properties.virtualserver_codec_encryption_mode == 0) + container.text(tr("Globally off")); + else if(server.properties.virtualserver_codec_encryption_mode == 1) + container.text(tr("Individually configured per channel")); + else + container.text(tr("Globally on")); + } + + /* channel count */ + { + const container = tag.find(".server-channel-count"); + container.text(server.properties.virtualserver_channelsonline); + } + + /* minimal security level */ + { + const container = tag.find(".server-min-security-level"); + container.text(server.properties.virtualserver_needed_identity_security_level); + } + + /* complains */ + { + const container = tag.find(".server-complains"); + container.text(server.properties.virtualserver_complain_autoban_count); + } + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalSettings.ts b/shared/js/ui/modal/ModalSettings.ts index 6977d109..95509279 100644 --- a/shared/js/ui/modal/ModalSettings.ts +++ b/shared/js/ui/modal/ModalSettings.ts @@ -1005,7 +1005,7 @@ namespace Modals { const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; if (!profile) return; - Modals.spawnTeamSpeakIdentityImprove(profile).close_listener.push(() => { + Modals.spawnTeamSpeakIdentityImprove(profile, selected_profile.identity.profile_name).close_listener.push(() => { profiles.mark_need_save(); for(const listener of profile_identity_changed) listener(); diff --git a/shared/js/ui/modal/permission/ModalPermissionEdit.ts b/shared/js/ui/modal/permission/ModalPermissionEdit.ts index ff7f7791..e76d54d6 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEdit.ts +++ b/shared/js/ui/modal/permission/ModalPermissionEdit.ts @@ -240,7 +240,6 @@ namespace Modals { { const pe_client = tab_right.find(".permission-editor"); tab_right.on('show', event => { - console.error("client channel tab show"); editor.set_toggle_button(undefined, undefined); pe_client.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { @@ -407,8 +406,6 @@ namespace Modals { { const pe_client = tab_right.find("permission-editor.client"); tab_right.on('show', event => { - console.error("Channel tab show"); - editor.set_toggle_button(undefined, undefined); pe_client.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { @@ -557,7 +554,6 @@ namespace Modals { { const pe_channel = tab_right.find(".permission-editor"); tab_right.on('show', event => { - console.error("Channel tab show"); editor.set_toggle_button(undefined, undefined); pe_channel.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1)) @@ -672,7 +668,6 @@ namespace Modals { { const pe_server = tab_right.find(".permission-editor"); tab_right.on('show', event => { - console.error("Channel group tab show"); editor.set_toggle_button(undefined, undefined); pe_server.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1)) @@ -1162,7 +1157,6 @@ namespace Modals { { const pe_server = tab_right.find(".permission-editor"); tab_right.on('show', event => { - console.error("Server tab show"); pe_server.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1)) editor.set_mode(PermissionEditorMode.VISIBLE); diff --git a/shared/js/ui/server.ts b/shared/js/ui/server.ts index 73af51c3..6d424d29 100644 --- a/shared/js/ui/server.ts +++ b/shared/js/ui/server.ts @@ -16,6 +16,7 @@ class ServerProperties { virtualserver_queryclientsonline: number = 0; virtualserver_channelsonline: number = 0; virtualserver_uptime: number = 0; + virtualserver_created: number = 0; virtualserver_maxclients: number = 0; virtualserver_reserved_slots: number = 0; @@ -198,12 +199,17 @@ class ServerEntry { name: tr("Show server info"), callback: () => { trigger_close = false; - - //TODO - alert("inplement me"); + Modals.openServerInfo(this); }, - icon_class: "client-about", - visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) + icon_class: "client-about" + }, { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-invite_buddy", + name: tr("Invite buddy"), + callback: () => Modals.spawnInviteEditor(this.channelTree.client) + }, { + type: contextmenu.MenuEntryType.HR, + name: '' }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_switch", @@ -213,10 +219,6 @@ class ServerEntry { this.channelTree.client.side_bar.show_channel_conversations(); }, visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) - }, { - type: contextmenu.MenuEntryType.HR, - visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT), - name: '' }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-virtualserver_edit", @@ -235,6 +237,10 @@ class ServerEntry { return Promise.resolve(); }); } + }, { + type: contextmenu.MenuEntryType.HR, + visible: true, + name: '' }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-iconviewer", @@ -245,13 +251,8 @@ class ServerEntry { icon_class: 'client-iconsview', name: tr("View avatars"), callback: () => Modals.spawnAvatarList(this.channelTree.client) - }, { - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-invite_buddy", - name: tr("Invite buddy"), - callback: () => Modals.spawnInviteEditor(this.channelTree.client) }, - contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))()) ); } @@ -266,7 +267,7 @@ class ServerEntry { value: variable.value, type: typeof (this.properties[variable.key]) }); - log.table("Server update properties", entries); + log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Server update properties", entries); } let update_bannner = false, update_button = false; @@ -366,7 +367,7 @@ class ServerEntry { }); this._info_connection_promise_timestamp = Date.now(); - this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo").catch(error => _local_reject(error)); + this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo", {}, {process_result: false}).catch(error => _local_reject(error)); return this._info_connection_promise; } diff --git a/shared/js/ui/view.ts b/shared/js/ui/view.ts index ca927683..bb5ff66f 100644 --- a/shared/js/ui/view.ts +++ b/shared/js/ui/view.ts @@ -494,7 +494,6 @@ class ChannelTree { this.client.side_bar.show_channel_conversations(); } } - this.client.select_info.setCurrentSelected($.isArray(this.currently_selected) ? undefined : entry); } private callback_multiselect_channel(event) { @@ -748,7 +747,7 @@ class ChannelTree { } handle_key_press(event: KeyboardEvent) { - console.log("Keydown: %o | %o | %o", this._focused, this.currently_selected, Array.isArray(this.currently_selected)); + //console.log("Keydown: %o | %o | %o", this._focused, this.currently_selected, Array.isArray(this.currently_selected)); if(!this._focused || !this.currently_selected || Array.isArray(this.currently_selected)) return; if(event.keyCode == KeyCode.KEY_UP) { diff --git a/shared/js/voice/RecorderProfile.ts b/shared/js/voice/RecorderProfile.ts index 894e2ca0..aaf8b55d 100644 --- a/shared/js/voice/RecorderProfile.ts +++ b/shared/js/voice/RecorderProfile.ts @@ -97,13 +97,13 @@ class RecorderProfile { private initialize_input() { this.input = audio.recorder.create_input(); this.input.callback_begin = () => { - console.log("Voice start"); + log.debug(LogCategory.VOICE, "Voice start"); if(this.callback_start) this.callback_start(); }; this.input.callback_end = () => { - console.log("Voice end"); + log.debug(LogCategory.VOICE, "Voice end"); if(this.callback_stop) this.callback_stop(); }; @@ -153,11 +153,11 @@ class RecorderProfile { const devices = all_devices.filter(e => e.default_input || e.unique_id === this.config.device_id); const device = devices.find(e => e.unique_id === this.config.device_id) || devices[0]; - console.log(tr("Loaded record profile device %s | %o (%o)"), this.config.device_id, device, all_devices); + log.info(LogCategory.VOICE, tr("Loaded record profile device %s | %o (%o)"), this.config.device_id, device, all_devices); try { await this.input.set_device(device); } catch(error) { - console.error(tr("Failed to set input device (%o)"), error); + log.error(LogCategory.VOICE, tr("Failed to set input device (%o)"), error); } } } @@ -207,7 +207,7 @@ class RecorderProfile { try { await this.input.set_consumer(undefined); } catch(error) { - console.warn(tr("Failed to unmount input consumer for profile (%o)"), error); + log.warn(LogCategory.VOICE, tr("Failed to unmount input consumer for profile (%o)"), error); } } diff --git a/shared/loader/app.ts b/shared/loader/app.ts new file mode 100644 index 00000000..6bc9bd5e --- /dev/null +++ b/shared/loader/app.ts @@ -0,0 +1,605 @@ +/// + +interface Window { + $: JQuery; +} + +namespace app { + export enum Type { + UNKNOWN, + CLIENT_RELEASE, + CLIENT_DEBUG, + WEB_DEBUG, + WEB_RELEASE + } + export let type: Type = Type.UNKNOWN; + + export function is_web() { + return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG; + } + + let _ui_version; + export function ui_version() { + if(typeof(_ui_version) !== "string") { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; + + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; + + return (_ui_version = version); + } + return _ui_version; + } +} + +/* all javascript loaders */ +const loader_javascript = { + detect_type: async () => { + if(window.require) { + const request = new Request("js/proto.js"); + let file_path = request.url; + if(!file_path.startsWith("file://")) + throw "Invalid file path (" + file_path + ")"; + file_path = file_path.substring(process.platform === "win32" ? 8 : 7); + + const fs = require('fs'); + if(fs.existsSync(file_path)) { + app.type = app.Type.CLIENT_DEBUG; + } else { + app.type = app.Type.CLIENT_RELEASE; + } + } else { + /* test if js/proto.js is available. If so we're in debug mode */ + const request = new XMLHttpRequest(); + request.open('GET', 'js/proto.js', true); + + await new Promise((resolve, reject) => { + request.onreadystatechange = () => { + if (request.readyState === 4){ + if (request.status === 404) { + app.type = app.Type.WEB_RELEASE; + } else { + app.type = app.Type.WEB_DEBUG; + } + resolve(); + } + }; + request.onerror = () => { + reject("Failed to detect app type"); + }; + request.send(); + }); + } + }, + load_scripts: async () => { + /* + if(window.require !== undefined) { + console.log("Loading node specific things"); + const remote = require('electron').remote; + module.paths.push(remote.app.getAppPath() + "/node_modules"); + module.paths.push(remote.app.getAppPath() + "/app"); + module.paths.push(remote.getGlobal("browser-root") + "js/"); + window.$ = require("assets/jquery.min.js"); + require("native/loader_adapter.js"); + } + */ + + if(!window.require) { + await loader.load_script(["vendor/jquery/jquery.min.js"]); + } else { + /* + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "forum sync", + priority: 10, + function: async () => { + forum.sync_main(); + } + }); + */ + } + await loader.load_script(["vendor/DOMPurify/purify.min.js"]); + + /* bootstrap material design and libs */ + //await loader.load_script(["vendor/popper/popper.js"]); + + //depends on popper + //await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]); + + /* + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "materialize body", + priority: 10, + function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); } + }); + */ + + await loader.load_script("vendor/jsrender/jsrender.min.js"); + await loader.load_scripts([ + ["vendor/xbbcode/src/parser.js"], + ["vendor/moment/moment.js"], + ["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */ + ["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */ + ["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */ + ["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"] + ]); + await loader.load_scripts([ + ["vendor/emoji-picker/src/jquery.lsxemojipicker.js"] + ]); + + if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) { + loader.register_task(loader.Stage.JAVASCRIPT, { + name: "scripts release", + priority: 20, + function: loader_javascript.load_release + }); + } else { + loader.register_task(loader.Stage.JAVASCRIPT, { + name: "scripts debug", + priority: 20, + function: loader_javascript.load_scripts_debug + }); + } + }, + load_scripts_debug: async () => { + /* test if we're loading as TeaClient or WebClient */ + if(!window.require) { + loader.register_task(loader.Stage.JAVASCRIPT, { + name: "javascript web", + priority: 10, + function: loader_javascript.load_scripts_debug_web + }); + } else { + loader.register_task(loader.Stage.JAVASCRIPT, { + name: "javascript client", + priority: 10, + function: loader_javascript.load_scripts_debug_client + }); + } + + /* load some extends classes */ + await loader.load_scripts([ + ["js/connection/ConnectionBase.js"] + ]); + + /* load the main app */ + await loader.load_scripts([ + //Load general API's + "js/proto.js", + "js/i18n/localize.js", + "js/i18n/country.js", + "js/log.js", + + "js/sound/Sounds.js", + + "js/utils/helpers.js", + + "js/crypto/sha.js", + "js/crypto/hex.js", + "js/crypto/asn1.js", + "js/crypto/crc32.js", + + //load the profiles + "js/profiles/ConnectionProfile.js", + "js/profiles/Identity.js", + "js/profiles/identities/teaspeak-forum.js", + + //Basic UI elements + "js/ui/elements/context_divider.js", + "js/ui/elements/context_menu.js", + "js/ui/elements/modal.js", + "js/ui/elements/tab.js", + "js/ui/elements/slider.js", + "js/ui/elements/tooltip.js", + + //Load UI + "js/ui/modal/ModalAbout.js", + "js/ui/modal/ModalAvatar.js", + "js/ui/modal/ModalAvatarList.js", + "js/ui/modal/ModalClientInfo.js", + "js/ui/modal/ModalQuery.js", + "js/ui/modal/ModalQueryManage.js", + "js/ui/modal/ModalPlaylistList.js", + "js/ui/modal/ModalPlaylistEdit.js", + "js/ui/modal/ModalBookmarks.js", + "js/ui/modal/ModalConnect.js", + "js/ui/modal/ModalSettings.js", + "js/ui/modal/ModalCreateChannel.js", + "js/ui/modal/ModalServerEdit.js", + "js/ui/modal/ModalServerInfo.js", + "js/ui/modal/ModalChangeVolume.js", + "js/ui/modal/ModalBanClient.js", + "js/ui/modal/ModalIconSelect.js", + "js/ui/modal/ModalInvite.js", + "js/ui/modal/ModalIdentity.js", + "js/ui/modal/ModalBanCreate.js", + "js/ui/modal/ModalBanList.js", + "js/ui/modal/ModalYesNo.js", + "js/ui/modal/ModalPoke.js", + "js/ui/modal/ModalKeySelect.js", + "js/ui/modal/ModalGroupAssignment.js", + "js/ui/modal/permission/ModalPermissionEdit.js", + {url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, + {url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, + + "js/ui/channel.js", + "js/ui/client.js", + "js/ui/server.js", + "js/ui/view.js", + "js/ui/client_move.js", + "js/ui/htmltags.js", + + "js/ui/frames/ControlBar.js", + "js/ui/frames/chat.js", + "js/ui/frames/chat_frame.js", + "js/ui/frames/connection_handlers.js", + "js/ui/frames/server_log.js", + "js/ui/frames/hostbanner.js", + "js/ui/frames/MenuBar.js", + + //Load permissions + "js/permission/PermissionManager.js", + "js/permission/GroupManager.js", + + //Load audio + "js/voice/RecorderBase.js", + "js/voice/RecorderProfile.js", + + //Load general stuff + "js/settings.js", + "js/bookmarks.js", + "js/FileManager.js", + "js/ConnectionHandler.js", + "js/BrowserIPC.js", + "js/dns.js", + + //Connection + "js/connection/CommandHandler.js", + "js/connection/CommandHelper.js", + "js/connection/HandshakeHandler.js", + "js/connection/ServerConnectionDeclaration.js", + + "js/stats.js", + "js/PPTListener.js", + + "js/profiles/identities/NameIdentity.js", //Depends on Identity + "js/profiles/identities/TeaForumIdentity.js", //Depends on Identity + "js/profiles/identities/TeamSpeakIdentity.js", //Depends on Identity + ]); + + await loader.load_script("js/main.js"); + }, + load_scripts_debug_web: async () => { + await loader.load_scripts([ + ["js/audio/AudioPlayer.js"], + ["js/audio/WebCodec.js"], + ["js/WebPPTListener.js"], + + "js/voice/AudioResampler.js", + "js/voice/JavascriptRecorder.js", + "js/voice/VoiceHandler.js", + "js/voice/VoiceClient.js", + + //Connection + "js/connection/ServerConnection.js", + + //Load codec + "js/codec/Codec.js", + "js/codec/BasicCodec.js", + {url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]}, + ]); + }, + load_scripts_debug_client: async () => { + await loader.load_scripts([ + ]); + }, + + load_release: async () => { + console.log("Load for release!"); + + await loader.load_scripts([ + //Load general API's + ["js/client.min.js", "js/client.js"] + ]); + } +}; + +const loader_webassembly = { + test_webassembly: async () => { + /* We dont required WebAssembly anymore for fundamental functions, only for auto decoding + if(typeof (WebAssembly) === "undefined" || typeof (WebAssembly.compile) === "undefined") { + console.log(navigator.browserSpecs); + if (navigator.browserSpecs.name == 'Safari') { + if (parseInt(navigator.browserSpecs.version) < 11) { + displayCriticalError("You require Safari 11 or higher to use the web client!
Safari " + navigator.browserSpecs.version + " does not support WebAssambly!"); + return; + } + } + else { + // Do something for all other browsers. + } + displayCriticalError("You require WebAssembly for TeaSpeak-Web!"); + throw "Missing web assembly"; + } + */ + } +}; + +const loader_style = { + load_style: async () => { + await loader.load_styles([ + "vendor/xbbcode/src/xbbcode.css" + ]); + await loader.load_styles([ + "vendor/emoji-picker/src/jquery.lsxemojipicker.css" + ]); + await loader.load_styles([ + ["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */ + ]); + + if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) { + await loader_style.load_style_debug(); + } else { + await loader_style.load_style_release(); + } + }, + + load_style_debug: async () => { + await loader.load_styles([ + "css/static/main.css", + "css/static/main-layout.css", + "css/static/helptag.css", + "css/static/scroll.css", + "css/static/channel-tree.css", + "css/static/ts/tab.css", + "css/static/ts/chat.css", + "css/static/ts/icons.css", + "css/static/ts/icons_em.css", + "css/static/ts/country.css", + "css/static/general.css", + "css/static/modal.css", + "css/static/modals.css", + "css/static/modal-about.css", + "css/static/modal-avatar.css", + "css/static/modal-icons.css", + "css/static/modal-bookmarks.css", + "css/static/modal-connect.css", + "css/static/modal-channel.css", + "css/static/modal-query.css", + "css/static/modal-invite.css", + "css/static/modal-playlist.css", + "css/static/modal-banlist.css", + "css/static/modal-bancreate.css", + "css/static/modal-clientinfo.css", + "css/static/modal-serverinfo.css", + "css/static/modal-identity.css", + "css/static/modal-settings.css", + "css/static/modal-poke.css", + "css/static/modal-server.css", + "css/static/modal-keyselect.css", + "css/static/modal-permissions.css", + "css/static/modal-group-assignment.css", + "css/static/music/info_plate.css", + "css/static/frame/SelectInfo.css", + "css/static/control_bar.css", + "css/static/context_menu.css", + "css/static/frame-chat.css", + "css/static/connection_handlers.css", + "css/static/server-log.css", + "css/static/htmltags.css", + "css/static/hostbanner.css", + "css/static/menu-bar.css" + ]); + }, + + load_style_release: async () => { + await loader.load_styles([ + "css/static/base.css", + "css/static/main.css", + ]); + } +}; + +async function load_templates() { + try { + const response = await $.ajax("templates.html" + loader.get_cache_version()); + + let node = document.createElement("html"); + node.innerHTML = response; + let tags: HTMLCollection; + if(node.getElementsByTagName("body").length > 0) + tags = node.getElementsByTagName("body")[0].children; + else + tags = node.children; + + let root = document.getElementById("templates"); + if(!root) { + loader.critical_error("Failed to find template tag!"); + return; + } + while(tags.length > 0){ + let tag = tags.item(0); + root.appendChild(tag); + + } + } catch(error) { + loader.critical_error("Failed to find template tag!"); + throw "template error"; + } +} + +//FUN: loader_ignore_age=0&loader_default_duration=1500&loader_default_age=5000 +let _fadeout_warned = false; +function fadeoutLoader(duration = undefined, minAge = undefined, ignoreAge = undefined) { + if(typeof($) === "undefined") { + if(!_fadeout_warned) + console.warn("Could not fadeout loader screen. Missing jquery functions."); + _fadeout_warned = true; + return; + } + + let settingsDefined = typeof(StaticSettings) !== "undefined"; + if(!duration) { + if(settingsDefined) + duration = StaticSettings.instance.static("loader_default_duration", 750); + else duration = 750; + } + if(!minAge) { + if(settingsDefined) + minAge = StaticSettings.instance.static("loader_default_age", 1750); + else minAge = 750; + } + if(!ignoreAge) { + if(settingsDefined) + ignoreAge = StaticSettings.instance.static("loader_ignore_age", false); + else ignoreAge = false; + } + + /* + let age = Date.now() - app.appLoaded; + if(age < minAge && !ignoreAge) { + setTimeout(() => fadeoutLoader(duration, 0, true), minAge - age); + return; + } + */ + + $(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, duration); + $(".loader .half").animate({width: 0}, duration, () => { + $(".loader").detach(); + }); +} + +/* register tasks */ +loader.register_task(loader.Stage.INITIALIZING, { + name: "safari fix", + function: async () => { + /* safari remove "fix" */ + if(Element.prototype.remove === undefined) + Object.defineProperty(Element.prototype, "remove", { + enumerable: false, + configurable: false, + writable: false, + value: function(){ + this.parentElement.removeChild(this); + } + }); + }, + priority: 50 +}); + +loader.register_task(loader.Stage.INITIALIZING, { + name: "Browser detection", + function: async () => { + navigator.browserSpecs = (function(){ + let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; + if(/trident/i.test(M[1])){ + tem = /\brv[ :]+(\d+)/g.exec(ua) || []; + return {name:'IE',version:(tem[1] || '')}; + } + if(M[1]=== 'Chrome'){ + tem = ua.match(/\b(OPR|Edge)\/(\d+)/); + if(tem != null) return {name:tem[1].replace('OPR', 'Opera'),version:tem[2]}; + } + M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?']; + if((tem = ua.match(/version\/(\d+)/i))!= null) + M.splice(1, 1, tem[1]); + return {name:M[0], version:M[1]}; + })(); + + console.log("Resolved browser specs: %o", navigator.browserSpecs); //Object { name: "Firefox", version: "42" } + }, + priority: 30 +}); + +loader.register_task(loader.Stage.INITIALIZING, { + name: "secure tester", + function: async () => { + /* we need https or localhost to use some things like the storage API */ + if(typeof isSecureContext === "undefined") + (window)["isSecureContext"] = location.protocol !== 'https:' && location.hostname !== 'localhost'; + + if(!isSecureContext) { + loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!"); + throw "App requires a secure context!" + } + }, + priority: 20 +}); + +loader.register_task(loader.Stage.INITIALIZING, { + name: "webassembly tester", + function: loader_webassembly.test_webassembly, + priority: 20 +}); + +loader.register_task(loader.Stage.INITIALIZING, { + name: "app type test", + function: loader_javascript.detect_type, + priority: 20 +}); + +loader.register_task(loader.Stage.JAVASCRIPT, { + name: "javascript", + function: loader_javascript.load_scripts, + priority: 10 +}); + +loader.register_task(loader.Stage.STYLE, { + name: "style", + function: loader_style.load_style, + priority: 10 +}); + +loader.register_task(loader.Stage.TEMPLATES, { + name: "templates", + function: load_templates, + priority: 10 +}); + +loader.register_task(loader.Stage.LOADED, { + name: "loaded handler", + function: async () => { + fadeoutLoader(); + }, + priority: 10 +}); + +loader.register_task(loader.Stage.LOADED, { + name: "error task", + function: async () => { + if(Settings.instance.static(Settings.KEY_LOAD_DUMMY_ERROR, false)) { + loader.critical_error("The tea is cold!", "Argh, this is evil! Cold tea dosn't taste good."); + throw "The tea is cold!"; + } + }, + priority: 20 +}); + +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "lsx emoji picker setup", + function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}), + priority: 10 +}); + +window["Module"] = (window["Module"] || {}) as any; +/* TeaClient */ +if(window.require) { + const path = require("path"); + const remote = require('electron').remote; + module.paths.push(path.join(remote.app.getAppPath(), "/modules")); + module.paths.push(path.join(path.dirname(remote.getGlobal("browser-root")), "js")); + + const connector = require("renderer"); + console.log(connector); + + loader.register_task(loader.Stage.INITIALIZING, { + name: "teaclient initialize", + function: connector.initialize, + priority: 40 + }); +} + +if(!loader.running()) { + /* we know that we want to load the app */ + loader.execute_managed(); +} \ No newline at end of file diff --git a/shared/loader/loader.ts b/shared/loader/loader.ts new file mode 100644 index 00000000..e854fb1d --- /dev/null +++ b/shared/loader/loader.ts @@ -0,0 +1,643 @@ +interface Window { + tr(message: string) : string; +} + +namespace loader { + export namespace config { + export const loader_groups = false; + export const verbose = false; + export const error = true; + } + + export type Task = { + name: string, + priority: number, /* tasks with the same priority will be executed in sync */ + function: () => Promise + }; + + export enum Stage { + /* + loading loader required files (incl this) + */ + INITIALIZING, + /* + setting up the loading process + */ + SETUP, + /* + loading all style sheet files + */ + STYLE, + /* + loading all javascript files + */ + JAVASCRIPT, + /* + loading all template files + */ + TEMPLATES, + /* + initializing static/global stuff + */ + JAVASCRIPT_INITIALIZING, + /* + finalizing load process + */ + FINALIZING, + /* + invoking main task + */ + LOADED, + + DONE + } + + let cache_tag: string | undefined; + let current_stage: Stage = undefined; + const tasks: {[key:number]:Task[]} = {}; + + /* test if all files shall be load from cache or fetch again */ + function loader_cache_tag() { + const app_version = (() => { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; + + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; + + if(!version || version == "unknown" || version.replace(/0+/, "").length == 0) + return undefined; + + return version; + })(); + if(config.verbose) console.log("Found current app version: %o", app_version); + + if(!app_version) { + /* TODO add warning */ + cache_tag = "?_ts=" + Date.now(); + return; + } + const cached_version = localStorage.getItem("cached_version"); + if(!cached_version || cached_version != app_version) { + loader.register_task(loader.Stage.LOADED, { + priority: 0, + name: "cached version updater", + function: async () => { + localStorage.setItem("cached_version", app_version); + } + }); + } + cache_tag = "?_version=" + app_version; + } + + export function get_cache_version() { return cache_tag; } + + export function finished() { + return current_stage == Stage.DONE; + } + export function running() { return typeof(current_stage) !== "undefined"; } + + export function register_task(stage: Stage, task: Task) { + if(current_stage > stage) { + if(config.error) + console.warn("Register loading task, but it had already been finished. Executing task anyways!"); + task.function().catch(error => { + if(config.error) { + console.error("Failed to execute delayed loader task!"); + console.log(" - %s: %o", task.name, error); + } + + loader.critical_error(error); + }); + return; + } + + const task_array = tasks[stage] || []; + task_array.push(task); + tasks[stage] = task_array.sort((a, b) => a.priority - b.priority); + } + + export async function execute() { + document.getElementById("loader-overlay").classList.add("started"); + loader_cache_tag(); + + const load_begin = Date.now(); + + let begin: number = 0; + let end: number = Date.now(); + while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") { + + let current_tasks: Task[] = []; + while((tasks[current_stage] || []).length > 0) { + if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) { + current_tasks.push(tasks[current_stage].pop()); + } else break; + } + + const errors: { + error: any, + task: Task + }[] = []; + + const promises: Promise[] = []; + for(const task of current_tasks) { + try { + if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority); + promises.push(task.function().catch(error => { + errors.push({ + task: task, + error: error + }); + return Promise.resolve(); + })); + } catch(error) { + errors.push({ + task: task, + error: error + }); + } + } + + if(promises.length > 0) { + await Promise.all([...promises]); + } + + if(errors.length > 0) { + if(config.loader_groups) console.groupEnd(); + console.error("Failed to execute loader. The following tasks failed (%d):", errors.length); + for(const error of errors) + console.error(" - %s: %o", error.task.name, error.error); + + throw "failed to process step " + Stage[current_stage]; + } + + if(current_tasks.length == 0) { + if(typeof(current_stage) === "undefined") { + current_stage = -1; + if(config.verbose) console.debug("[loader] Booting app"); + } else if(current_stage < Stage.INITIALIZING) { + if(config.loader_groups) console.groupEnd(); + if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin); + } else { + if(config.loader_groups) console.groupEnd(); + if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin); + } + + begin = end; + current_stage += 1; + + if(current_stage != Stage.DONE && config.loader_groups) + console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); + } + } + + /* cleanup */ + { + _script_promises = {}; + } + if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); + } + export function execute_managed() { + loader.execute().then(() => { + if(config.verbose) { + let message; + if(typeof(window.tr) !== "undefined") + message = tr("App loaded successfully!"); + else + message = "App loaded successfully!"; + + if(typeof(log) !== "undefined") { + /* We're having our log module */ + log.info(LogCategory.GENERAL, message); + } else { + console.log(message); + } + } + }).catch(error => { + if(config.error) { + console.error("App loading failed: %o", error); + } + loader.critical_error("Failed to execute loader", "Lookup the console for more detail"); + }); + } + + export type DependSource = { + url: string; + depends: string[]; + } + export type SourcePath = string | DependSource | string[]; + + function script_name(path: SourcePath) { + if(Array.isArray(path)) { + let buffer = ""; + let _or = " or "; + for(let entry of path) + buffer += _or + script_name(entry); + return buffer.slice(_or.length); + } else if(typeof(path) === "string") + return "" + path + ""; + else + return "" + path.url + ""; + } + + class SyntaxError { + source: any; + + constructor(source: any) { + this.source = source; + } + } + + let _script_promises: {[key: string]: Promise} = {}; + export async function load_script(path: SourcePath) : Promise { + if(Array.isArray(path)) { //We have some fallback + return load_script(path[0]).catch(error => { + if(error instanceof SyntaxError) + return Promise.reject(error.source); + + if(path.length > 1) + return load_script(path.slice(1)); + + return Promise.reject(error); + }); + } else { + const source = typeof(path) === "string" ? {url: path, depends: []} : path; + if(source.url.length == 0) return Promise.resolve(); + + return _script_promises[source.url] = (async () => { + /* await depends */ + for(const depend of source.depends) { + if(!_script_promises[depend]) + throw "Missing dependency " + depend; + await _script_promises[depend]; + } + + const tag: HTMLScriptElement = document.createElement("script"); + + await new Promise((resolve, reject) => { + let error = false; + const error_handler = (event: ErrorEvent) => { + if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error + if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); + window.removeEventListener('error', error_handler as any); + + reject(new SyntaxError(event.error)); + event.preventDefault(); + error = true; + } + }; + window.addEventListener('error', error_handler as any); + + const cleanup = () => { + tag.onerror = undefined; + tag.onload = undefined; + + clearTimeout(timeout_handle); + window.removeEventListener('error', error_handler as any); + }; + const timeout_handle = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 5000); + tag.type = "application/javascript"; + tag.async = true; + tag.defer = true; + tag.onerror = error => { + cleanup(); + tag.remove(); + reject(error); + }; + tag.onload = () => { + cleanup(); + + if(config.verbose) console.debug("Script %o loaded", path); + setTimeout(resolve, 100); + }; + + document.getElementById("scripts").appendChild(tag); + + tag.src = source.url + (cache_tag || ""); + }); + })(); + } + } + + export async function load_scripts(paths: SourcePath[]) : Promise { + const promises: Promise[] = []; + const errors: { + script: SourcePath, + error: any + }[] = []; + + for(const script of paths) + promises.push(load_script(script).catch(error => { + errors.push({ + script: script, + error: error + }); + return Promise.resolve(); + })); + + await Promise.all([...promises]); + + if(errors.length > 0) { + if(config.error) { + console.error("Failed to load the following scripts:"); + for(const script of errors) + console.log(" - %o: %o", script.script, script.error); + } + + loader.critical_error("Failed to load script " + script_name(errors[0].script) + "
" + "View the browser console for more information!"); + throw "failed to load script " + script_name(errors[0].script); + } + } + + export async function load_style(path: SourcePath) : Promise { + if(Array.isArray(path)) { //We have some fallback + return load_style(path[0]).catch(error => { + if(error instanceof SyntaxError) + return Promise.reject(error.source); + + if(path.length > 1) + return load_script(path.slice(1)); + + return Promise.reject(error); + }); + } else { + if(!path) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const tag: HTMLLinkElement = document.createElement("link"); + + let error = false; + const error_handler = (event: ErrorEvent) => { + if(config.verbose) console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); + if(event.filename == tag.href) { //FIXME! + window.removeEventListener('error', error_handler as any); + + reject(new SyntaxError(event.error)); + event.preventDefault(); + error = true; + } + }; + window.addEventListener('error', error_handler as any); + + tag.type = "text/css"; + tag.rel = "stylesheet"; + + const cleanup = () => { + tag.onerror = undefined; + tag.onload = undefined; + + clearTimeout(timeout_handle); + window.removeEventListener('error', error_handler as any); + }; + + const timeout_handle = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 5000); + + tag.onerror = error => { + cleanup(); + tag.remove(); + if(config.error) + console.error("File load error for file %s: %o", path, error); + reject("failed to load file " + path); + }; + tag.onload = () => { + cleanup(); + { + const css: CSSStyleSheet = tag.sheet as CSSStyleSheet; + const rules = css.cssRules; + const rules_remove: number[] = []; + const rules_add: string[] = []; + + for(let index = 0; index < rules.length; index++) { + const rule = rules.item(index); + let rule_text = rule.cssText; + + if(rule.cssText.indexOf("%%base_path%%") != -1) { + rules_remove.push(index); + rules_add.push(rule_text.replace("%%base_path%%", document.location.origin + document.location.pathname)); + } + } + + for(const index of rules_remove.sort((a, b) => b > a ? 1 : 0)) { + if(css.removeRule) + css.removeRule(index); + else + css.deleteRule(index); + } + for(const rule of rules_add) + css.insertRule(rule, rules_remove[0]); + } + + if(config.verbose) console.debug("Style sheet %o loaded", path); + setTimeout(resolve, 100); + }; + + document.getElementById("style").appendChild(tag); + tag.href = path + (cache_tag || ""); + }); + } + } + + export async function load_styles(paths: SourcePath[]) : Promise { + const promises: Promise[] = []; + const errors: { + sheet: SourcePath, + error: any + }[] = []; + + for(const sheet of paths) + promises.push(load_style(sheet).catch(error => { + errors.push({ + sheet: sheet, + error: error + }); + return Promise.resolve(); + })); + + await Promise.all([...promises]); + + if(errors.length > 0) { + if(loader.config.error) { + console.error("Failed to load the following style sheet:"); + for(const sheet of errors) + console.log(" - %o: %o", sheet.sheet, sheet.error); + } + + loader.critical_error("Failed to load style sheet " + script_name(errors[0].sheet) + "
" + "View the browser console for more information!"); + throw "failed to load style sheet " + script_name(errors[0].sheet); + } + } + + + export type ErrorHandler = (message: string, detail: string) => void; + + let _callback_critical_error: ErrorHandler; + let _callback_critical_called: boolean = false; + + export function critical_error(message: string, detail?: string) { + if(_callback_critical_called) { + console.warn("[CRITICAL] %s", message); + if(typeof(detail) === "string") + console.warn("[CRITICAL] %s", detail); + return; + } + + if(_callback_critical_error) { + _callback_critical_error(message, detail); + return; + } + + /* default handling */ + let tag = document.getElementById("critical-load"); + + { + const error_tags = tag.getElementsByClassName("error"); + error_tags[0].innerHTML = message; + } + + if(typeof(detail) === "string") { + let node_detail = tag.getElementsByClassName("detail")[0]; + node_detail.innerHTML = detail; + } + + tag.style.display = "block"; + } + + export function critical_error_handler(handler?: ErrorHandler, override?: boolean) : ErrorHandler { + if((typeof(handler) === "object" && handler !== _callback_critical_error) || override) + _callback_critical_error = handler; + return _callback_critical_error; + } +} + +{ + + const hello_world = () => { + const clog = console.log; + const print_security = () => { + { + const css = [ + "display: block", + "text-align: center", + "font-size: 42px", + "font-weight: bold", + "-webkit-text-stroke: 2px black", + "color: red" + ].join(";"); + clog("%c ", "font-size: 100px;"); + clog("%cSecurity warning:", css); + } + { + const css = [ + "display: block", + "text-align: center", + "font-size: 18px", + "font-weight: bold" + ].join(";"); + + clog("%cPasting anything in here could give attackers access to your data.", css); + clog("%cUnless you understand exactly what you are doing, close this window and stay safe.", css); + clog("%c ", "font-size: 100px;"); + } + }; + + /* print the hello world */ + { + const css = [ + "display: block", + "text-align: center", + "font-size: 72px", + "font-weight: bold", + "-webkit-text-stroke: 2px black", + "color: #18BC9C" + ].join(";"); + clog("%cHey, hold on!", css); + } + { + const css = [ + "display: block", + "text-align: center", + "font-size: 26px", + "font-weight: bold" + ].join(";"); + + const css_2 = [ + "display: block", + "text-align: center", + "font-size: 26px", + "font-weight: bold", + "color: blue" + ].join(";"); + + const display_detect = /./; + display_detect.toString = function() { print_security(); return ""; }; + + clog("%cLovely to see you using and debugging the TeaSpeak Web client.", css); + clog("%cIf you have some good ideas or already done some incredible changes,", css); + clog("%cyou'll be may interested to share them here: %chttps://github.com/TeaSpeak/TeaWeb", css, css_2); + clog("%c ", display_detect); + } + }; + + try { /* lets try to print it as VM code :)*/ + let hello_world_code = hello_world.toString(); + hello_world_code = hello_world_code.substr(hello_world_code.indexOf('() => {') + 8); + hello_world_code = hello_world_code.substring(0, hello_world_code.lastIndexOf("}")); + + //Look aheads are not possible with firefox + //hello_world_code = hello_world_code.replace(/(? { + if(loader.running()) { + if(loader.config.verbose) + console.debug("Do not execute debug loading"); + return; + } + + loader.register_task(loader.Stage.INITIALIZING, { + priority: 100, + function: async () => { + let loader_type; + location.search.replace(/(?:^\?|&)([a-zA-Z_]+)=([.a-zA-Z0-9]+)(?=$|&)/g, (_, key: string, value: string) => { + if(key.toLowerCase() == "loader_target") + loader_type = value; + return ""; + }); + loader_type = loader_type || "app"; + if(loader_type === "app") { + try { + await loader.load_scripts(["loader/app.js"]); + } catch (error) { + console.error("Failed to load main app script: %o", error); + loader.critical_error("Failed to load main app script", error); + throw "app loader failed"; + } + } else { + loader.critical_error("Missing loader target: " + loader_type); + throw "Missing loader target: " + loader_type; + } + }, + name: "loading target" + }); + + loader.execute_managed(); +}, 0); + + + diff --git a/shared/tsconfig/dtsconfig_loader.json b/shared/tsconfig/dtsconfig_loader.json deleted file mode 100644 index 6ab9e9de..00000000 --- a/shared/tsconfig/dtsconfig_loader.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "source_files": [ - "../js/load.ts" - ], - "target_file": "../declarations/exports_loader.d.ts" -} \ No newline at end of file diff --git a/shared/tsconfig/dtsconfig_loader_app.json b/shared/tsconfig/dtsconfig_loader_app.json new file mode 100644 index 00000000..0c46a347 --- /dev/null +++ b/shared/tsconfig/dtsconfig_loader_app.json @@ -0,0 +1,7 @@ +{ + "source_files": [ + "../loader/loader.ts", + "../loader/app.ts" + ], + "target_file": "../declarations/exports_loader_app.d.ts" +} \ No newline at end of file diff --git a/shared/tsconfig/dtsconfig_packed.json b/shared/tsconfig/dtsconfig_packed.json index 633c619a..0da6c7c6 100644 --- a/shared/tsconfig/dtsconfig_packed.json +++ b/shared/tsconfig/dtsconfig_packed.json @@ -3,8 +3,7 @@ "../js/**/*.ts" ], "exclude": [ - "../js/workers/**/*.ts", - "../js/load.ts" + "../js/workers/**/*.ts" ], "target_file": "../declarations/exports_packed.d.ts" } diff --git a/shared/tsconfig/tsconfig.json b/shared/tsconfig/tsconfig.json index 4d7bf063..6fe31d1c 100644 --- a/shared/tsconfig/tsconfig.json +++ b/shared/tsconfig/tsconfig.json @@ -20,6 +20,7 @@ "include": [ "../types", "../declarations/imports_*.d.ts", + "../declarations/exports_loader_app.d.ts", "../backend", "../js/**/*.ts" ] diff --git a/shared/tsconfig/tsconfig_packed.json b/shared/tsconfig/tsconfig_packed.json index 8ae09e51..7bec7c94 100644 --- a/shared/tsconfig/tsconfig_packed.json +++ b/shared/tsconfig/tsconfig_packed.json @@ -17,13 +17,12 @@ ] }, "exclude": [ - "../js/workers", - "../js/load.ts" + "../js/workers" ], "include": [ "../types", "../declarations/imports_*.d.ts", - "../declarations/exports_loader.d.ts", + "../declarations/exports_loader_app.d.ts", "../js/**/*.ts", "../backend" ] diff --git a/shared/tsconfig/tsconfig_packed_loader.json b/shared/tsconfig/tsconfig_packed_loader_app.json similarity index 56% rename from shared/tsconfig/tsconfig_packed_loader.json rename to shared/tsconfig/tsconfig_packed_loader_app.json index 5679e6d8..fcdc4926 100644 --- a/shared/tsconfig/tsconfig_packed_loader.json +++ b/shared/tsconfig/tsconfig_packed_loader_app.json @@ -1,14 +1,16 @@ { "compilerOptions": { "target": "es6", - "module": "commonjs", - "sourceMap": true + "module": "none", + "sourceMap": true, + "outFile": "../generated/loader_app.js" }, "include": [ "../types", "../declarations/imports_*.d.ts", "../declarations/exports_packed.d.ts", - "../js/load.ts", + "../loader/loader.ts", + "../loader/app.ts", "../backend" ] } \ No newline at end of file diff --git a/todo.txt b/todo.txt index 40b11561..be680d56 100644 --- a/todo.txt +++ b/todo.txt @@ -15,22 +15,63 @@ - Icon uploiad - Avatar list - Server group assignments checkbox + - Invite buddy - Identity improve - Identity import +- Ban Liste + - Übersicht + - Suchfunktion + - Text + - "Hightlisht own bans" + - "Show only own bans" + Pro Ban: + - Globaler ban oder server ban + - Name (Nicht immer gegeben) + - IP (Nicht immer gegeben) + - UID (Nicht immer gegeben) + - HWID (Nicht immer gegeben) + - Reason + - Creator + - Created/Expires + - "More info" Button (Dort sieht man auch dann die "Enforcements") -- Application Options - - Crash - - Focus crash window on crash - - Add a notification (Like the browser notifications) + - Reload button + - Hinzufügen + - Bearbeiten + - Löschen +- Ban "More Info" Dialog + Alle daten wie bei der liste, und eine liste mit "trigger" events (enforcements) + Pro entry: + - Unqiue ID + - Hardware ID + - Client Name + - Connection IP + - Timestamp +- Modal Bookmarks + - Bookmark list (+ Subdirectories) + - Bookmark Settings + - Bookmark/Directory name + - Only for bookmarks: + - Connect profile + - Server address + - Server port + - Server password + - Only for directories + - Parent directory +- Modal Bookmark create + - Type: Bookmark | Directory + - Parent directory + - Name - Client info popup - Basic Info - Name/Unique ID (Database ID vil auch? Oder als hover irgendwo) - TeaForo connected? Ist premium ja nein? + - Country - Avatar - IP (Wenn permission) @@ -44,15 +85,30 @@ - Description - Server groups - Channel group - - Connect count (Wie oft der client schon connectet ist) - - Online seit | Idle time + - Connect count (Wie oft der client schon connected ist) + - Online seid | Idle time - Ping + - Client version + - First connected Nur TeaClient; Nicht für WebClient clients - Bandwidth - Packets send/received | Packet Loss - Up/Download Quota +- Server + - Server region/country + - Bandwidths & Transferred data + +- Channel + - Audio Codec (Opus (4)) + - Channel type (Wenn TEMP dann delete delay (nur wenn empty)) + - Channel Topic + - Current clients "2/Unlimited" + - Description + - Password protected + - "Texting mode" | Private | "No saving" | "Logged (+ history length)" + - Server "short" info? @@ -71,6 +127,12 @@ - File Transfer bytes transferred, month & global (Up + Download) TODO: +Server Info (Bandwidth usage) Fix these icons: https://img.did.science/Screenshot_20-11-06.png -Fix auto TS compile for vendor emoji-picker -Make identity settings a bit smaller (Even scroll on my Laptop) \ No newline at end of file +Make identity settings a bit smaller (Even scroll on my Laptop) + + +- Application Options + - Crash + - Focus crash window on crash + - Add a notification (Like the browser notifications) \ No newline at end of file diff --git a/tools/dtsgen/out.d.ts b/tools/dtsgen/out.d.ts index 47527988..2ae7e1fb 100644 --- a/tools/dtsgen/out.d.ts +++ b/tools/dtsgen/out.d.ts @@ -1,13 +1,6 @@ -/* File: C:\Users\WolverinDEV\TeaSpeak\TeaWeb\tools\dtsgen\test\test_03.ts */ -declare enum YY { - H = "C", - B = "Y" -} -declare interface X { - type: any; - c: YY.B; -} -declare class X { - static x(); +/* File: /home/wolverindev/TeaSpeak/Web-Client/tools/dtsgen/test/test_07.ts */ +declare namespace C { } +declare namespace C { + export function test(arg: string); } diff --git a/vendor/xbbcode b/vendor/xbbcode index d75eb02e..983c9d59 160000 --- a/vendor/xbbcode +++ b/vendor/xbbcode @@ -1 +1 @@ -Subproject commit d75eb02ef8038595da5cc82d5953b170df9c49eb +Subproject commit 983c9d59542a003df38e80fa1680ab9cfda4c530 diff --git a/web/generate_packed.sh b/web/generate_packed.sh index 0c679847..03f929c1 100755 --- a/web/generate_packed.sh +++ b/web/generate_packed.sh @@ -4,30 +4,34 @@ BASEDIR=$(dirname "$0") cd "$BASEDIR" source ../scripts/resolve_commands.sh -if [ ! -e declarations/imports_shared.d.ts ]; then +if [[ ! -e declarations/imports_shared.d.ts ]]; then echo "generate the declarations first!" echo "Execute: /scripts/build_declarations.sh" exit 1 fi -if [ ! -e ../shared/generated/shared.js ]; then +if [[ ! -e ../shared/generated/shared.js ]]; then echo "generate the shared packed file first!" echo "Execute: /shared/generate_packed.sh" exit 1 fi execute_tsc -p tsconfig/tsconfig_packed.json -if [ $? -ne 0 ]; then +if [[ $? -ne 0 ]]; then echo "Failed to build file" exit 1 fi -echo "Mergin files" +echo "Merging files" -if [ -e generated/client.js ]; then +if [[ -e generated/client.js ]]; then rm generated/client.js fi cat ../shared/generated/shared.js > generated/client.js cat generated/web.js >> generated/client.js -npm run minify-web-rel-file \ No newline at end of file +if [[ -e generated/client.min.js ]]; then + rm generated/client.min.js +fi + +npm run minify-web-rel-file `pwd`/generated/client.min.js `pwd`/generated/client.js \ No newline at end of file diff --git a/web/js/WebPPTListener.ts b/web/js/WebPPTListener.ts index e84cc0ac..b145058c 100644 --- a/web/js/WebPPTListener.ts +++ b/web/js/WebPPTListener.ts @@ -120,7 +120,7 @@ namespace ppt { new_hooks.push(hook); if(!old_hooks.remove(hook) && hook.callback_press) { hook.callback_press(); - console.debug("Trigger key press for %o!", hook); + log.trace(LogCategory.GENERAL, tr("Trigger key press for %o!"), hook); } } } @@ -129,7 +129,7 @@ namespace ppt { for(const hook of old_hooks) if(hook.callback_release) { hook.callback_release(); - console.debug("Trigger key release for %o!", hook); + log.trace(LogCategory.GENERAL, tr("Trigger key release for %o!"), hook); } key_hooks_active = new_hooks; } diff --git a/web/js/audio/AudioPlayer.ts b/web/js/audio/AudioPlayer.ts index 9f1a03bb..25538997 100644 --- a/web/js/audio/AudioPlayer.ts +++ b/web/js/audio/AudioPlayer.ts @@ -17,7 +17,7 @@ namespace audio.player { } function fire_initialized() { - console.log("Fire initialized: %o", _initialized_listener); + log.info(LogCategory.AUDIO, tr("File initialized for %d listeners"), _initialized_listener.length); while(_initialized_listener.length > 0) _initialized_listener.pop_front()(); } @@ -39,7 +39,7 @@ namespace audio.player { (_globalContextPromise = _globalContext.resume()).then(() => { fire_initialized(); }).catch(error => { - displayCriticalError("Failed to initialize global audio context! (" + error + ")"); + loader.critical_error("Failed to initialize global audio context! (" + error + ")"); }); } _globalContext.resume(); //We already have our listener diff --git a/web/js/codec/BasicCodec.ts b/web/js/codec/BasicCodec.ts index b92d13c3..5334686d 100644 --- a/web/js/codec/BasicCodec.ts +++ b/web/js/codec/BasicCodec.ts @@ -49,7 +49,7 @@ abstract class BasicCodec implements Codec { encodeSamples(cache: CodecClientCache, pcm: AudioBuffer) { - this._encodeResampler.resample(pcm).catch(error => console.error(tr("Could not resample PCM data for codec. Error: %o"), error)) + this._encodeResampler.resample(pcm).catch(error => log.error(LogCategory.VOICE, tr("Could not resample PCM data for codec. Error: %o"), error)) .then(buffer => this.encodeSamples0(cache, buffer as any)).catch(error => console.error(tr("Could not encode PCM data for codec. Error: %o"), error)) } @@ -74,12 +74,12 @@ abstract class BasicCodec implements Codec { if(result instanceof Uint8Array) { let time = Date.now() - encodeBegin; if(time > 20) - console.error(tr("Required time: %d"), time); + log.warn(LogCategory.VOICE, tr("Voice buffer stalled in WorkerPipe longer then expected: %d"), time); //if(time > 20) // chat.serverChat().appendMessage("Required decode time: " + time); this.on_encoded_data(result); } - else console.error("[Codec][" + this.name() + "] Could not encode buffer. Result: " + result); //TODO tr + else log.error(LogCategory.VOICE, "[Codec][" + this.name() + "] Could not encode buffer. Result: " + result); //TODO tr }); } return true; diff --git a/web/js/codec/CodecWrapperWorker.ts b/web/js/codec/CodecWrapperWorker.ts index f59372e0..de79127b 100644 --- a/web/js/codec/CodecWrapperWorker.ts +++ b/web/js/codec/CodecWrapperWorker.ts @@ -152,16 +152,14 @@ class CodecWrapperWorker extends BasicCodec { } private onWorkerMessage(message: any) { - if(Date.now() - message["timestamp"] > 5) - console.warn(tr("Worker message stock time: %d"), Date.now() - message["timestamp"]); if(!message["token"]) { - console.error(tr("Invalid worker token!")); + log.error(LogCategory.VOICE, tr("Invalid worker token!")); return; } if(message["token"] == this._workerCallbackToken) { if(message["type"] == "loaded") { - console.log(tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]); + log.info(LogCategory.VOICE, tr("[Codec] Got worker init response: Success: %o Message: %o"), message["success"], message["message"]); if(message["success"]) { if(this._workerCallbackResolve) this._workerCallbackResolve(); @@ -176,10 +174,14 @@ class CodecWrapperWorker extends BasicCodec { //FIXME? return; } - console.log(tr("Costume callback! (%o)"), message); + log.debug(LogCategory.VOICE, tr("Costume callback! (%o)"), message); return; } + /* lets warn on general packets. Control packets are allowed to "stuck" a bit longer */ + if(Date.now() - message["timestamp"] > 5) + log.warn(LogCategory.VOICE, tr("Worker message stock time: %d"), Date.now() - message["timestamp"]); + for(let entry of this._workerListener) { if(entry.token == message["token"]) { entry.resolve(message); @@ -188,8 +190,7 @@ class CodecWrapperWorker extends BasicCodec { } } - //TODO tr - console.error("Could not find worker token entry! (" + message["token"] + ")"); + log.error(LogCategory.VOICE, tr("Could not find worker token entry! (%o)"), message["token"]); } private spawnWorker() : Promise { diff --git a/web/js/connection/ServerConnection.ts b/web/js/connection/ServerConnection.ts index 1c183b43..dd4381c8 100644 --- a/web/js/connection/ServerConnection.ts +++ b/web/js/connection/ServerConnection.ts @@ -58,7 +58,7 @@ namespace connection { destroy() { this.disconnect("handle destroyed").catch(error => { - console.warn(tr("Failed to disconnect on server connection destroy: %o"), error); + log.warn(LogCategory.NETWORKING, tr("Failed to disconnect on server connection destroy: %o"), error); }).then(() => { clearInterval(this._ping.thread_id); clearTimeout(this._connect_timeout_timer); @@ -67,7 +67,7 @@ namespace connection { try { listener.reject("handler destroyed"); } catch(error) { - console.warn(tr("Failed to reject command promise: %o"), error); + log.warn(LogCategory.NETWORKING, tr("Failed to reject command promise: %o"), error); } } this._retListener = undefined; @@ -86,7 +86,7 @@ namespace connection { } on_connect: () => void = () => { - console.log(tr("Socket connected")); + log.info(LogCategory.NETWORKING, tr("Socket connected")); this.client.log.log(log.server.Type.CONNECTION_LOGIN, {}); this._handshakeHandler.initialize(); this._handshakeHandler.startHandshake(); @@ -105,7 +105,7 @@ namespace connection { try { await this.disconnect() } catch(error) { - console.error(tr("Failed to close old connection properly. Error: %o"), error); + log.error(LogCategory.NETWORKING, tr("Failed to close old connection properly. Error: %o"), error); throw "failed to cleanup old connection"; } } @@ -177,7 +177,7 @@ namespace connection { local_socket.onerror = e => { if(this._socket != local_socket) return; /* this socket isn't from interest anymore */ - console.log(tr("Received web socket error: (%o)"), e); + log.warn(LogCategory.NETWORKING, tr("Received web socket error: (%o)"), e); }; local_socket.onmessage = msg => { @@ -241,12 +241,12 @@ namespace connection { try { json = JSON.parse(data); } catch(e) { - console.error(tr("Could not parse message json!")); + log.warn(LogCategory.NETWORKING, tr("Could not parse message json!")); alert(e); // error in the above string (in this case, yes)! return; } if(json["type"] === undefined) { - console.log(tr("Missing data type!")); + log.warn(LogCategory.NETWORKING, tr("Missing data type in message!")); return; } if(json["type"] === "command") { @@ -271,7 +271,7 @@ namespace connection { if(this._voice_connection) this._voice_connection.handleControlPacket(json); else - console.log(tr("Dropping WebRTC command packet, because we haven't a bridge.")) + log.warn(LogCategory.NETWORKING, tr("Dropping WebRTC command packet, because we haven't a bridge.")) } else if(json["type"] === "ping") { this.sendData(JSON.stringify({ type: 'pong', @@ -288,7 +288,7 @@ namespace connection { log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3)); } } else { - console.log(tr("Unknown command type %o"), json["type"]); + log.warn(LogCategory.NETWORKING, tr("Unknown command type %o"), json["type"]); } } else { log.warn(LogCategory.NETWORKING, tr("Received unknown message of type %s. Dropping message"), typeof(data)); @@ -317,7 +317,7 @@ namespace connection { send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise { if(!this._socket || !this.connected()) { - console.warn(tr("Tried to send a command without a valid connection.")); + log.warn(LogCategory.NETWORKING, tr("Tried to send a command without a valid connection.")); return Promise.reject(tr("not connected")); } diff --git a/web/js/voice/AudioResampler.ts b/web/js/voice/AudioResampler.ts index 01524852..1092eea1 100644 --- a/web/js/voice/AudioResampler.ts +++ b/web/js/voice/AudioResampler.ts @@ -9,7 +9,7 @@ class AudioResampler { resample(buffer: AudioBuffer) : Promise { if(!buffer) { - console.warn(tr("Received empty buffer as input! Returning empty output!")); + log.warn(LogCategory.AUDIO, tr("Received empty buffer as input! Returning empty output!")); return Promise.resolve(buffer); } //console.log("Encode from %i to %i", buffer.sampleRate, this.targetSampleRate); diff --git a/web/js/voice/JavascriptRecorder.ts b/web/js/voice/JavascriptRecorder.ts index f3762984..bb21c80d 100644 --- a/web/js/voice/JavascriptRecorder.ts +++ b/web/js/voice/JavascriptRecorder.ts @@ -46,7 +46,7 @@ namespace audio { if(_queried_devices.length > 0 && _queried_devices.filter(e => e.default_input).length == 0) _queried_devices[0].default_input = true; } catch(error) { - console.warn(tr("Failed to query microphone devices (%o)"), error); + log.error(LogCategory.AUDIO, tr("Failed to query microphone devices (%o)"), error); _queried_devices = []; } } @@ -357,7 +357,7 @@ namespace audio { if(callback.callback_audio) callback.callback_audio(event.inputBuffer); if(callback.callback_buffer) { - console.warn(tr("AudioInput has callback buffer, but this isn't supported yet!")); + log.warn(LogCategory.AUDIO, tr("AudioInput has callback buffer, but this isn't supported yet!")); } } @@ -371,7 +371,7 @@ namespace audio { if(this._state != InputState.PAUSED) return; } catch(error) { - console.debug(tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error); + log.debug(LogCategory.AUDIO, tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error); } } @@ -400,7 +400,7 @@ namespace audio { if(!media_function) return InputStartResult.ENOTSUPPORTED; try { - console.info(tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id); + log.info(LogCategory.AUDIO, tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id); const audio_constrains: MediaTrackConstraints = {}; audio_constrains.deviceId = device_id; @@ -420,13 +420,13 @@ namespace audio { //createErrorModal(tr("Failed to create microphone"), tr("Microphone recording failed. Please allow TeaWeb access to your microphone")).open(); //FIXME: Move this to somewhere else! - console.warn(tr("Microphone request failed (No permissions). Browser message: %o"), error.message); + log.warn(LogCategory.AUDIO, tr("Microphone request failed (No permissions). Browser message: %o"), error.message); return InputStartResult.ENOTALLOWED; } else { - console.warn(tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error); + log.warn(LogCategory.AUDIO, tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error); } } else { - console.warn(tr("Failed to initialize recording stream (%o)"), error); + log.warn(LogCategory.AUDIO, tr("Failed to initialize recording stream (%o)"), error); } return InputStartResult.EUNKNOWN; } @@ -506,7 +506,7 @@ namespace audio { try { await this.stop(); } catch(error) { - console.warn(tr("Failed to stop previous record session (%o)"), error); + log.warn(LogCategory.AUDIO, tr("Failed to stop previous record session (%o)"), error); } this._current_device = device as any; /* TODO: Test for device_id and device_group */ @@ -519,7 +519,7 @@ namespace audio { try { await this.start() } catch(error) { - console.warn(tr("Failed to start new recording stream (%o)"), error); + log.warn(LogCategory.AUDIO, tr("Failed to start new recording stream (%o)"), error); throw "failed to start record"; } } diff --git a/web/js/voice/VoiceClient.ts b/web/js/voice/VoiceClient.ts index 357be319..3c3cbaf6 100644 --- a/web/js/voice/VoiceClient.ts +++ b/web/js/voice/VoiceClient.ts @@ -34,23 +34,23 @@ namespace audio { playback_buffer(buffer: AudioBuffer) { if(!buffer) { - console.warn(tr("[AudioController] Got empty or undefined buffer! Dropping it")); + log.warn(LogCategory.VOICE, tr("[AudioController] Got empty or undefined buffer! Dropping it")); return; } if(!this.speakerContext) { - console.warn(tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!")); + log.warn(LogCategory.VOICE, tr("[AudioController] Failed to replay audio. Global audio context not initialized yet!")); return; } if (buffer.sampleRate != this.speakerContext.sampleRate) - console.warn(tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate); + log.warn(LogCategory.VOICE, tr("[AudioController] Source sample rate isn't equal to playback sample rate! (%o | %o)"), buffer.sampleRate, this.speakerContext.sampleRate); this.apply_volume_to_buffer(buffer); this._buffered_samples.push(buffer); if(this._player_state == connection.voice.PlayerState.STOPPED || this._player_state == connection.voice.PlayerState.STOPPING) { - console.log(tr("[Audio] Starting new playback")); + log.info(LogCategory.VOICE, tr("[Audio] Starting new playback")); this.set_state(connection.voice.PlayerState.PREBUFFERING); } @@ -67,11 +67,11 @@ namespace audio { break; } if(this._player_state == connection.voice.PlayerState.PREBUFFERING) { - console.log(tr("[Audio] Prebuffering succeeded (Replaying now)")); + log.info(LogCategory.VOICE, tr("[Audio] Prebuffering succeeded (Replaying now)")); if(this.callback_playback) this.callback_playback(); } else if(this.allowBuffering) { - console.log(tr("[Audio] Buffering succeeded (Replaying now)")); + log.info(LogCategory.VOICE, tr("[Audio] Buffering succeeded (Replaying now)")); } this._player_state = connection.voice.PlayerState.PLAYING; case connection.voice.PlayerState.PLAYING: @@ -86,7 +86,7 @@ namespace audio { let buffer: AudioBuffer; while((buffer = this._buffered_samples.pop_front())) { if(this._playing_nodes.length >= this._latency_buffer_length * 1.5 + 3) { - console.log(tr("Dropping buffer because playing queue grows to much")); + log.info(LogCategory.VOICE, tr("Dropping buffer because playing queue grows to much")); continue; /* drop the data (we're behind) */ } if(this._time_index < this.speakerContext.currentTime) @@ -135,7 +135,7 @@ namespace audio { this._player_state = connection.voice.PlayerState.BUFFERING; if(!this.allowBuffering) - console.warn(tr("[Audio] Detected a buffer underflow!")); + log.warn(LogCategory.VOICE, tr("[Audio] Detected a buffer underflow!")); this.reset_buffer_timeout(true); } else { this._player_state = connection.voice.PlayerState.STOPPED; @@ -152,7 +152,7 @@ namespace audio { if(restart) this._buffer_timeout = setTimeout(() => { if(this._player_state == connection.voice.PlayerState.PREBUFFERING || this._player_state == connection.voice.PlayerState.BUFFERING) { - console.warn(tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay")); + log.warn(LogCategory.VOICE, tr("[Audio] Buffering exceeded timeout. Flushing and stopping replay")); this.stopAudio(); } this._buffer_timeout = undefined; diff --git a/web/js/voice/VoiceHandler.ts b/web/js/voice/VoiceHandler.ts index 22c28073..ef710a4e 100644 --- a/web/js/voice/VoiceHandler.ts +++ b/web/js/voice/VoiceHandler.ts @@ -26,16 +26,16 @@ namespace audio { const dummy_client_id = 0xFFEF; this.ownCodec(dummy_client_id, _ => {}).then(codec => { - console.log(tr("Release again! (%o)"), codec); + log.info(LogCategory.VOICE, tr("Release again! (%o)"), codec); this.releaseCodec(dummy_client_id); }).catch(error => { if(this._supported) { - console.warn(tr("Disabling codec support for "), this.name); + log.warn(LogCategory.VOICE, tr("Disabling codec support for "), this.name); createErrorModal(tr("Could not load codec driver"), tr("Could not load or initialize codec ") + this.name + "
" + "Error: " + JSON.stringify(error) + "").open(); - console.error(tr("Failed to initialize the opus codec. Error: %o"), error); + log.error(LogCategory.VOICE, tr("Failed to initialize the opus codec. Error: %o"), error); } else { - console.debug(tr("Failed to initialize already disabled codec. Error: %o"), error); + log.debug(LogCategory.VOICE, tr("Failed to initialize already disabled codec. Error: %o"), error); } this._supported = false; }); @@ -61,7 +61,7 @@ namespace audio { //TODO test success flag this.ownCodec(clientId, callback_encoded, false).then(resolve).catch(reject); }).catch(error => { - console.error(tr("Could not initialize codec!\nError: %o"), error); + log.error(LogCategory.VOICE, tr("Could not initialize codec!\nError: %o"), error); reject(typeof(error) === 'string' ? error : tr("Could not initialize codec!")); }); } @@ -149,7 +149,7 @@ namespace audio { clearInterval(this.send_task); this.dropSession(); this.acquire_voice_recorder(undefined, true).catch(error => { - console.warn(tr("Failed to release voice recorder: %o"), error); + log.warn(LogCategory.VOICE, tr("Failed to release voice recorder: %o"), error); }).then(() => { for(const client of this._audio_clients) { client.abort_replay(); @@ -306,17 +306,17 @@ namespace audio { try { this.dataChannel.send(packet); } catch (error) { - console.warn(tr("Failed to send voice packet. Error: %o"), error); + log.warn(LogCategory.VOICE, tr("Failed to send voice packet. Error: %o"), error); } } else { - console.warn(tr("Could not transfer audio (not connected)")); + log.warn(LogCategory.VOICE, tr("Could not transfer audio (not connected)")); } } createSession() { if(!audio.player.initialized()) { - console.log(tr("Audio player isn't initialized yet. Waiting for gesture.")); + log.info(LogCategory.VOICE, tr("Audio player isn't initialized yet. Waiting for gesture.")); audio.player.on_ready(() => this.createSession()); return; } @@ -352,13 +352,13 @@ namespace audio { this.rtcPeerConnection.onicecandidate = this.on_local_ice_candidate.bind(this); if(this.local_audio_stream) { //May a typecheck? this.rtcPeerConnection.addStream(this.local_audio_stream.stream); - console.log(tr("Adding stream (%o)!"), this.local_audio_stream.stream); + log.info(LogCategory.VOICE, tr("Adding stream (%o)!"), this.local_audio_stream.stream); } this.rtcPeerConnection.createOffer(sdpConstraints).then(offer => { this.on_local_offer_created(offer); }).catch(error => { - console.error(tr("Could not create ice offer! error: %o"), error); + log.error(LogCategory.VOICE, tr("Could not create ice offer! error: %o"), error); }); } @@ -384,27 +384,27 @@ namespace audio { handleControlPacket(json) { if(json["request"] === "answer") { const session_description = new RTCSessionDescription(json["msg"]); - console.log(tr("Received answer to our offer. Answer: %o"), session_description); + log.info(LogCategory.VOICE, tr("Received answer to our offer. Answer: %o"), session_description); this.rtcPeerConnection.setRemoteDescription(session_description).then(() => { - console.log(tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length); + log.info(LogCategory.VOICE, tr("Answer applied successfully. Applying ICE candidates (%d)."), this._ice_cache.length); this._ice_use_cache = false; for(let msg of this._ice_cache) { this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg)).catch(error => { - console.log(tr("Failed to add remote cached ice candidate %s: %o"), msg, error); + log.info(LogCategory.VOICE, tr("Failed to add remote cached ice candidate %s: %o"), msg, error); }); } this._ice_cache = []; }).catch(error => { - console.log(tr("Failed to apply remote description: %o"), error); //FIXME error handling! + log.info(LogCategory.VOICE, tr("Failed to apply remote description: %o"), error); //FIXME error handling! }); } else if(json["request"] === "ice") { if(!this._ice_use_cache) { - console.log(tr("Add remote ice! (%o)"), json["msg"]); + log.info(LogCategory.VOICE, tr("Add remote ice! (%o)"), json["msg"]); this.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(json["msg"])).catch(error => { - console.log(tr("Failed to add remote ice candidate %s: %o"), json["msg"], error); + log.info(LogCategory.VOICE, tr("Failed to add remote ice candidate %s: %o"), json["msg"], error); }); } else { - console.log(tr("Cache remote ice! (%o)"), json["msg"]); + log.info(LogCategory.VOICE, tr("Cache remote ice! (%o)"), json["msg"]); this._ice_cache.push(json["msg"]); } } else if(json["request"] == "status") { @@ -429,7 +429,7 @@ namespace audio { //if(event.candidate && event.candidate.protocol !== "udp") // return; - console.log(tr("Gathered local ice candidate %o."), event.candidate); + log.info(LogCategory.VOICE, tr("Gathered local ice candidate %o."), event.candidate); if(event.candidate) { this.connection.sendData(JSON.stringify({ type: 'WebRTC', @@ -446,18 +446,18 @@ namespace audio { } private on_local_offer_created(localSession) { - console.log(tr("Local offer created. Setting up local description. (%o)"), localSession); + log.info(LogCategory.VOICE, tr("Local offer created. Setting up local description. (%o)"), localSession); this.rtcPeerConnection.setLocalDescription(localSession).then(() => { - console.log(tr("Offer applied successfully. Sending offer to server.")); + log.info(LogCategory.VOICE, tr("Offer applied successfully. Sending offer to server.")); this.connection.sendData(JSON.stringify({type: 'WebRTC', request: "create", msg: localSession})); }).catch(error => { - console.log(tr("Failed to apply local description: %o"), error); + log.info(LogCategory.VOICE, tr("Failed to apply local description: %o"), error); //FIXME error handling }); } private on_data_channel(channel) { - console.log(tr("Got new data channel! (%s)"), this.dataChannel.readyState); + log.info(LogCategory.VOICE, tr("Got new data channel! (%s)"), this.dataChannel.readyState); this.connection.client.update_voice_status(); } @@ -471,16 +471,16 @@ namespace audio { let clientId = bin[2] << 8 | bin[3]; let packetId = bin[0] << 8 | bin[1]; let codec = bin[4]; - //console.log("Client id " + clientId + " PacketID " + packetId + " Codec: " + codec); + //log.info(LogCategory.VOICE, "Client id " + clientId + " PacketID " + packetId + " Codec: " + codec); let client = this.find_client(clientId); if(!client) { - console.error(tr("Having voice from unknown audio client? (ClientID: %o)"), clientId); + log.error(LogCategory.VOICE, tr("Having voice from unknown audio client? (ClientID: %o)"), clientId); return; } let codec_pool = VoiceConnection.codec_pool[codec]; if(!codec_pool) { - console.error(tr("Could not playback codec %o"), codec); + log.error(LogCategory.VOICE, tr("Could not playback codec %o"), codec); return; } @@ -496,9 +496,9 @@ namespace audio { codec_pool.ownCodec(clientId, e => this.handleEncodedVoicePacket(e, codec), true) .then(decoder => decoder.decodeSamples(client.get_codec_cache(codec), encodedData)) .then(buffer => client.playback_buffer(buffer)).catch(error => { - console.error(tr("Could not playback client's (%o) audio (%o)"), clientId, error); + log.error(LogCategory.VOICE, tr("Could not playback client's (%o) audio (%o)"), clientId, error); if(error instanceof Error) - console.error(error.stack); + log.error(LogCategory.VOICE, error.stack); }); } } @@ -539,7 +539,7 @@ namespace audio { return false; if(chandler.client_status.input_muted) return false; - console.log(tr("Local voice ended")); + log.info(LogCategory.VOICE, tr("Local voice ended")); if(this.dataChannel) this.send_voice_packet(new Uint8Array(0), this.current_channel_codec()); @@ -547,14 +547,14 @@ namespace audio { private handleVoiceStarted() { const chandler = this.connection.client; - console.log(tr("Local voice started")); + log.info(LogCategory.VOICE, tr("Local voice started")); const ch = chandler.getClient(); if(ch) ch.speaking = true; } private on_recoder_yield() { - console.log("Lost recorder!"); + log.info(LogCategory.VOICE, "Lost recorder!"); this._audio_source = undefined; this.acquire_voice_recorder(undefined, true); /* we can ignore the promise because we should finish this directly */ }