A lot of changes
This commit is contained in:
parent
d9c0fa37f7
commit
040c218fb2
35 changed files with 1064 additions and 357 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -5,3 +5,6 @@
|
||||||
[submodule "vendor/bbcode"]
|
[submodule "vendor/bbcode"]
|
||||||
path = vendor/bbcode
|
path = vendor/bbcode
|
||||||
url = https://github.com/WolverinDEV/Extendible-BBCode-Parser.git
|
url = https://github.com/WolverinDEV/Extendible-BBCode-Parser.git
|
||||||
|
[submodule "vendor\\ua-parser-js"]
|
||||||
|
path = vendor\\ua-parser-js
|
||||||
|
url = https://github.com/WolverinDEV/ua-parser-js
|
||||||
|
|
20
ChangeLog.md
20
ChangeLog.md
|
@ -1,4 +1,24 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **XXX**
|
||||||
|
- Using VAD by default instead of PPT
|
||||||
|
- Improved mobile experience:
|
||||||
|
- Double touch join channel
|
||||||
|
- Removed the info bar for devices smaller than 500px
|
||||||
|
- Added country flags and names
|
||||||
|
- Added favicon, which change when you're recording
|
||||||
|
- Fixed double cache loading
|
||||||
|
- Fixed modal sizing scroll bug
|
||||||
|
- Added a channel subscribe all button
|
||||||
|
- Added individual channel subscribe settings
|
||||||
|
- Improved chat switch performance
|
||||||
|
- Added a chat message URL finder
|
||||||
|
- Escape URL detection with `!<url>`
|
||||||
|
- Improved chat experience
|
||||||
|
- Displaying offline chats as offline
|
||||||
|
- Notify when user closes the chat
|
||||||
|
- Notify when user disconnect/reconnects
|
||||||
|
- Preloading hostbanners to prevent flickering
|
||||||
|
|
||||||
* **17.02.19**
|
* **17.02.19**
|
||||||
- Removed WebAssembly as dependency (Now working with MS Edge as well (but without audio))
|
- Removed WebAssembly as dependency (Now working with MS Edge as well (but without audio))
|
||||||
- Improved channel tree performance
|
- Improved channel tree performance
|
||||||
|
|
|
@ -135,6 +135,10 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon_no_sound {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-clients {
|
.container-clients {
|
||||||
|
|
|
@ -47,13 +47,11 @@ $background:lightgray;
|
||||||
|
|
||||||
.button-dropdown {
|
.button-dropdown {
|
||||||
.buttons {
|
.buttons {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto auto;
|
flex-direction: row;
|
||||||
grid-template-rows: 100%;
|
|
||||||
grid-gap: 2px;
|
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin-right: 0px;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-dropdown {
|
.button-dropdown {
|
||||||
|
@ -83,6 +81,7 @@ $background:lightgray;
|
||||||
background-color: rgba(0,0,0,0.4);
|
background-color: rgba(0,0,0,0.4);
|
||||||
border-color: rgba(255, 255, 255, .75);
|
border-color: rgba(255, 255, 255, .75);
|
||||||
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||||
|
border-left: 2px solid rgba(255, 255, 255, .75);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +102,11 @@ $background:lightgray;
|
||||||
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
@ -131,8 +135,8 @@ $background:lightgray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover.displayed {
|
||||||
.dropdown.displayed {
|
.dropdown {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-banner {
|
.container-banner {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 2;
|
flex-shrink: 2;
|
||||||
max-height: 25%;
|
max-height: 25%;
|
||||||
|
@ -78,8 +80,28 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
img {
|
.image-container {
|
||||||
position: absolute;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
div {
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
&.hostbanner-mode-0 { }
|
||||||
|
|
||||||
|
&.hostbanner-mode-1 {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hostbanner-mode-2 {
|
||||||
|
background-size: contain!important;
|
||||||
|
width:100%;
|
||||||
|
height:100%
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,4 +131,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-browser-info {
|
||||||
|
vertical-align: bottom;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -228,9 +228,12 @@ footer .container {
|
||||||
}
|
}
|
||||||
|
|
||||||
$separator_thickness: 4px;
|
$separator_thickness: 4px;
|
||||||
$small_device: 500px;
|
$small_device: 650px;
|
||||||
|
$animation_length: .5s;
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
min-width: 350px;
|
||||||
|
|
||||||
.container-app-main {
|
.container-app-main {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -304,9 +307,33 @@ $small_device: 500px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.hide-small {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity $animation_length linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-small {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity $animation_length linear;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $small_device) {
|
@media only screen and (max-width: $small_device) {
|
||||||
|
.app-container {
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 25px;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
transition: all $animation_length linear;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
.container-app-main {
|
.container-app-main {
|
||||||
.container-info {
|
.container-info {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -330,6 +357,7 @@ $small_device: 500px;
|
||||||
|
|
||||||
.container-channel-chat + .container-seperator {
|
.container-channel-chat + .container-seperator {
|
||||||
display: none;
|
display: none;
|
||||||
|
animation: fadeout $animation_length linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-channel-chat {
|
.container-channel-chat {
|
||||||
|
@ -338,6 +366,19 @@ $small_device: 500px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-small {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity $animation_length linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-small {
|
||||||
|
display: block!important;
|
||||||
|
|
||||||
|
opacity: 1!important;
|
||||||
|
transition: opacity $animation_length linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
.container-seperator {
|
.container-seperator {
|
||||||
background: lightgray;
|
background: lightgray;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -399,15 +440,26 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-playlist-manage {
|
.icon-playlist-manage {
|
||||||
display: inline-block;
|
&.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
background-position: -5px -5px;
|
||||||
|
background-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon_x32 {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
background: url('../../img/music/playlist.svg') no-repeat;
|
|
||||||
background-position: -11px -9px;
|
background-position: -11px -9px;
|
||||||
background-size: 50px;
|
background-size: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
background: url('../../img/music/playlist.svg') no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
x-content {
|
x-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
|
|
@ -35,6 +35,20 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-message { /* special formated messages */
|
||||||
|
&.event-partner-disconnect {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.event-partner-connect {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.event-partner-closed {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,11 +76,9 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: #11111111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn_close {
|
.btn_close {
|
||||||
|
display: none;
|
||||||
|
|
||||||
float: none;
|
float: none;
|
||||||
margin-right: -5px;
|
margin-right: -5px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@ -78,9 +90,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name, .chatIcon {
|
.name, .chat-type {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closeable {
|
||||||
|
.btn_close {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #11111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline {
|
||||||
|
.name {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.unread {
|
||||||
|
.name {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1031,7 +1031,7 @@
|
||||||
}
|
}
|
||||||
.icon_x32.client-refresh {
|
.icon_x32.client-refresh {
|
||||||
background-position: calc(-224px * 2) calc(-256px * 2);
|
background-position: calc(-224px * 2) calc(-256px * 2);
|
||||||
}pe the key you wish
|
}
|
||||||
.icon_x32.client-register {
|
.icon_x32.client-register {
|
||||||
background-position: calc(-256px * 2) calc(-256px * 2);
|
background-position: calc(-256px * 2) calc(-256px * 2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
x-tab { display:none }
|
x-tab { display:none }
|
||||||
x-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
@ -18,15 +15,19 @@ x-content {
|
||||||
.tab .tab-content {
|
.tab .tab-content {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
|
||||||
border-color: #6f6f6f;
|
border-radius: 0 2px 2px 2px;
|
||||||
border-radius: 0px 2px 2px 2px;
|
border: solid #6f6f6f;
|
||||||
border-style: solid;
|
overflow-y: hidden;
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
x-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -39,7 +40,7 @@ x-content {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.tab .tab-header {
|
.tab .tab-header {
|
||||||
font-family: Arial;
|
font-family: Arial, serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
/*white-space: pre;*/
|
/*white-space: pre;*/
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
@ -64,14 +65,10 @@ x-content {
|
||||||
.tab .tab-header .entry {
|
.tab .tab-header .entry {
|
||||||
background: #5f5f5f5f;
|
background: #5f5f5f5f;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: #6f6f6f;
|
border: 1px solid #6f6f6f;
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 2px 2px 0px 0px;
|
border-radius: 2px 2px 0px 0px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 2px;
|
padding: 2px 5px;
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,9 +190,9 @@
|
||||||
|
|
||||||
<footer style="<?php echo $footer_style; ?>">
|
<footer style="<?php echo $footer_style; ?>">
|
||||||
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;">
|
<div class="container" style="display: flex; flex-direction: row; align-content: space-between;">
|
||||||
<div style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
|
<div class="hide-small" style="align-self: center; position: fixed; left: 5px;">Open source on <a href="https://github.com/TeaSpeak/TeaSpeak-Web" style="display: inline-block; position: relative">github.com</a></div>
|
||||||
<div style="align-self: center;">TeaSpeak Web (<?php echo $version; ?>) by WolverinDEV</div>
|
<div style="align-self: center;">TeaSpeak Web (<?php echo $version; ?>) by WolverinDEV</div>
|
||||||
<div style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
|
<div class="hide-small" style="align-self: center; position: fixed; right: 5px;"><?php echo $footer_forum; ?></div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</html>
|
</html>
|
|
@ -6,7 +6,6 @@
|
||||||
<title>TeaSpeak-Web client templates</title>
|
<title>TeaSpeak-Web client templates</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- main frame TODO tr -->
|
|
||||||
<script class="jsrender-template" id="tmpl_main" type="text/html">
|
<script class="jsrender-template" id="tmpl_main" type="text/html">
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<div class="app">
|
<div class="app">
|
||||||
|
@ -38,7 +37,7 @@
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
|
<div class="hide-small button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="button icon_x32 client-away btn_away_toggle"></div>
|
<div class="button icon_x32 client-away btn_away_toggle"></div>
|
||||||
<div class="button-dropdown">
|
<div class="button-dropdown">
|
||||||
|
@ -50,15 +49,36 @@
|
||||||
<div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></div>
|
<div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_mute_input">
|
<div class="hide-small button btn_mute_input">
|
||||||
<div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
|
<div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_mute_output">
|
<div class="hide-small button btn_mute_output">
|
||||||
<div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div>
|
<div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="button-dropdown btn_token" title="{{tr 'Use token' /}}">
|
<div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button button-display icon_x32 client-music"></div>
|
||||||
|
<div class="button-dropdown">
|
||||||
|
<div class="arrow down"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<div class="btn_mute_input" title="{{tr 'Mute/unmute microphone' /}}">
|
||||||
|
<div class="icon client-input_muted"></div>
|
||||||
|
<a>{{tr "Mute/unmute microphone" /}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="btn_mute_output" title="{{tr 'Mute/unmute headphones' /}}">
|
||||||
|
<div class="icon client-output_muted"></div>
|
||||||
|
<a>{{tr "Mute/unmute headphones" /}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="button button-subscribe-mode">
|
||||||
|
<div class="icon_x32" title="{{tr 'Toggle channel subscribe mode' /}}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hide-small button-dropdown btn_token" title="{{tr 'Use token' /}}">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="button icon_x32 client-token btn_token_use"></div>
|
<div class="button icon_x32 client-token btn_token_use"></div>
|
||||||
<div class="button-dropdown">
|
<div class="button-dropdown">
|
||||||
|
@ -72,13 +92,37 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="width: 100%"></div>
|
<div style="width: 100%"></div>
|
||||||
<div class="button button-playlist-manage" title="{{tr 'Playlists' /}}">
|
|
||||||
<div class="icon-playlist-manage"></div>
|
<div class="show-small button-dropdown dropdown-servertools" title="{{tr 'Server tools' /}}">
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button button-display icon_x32 client-virtualserver_edit"></div>
|
||||||
|
<div class="button-dropdown">
|
||||||
|
<div class="arrow down"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_banlist" title="{{tr 'Banlist' /}}">
|
</div>
|
||||||
|
<div class="dropdown right">
|
||||||
|
<div class="button-playlist-manage" title="{{tr 'Playlists' /}}">
|
||||||
|
<div class="icon icon-playlist-manage"></div>
|
||||||
|
<a>{{tr "Playlists" /}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="btn_banlist" title="{{tr 'Banlist' /}}">
|
||||||
|
<div class="icon client-ban_list"></div>
|
||||||
|
<a>{{tr "Banlist" /}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
||||||
|
<div class="icon client-permission_overview"></div>
|
||||||
|
<a>{{tr "View/edit permissions" /}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hide-small button button-playlist-manage" title="{{tr 'Playlists' /}}">
|
||||||
|
<div class="icon_x32 icon-playlist-manage"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hide-small button btn_banlist" title="{{tr 'Banlist' /}}">
|
||||||
<div class="icon_x32 client-ban_list"></div>
|
<div class="icon_x32 client-ban_list"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
<div class="hide-small button btn_permissions" title="{{tr 'View/edit permissions' /}}">
|
||||||
<div class="icon_x32 client-permission_overview"></div>
|
<div class="icon_x32 client-permission_overview"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2078,7 +2122,14 @@
|
||||||
{{if !client_is_query}}
|
{{if !client_is_query}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{tr "Version:"/}}</td>
|
<td>{{tr "Version:"/}}</td>
|
||||||
<td><a title="{{>property_client_version}}">{{*: data.property_client_version.split(" ")[0]; }}</a> on {{>property_client_platform}}</td>
|
<td>
|
||||||
|
<a title="{{>property_client_version}}">{{*: data.property_client_version.split(" ")[0]; }}</a>
|
||||||
|
{{if client_is_web && false}} <!-- we cant show any browser info because every browser claims to be any browser as well -->
|
||||||
|
<div class="icon client-message_info button-browser-info" title="{{tr 'Browser info' /}}"></div>
|
||||||
|
{{/if}}
|
||||||
|
on
|
||||||
|
<a>{{>property_client_platform}}</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -2252,26 +2303,11 @@
|
||||||
</script>
|
</script>
|
||||||
<script class="jsrender-template" id="tmpl_selected_hostbanner" type="text/html">
|
<script class="jsrender-template" id="tmpl_selected_hostbanner" type="text/html">
|
||||||
<div class="hostbanner">
|
<div class="hostbanner">
|
||||||
<a href="{{:property_virtualserver_hostbanner_url}}" target="_blank" style="display: flex; flex-direction: row; justify-content: center; height: 100%">
|
<a class="image-container" href="{{:property_virtualserver_hostbanner_url}}" target="_blank">
|
||||||
|
<div
|
||||||
<div style="
|
style="background: center no-repeat url({{:hostbanner_gfx_url}})"
|
||||||
background:center no-repeat url(
|
alt="{{tr 'Host banner'/}}"
|
||||||
{{:property_virtualserver_hostbanner_gfx_url}}{{:cache_tag}}
|
class="hostbanner-image hostbanner-mode-{{:property_virtualserver_hostbanner_mode}}"
|
||||||
);
|
|
||||||
|
|
||||||
background-position: center;
|
|
||||||
{{if property_virtualserver_hostbanner_mode == 0}}
|
|
||||||
|
|
||||||
{{else property_virtualserver_hostbanner_mode == 1}}
|
|
||||||
width: 100%; height: auto;
|
|
||||||
{{else property_virtualserver_hostbanner_mode == 2}}
|
|
||||||
background-size:contain;
|
|
||||||
width:100%;
|
|
||||||
height:100%
|
|
||||||
{{/if}}
|
|
||||||
"
|
|
||||||
|
|
||||||
alt="{{tr "Host banner"/}}"
|
|
||||||
></div>
|
></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
/// <reference path="client.ts" />
|
/// <reference path="client.ts" />
|
||||||
/// <reference path="connection/ConnectionBase.ts" />
|
/// <reference path="connection/ConnectionBase.ts" />
|
||||||
|
|
||||||
|
/*
|
||||||
|
FIXME: Dont use item storage with base64! Use the larger cache API and drop IE support!
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage#Browser_compatibility
|
||||||
|
*/
|
||||||
class FileEntry {
|
class FileEntry {
|
||||||
name: string;
|
name: string;
|
||||||
datetime: number;
|
datetime: number;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import LogType = log.LogType;
|
||||||
|
|
||||||
enum ChatType {
|
enum ChatType {
|
||||||
GENERAL,
|
GENERAL,
|
||||||
SERVER,
|
SERVER,
|
||||||
|
@ -65,12 +67,11 @@ namespace MessageHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(objects.length < number)
|
if(objects.length < number)
|
||||||
console.warn(tr("Message to format contains invalid index (%o)"), number);
|
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
|
||||||
|
|
||||||
result.push(...formatElement(objects[number]));
|
result.push(...formatElement(objects[number]));
|
||||||
found = found + 1 + offset;
|
found = found + 1 + offset;
|
||||||
begin = found + 1;
|
begin = found + 1;
|
||||||
console.log(tr("Offset: %d Number: %d"), offset, number);
|
|
||||||
} while(found++);
|
} while(found++);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -93,7 +94,7 @@ namespace MessageHelper {
|
||||||
});
|
});
|
||||||
|
|
||||||
if(result.error) {
|
if(result.error) {
|
||||||
console.log("BBCode parse error: %o", result.errorQueue);
|
log.error(LogCategory.GENERAL, tr("BBCode parse error: %o"), result.errorQueue);
|
||||||
return formatElement(message);
|
return formatElement(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +105,7 @@ namespace MessageHelper {
|
||||||
class ChatMessage {
|
class ChatMessage {
|
||||||
date: Date;
|
date: Date;
|
||||||
message: JQuery[];
|
message: JQuery[];
|
||||||
private _htmlTag: JQuery<HTMLElement>;
|
private _html_tag: JQuery<HTMLElement>;
|
||||||
|
|
||||||
constructor(message: JQuery[]) {
|
constructor(message: JQuery[]) {
|
||||||
this.date = new Date();
|
this.date = new Date();
|
||||||
|
@ -117,8 +118,8 @@ class ChatMessage {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
get htmlTag() {
|
get html_tag() {
|
||||||
if(this._htmlTag) return this._htmlTag;
|
if(this._html_tag) return this._html_tag;
|
||||||
|
|
||||||
let tag = $.spawn("div");
|
let tag = $.spawn("div");
|
||||||
tag.addClass("message");
|
tag.addClass("message");
|
||||||
|
@ -128,26 +129,30 @@ class ChatMessage {
|
||||||
dateTag.css("margin-right", "4px");
|
dateTag.css("margin-right", "4px");
|
||||||
dateTag.css("color", "dodgerblue");
|
dateTag.css("color", "dodgerblue");
|
||||||
|
|
||||||
this._htmlTag = tag;
|
this._html_tag = tag;
|
||||||
tag.append(dateTag);
|
tag.append(dateTag);
|
||||||
this.message.forEach(e => e.appendTo(tag));
|
this.message.forEach(e => e.appendTo(tag));
|
||||||
tag.hide();
|
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatEntry {
|
class ChatEntry {
|
||||||
handle: ChatBox;
|
readonly handle: ChatBox;
|
||||||
type: ChatType;
|
type: ChatType;
|
||||||
key: string;
|
key: string;
|
||||||
history: ChatMessage[];
|
history: ChatMessage[];
|
||||||
|
|
||||||
|
owner_unique_id?: string;
|
||||||
|
|
||||||
private _name: string;
|
private _name: string;
|
||||||
private _htmlTag: any;
|
private _html_tag: any;
|
||||||
private _closeable: boolean;
|
|
||||||
private _unread : boolean;
|
private _flag_closeable: boolean = true;
|
||||||
|
private _flag_unread : boolean = false;
|
||||||
|
private _flag_offline: boolean = false;
|
||||||
|
|
||||||
onMessageSend: (text: string) => void;
|
onMessageSend: (text: string) => void;
|
||||||
onClose: () => boolean;
|
onClose: () => boolean = () => true;
|
||||||
|
|
||||||
constructor(handle, type : ChatType, key) {
|
constructor(handle, type : ChatType, key) {
|
||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
|
@ -155,8 +160,6 @@ class ChatEntry {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this._name = key;
|
this._name = key;
|
||||||
this.history = [];
|
this.history = [];
|
||||||
|
|
||||||
this.onClose = function () { return true; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appendError(message: string, ...args) {
|
appendError(message: string, ...args) {
|
||||||
|
@ -173,7 +176,7 @@ class ChatEntry {
|
||||||
this.history.push(entry);
|
this.history.push(entry);
|
||||||
while(this.history.length > 100) {
|
while(this.history.length > 100) {
|
||||||
let elm = this.history.pop_front();
|
let elm = this.history.pop_front();
|
||||||
elm.htmlTag.animate({opacity: 0}, 200, function () {
|
elm.html_tag.animate({opacity: 0}, 200, function () {
|
||||||
$(this).detach();
|
$(this).detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -181,66 +184,75 @@ class ChatEntry {
|
||||||
let box = $(this.handle.htmlTag).find(".messages");
|
let box = $(this.handle.htmlTag).find(".messages");
|
||||||
let mbox = $(this.handle.htmlTag).find(".message_box");
|
let mbox = $(this.handle.htmlTag).find(".message_box");
|
||||||
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
|
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
|
||||||
mbox.append(entry.htmlTag);
|
mbox.append(entry.html_tag);
|
||||||
entry.htmlTag.show().css("opacity", "0").animate({opacity: 1}, 100);
|
entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100);
|
||||||
if(bottom) box.scrollTop(mbox.height());
|
if(bottom) box.scrollTop(mbox.height());
|
||||||
} else {
|
} else {
|
||||||
this.unread = true;
|
this.flag_unread = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
displayHistory() {
|
displayHistory() {
|
||||||
this.unread = false;
|
this.flag_unread = false;
|
||||||
let box = $(this.handle.htmlTag).find(".messages");
|
let box = this.handle.htmlTag.find(".messages");
|
||||||
let mbox = $(this.handle.htmlTag).find(".message_box");
|
let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */
|
||||||
mbox.empty();
|
mbox.empty();
|
||||||
|
|
||||||
for(let e of this.history) {
|
for(let e of this.history) {
|
||||||
mbox.append(e.htmlTag);
|
mbox.append(e.html_tag);
|
||||||
if(e.htmlTag.is(":hidden")) e.htmlTag.show();
|
/* TODO Is this really totally useless?
|
||||||
|
Because its at least a performance bottleneck because is(...) recalculates the page style
|
||||||
|
if(e.htmlTag.is(":hidden"))
|
||||||
|
e.htmlTag.show();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mbox.appendTo(box);
|
||||||
box.scrollTop(mbox.height());
|
box.scrollTop(mbox.height());
|
||||||
}
|
}
|
||||||
|
|
||||||
get htmlTag() {
|
get html_tag() {
|
||||||
if(this._htmlTag) return this._htmlTag;
|
if(this._html_tag)
|
||||||
|
return this._html_tag;
|
||||||
|
|
||||||
let tag = $.spawn("div");
|
let tag = $.spawn("div");
|
||||||
tag.addClass("chat");
|
tag.addClass("chat");
|
||||||
|
if(this._flag_unread)
|
||||||
|
tag.addClass('unread');
|
||||||
|
if(this._flag_offline)
|
||||||
|
tag.addClass('offline');
|
||||||
|
if(this._flag_closeable)
|
||||||
|
tag.addClass('closeable');
|
||||||
|
|
||||||
tag.append("<div class=\"chatIcon icon " + this.chatIcon() + "\"></div>");
|
tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon()));
|
||||||
tag.append("<a class='name'>" + this._name + "</a>");
|
tag.append($.spawn("a").addClass("name").text(this._name));
|
||||||
|
|
||||||
let closeTag = $.spawn("div");
|
let tag_close = $.spawn("div");
|
||||||
closeTag.addClass("btn_close icon client-tab_close_button");
|
tag_close.addClass("btn_close icon client-tab_close_button");
|
||||||
if(!this._closeable) closeTag.hide();
|
if(!this._flag_closeable) tag_close.hide();
|
||||||
tag.append(closeTag);
|
tag.append(tag_close);
|
||||||
|
|
||||||
const _this = this;
|
tag.click(() => { this.handle.activeChat = this; });
|
||||||
tag.click(function () {
|
tag.on("contextmenu", (e) => {
|
||||||
_this.handle.activeChat = _this;
|
|
||||||
});
|
|
||||||
tag.on("contextmenu", function (e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let actions = [];
|
let actions: ContextMenuEntry[] = [];
|
||||||
actions.push({
|
actions.push({
|
||||||
type: MenuEntryType.ENTRY,
|
type: MenuEntryType.ENTRY,
|
||||||
icon: "",
|
icon: "",
|
||||||
name: tr("Clear"),
|
name: tr("Clear"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
_this.history = [];
|
this.history = [];
|
||||||
_this.displayHistory();
|
this.displayHistory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(_this.closeable) {
|
if(this.flag_closeable) {
|
||||||
actions.push({
|
actions.push({
|
||||||
type: MenuEntryType.ENTRY,
|
type: MenuEntryType.ENTRY,
|
||||||
icon: "client-tab_close_button",
|
icon: "client-tab_close_button",
|
||||||
name: tr("Close"),
|
name: tr("Close"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
chat.deleteChat(_this);
|
chat.deleteChat(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -251,18 +263,20 @@ class ChatEntry {
|
||||||
name: tr("Close all private tabs"),
|
name: tr("Close all private tabs"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
//TODO Implement this?
|
//TODO Implement this?
|
||||||
}
|
},
|
||||||
|
visible: false
|
||||||
});
|
});
|
||||||
spawn_context_menu(e.pageX, e.pageY, ...actions);
|
spawn_context_menu(e.pageX, e.pageY, ...actions);
|
||||||
});
|
});
|
||||||
|
|
||||||
closeTag.click(function () {
|
tag_close.click(() => {
|
||||||
if($.isFunction(_this.onClose) && !_this.onClose()) return;
|
if($.isFunction(this.onClose) && !this.onClose())
|
||||||
_this.handle.deleteChat(_this);
|
return;
|
||||||
|
|
||||||
|
this.handle.deleteChat(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._htmlTag = tag;
|
return this._html_tag = tag;
|
||||||
return tag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
|
@ -271,33 +285,37 @@ class ChatEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
set name(newName : string) {
|
set name(newName : string) {
|
||||||
console.log(tr("Change name!"));
|
|
||||||
this._name = newName;
|
this._name = newName;
|
||||||
this.htmlTag.find(".name").text(this._name);
|
this.html_tag.find(".name").text(this._name);
|
||||||
}
|
}
|
||||||
|
|
||||||
set closeable(flag : boolean) {
|
set flag_closeable(flag : boolean) {
|
||||||
if(this._closeable == flag) return;
|
if(this._flag_closeable == flag) return;
|
||||||
|
|
||||||
this._closeable = flag;
|
this._flag_closeable = flag;
|
||||||
console.log(tr("Set closeable: ") + this._closeable);
|
|
||||||
if(flag) this.htmlTag.find(".btn_close").show();
|
this.html_tag.toggleClass('closeable', flag);
|
||||||
else this.htmlTag.find(".btn_close").hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set unread(flag : boolean) {
|
set flag_unread(flag : boolean) {
|
||||||
if(this._unread == flag) return;
|
if(this._flag_unread == flag) return;
|
||||||
this._unread = flag;
|
this._flag_unread = flag;
|
||||||
this.htmlTag.find(".chatIcon").attr("class", "chatIcon icon " + this.chatIcon());
|
this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon());
|
||||||
if(flag) {
|
this.html_tag.toggleClass('unread', flag);
|
||||||
this.htmlTag.find(".name").css("color", "blue");
|
|
||||||
} else {
|
|
||||||
this.htmlTag.find(".name").css("color", "black");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private chatIcon() : string {
|
get flag_offline() { return this._flag_offline; }
|
||||||
if(this._unread) {
|
|
||||||
|
set flag_offline(flag: boolean) {
|
||||||
|
if(flag == this._flag_offline)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._flag_offline = flag;
|
||||||
|
this.html_tag.toggleClass('offline', flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
private chat_icon() : string {
|
||||||
|
if(this._flag_unread) {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case ChatType.CLIENT:
|
case ChatType.CLIENT:
|
||||||
return "client-new_chat";
|
return "client-new_chat";
|
||||||
|
@ -319,6 +337,10 @@ class ChatEntry {
|
||||||
|
|
||||||
|
|
||||||
class ChatBox {
|
class ChatBox {
|
||||||
|
//https://regex101.com/r/YQbfcX/2
|
||||||
|
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
||||||
|
static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
||||||
|
|
||||||
htmlTag: JQuery;
|
htmlTag: JQuery;
|
||||||
chats: ChatEntry[];
|
chats: ChatEntry[];
|
||||||
private _activeChat: ChatEntry;
|
private _activeChat: ChatEntry;
|
||||||
|
@ -359,10 +381,11 @@ class ChatBox {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
chat.serverChat().appendMessage(tr("Failed to send text message."));
|
chat.serverChat().appendMessage(tr("Failed to send text message."));
|
||||||
console.error(tr("Failed to send server text message: %o"), error);
|
log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this.serverChat().name = tr("Server chat");
|
this.serverChat().name = tr("Server chat");
|
||||||
|
this.serverChat().flag_closeable = false;
|
||||||
|
|
||||||
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
|
||||||
if(!globalClient.serverConnection) {
|
if(!globalClient.serverConnection) {
|
||||||
|
@ -372,10 +395,11 @@ class ChatBox {
|
||||||
|
|
||||||
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => {
|
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => {
|
||||||
chat.channelChat().appendMessage(tr("Failed to send text message."));
|
chat.channelChat().appendMessage(tr("Failed to send text message."));
|
||||||
console.error(tr("Failed to send channel text message: %o"), error);
|
log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this.channelChat().name = tr("Channel chat");
|
this.channelChat().name = tr("Channel chat");
|
||||||
|
this.channelChat().flag_closeable = false;
|
||||||
|
|
||||||
globalClient.permissions.initializedListener.push(flag => {
|
globalClient.permissions.initializedListener.push(flag => {
|
||||||
if(flag) this.activeChat0(this._activeChat);
|
if(flag) this.activeChat0(this._activeChat);
|
||||||
|
@ -385,11 +409,15 @@ class ChatBox {
|
||||||
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
|
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
|
||||||
let chat = new ChatEntry(this, type, key);
|
let chat = new ChatEntry(this, type, key);
|
||||||
this.chats.push(chat);
|
this.chats.push(chat);
|
||||||
this.htmlTag.find(".chats").append(chat.htmlTag);
|
this.htmlTag.find(".chats").append(chat.html_tag);
|
||||||
if(!this._activeChat) this.activeChat = chat;
|
if(!this._activeChat) this.activeChat = chat;
|
||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open_chats() : ChatEntry[] {
|
||||||
|
return this.chats;
|
||||||
|
}
|
||||||
|
|
||||||
findChat(key : string) : ChatEntry {
|
findChat(key : string) : ChatEntry {
|
||||||
for(let e of this.chats)
|
for(let e of this.chats)
|
||||||
if(e.key == key) return e;
|
if(e.key == key) return e;
|
||||||
|
@ -398,7 +426,7 @@ class ChatBox {
|
||||||
|
|
||||||
deleteChat(chat : ChatEntry) {
|
deleteChat(chat : ChatEntry) {
|
||||||
this.chats.remove(chat);
|
this.chats.remove(chat);
|
||||||
chat.htmlTag.detach();
|
chat.html_tag.detach();
|
||||||
if(this._activeChat === chat) {
|
if(this._activeChat === chat) {
|
||||||
if(this.chats.length > 0)
|
if(this.chats.length > 0)
|
||||||
this.activeChat = this.chats.last();
|
this.activeChat = this.chats.last();
|
||||||
|
@ -414,8 +442,38 @@ class ChatBox {
|
||||||
this._input_message.val("");
|
this._input_message.val("");
|
||||||
this._input_message.trigger("input");
|
this._input_message.trigger("input");
|
||||||
|
|
||||||
|
/* preprocessing text */
|
||||||
|
const words = text.split(/[ \n]/);
|
||||||
|
for(let index = 0; index < words.length; index++) {
|
||||||
|
const flag_escaped = words[index].startsWith('!');
|
||||||
|
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
||||||
|
|
||||||
|
_try:
|
||||||
|
try {
|
||||||
|
const url = new URL(unescaped);
|
||||||
|
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
||||||
|
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||||
|
break _try;
|
||||||
|
if(flag_escaped)
|
||||||
|
words[index] = unescaped;
|
||||||
|
else {
|
||||||
|
text = undefined;
|
||||||
|
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
||||||
|
}
|
||||||
|
} catch(e) { /* word isn't an url */ }
|
||||||
|
|
||||||
|
if(unescaped.match(ChatBox.URL_REGEX)) {
|
||||||
|
if(flag_escaped)
|
||||||
|
words[index] = unescaped;
|
||||||
|
else {
|
||||||
|
text = undefined;
|
||||||
|
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
|
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
|
||||||
this._activeChat.onMessageSend(text);
|
this._activeChat.onMessageSend(text || words.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
set activeChat(chat : ChatEntry) {
|
set activeChat(chat : ChatEntry) {
|
||||||
|
@ -427,27 +485,27 @@ class ChatBox {
|
||||||
private activeChat0(chat: ChatEntry) {
|
private activeChat0(chat: ChatEntry) {
|
||||||
this._activeChat = chat;
|
this._activeChat = chat;
|
||||||
for(let e of this.chats)
|
for(let e of this.chats)
|
||||||
e.htmlTag.removeClass("active");
|
e.html_tag.removeClass("active");
|
||||||
|
|
||||||
let flagAllowSend = false;
|
let disable_input = !chat;
|
||||||
if(this._activeChat) {
|
if(this._activeChat) {
|
||||||
this._activeChat.htmlTag.addClass("active");
|
this._activeChat.html_tag.addClass("active");
|
||||||
this._activeChat.displayHistory();
|
this._activeChat.displayHistory();
|
||||||
|
|
||||||
if(globalClient && globalClient.permissions && globalClient.permissions.initialized())
|
if(!disable_input && globalClient && globalClient.permissions && globalClient.permissions.initialized())
|
||||||
switch (this._activeChat.type) {
|
switch (this._activeChat.type) {
|
||||||
case ChatType.CLIENT:
|
case ChatType.CLIENT:
|
||||||
flagAllowSend = true;
|
disable_input = false;
|
||||||
break;
|
break;
|
||||||
case ChatType.SERVER:
|
case ChatType.SERVER:
|
||||||
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
|
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
|
||||||
break;
|
break;
|
||||||
case ChatType.CHANNEL:
|
case ChatType.CHANNEL:
|
||||||
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
|
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._input_message.prop("disabled", !flagAllowSend);
|
this._input_message.prop("disabled", disable_input);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeChat(){ return this._activeChat; }
|
get activeChat(){ return this._activeChat; }
|
||||||
|
|
|
@ -116,7 +116,7 @@ class TSClient {
|
||||||
|
|
||||||
|
|
||||||
getClient() : LocalClientEntry { return this._ownEntry; }
|
getClient() : LocalClientEntry { return this._ownEntry; }
|
||||||
getClientId() { return this._clientId; } //TODO here
|
getClientId() { return this._clientId; }
|
||||||
|
|
||||||
set clientId(id: number) {
|
set clientId(id: number) {
|
||||||
this._clientId = id;
|
this._clientId = id;
|
||||||
|
@ -138,11 +138,13 @@ class TSClient {
|
||||||
this.channelTree.registerClient(this._ownEntry);
|
this.channelTree.registerClient(this._ownEntry);
|
||||||
settings.setServer(this.channelTree.server);
|
settings.setServer(this.channelTree.server);
|
||||||
this.permissions.requestPermissionList();
|
this.permissions.requestPermissionList();
|
||||||
this.serverConnection.send_command("channelsubscribeall");
|
|
||||||
if(this.groups.serverGroups.length == 0)
|
if(this.groups.serverGroups.length == 0)
|
||||||
this.groups.requestGroups();
|
this.groups.requestGroups();
|
||||||
this.controlBar.updateProperties();
|
this.controlBar.updateProperties();
|
||||||
|
if(this.controlBar.channel_subscribe_all)
|
||||||
|
this.channelTree.subscribe_all_channels();
|
||||||
|
else
|
||||||
|
this.channelTree.unsubscribe_all_channels();
|
||||||
if(this.voiceConnection && !this.voiceConnection.current_encoding_supported())
|
if(this.voiceConnection && !this.voiceConnection.current_encoding_supported())
|
||||||
createErrorModal(tr("Codec encode type not supported!"), tr("Codec encode type " + VoiceConnectionType[this.voiceConnection.type] + " not supported by this browser!<br>Choose another one!")).open(); //TODO tr
|
createErrorModal(tr("Codec encode type not supported!"), tr("Codec encode type " + VoiceConnectionType[this.voiceConnection.type] + " not supported by this browser!<br>Choose another one!")).open(); //TODO tr
|
||||||
}
|
}
|
||||||
|
@ -296,7 +298,7 @@ class TSClient {
|
||||||
this._reconnect_timer = setTimeout(() => {
|
this._reconnect_timer = setTimeout(() => {
|
||||||
this._reconnect_timer = undefined;
|
this._reconnect_timer = undefined;
|
||||||
chat.serverChat().appendMessage(tr("Reconnecting..."));
|
chat.serverChat().appendMessage(tr("Reconnecting..."));
|
||||||
console.log(tr("Reconnecting..."));
|
log.info(LogCategory.NETWORKING, tr("Reconnecting..."))
|
||||||
this.startConnection(server_address.host + ":" + server_address.port, profile, name, password ? { password: password, hashed: true} : undefined);
|
this.startConnection(server_address.host + ":" + server_address.port, profile, name, password ? { password: password, hashed: true} : undefined);
|
||||||
this._reconnect_attempt = true;
|
this._reconnect_attempt = true;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
namespace connection {
|
namespace connection {
|
||||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||||
constructor(connection: AbstractServerConnection) {
|
constructor(connection: AbstractServerConnection) {
|
||||||
|
@ -26,6 +27,7 @@ namespace connection {
|
||||||
this["notifychannelmoved"] = this.handleNotifyChannelMoved;
|
this["notifychannelmoved"] = this.handleNotifyChannelMoved;
|
||||||
this["notifychanneledited"] = this.handleNotifyChannelEdited;
|
this["notifychanneledited"] = this.handleNotifyChannelEdited;
|
||||||
this["notifytextmessage"] = this.handleNotifyTextMessage;
|
this["notifytextmessage"] = this.handleNotifyTextMessage;
|
||||||
|
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
|
||||||
this["notifyclientupdated"] = this.handleNotifyClientUpdated;
|
this["notifyclientupdated"] = this.handleNotifyClientUpdated;
|
||||||
this["notifyserveredited"] = this.handleNotifyServerEdited;
|
this["notifyserveredited"] = this.handleNotifyServerEdited;
|
||||||
this["notifyserverupdated"] = this.handleNotifyServerUpdated;
|
this["notifyserverupdated"] = this.handleNotifyServerUpdated;
|
||||||
|
@ -37,6 +39,9 @@ namespace connection {
|
||||||
this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd;
|
this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd;
|
||||||
this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove;
|
this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove;
|
||||||
this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged;
|
this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged;
|
||||||
|
|
||||||
|
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
|
||||||
|
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_command(command: ServerCommand) : boolean {
|
handle_command(command: ServerCommand) : boolean {
|
||||||
|
@ -289,6 +294,26 @@ namespace connection {
|
||||||
|
|
||||||
client.updateVariables(...updates);
|
client.updateVariables(...updates);
|
||||||
|
|
||||||
|
{
|
||||||
|
let client_chat = client.chat(false);
|
||||||
|
if(!client_chat) {
|
||||||
|
for(const c of chat.open_chats()) {
|
||||||
|
if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) {
|
||||||
|
client_chat = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(client_chat) {
|
||||||
|
client_chat.appendMessage(
|
||||||
|
"{0}", true,
|
||||||
|
$.spawn("div")
|
||||||
|
.addClass("event-message event-partner-connect")
|
||||||
|
.text(tr("Your chat partner has reconnected"))
|
||||||
|
);
|
||||||
|
client_chat.flag_offline = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if(client instanceof LocalClientEntry)
|
if(client instanceof LocalClientEntry)
|
||||||
this.connection.client.controlBar.updateVoice();
|
this.connection.client.controlBar.updateVoice();
|
||||||
}
|
}
|
||||||
|
@ -368,6 +393,19 @@ namespace connection {
|
||||||
} else {
|
} else {
|
||||||
console.error(tr("Unknown client left reason!"));
|
console.error(tr("Unknown client left reason!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const chat = client.chat(false);
|
||||||
|
if(chat) {
|
||||||
|
chat.flag_offline = true;
|
||||||
|
chat.appendMessage(
|
||||||
|
"{0}", true,
|
||||||
|
$.spawn("div")
|
||||||
|
.addClass("event-message event-partner-disconnect")
|
||||||
|
.text(tr("Your chat partner has disconnected"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.deleteClient(client);
|
tree.deleteClient(client);
|
||||||
|
@ -503,7 +541,6 @@ namespace connection {
|
||||||
handleNotifyTextMessage(json) {
|
handleNotifyTextMessage(json) {
|
||||||
json = json[0]; //Only one bulk
|
json = json[0]; //Only one bulk
|
||||||
|
|
||||||
//TODO chat format?
|
|
||||||
let mode = json["targetmode"];
|
let mode = json["targetmode"];
|
||||||
if(mode == 1){
|
if(mode == 1){
|
||||||
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]);
|
let invoker = this.connection.client.channelTree.findClient(json["invokerid"]);
|
||||||
|
@ -534,6 +571,38 @@ namespace connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNotifyClientChatClosed(json) {
|
||||||
|
json = json[0]; //Only one bulk
|
||||||
|
|
||||||
|
//Chat partner has closed the conversation
|
||||||
|
|
||||||
|
//clid: "6"
|
||||||
|
//cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY="
|
||||||
|
|
||||||
|
const client = this.connection.client.channelTree.findClient(json["clid"]);
|
||||||
|
if(!client) {
|
||||||
|
log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(client.properties.client_unique_identifier !== json["cluid"]) {
|
||||||
|
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but unique ids dosn't match. (expected %o, received %o)"), client.properties.client_unique_identifier, json["cluid"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat = client.chat(false);
|
||||||
|
if(!chat) {
|
||||||
|
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chat.flag_offline = true;
|
||||||
|
chat.appendMessage(
|
||||||
|
"{0}", true,
|
||||||
|
$.spawn("div")
|
||||||
|
.addClass("event-message event-partner-closed")
|
||||||
|
.text(tr("Your chat partner has close the conversation"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handleNotifyClientUpdated(json) {
|
handleNotifyClientUpdated(json) {
|
||||||
json = json[0]; //Only one bulk
|
json = json[0]; //Only one bulk
|
||||||
|
|
||||||
|
@ -649,5 +718,31 @@ namespace connection {
|
||||||
sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF);
|
sound.play(Sound.GROUP_CHANNEL_CHANGED_SELF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNotifyChannelSubscribed(json) {
|
||||||
|
for(const entry of json) {
|
||||||
|
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
|
||||||
|
if(!channel) {
|
||||||
|
console.warn(tr("Received channel subscribed for not visible channel (cid: %d)"), entry['cid']);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.flag_subscribed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNotifyChannelUnsubscribed(json) {
|
||||||
|
for(const entry of json) {
|
||||||
|
const channel = this.connection.client.channelTree.findChannel(entry["cid"]);
|
||||||
|
if(!channel) {
|
||||||
|
console.warn(tr("Received channel unsubscribed for not visible channel (cid: %d)"), entry['cid']);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.flag_subscribed = false;
|
||||||
|
for(const client of channel.clients(false))
|
||||||
|
this.connection.client.channelTree.deleteClient(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -238,7 +238,7 @@ namespace connection {
|
||||||
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
|
send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise<CommandResult> {
|
||||||
if(!this._socket || !this.connected()) {
|
if(!this._socket || !this.connected()) {
|
||||||
console.warn(tr("Tried to send a command without a valid connection."));
|
console.warn(tr("Tried to send a command without a valid connection."));
|
||||||
return;
|
return Promise.reject(tr("not connected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: CommandOptions = {};
|
const options: CommandOptions = {};
|
||||||
|
@ -246,6 +246,8 @@ namespace connection {
|
||||||
Object.assign(options, _options);
|
Object.assign(options, _options);
|
||||||
|
|
||||||
data = $.isArray(data) ? data : [data || {}];
|
data = $.isArray(data) ? data : [data || {}];
|
||||||
|
if(data.length == 0) /* we require min one arg to append return_code */
|
||||||
|
data.push({});
|
||||||
|
|
||||||
const _this = this;
|
const _this = this;
|
||||||
let result = new Promise<CommandResult>((resolve, failed) => {
|
let result = new Promise<CommandResult>((resolve, failed) => {
|
||||||
|
|
|
@ -443,6 +443,7 @@ const loader_javascript = {
|
||||||
await loader.load_scripts([
|
await loader.load_scripts([
|
||||||
["vendor/bbcode/xbbcode.js"],
|
["vendor/bbcode/xbbcode.js"],
|
||||||
["vendor/moment/moment.js"],
|
["vendor/moment/moment.js"],
|
||||||
|
["vendor/ua-parser-js/dist/ua-parser.min.js"],
|
||||||
["https://webrtc.github.io/adapter/adapter-latest.js"]
|
["https://webrtc.github.io/adapter/adapter-latest.js"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -664,7 +665,7 @@ const loader_style = {
|
||||||
|
|
||||||
async function load_templates() {
|
async function load_templates() {
|
||||||
try {
|
try {
|
||||||
const response = await $.ajax("templates.html" + (loader.allow_cached_files ? "" : "?_ts" + Date.now()));
|
const response = await $.ajax("templates.html" + (loader.cache_tag || "");
|
||||||
|
|
||||||
let node = document.createElement("html");
|
let node = document.createElement("html");
|
||||||
node.innerHTML = response;
|
node.innerHTML = response;
|
||||||
|
|
|
@ -7,7 +7,8 @@ enum LogCategory {
|
||||||
GENERAL,
|
GENERAL,
|
||||||
NETWORKING,
|
NETWORKING,
|
||||||
VOICE,
|
VOICE,
|
||||||
I18N
|
I18N,
|
||||||
|
IDENTITIES
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace log {
|
namespace log {
|
||||||
|
@ -21,14 +22,15 @@ namespace log {
|
||||||
|
|
||||||
let category_mapping = new Map<number, string>([
|
let category_mapping = new Map<number, string>([
|
||||||
[LogCategory.CHANNEL, "Channel "],
|
[LogCategory.CHANNEL, "Channel "],
|
||||||
[LogCategory.CLIENT, "Channel "],
|
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
|
||||||
[LogCategory.CHANNEL_PROPERTIES, "Client "],
|
[LogCategory.CLIENT, "Client "],
|
||||||
[LogCategory.SERVER, "Server "],
|
[LogCategory.SERVER, "Server "],
|
||||||
[LogCategory.PERMISSIONS, "Permission "],
|
[LogCategory.PERMISSIONS, "Permission "],
|
||||||
[LogCategory.GENERAL, "General "],
|
[LogCategory.GENERAL, "General "],
|
||||||
[LogCategory.NETWORKING, "Network "],
|
[LogCategory.NETWORKING, "Network "],
|
||||||
[LogCategory.VOICE, "Voice "],
|
[LogCategory.VOICE, "Voice "],
|
||||||
[LogCategory.I18N, "I18N "]
|
[LogCategory.I18N, "I18N "],
|
||||||
|
[LogCategory.IDENTITIES, "IDENTITIES "]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export let enabled_mapping = new Map<number, boolean>([
|
export let enabled_mapping = new Map<number, boolean>([
|
||||||
|
@ -40,7 +42,8 @@ namespace log {
|
||||||
[LogCategory.GENERAL, true],
|
[LogCategory.GENERAL, true],
|
||||||
[LogCategory.NETWORKING, true],
|
[LogCategory.NETWORKING, true],
|
||||||
[LogCategory.VOICE, true],
|
[LogCategory.VOICE, true],
|
||||||
[LogCategory.I18N, false]
|
[LogCategory.I18N, false],
|
||||||
|
[LogCategory.IDENTITIES, true]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
loader.register_task(loader.Stage.LOADED, {
|
loader.register_task(loader.Stage.LOADED, {
|
||||||
|
@ -109,10 +112,16 @@ namespace log {
|
||||||
name = "[%s] " + name;
|
name = "[%s] " + name;
|
||||||
optionalParams.unshift(category_mapping.get(category));
|
optionalParams.unshift(category_mapping.get(category));
|
||||||
|
|
||||||
return new Group(level, category, name, optionalParams);
|
return new Group(GroupMode.PREFIX, level, category, name, optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GroupMode {
|
||||||
|
NATIVE,
|
||||||
|
PREFIX
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Group {
|
export class Group {
|
||||||
|
readonly mode: GroupMode;
|
||||||
readonly level: LogType;
|
readonly level: LogType;
|
||||||
readonly category: LogCategory;
|
readonly category: LogCategory;
|
||||||
readonly enabled: boolean;
|
readonly enabled: boolean;
|
||||||
|
@ -123,9 +132,11 @@ namespace log {
|
||||||
private readonly optionalParams: any[][];
|
private readonly optionalParams: any[][];
|
||||||
private _collapsed: boolean = true;
|
private _collapsed: boolean = true;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
|
private _log_prefix: string;
|
||||||
|
|
||||||
constructor(level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
|
constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
|
||||||
this.level = level;
|
this.level = level;
|
||||||
|
this.mode = mode;
|
||||||
this.category = category;
|
this.category = category;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.optionalParams = optionalParams;
|
this.optionalParams = optionalParams;
|
||||||
|
@ -133,7 +144,7 @@ namespace log {
|
||||||
}
|
}
|
||||||
|
|
||||||
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
|
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
|
||||||
return new Group(level, this.category, name, optionalParams, this);
|
return new Group(this.mode, level, this.category, name, optionalParams, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
collapsed(flag: boolean = true) : this {
|
collapsed(flag: boolean = true) : this {
|
||||||
|
@ -146,19 +157,43 @@ namespace log {
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
if(!this.initialized) {
|
if(!this.initialized) {
|
||||||
|
if(this.mode == GroupMode.NATIVE) {
|
||||||
if(this._collapsed && console.groupCollapsed)
|
if(this._collapsed && console.groupCollapsed)
|
||||||
console.groupCollapsed(this.name, ...this.optionalParams);
|
console.groupCollapsed(this.name, ...this.optionalParams);
|
||||||
else
|
else
|
||||||
console.group(this.name, ...this.optionalParams);
|
console.group(this.name, ...this.optionalParams);
|
||||||
|
} else {
|
||||||
|
this._log_prefix = " ";
|
||||||
|
let parent = this.owner;
|
||||||
|
while(parent) {
|
||||||
|
if(parent.mode == GroupMode.PREFIX)
|
||||||
|
this._log_prefix = this._log_prefix + parent._log_prefix;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
if(this.mode == GroupMode.NATIVE)
|
||||||
logDirect(this.level, message, ...optionalParams);
|
logDirect(this.level, message, ...optionalParams);
|
||||||
|
else
|
||||||
|
logDirect(this.level, this._log_prefix + message, ...optionalParams);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
if(this.initialized)
|
if(this.initialized) {
|
||||||
|
if(this.mode == GroupMode.NATIVE)
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get prefix() : string {
|
||||||
|
return this._log_prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
set prefix(prefix: string) {
|
||||||
|
this._log_prefix = prefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -238,15 +238,15 @@ function main() {
|
||||||
chat = new ChatBox($("#chat"));
|
chat = new ChatBox($("#chat"));
|
||||||
globalClient.setup();
|
globalClient.setup();
|
||||||
|
|
||||||
if(settings.static("connect_default", false) && settings.static("connect_address", "")) {
|
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
|
||||||
const profile_uuid = settings.static("connect_profile") as string;
|
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
|
||||||
console.log("UUID: %s", profile_uuid);
|
console.log("UUID: %s", profile_uuid);
|
||||||
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
|
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
|
||||||
const address = settings.static("connect_address", "");
|
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
|
||||||
const username = settings.static("connect_username", "Another TeaSpeak user");
|
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
|
||||||
|
|
||||||
const password = settings.static("connect_password", "");
|
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
|
||||||
const password_hashed = settings.static("connect_password_hashed", false);
|
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
|
||||||
|
|
||||||
if(profile && profile.valid()) {
|
if(profile && profile.valid()) {
|
||||||
globalClient.startConnection(address, profile, username, password.length > 0 ? {
|
globalClient.startConnection(address, profile, username, password.length > 0 ? {
|
||||||
|
|
|
@ -20,8 +20,7 @@ namespace profiles.identities {
|
||||||
authentication_method: this.identity.type(),
|
authentication_method: this.identity.type(),
|
||||||
client_nickname: this.identity.name()
|
client_nickname: this.identity.name()
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(tr("Failed to initialize name based handshake. Error: %o"), error);
|
log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error);
|
||||||
|
|
||||||
if(error instanceof CommandResult)
|
if(error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
this.trigger_fail("failed to execute begin (" + error + ")");
|
this.trigger_fail("failed to execute begin (" + error + ")");
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace profiles.identities {
|
||||||
authentication_method: this.identity.type(),
|
authentication_method: this.identity.type(),
|
||||||
data: this.identity.data_json()
|
data: this.identity.data_json()
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
|
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
|
||||||
|
|
||||||
if(error instanceof CommandResult)
|
if(error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
|
@ -32,7 +32,7 @@ namespace profiles.identities {
|
||||||
this.connection.send_command("handshakeindentityproof", {
|
this.connection.send_command("handshakeindentityproof", {
|
||||||
proof: this.identity.data_sign()
|
proof: this.identity.data_sign()
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(tr("Failed to proof the identity. Error: %o"), error);
|
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
|
||||||
|
|
||||||
if(error instanceof CommandResult)
|
if(error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
|
|
|
@ -214,7 +214,7 @@ namespace profiles.identities {
|
||||||
authentication_method: this.identity.type(),
|
authentication_method: this.identity.type(),
|
||||||
publicKey: this.identity.public_key
|
publicKey: this.identity.public_key
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
|
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
|
||||||
|
|
||||||
if(error instanceof CommandResult)
|
if(error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
|
@ -230,7 +230,7 @@ namespace profiles.identities {
|
||||||
|
|
||||||
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
|
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
|
||||||
this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => {
|
this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => {
|
||||||
console.error(tr("Failed to proof the identity. Error: %o"), error);
|
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
|
||||||
|
|
||||||
if(error instanceof CommandResult)
|
if(error instanceof CommandResult)
|
||||||
error = error.extra_message || error.message;
|
error = error.extra_message || error.message;
|
||||||
|
@ -281,7 +281,7 @@ namespace profiles.identities {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
this._worker.onerror = event => {
|
this._worker.onerror = event => {
|
||||||
console.error("POW Worker error %o", event);
|
log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event);
|
||||||
clearTimeout(timeout_id);
|
clearTimeout(timeout_id);
|
||||||
reject("Failed to load worker (" + event.message + ")");
|
reject("Failed to load worker (" + event.message + ")");
|
||||||
};
|
};
|
||||||
|
@ -394,7 +394,7 @@ namespace profiles.identities {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.warn("Failed to finalize POW worker! (%o)", error);
|
log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._worker.terminate();
|
this._worker.terminate();
|
||||||
|
@ -402,7 +402,7 @@ namespace profiles.identities {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handle_message(message: any) {
|
private handle_message(message: any) {
|
||||||
console.log("Received message: %o", message);
|
log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,7 +412,7 @@ namespace profiles.identities {
|
||||||
try {
|
try {
|
||||||
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(tr("Could not generate a new key: %o"), e);
|
log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e);
|
||||||
throw "Failed to generate keypair";
|
throw "Failed to generate keypair";
|
||||||
}
|
}
|
||||||
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
|
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
|
||||||
|
@ -483,7 +483,7 @@ namespace profiles.identities {
|
||||||
|
|
||||||
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
|
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
|
||||||
this.initialize().catch(error => {
|
this.initialize().catch(error => {
|
||||||
console.error("Failed to initialize TeaSpeakIdentity (%s)", error);
|
log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error);
|
||||||
this._initialized = false;
|
this._initialized = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -633,7 +633,7 @@ namespace profiles.identities {
|
||||||
try {
|
try {
|
||||||
await Promise.all(initialize_promise);
|
await Promise.all(initialize_promise);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(error);
|
log.error(LogCategory.IDENTITIES, error);
|
||||||
throw "failed to initialize";
|
throw "failed to initialize";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -688,7 +688,7 @@ namespace profiles.identities {
|
||||||
if(worker.current_level() > best_level) {
|
if(worker.current_level() > best_level) {
|
||||||
this.hash_number = worker.current_hash();
|
this.hash_number = worker.current_hash();
|
||||||
|
|
||||||
console.log("Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
|
log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
|
||||||
best_level = worker.current_level();
|
best_level = worker.current_level();
|
||||||
if(callback_level)
|
if(callback_level)
|
||||||
callback_level(best_level);
|
callback_level(best_level);
|
||||||
|
@ -712,7 +712,7 @@ namespace profiles.identities {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
worker_promise.remove(p);
|
worker_promise.remove(p);
|
||||||
|
|
||||||
console.warn("POW worker error %o", error);
|
log.warn(LogCategory.IDENTITIES, "POW worker error %o", error);
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -736,7 +736,7 @@ namespace profiles.identities {
|
||||||
try {
|
try {
|
||||||
await Promise.all(finalize_promise);
|
await Promise.all(finalize_promise);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(error);
|
log.error(LogCategory.IDENTITIES, error);
|
||||||
throw "failed to finalize";
|
throw "failed to finalize";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -761,14 +761,14 @@ namespace profiles.identities {
|
||||||
try {
|
try {
|
||||||
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
|
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(error);
|
log.error(LogCategory.IDENTITIES, error);
|
||||||
throw "failed to create crypto sign key";
|
throw "failed to create crypto sign key";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(error);
|
log.error(LogCategory.IDENTITIES, error);
|
||||||
throw "failed to create crypto key";
|
throw "failed to create crypto key";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,7 +776,7 @@ namespace profiles.identities {
|
||||||
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
|
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
|
||||||
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
|
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error(error);
|
log.error(LogCategory.IDENTITIES, error);
|
||||||
throw "failed to calculate unique id";
|
throw "failed to calculate unique id";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,15 @@ if(typeof(customElements) !== "undefined") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* T = value type */
|
||||||
|
interface SettingsKey<T> {
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
fallback_keys?: string | string[];
|
||||||
|
fallback_imports?: {[key: string]:(value: string) => T};
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
class StaticSettings {
|
class StaticSettings {
|
||||||
private static _instance: StaticSettings;
|
private static _instance: StaticSettings;
|
||||||
static get instance() : StaticSettings {
|
static get instance() : StaticSettings {
|
||||||
|
@ -20,12 +29,14 @@ class StaticSettings {
|
||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static transformStO?<T>(input?: string, _default?: T) : T {
|
protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
|
||||||
|
default_type = default_type || typeof _default;
|
||||||
|
|
||||||
if (typeof input === "undefined") return _default;
|
if (typeof input === "undefined") return _default;
|
||||||
if (typeof _default === "string") return input as any;
|
if (default_type === "string") return input as any;
|
||||||
else if (typeof _default === "number") return parseInt(input) as any;
|
else if (default_type === "number") return parseInt(input) as any;
|
||||||
else if (typeof _default === "boolean") return (input == "1" || input == "true") as any;
|
else if (default_type === "boolean") return (input == "1" || input == "true") as any;
|
||||||
else if (typeof _default === "undefined") return input as any;
|
else if (default_type === "undefined") return input as any;
|
||||||
return JSON.parse(input) as any;
|
return JSON.parse(input) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +48,35 @@ class StaticSettings {
|
||||||
return JSON.stringify(input);
|
return JSON.stringify(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static resolveKey<T>(key: SettingsKey<T>, _default: T, resolver: (key: string) => string | boolean, default_type?: string) : T {
|
||||||
|
let value = resolver(key.key);
|
||||||
|
if(!value) {
|
||||||
|
/* trying fallbacks */
|
||||||
|
for(const fallback of key.fallback_keys || []) {
|
||||||
|
value = resolver(fallback);
|
||||||
|
if(typeof(value) === "string") {
|
||||||
|
/* fallback key succeeded */
|
||||||
|
const importer = (key.fallback_imports || {})[fallback];
|
||||||
|
if(importer)
|
||||||
|
return importer(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(typeof(value) !== 'string')
|
||||||
|
return _default;
|
||||||
|
|
||||||
|
return StaticSettings.transformStO(value as string, _default, default_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static keyify<T>(key: string | SettingsKey<T>) : SettingsKey<T> {
|
||||||
|
if(typeof(key) === "string")
|
||||||
|
return {key: key};
|
||||||
|
if(typeof(key) === "object" && key.key)
|
||||||
|
return key;
|
||||||
|
throw "key is not a key";
|
||||||
|
}
|
||||||
|
|
||||||
protected _handle: StaticSettings;
|
protected _handle: StaticSettings;
|
||||||
protected _staticPropsTag: JQuery;
|
protected _staticPropsTag: JQuery;
|
||||||
|
|
||||||
|
@ -59,26 +99,98 @@ class StaticSettings {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static?<T>(key: string, _default?: T) : T {
|
static?<T>(key: string | SettingsKey<T>, _default?: T, default_type?: string) : T {
|
||||||
if(this._handle) return this._handle.static<T>(key, _default);
|
if(this._handle) return this._handle.static<T>(key, _default, default_type);
|
||||||
|
|
||||||
|
key = StaticSettings.keyify(key);
|
||||||
|
return StaticSettings.resolveKey(key, _default, key => {
|
||||||
let result = this._staticPropsTag.find("[key='" + key + "']");
|
let result = this._staticPropsTag.find("[key='" + key + "']");
|
||||||
return StaticSettings.transformStO(result.length > 0 ? decodeURIComponent(result.last().attr("value")) : undefined, _default);
|
if(result.length > 0)
|
||||||
|
return decodeURIComponent(result.last().attr('value'));
|
||||||
|
return false;
|
||||||
|
}, default_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteStatic(key: string) {
|
deleteStatic<T>(key: string | SettingsKey<T>) {
|
||||||
if(this._handle) {
|
if(this._handle) {
|
||||||
this._handle.deleteStatic(key);
|
this._handle.deleteStatic<T>(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let result = this._staticPropsTag.find("[key='" + key + "']");
|
|
||||||
|
key = StaticSettings.keyify(key);
|
||||||
|
let result = this._staticPropsTag.find("[key='" + key.key + "']");
|
||||||
if(result.length != 0) result.detach();
|
if(result.length != 0) result.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Settings extends StaticSettings {
|
class Settings extends StaticSettings {
|
||||||
static readonly KEY_DISABLE_CONTEXT_MENU = "disableContextMenu";
|
static readonly KEY_DISABLE_CONTEXT_MENU: SettingsKey<boolean> = {
|
||||||
static readonly KEY_DISABLE_UNLOAD_DIALOG = "disableUnloadDialog";
|
key: 'disableContextMenu',
|
||||||
static readonly KEY_DISABLE_VOICE = "disableVoice";
|
description: 'Disable the context menu for the channel tree which allows to debug the DOM easier'
|
||||||
|
};
|
||||||
|
static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey<boolean> = {
|
||||||
|
key: 'disableUnloadDialog',
|
||||||
|
description: 'Disables the unload popup on side closing'
|
||||||
|
};
|
||||||
|
static readonly KEY_DISABLE_VOICE: SettingsKey<boolean> = {
|
||||||
|
key: 'disableVoice',
|
||||||
|
description: 'Disables the voice bridge. If disabled, the audio and codec workers aren\'t required anymore'
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Control bar */
|
||||||
|
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = {
|
||||||
|
key: 'mute_input'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONTROL_MUTE_OUTPUT: SettingsKey<boolean> = {
|
||||||
|
key: 'mute_output'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONTROL_SHOW_QUERIES: SettingsKey<boolean> = {
|
||||||
|
key: 'show_server_queries'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL: SettingsKey<boolean> = {
|
||||||
|
key: 'channel_subscribe_all'
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Connect parameters */
|
||||||
|
static readonly KEY_FLAG_CONNECT_DEFAULT: SettingsKey<boolean> = {
|
||||||
|
key: 'connect_default'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONNECT_ADDRESS: SettingsKey<string> = {
|
||||||
|
key: 'connect_address'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONNECT_PROFILE: SettingsKey<string> = {
|
||||||
|
key: 'connect_profile'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONNECT_USERNAME: SettingsKey<string> = {
|
||||||
|
key: 'connect_username'
|
||||||
|
};
|
||||||
|
static readonly KEY_CONNECT_PASSWORD: SettingsKey<string> = {
|
||||||
|
key: 'connect_password'
|
||||||
|
};
|
||||||
|
static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey<boolean> = {
|
||||||
|
key: 'connect_password_hashed'
|
||||||
|
};
|
||||||
|
|
||||||
|
static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey<ChannelSubscribeMode> = channel => {
|
||||||
|
return {
|
||||||
|
key: 'channel_subscribe_mode_' + channel.getChannelId()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static readonly KEYS = (() => {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for(const key in Settings) {
|
||||||
|
if(!key.toUpperCase().startsWith("KEY_"))
|
||||||
|
continue;
|
||||||
|
if(key.toUpperCase() == "KEYS")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
result.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})();
|
||||||
|
|
||||||
private static readonly UPDATE_DIRECT: boolean = true;
|
private static readonly UPDATE_DIRECT: boolean = true;
|
||||||
private cacheGlobal = {};
|
private cacheGlobal = {};
|
||||||
|
@ -97,37 +209,41 @@ class Settings extends StaticSettings {
|
||||||
}, 5 * 1000);
|
}, 5 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static_global?<T>(key: string, _default?: T) : T {
|
static_global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||||
let _static = this.static<string>(key);
|
const default_object = { seed: Math.random() } as any;
|
||||||
if(_static) return StaticSettings.transformStO(_static, _default);
|
let _static = this.static(key, default_object, typeof _default);
|
||||||
|
if(_static !== default_object) return StaticSettings.transformStO(_static, _default);
|
||||||
return this.global<T>(key, _default);
|
return this.global<T>(key, _default);
|
||||||
}
|
}
|
||||||
|
|
||||||
global?<T>(key: string, _default?: T) : T {
|
global?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||||
let result = this.cacheGlobal[key];
|
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]);
|
||||||
return StaticSettings.transformStO(result, _default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server?<T>(key: string, _default?: T) : T {
|
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||||
let result = this.cacheServer[key];
|
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
|
||||||
return StaticSettings.transformStO(result, _default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeGlobal<T>(key: string, value?: T){
|
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
|
||||||
if(this.cacheGlobal[key] == value) return;
|
key = Settings.keyify(key);
|
||||||
|
|
||||||
|
|
||||||
|
if(this.cacheGlobal[key.key] == value) return;
|
||||||
|
|
||||||
this.updated = true;
|
this.updated = true;
|
||||||
this.cacheGlobal[key] = StaticSettings.transformOtS(value);
|
this.cacheGlobal[key.key] = StaticSettings.transformOtS(value);
|
||||||
|
|
||||||
if(Settings.UPDATE_DIRECT)
|
if(Settings.UPDATE_DIRECT)
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
changeServer<T>(key: string, value?: T) {
|
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
||||||
if(this.cacheServer[key] == value) return;
|
key = Settings.keyify(key);
|
||||||
|
|
||||||
|
if(this.cacheServer[key.key] == value) return;
|
||||||
|
|
||||||
this.updated = true;
|
this.updated = true;
|
||||||
this.cacheServer[key] = StaticSettings.transformOtS(value);
|
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
||||||
|
|
||||||
if(Settings.UPDATE_DIRECT)
|
if(Settings.UPDATE_DIRECT)
|
||||||
this.save();
|
this.save();
|
||||||
|
|
|
@ -286,7 +286,7 @@ namespace sound {
|
||||||
try {
|
try {
|
||||||
console.log(tr("Decoding data"));
|
console.log(tr("Decoding data"));
|
||||||
context.decodeAudioData(buffer, result => {
|
context.decodeAudioData(buffer, result => {
|
||||||
console.log(tr("Got decoded data"));
|
log.info(LogCategory.VOICE, tr("Got decoded data"));
|
||||||
file.cached = result;
|
file.cached = result;
|
||||||
play(sound, options);
|
play(sound, options);
|
||||||
}, error => {
|
}, error => {
|
||||||
|
|
|
@ -14,6 +14,12 @@ namespace ChannelType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ChannelSubscribeMode {
|
||||||
|
SUBSCRIBED,
|
||||||
|
UNSUBSCRIBED,
|
||||||
|
INHERITED
|
||||||
|
}
|
||||||
|
|
||||||
class ChannelProperties {
|
class ChannelProperties {
|
||||||
channel_order: number = 0;
|
channel_order: number = 0;
|
||||||
channel_name: string = "";
|
channel_name: string = "";
|
||||||
|
@ -71,6 +77,9 @@ class ChannelEntry {
|
||||||
private _cached_channel_description_promise_resolve: any = undefined;
|
private _cached_channel_description_promise_resolve: any = undefined;
|
||||||
private _cached_channel_description_promise_reject: any = undefined;
|
private _cached_channel_description_promise_reject: any = undefined;
|
||||||
|
|
||||||
|
private _flag_subscribed: boolean;
|
||||||
|
private _subscribe_mode: ChannelSubscribeMode;
|
||||||
|
|
||||||
constructor(channelId, channelName, parent = null) {
|
constructor(channelId, channelName, parent = null) {
|
||||||
this.properties = new ChannelProperties();
|
this.properties = new ChannelProperties();
|
||||||
this.channelId = channelId;
|
this.channelId = channelId;
|
||||||
|
@ -463,6 +472,28 @@ class ChannelEntry {
|
||||||
callback: () => this.joinChannel()
|
callback: () => this.joinChannel()
|
||||||
},
|
},
|
||||||
MenuEntry.HR(),
|
MenuEntry.HR(),
|
||||||
|
{
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
icon: "client-subscribe_to_channel",
|
||||||
|
name: tr("<b>Subscribe to channel</b>"),
|
||||||
|
callback: () => this.subscribe(),
|
||||||
|
visible: !this.flag_subscribed
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
icon: "client-channel_unsubscribed",
|
||||||
|
name: tr("<b>Unsubscribe from channel</b>"),
|
||||||
|
callback: () => this.unsubscribe(),
|
||||||
|
visible: this.flag_subscribed
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MenuEntryType.ENTRY,
|
||||||
|
icon: "client-subscribe_mode",
|
||||||
|
name: tr("<b>Use inherited subscribe mode</b>"),
|
||||||
|
callback: () => this.unsubscribe(true),
|
||||||
|
visible: this.subscribe_mode != ChannelSubscribeMode.INHERITED
|
||||||
|
},
|
||||||
|
MenuEntry.HR(),
|
||||||
{
|
{
|
||||||
type: MenuEntryType.ENTRY,
|
type: MenuEntryType.ENTRY,
|
||||||
icon: "client-channel_edit",
|
icon: "client-channel_edit",
|
||||||
|
@ -681,6 +712,7 @@ class ChannelEntry {
|
||||||
let tag = this.channelTag().find(".channel-type");
|
let tag = this.channelTag().find(".channel-type");
|
||||||
tag.removeAttr('class');
|
tag.removeAttr('class');
|
||||||
tag.addClass("show-channel-normal-only channel-type icon");
|
tag.addClass("show-channel-normal-only channel-type icon");
|
||||||
|
|
||||||
if(this._channel_name_formatted === undefined)
|
if(this._channel_name_formatted === undefined)
|
||||||
tag.addClass("channel-normal");
|
tag.addClass("channel-normal");
|
||||||
|
|
||||||
|
@ -695,7 +727,7 @@ class ChannelEntry {
|
||||||
else
|
else
|
||||||
type = "green";
|
type = "green";
|
||||||
|
|
||||||
tag.addClass("client-channel_" + type + "_subscribed");
|
tag.addClass("client-channel_" + type + (this._flag_subscribed ? "_subscribed" : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_bbcode() {
|
generate_bbcode() {
|
||||||
|
@ -740,6 +772,66 @@ class ChannelEntry {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async subscribe() : Promise<void> {
|
||||||
|
if(this.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.subscribe_mode = ChannelSubscribeMode.SUBSCRIBED;
|
||||||
|
|
||||||
|
const connection = this.channelTree.client.getServerConnection();
|
||||||
|
if(!this.flag_subscribed && connection)
|
||||||
|
await connection.send_command('channelsubscribe', {
|
||||||
|
'cid': this.getChannelId()
|
||||||
|
});
|
||||||
|
else
|
||||||
|
this.flag_subscribed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(inherited_subscription_mode?: boolean) : Promise<void> {
|
||||||
|
const connection = this.channelTree.client.getServerConnection();
|
||||||
|
let unsubscribe: boolean;
|
||||||
|
|
||||||
|
if(inherited_subscription_mode) {
|
||||||
|
this.subscribe_mode = ChannelSubscribeMode.INHERITED;
|
||||||
|
unsubscribe = this.flag_subscribed && !this.channelTree.client.controlBar.channel_subscribe_all;
|
||||||
|
} else {
|
||||||
|
this.subscribe_mode = ChannelSubscribeMode.UNSUBSCRIBED;
|
||||||
|
unsubscribe = this.flag_subscribed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(unsubscribe) {
|
||||||
|
if(connection)
|
||||||
|
await connection.send_command('channelunsubscribe', {
|
||||||
|
'cid': this.getChannelId()
|
||||||
|
});
|
||||||
|
else
|
||||||
|
this.flag_subscribed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get flag_subscribed() : boolean {
|
||||||
|
return this._flag_subscribed;
|
||||||
|
}
|
||||||
|
set flag_subscribed(flag: boolean) {
|
||||||
|
if(this._flag_subscribed == flag)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._flag_subscribed = flag;
|
||||||
|
this.updateChannelTypeIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscribe_mode() : ChannelSubscribeMode {
|
||||||
|
return typeof(this._subscribe_mode) !== 'undefined' ? this._subscribe_mode : (this._subscribe_mode = settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), ChannelSubscribeMode.INHERITED));
|
||||||
|
}
|
||||||
|
|
||||||
|
set subscribe_mode(mode: ChannelSubscribeMode) {
|
||||||
|
if(this.subscribe_mode == mode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._subscribe_mode = mode;
|
||||||
|
settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Global functions
|
//Global functions
|
||||||
|
|
|
@ -613,6 +613,9 @@ class ClientEntry {
|
||||||
this.updateClientIcon();
|
this.updateClientIcon();
|
||||||
if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups")
|
if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups")
|
||||||
this.update_displayed_client_groups();
|
this.update_displayed_client_groups();
|
||||||
|
if(variable.key == "client_version") {
|
||||||
|
console.log(UAParser(variable.value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* process updates after variables have been set */
|
/* process updates after variables have been set */
|
||||||
|
@ -673,19 +676,21 @@ class ClientEntry {
|
||||||
chat(create: boolean = false) : ChatEntry {
|
chat(create: boolean = false) : ChatEntry {
|
||||||
let chatName = "client_" + this.clientUid() + ":" + this.clientId();
|
let chatName = "client_" + this.clientUid() + ":" + this.clientId();
|
||||||
let c = chat.findChat(chatName);
|
let c = chat.findChat(chatName);
|
||||||
if((!c) && create) {
|
if(!c && create) {
|
||||||
c = chat.createChat(chatName);
|
c = chat.createChat(chatName);
|
||||||
c.closeable = true;
|
c.flag_closeable = true;
|
||||||
c.name = this.clientNickName();
|
c.name = this.clientNickName();
|
||||||
|
c.owner_unique_id = this.properties.client_unique_identifier;
|
||||||
|
|
||||||
const _this = this;
|
c.onMessageSend = text => {
|
||||||
c.onMessageSend = function (text: string) {
|
this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this);
|
||||||
_this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, _this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
c.onClose = function () : boolean {
|
c.onClose = () => {
|
||||||
//TODO check online?
|
if(!c.flag_offline)
|
||||||
_this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": _this.clientId()});
|
this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": this.clientId()}, {process_result: false}).catch(error => {
|
||||||
|
log.warn(LogCategory.GENERAL, tr("Failed to notify chat participant (%o) that the chat has been closed. Error: %o"), this, error);
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ class ControlBar {
|
||||||
private _away: boolean;
|
private _away: boolean;
|
||||||
private _query_visible: boolean;
|
private _query_visible: boolean;
|
||||||
private _awayMessage: string;
|
private _awayMessage: string;
|
||||||
|
private _channel_subscribe_all: boolean;
|
||||||
|
|
||||||
private codec_supported: boolean = false;
|
private codec_supported: boolean = false;
|
||||||
private support_playback: boolean = false;
|
private support_playback: boolean = false;
|
||||||
|
@ -40,39 +41,43 @@ class ControlBar {
|
||||||
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.bind(this));
|
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.bind(this));
|
||||||
this.htmlTag.find(".btn_permissions").on('click', this.onPermission.bind(this));
|
this.htmlTag.find(".btn_permissions").on('click', this.onPermission.bind(this));
|
||||||
this.htmlTag.find(".btn_banlist").on('click', this.onBanlist.bind(this));
|
this.htmlTag.find(".btn_banlist").on('click', this.onBanlist.bind(this));
|
||||||
|
this.htmlTag.find(".button-subscribe-mode").on('click', this.on_toggle_channel_subscribe_all.bind(this));
|
||||||
this.htmlTag.find(".button-playlist-manage").on('click', this.on_playlist_manage.bind(this));
|
this.htmlTag.find(".button-playlist-manage").on('click', this.on_playlist_manage.bind(this));
|
||||||
|
|
||||||
|
let dropdownify = (tag: JQuery) => {
|
||||||
|
tag.find(".button-dropdown").on('click', () => {
|
||||||
|
tag.addClass("displayed");
|
||||||
|
}).hover(() => {
|
||||||
|
console.log("Add");
|
||||||
|
tag.addClass("displayed");
|
||||||
|
}, () => {
|
||||||
|
if(tag.find(".dropdown:hover").length > 0)
|
||||||
|
return;
|
||||||
|
console.log("Removed");
|
||||||
|
tag.removeClass("displayed");
|
||||||
|
});
|
||||||
|
tag.on('mouseleave', () => {
|
||||||
|
tag.removeClass("displayed");
|
||||||
|
});
|
||||||
|
};
|
||||||
{
|
{
|
||||||
let tokens = this.htmlTag.find(".btn_token");
|
let tokens = this.htmlTag.find(".btn_token");
|
||||||
tokens.find(".button-dropdown").on('click', () => {
|
dropdownify(tokens);
|
||||||
tokens.find(".dropdown").addClass("displayed");
|
|
||||||
});
|
|
||||||
tokens.on('mouseleave', () => {
|
|
||||||
tokens.find(".dropdown").removeClass("displayed");
|
|
||||||
});
|
|
||||||
|
|
||||||
tokens.find(".btn_token_use").on('click', this.on_token_use.bind(this));
|
tokens.find(".btn_token_use").on('click', this.on_token_use.bind(this));
|
||||||
tokens.find(".btn_token_list").on('click', this.on_token_list.bind(this));
|
tokens.find(".btn_token_list").on('click', this.on_token_list.bind(this));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let away = this.htmlTag.find(".btn_away");
|
let away = this.htmlTag.find(".btn_away");
|
||||||
away.find(".button-dropdown").on('click', () => {
|
dropdownify(away);
|
||||||
away.find(".dropdown").addClass("displayed");
|
|
||||||
});
|
|
||||||
away.on('mouseleave', () => {
|
|
||||||
away.find(".dropdown").removeClass("displayed");
|
|
||||||
});
|
|
||||||
|
|
||||||
away.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
|
away.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
|
||||||
away.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
|
away.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let bookmark = this.htmlTag.find(".btn_bookmark");
|
let bookmark = this.htmlTag.find(".btn_bookmark");
|
||||||
bookmark.find(".button-dropdown").on('click', () => {
|
dropdownify(bookmark);
|
||||||
bookmark.find("> .dropdown").addClass("displayed");
|
|
||||||
});
|
|
||||||
bookmark.on('mouseleave', () => {
|
|
||||||
bookmark.find("> .dropdown").removeClass("displayed");
|
|
||||||
});
|
|
||||||
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
|
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
|
||||||
bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
|
bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
|
||||||
|
|
||||||
|
@ -81,22 +86,30 @@ class ControlBar {
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let query = this.htmlTag.find(".btn_query");
|
let query = this.htmlTag.find(".btn_query");
|
||||||
query.find(".button-dropdown").on('click', () => {
|
dropdownify(query);
|
||||||
query.find(".dropdown").addClass("displayed");
|
|
||||||
});
|
|
||||||
query.on('mouseleave', () => {
|
|
||||||
query.find(".dropdown").removeClass("displayed");
|
|
||||||
});
|
|
||||||
|
|
||||||
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
|
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
|
||||||
query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
|
query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
|
||||||
query.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
|
query.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile dropdowns */
|
||||||
|
{
|
||||||
|
const dropdown = this.htmlTag.find(".dropdown-audio");
|
||||||
|
dropdownify(dropdown);
|
||||||
|
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const dropdown = this.htmlTag.find(".dropdown-servertools");
|
||||||
|
dropdownify(dropdown);
|
||||||
|
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
|
||||||
|
}
|
||||||
|
|
||||||
//Need an initialise
|
//Need an initialise
|
||||||
this.muteInput = settings.static_global("mute_input", false);
|
this.muteInput = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false);
|
||||||
this.muteOutput = settings.static_global("mute_output", false);
|
this.muteOutput = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false);
|
||||||
this.query_visible = settings.static_global("show_server_queries", false);
|
this.query_visible = settings.static_global(Settings.KEY_CONTROL_SHOW_QUERIES, false);
|
||||||
|
this.channel_subscribe_all = settings.static_global(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,22 +138,20 @@ class ControlBar {
|
||||||
this._muteInput = flag;
|
this._muteInput = flag;
|
||||||
|
|
||||||
let tag = this.htmlTag.find(".btn_mute_input");
|
let tag = this.htmlTag.find(".btn_mute_input");
|
||||||
if(flag) {
|
const tag_icon = tag.find(".icon_x32, .icon");
|
||||||
if(!tag.hasClass("activated"))
|
|
||||||
tag.addClass("activated");
|
tag.toggleClass('activated', flag)
|
||||||
tag.find(".icon_x32").attr("class", "icon_x32 client-input_muted");
|
|
||||||
} else {
|
tag_icon
|
||||||
if(tag.hasClass("activated"))
|
.toggleClass('client-input_muted', flag)
|
||||||
tag.removeClass("activated");
|
.toggleClass('client-capture', !flag);
|
||||||
tag.find(".icon_x32").attr("class", "icon_x32 client-capture");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if(this.handle.serverConnection.connected)
|
if(this.handle.serverConnection.connected())
|
||||||
this.handle.serverConnection.send_command("clientupdate", {
|
this.handle.serverConnection.send_command("clientupdate", {
|
||||||
client_input_muted: this._muteInput
|
client_input_muted: this._muteInput
|
||||||
});
|
});
|
||||||
settings.changeGlobal("mute_input", this._muteInput);
|
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this._muteInput);
|
||||||
this.updateMicrophoneRecordState();
|
this.updateMicrophoneRecordState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,22 +161,21 @@ class ControlBar {
|
||||||
if(this._muteOutput == flag) return;
|
if(this._muteOutput == flag) return;
|
||||||
this._muteOutput = flag;
|
this._muteOutput = flag;
|
||||||
|
|
||||||
let tag = this.htmlTag.find(".btn_mute_output");
|
|
||||||
if(flag) {
|
|
||||||
if(!tag.hasClass("activated"))
|
|
||||||
tag.addClass("activated");
|
|
||||||
tag.find(".icon_x32").attr("class", "icon_x32 client-output_muted");
|
|
||||||
} else {
|
|
||||||
if(tag.hasClass("activated"))
|
|
||||||
tag.removeClass("activated");
|
|
||||||
tag.find(".icon_x32").attr("class", "icon_x32 client-volume");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.handle.serverConnection.connected)
|
let tag = this.htmlTag.find(".btn_mute_output");
|
||||||
|
const tag_icon = tag.find(".icon_x32, .icon");
|
||||||
|
|
||||||
|
tag.toggleClass('activated', flag)
|
||||||
|
|
||||||
|
tag_icon
|
||||||
|
.toggleClass('client-output_muted', flag)
|
||||||
|
.toggleClass('client-volume', !flag);
|
||||||
|
|
||||||
|
if(this.handle.serverConnection.connected())
|
||||||
this.handle.serverConnection.send_command("clientupdate", {
|
this.handle.serverConnection.send_command("clientupdate", {
|
||||||
client_output_muted: this._muteOutput
|
client_output_muted: this._muteOutput
|
||||||
});
|
});
|
||||||
settings.changeGlobal("mute_output", this._muteOutput);
|
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this._muteOutput);
|
||||||
this.updateMicrophoneRecordState();
|
this.updateMicrophoneRecordState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,7 +433,7 @@ class ControlBar {
|
||||||
if(this._query_visible == flag) return;
|
if(this._query_visible == flag) return;
|
||||||
|
|
||||||
this._query_visible = flag;
|
this._query_visible = flag;
|
||||||
settings.changeGlobal("show_server_queries", flag);
|
settings.changeGlobal(Settings.KEY_CONTROL_SHOW_QUERIES, flag);
|
||||||
this.update_query_visibility_button();
|
this.update_query_visibility_button();
|
||||||
this.handle.channelTree.toggle_server_queries(flag);
|
this.handle.channelTree.toggle_server_queries(flag);
|
||||||
}
|
}
|
||||||
|
@ -434,12 +444,7 @@ class ControlBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
private update_query_visibility_button() {
|
private update_query_visibility_button() {
|
||||||
let tag = this.htmlTag.find(".btn_query_toggle");
|
this.htmlTag.find(".btn_query_toggle").toggleClass('activated', this._query_visible);
|
||||||
if(this._query_visible) {
|
|
||||||
tag.addClass("activated");
|
|
||||||
} else {
|
|
||||||
tag.removeClass("activated");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private on_query_create() {
|
private on_query_create() {
|
||||||
|
@ -466,4 +471,33 @@ class ControlBar {
|
||||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
|
createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get channel_subscribe_all() : boolean {
|
||||||
|
return this._channel_subscribe_all;
|
||||||
|
}
|
||||||
|
|
||||||
|
set channel_subscribe_all(flag: boolean) {
|
||||||
|
if(this._channel_subscribe_all == flag)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._channel_subscribe_all = flag;
|
||||||
|
|
||||||
|
this.htmlTag
|
||||||
|
.find(".button-subscribe-mode")
|
||||||
|
.toggleClass('activated', this._channel_subscribe_all)
|
||||||
|
.find('.icon_x32')
|
||||||
|
.toggleClass('client-unsubscribe_from_all_channels', !this._channel_subscribe_all)
|
||||||
|
.toggleClass('client-subscribe_to_all_channels', this._channel_subscribe_all);
|
||||||
|
|
||||||
|
settings.changeGlobal(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, flag);
|
||||||
|
|
||||||
|
if(flag)
|
||||||
|
this.handle.channelTree.subscribe_all_channels();
|
||||||
|
else
|
||||||
|
this.handle.channelTree.unsubscribe_all_channels();
|
||||||
|
}
|
||||||
|
|
||||||
|
private on_toggle_channel_subscribe_all() {
|
||||||
|
this.channel_subscribe_all = !this.channel_subscribe_all;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/// <reference path="../../client.ts" />
|
/// <reference path="../../client.ts" />
|
||||||
/// <reference path="../../../../vendor/bbcode/xbbcode.ts" />
|
/// <reference path="../../../../vendor/bbcode/xbbcode.ts" />
|
||||||
|
/// <reference path="../../../../vendor/ua-parser-js/src/ua-parser.d.ts" />
|
||||||
|
|
||||||
abstract class InfoManagerBase {
|
abstract class InfoManagerBase {
|
||||||
private timers: NodeJS.Timer[] = [];
|
private timers: NodeJS.Timer[] = [];
|
||||||
|
@ -139,6 +140,11 @@ class InfoBar<AvailableTypes = ServerEntry | ChannelEntry | ClientEntry | undefi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
Image: typeof HTMLImageElement;
|
||||||
|
HTMLImageElement: typeof HTMLImageElement;
|
||||||
|
}
|
||||||
|
|
||||||
class Hostbanner {
|
class Hostbanner {
|
||||||
readonly html_tag: JQuery<HTMLElement>;
|
readonly html_tag: JQuery<HTMLElement>;
|
||||||
readonly client: TSClient;
|
readonly client: TSClient;
|
||||||
|
@ -160,9 +166,20 @@ class Hostbanner {
|
||||||
|
|
||||||
if(tag) {
|
if(tag) {
|
||||||
tag.then(element => {
|
tag.then(element => {
|
||||||
this.html_tag.empty();
|
const children = this.html_tag.children();
|
||||||
this.html_tag.append(element).removeClass("disabled");
|
this.html_tag.append(element).removeClass("disabled");
|
||||||
|
|
||||||
|
/* allow the new image be loaded from cache URL */
|
||||||
|
{
|
||||||
|
children
|
||||||
|
.css('z-index', '2')
|
||||||
|
.css('position', 'absolute')
|
||||||
|
.css('height', '100%')
|
||||||
|
.css('width', '100%');
|
||||||
|
setTimeout(() => {
|
||||||
|
children.detach();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.warn(tr("Failed to load hostbanner: %o"), error);
|
console.warn(tr("Failed to load hostbanner: %o"), error);
|
||||||
this.html_tag.empty().addClass("disabled");
|
this.html_tag.empty().addClass("disabled");
|
||||||
|
@ -183,44 +200,63 @@ class Hostbanner {
|
||||||
for(let key in server.properties)
|
for(let key in server.properties)
|
||||||
properties["property_" + key] = server.properties[key];
|
properties["property_" + key] = server.properties[key];
|
||||||
|
|
||||||
|
properties["hostbanner_gfx_url"] = server.properties.virtualserver_hostbanner_gfx_url;
|
||||||
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
|
if(server.properties.virtualserver_hostbanner_gfx_interval > 0) {
|
||||||
const update_interval = Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60);
|
const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60);
|
||||||
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
|
const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString();
|
||||||
try {
|
try {
|
||||||
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
|
const url = new URL(server.properties.virtualserver_hostbanner_gfx_url);
|
||||||
if(url.search.length == 0)
|
if(url.search.length == 0)
|
||||||
properties["cache_tag"] = "?_ts=" + update_timestamp;
|
properties["hostbanner_gfx_url"] += "?_ts=" + update_timestamp;
|
||||||
else
|
else
|
||||||
properties["cache_tag"] = "&_ts=" + update_timestamp;
|
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.warn(tr("Failed to parse banner URL: %o"), error);
|
console.warn(tr("Failed to parse banner URL: %o"), error);
|
||||||
properties["cache_tag"] = "&_ts=" + update_timestamp;
|
properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updater = setTimeout(() => this.update(), update_interval * 1000);
|
this.updater = setTimeout(() => this.update(), update_interval * 1000);
|
||||||
} else {
|
|
||||||
properties["cache_tag"] = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
|
const rendered = $("#tmpl_selected_hostbanner").renderTag(properties);
|
||||||
|
|
||||||
|
|
||||||
|
if(window.fetch) {
|
||||||
|
return (async () => {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const tag_image = rendered.find(".hostbanner-image");
|
||||||
|
|
||||||
|
_fetch:
|
||||||
|
try {
|
||||||
|
const result = await fetch(properties["hostbanner_gfx_url"]);
|
||||||
|
|
||||||
|
if(!result.ok) {
|
||||||
|
if(result.type === 'opaque' || result.type === 'opaqueredirect') {
|
||||||
|
log.warn(LogCategory.SERVER, tr("Could not load hostbanner because 'Access-Control-Allow-Origin' isnt valid!"));
|
||||||
|
break _fetch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(await result.blob());
|
||||||
|
tag_image.css('background-image', 'url(' + url + ')');
|
||||||
|
log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url);
|
||||||
|
|
||||||
|
if(URL.revokeObjectURL) {
|
||||||
|
setTimeout(() => {
|
||||||
|
log.debug(LogCategory.SERVER, tr("Revoked hostbanner url %s"), url);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
} catch(error) {
|
||||||
|
log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error);
|
||||||
|
}
|
||||||
|
return rendered;
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
console.debug(tr("Hostbanner has been loaded"));
|
console.debug(tr("Hostbanner has been loaded"));
|
||||||
return Promise.resolve(rendered);
|
return Promise.resolve(rendered);
|
||||||
/*
|
|
||||||
const image = rendered.find("img");
|
|
||||||
return new Promise<JQuery<HTMLElement>>((resolve, reject) => {
|
|
||||||
const node_image = image[0] as HTMLImageElement;
|
|
||||||
node_image.onload = () => {
|
|
||||||
console.debug(tr("Hostbanner has been loaded"));
|
|
||||||
if(server.properties.virtualserver_hostbanner_gfx_interval > 0)
|
|
||||||
this.updater = setTimeout(() => this.update(), Math.min(server.properties.virtualserver_hostbanner_gfx_interval, 60) * 1000);
|
|
||||||
resolve(rendered);
|
|
||||||
};
|
|
||||||
node_image.onerror = event => {
|
|
||||||
reject(event);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +293,7 @@ class ClientInfoManager extends InfoManager<ClientEntry> {
|
||||||
properties["client_onlinetime"] = formatDate(client.calculateOnlineTime());
|
properties["client_onlinetime"] = formatDate(client.calculateOnlineTime());
|
||||||
properties["sound_volume"] = client.audioController.volume * 100;
|
properties["sound_volume"] = client.audioController.volume * 100;
|
||||||
properties["client_is_query"] = client.properties.client_type == ClientType.CLIENT_QUERY;
|
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"] = [];
|
properties["group_server"] = [];
|
||||||
for(let groupId of client.assignedServerGroupIds()) {
|
for(let groupId of client.assignedServerGroupIds()) {
|
||||||
|
|
|
@ -31,11 +31,11 @@ namespace Modals {
|
||||||
input_nickname.attr("placeholder", "");
|
input_nickname.attr("placeholder", "");
|
||||||
|
|
||||||
let address = input_address.val().toString();
|
let address = input_address.val().toString();
|
||||||
settings.changeGlobal("connect_address", address);
|
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
|
||||||
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
|
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
|
||||||
|
|
||||||
let nickname = input_nickname.val().toString();
|
let nickname = input_nickname.val().toString();
|
||||||
settings.changeGlobal("connect_name", nickname);
|
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
|
||||||
let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32;
|
let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32;
|
||||||
|
|
||||||
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
|
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
|
||||||
|
@ -48,8 +48,8 @@ namespace Modals {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
input_nickname.val(settings.static_global("connect_name", undefined));
|
input_nickname.val(settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined));
|
||||||
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global("connect_address", defaultHost.url));
|
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
|
||||||
input_address
|
input_address
|
||||||
.on("keyup", () => updateFields())
|
.on("keyup", () => updateFields())
|
||||||
.on('keydown', event => {
|
.on('keydown', event => {
|
||||||
|
@ -150,7 +150,7 @@ namespace Modals {
|
||||||
},
|
},
|
||||||
|
|
||||||
width: '70%',
|
width: '70%',
|
||||||
//closeable: false
|
//flag_closeable: false
|
||||||
});
|
});
|
||||||
connectModal.open();
|
connectModal.open();
|
||||||
}
|
}
|
||||||
|
|
|
@ -755,4 +755,46 @@ class ChannelTree {
|
||||||
get_first_channel?() : ChannelEntry {
|
get_first_channel?() : ChannelEntry {
|
||||||
return this.channel_first;
|
return this.channel_first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsubscribe_all_channels(subscribe_specified?: boolean) {
|
||||||
|
if(!this.client.serverConnection || !this.client.serverConnection.connected())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.client.serverConnection.send_command('channelunsubscribeall').then(() => {
|
||||||
|
const channels: number[] = [];
|
||||||
|
for(const channel of this.channels) {
|
||||||
|
if(channel.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
|
||||||
|
channels.push(channel.getChannelId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(channels.length > 0) {
|
||||||
|
this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
|
||||||
|
console.warn(tr("Failed to subscribe to specific channels (%o)"), channels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.warn(tr("Failed to unsubscribe to all channels! (%o)"), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe_all_channels() {
|
||||||
|
if(!this.client.serverConnection || !this.client.serverConnection.connected())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.client.serverConnection.send_command('channelsubscribeall').then(() => {
|
||||||
|
const channels: number[] = [];
|
||||||
|
for(const channel of this.channels) {
|
||||||
|
if(channel.subscribe_mode == ChannelSubscribeMode.UNSUBSCRIBED)
|
||||||
|
channels.push(channel.getChannelId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(channels.length > 0) {
|
||||||
|
this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
|
||||||
|
console.warn(tr("Failed to unsubscribe to specific channels (%o)"), channels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.warn(tr("Failed to subscribe to all channels! (%o)"), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -40,11 +40,14 @@ var TabFunctions = {
|
||||||
let content = $.spawn("div");
|
let content = $.spawn("div");
|
||||||
content.addClass("tab-content");
|
content.addClass("tab-content");
|
||||||
|
|
||||||
|
content.append($.spawn("div").addClass("height-watcher"));
|
||||||
|
|
||||||
let silentContent = $.spawn("div");
|
let silentContent = $.spawn("div");
|
||||||
silentContent.addClass("tab-content-invisible");
|
silentContent.addClass("tab-content-invisible");
|
||||||
|
|
||||||
/* add some kind of min height */
|
/* add some kind of min height */
|
||||||
const update_height = () => {
|
const update_height = () => {
|
||||||
|
const height_watcher = tag.find("> .tab-content .height-watcher");
|
||||||
const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content");
|
const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content");
|
||||||
console.error(entries);
|
console.error(entries);
|
||||||
let max_height = 0;
|
let max_height = 0;
|
||||||
|
@ -56,13 +59,7 @@ var TabFunctions = {
|
||||||
max_height = height;
|
max_height = height;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.error("HIGHT: " + max_height);
|
height_watcher.css('min-height', max_height + "px");
|
||||||
entries.each((_, _e) => {
|
|
||||||
const entry = $(_e);
|
|
||||||
entry.animate({
|
|
||||||
'min-height': max_height + "px"
|
|
||||||
}, 250);
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template.find("x-entry").each( (_, _entry) => {
|
template.find("x-entry").each( (_, _entry) => {
|
||||||
|
|
1
vendor/jqueryjquery.min.js
vendored
Symbolic link
1
vendor/jqueryjquery.min.js
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
C:/Users/WolverinDEV/TeaSpeak/TeaWeb/vendor/jquery/jquery.min.js
|
2
vendor/jsrender/jsrender.min.js
vendored
2
vendor/jsrender/jsrender.min.js
vendored
File diff suppressed because one or more lines are too long
1
vendor/ua-parser-js
vendored
Submodule
1
vendor/ua-parser-js
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 732cf5834e6a8605c75f48db492a14426345d475
|
|
@ -19,6 +19,8 @@ html, body {
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
|
|
||||||
|
transition: all .5s linear;
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
Loading…
Add table
Reference in a new issue