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