A lot of changes

canary
WolverinDEV 2019-03-17 12:15:39 +01:00
parent d9c0fa37f7
commit 040c218fb2
35 changed files with 1064 additions and 357 deletions

3
.gitmodules vendored
View File

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

View File

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

View File

@ -135,6 +135,10 @@
display: block;
}
}
.icon_no_sound {
display: flex;
}
}
.container-clients {

View File

@ -47,13 +47,11 @@ $background:lightgray;
.button-dropdown {
.buttons {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: 100%;
grid-gap: 2px;
display: flex;
flex-direction: row;
.button {
margin-right: 0px;
margin-right: 0;
}
.button-dropdown {
@ -83,6 +81,7 @@ $background:lightgray;
background-color: rgba(0,0,0,0.4);
border-color: rgba(255, 255, 255, .75);
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
border-left: 2px solid rgba(255, 255, 255, .75);
}
}
}
@ -103,6 +102,11 @@ $background:lightgray;
z-index: 1000;
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/
&.right {
}
.icon {
vertical-align: middle;
margin-right: 5px;
@ -131,8 +135,8 @@ $background:lightgray;
}
}
&:hover {
.dropdown.displayed {
&:hover.displayed {
.dropdown {
display: block;
}
}

View File

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

View File

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

View File

@ -35,6 +35,20 @@
display: inline-block;
vertical-align: top;
}
.event-message { /* special formated messages */
&.event-partner-disconnect {
color: red;
}
&.event-partner-connect {
color: green;
}
&.event-partner-closed {
color: orange;
}
}
}
}
}
@ -62,11 +76,9 @@
cursor: pointer;
height: 18px;
&.active {
background: #11111111;
}
.btn_close {
display: none;
float: none;
margin-right: -5px;
margin-left: 8px;
@ -78,9 +90,34 @@
}
}
.name, .chatIcon {
.name, .chat-type {
display: inline-block;
}
.name {
color: black;
}
&.closeable {
.btn_close {
display: inline-block;
}
}
&.active {
background: #11111111;
}
&.offline {
.name {
color: gray;
}
}
&.unread {
.name {
color: blue;
}
}
}
}

View File

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

View File

@ -1,7 +1,4 @@
x-tab { display:none }
x-content {
width: 100%;
}
.tab {
padding: 2px;
@ -18,15 +15,19 @@ x-content {
.tab .tab-content {
min-height: 200px;
border-color: #6f6f6f;
border-radius: 0px 2px 2px 2px;
border-style: solid;
overflow-y: auto;
border-radius: 0 2px 2px 2px;
border: solid #6f6f6f;
overflow-y: hidden;
height: 100%;
padding: 2px;
display: flex;
flex-grow: 1;
x-content {
overflow-y: auto;
width: 100%;
}
}
/*
@ -39,7 +40,7 @@ x-content {
*/
.tab .tab-header {
font-family: Arial;
font-family: Arial, serif;
font-size: 12px;
/*white-space: pre;*/
line-height: 1;
@ -64,14 +65,10 @@ x-content {
.tab .tab-header .entry {
background: #5f5f5f5f;
display: inline-block;
border: #6f6f6f;
border-width: 1px;
border-style: solid;
border: 1px solid #6f6f6f;
border-radius: 2px 2px 0px 0px;
vertical-align: middle;
padding: 2px;
padding-left: 5px;
padding-right: 5px;
padding: 2px 5px;
cursor: pointer;
flex-grow: 1;
}

View File

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

View File

@ -6,7 +6,6 @@
<title>TeaSpeak-Web client templates</title>
</head>
<body>
<!-- main frame TODO tr -->
<script class="jsrender-template" id="tmpl_main" type="text/html">
<div class="app-container">
<div class="app">
@ -38,7 +37,7 @@
<div class="divider"></div>
<div class="button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
<div class="hide-small button-dropdown btn_away" title="{{tr 'Toggle away status' /}}">
<div class="buttons">
<div class="button icon_x32 client-away btn_away_toggle"></div>
<div class="button-dropdown">
@ -50,15 +49,36 @@
<div class="btn_away_message"><div class="icon client-away"></div><a>{{tr "Set away message" /}}</a></div>
</div>
</div>
<div class="button btn_mute_input">
<div class="hide-small button btn_mute_input">
<div class="icon_x32 client-input_muted" title="{{tr 'Mute/unmute microphone' /}}"></div>
</div>
<div class="button btn_mute_output">
<div class="hide-small button btn_mute_output">
<div class="icon_x32 client-output_muted" title="{{tr 'Mute/unmute headphones' /}}"></div>
</div>
<div class="divider"></div>
<div class="button-dropdown btn_token" title="{{tr 'Use token' /}}">
<div class="show-small button-dropdown dropdown-audio" title="{{tr 'Audio settings' /}}">
<div class="buttons">
<div class="button button-display icon_x32 client-music"></div>
<div class="button-dropdown">
<div class="arrow down"></div>
</div>
</div>
<div class="dropdown">
<div class="btn_mute_input" title="{{tr 'Mute/unmute microphone' /}}">
<div class="icon client-input_muted"></div>
<a>{{tr "Mute/unmute microphone" /}}</a>
</div>
<div class="btn_mute_output" title="{{tr 'Mute/unmute headphones' /}}">
<div class="icon client-output_muted"></div>
<a>{{tr "Mute/unmute headphones" /}}</a>
</div>
</div>
</div>
<div class="divider"></div>
<div class="button button-subscribe-mode">
<div class="icon_x32" title="{{tr 'Toggle channel subscribe mode' /}}"></div>
</div>
<div class="hide-small button-dropdown btn_token" title="{{tr 'Use token' /}}">
<div class="buttons">
<div class="button icon_x32 client-token btn_token_use"></div>
<div class="button-dropdown">
@ -72,13 +92,37 @@
</div>
<div style="width: 100%"></div>
<div class="button button-playlist-manage" title="{{tr 'Playlists' /}}">
<div class="icon-playlist-manage"></div>
<div class="show-small button-dropdown dropdown-servertools" title="{{tr 'Server tools' /}}">
<div class="buttons">
<div class="button button-display icon_x32 client-virtualserver_edit"></div>
<div class="button-dropdown">
<div class="arrow down"></div>
</div>
<div 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>

View File

@ -1,6 +1,10 @@
/// <reference path="client.ts" />
/// <reference path="connection/ConnectionBase.ts" />
/*
FIXME: Dont use item storage with base64! Use the larger cache API and drop IE support!
https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage#Browser_compatibility
*/
class FileEntry {
name: string;
datetime: number;

View File

@ -1,3 +1,5 @@
import LogType = log.LogType;
enum ChatType {
GENERAL,
SERVER,
@ -65,12 +67,11 @@ namespace MessageHelper {
}
if(objects.length < number)
console.warn(tr("Message to format contains invalid index (%o)"), number);
log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number);
result.push(...formatElement(objects[number]));
found = found + 1 + offset;
begin = found + 1;
console.log(tr("Offset: %d Number: %d"), offset, number);
} while(found++);
return result;
@ -93,7 +94,7 @@ namespace MessageHelper {
});
if(result.error) {
console.log("BBCode parse error: %o", result.errorQueue);
log.error(LogCategory.GENERAL, tr("BBCode parse error: %o"), result.errorQueue);
return formatElement(message);
}
@ -104,7 +105,7 @@ namespace MessageHelper {
class ChatMessage {
date: Date;
message: JQuery[];
private _htmlTag: JQuery<HTMLElement>;
private _html_tag: JQuery<HTMLElement>;
constructor(message: JQuery[]) {
this.date = new Date();
@ -117,8 +118,8 @@ class ChatMessage {
return str;
}
get htmlTag() {
if(this._htmlTag) return this._htmlTag;
get html_tag() {
if(this._html_tag) return this._html_tag;
let tag = $.spawn("div");
tag.addClass("message");
@ -128,26 +129,30 @@ class ChatMessage {
dateTag.css("margin-right", "4px");
dateTag.css("color", "dodgerblue");
this._htmlTag = tag;
this._html_tag = tag;
tag.append(dateTag);
this.message.forEach(e => e.appendTo(tag));
tag.hide();
return tag;
}
}
class ChatEntry {
handle: ChatBox;
readonly handle: ChatBox;
type: ChatType;
key: string;
history: ChatMessage[];
owner_unique_id?: string;
private _name: string;
private _htmlTag: any;
private _closeable: boolean;
private _unread : boolean;
private _html_tag: any;
private _flag_closeable: boolean = true;
private _flag_unread : boolean = false;
private _flag_offline: boolean = false;
onMessageSend: (text: string) => void;
onClose: () => boolean;
onClose: () => boolean = () => true;
constructor(handle, type : ChatType, key) {
this.handle = handle;
@ -155,8 +160,6 @@ class ChatEntry {
this.key = key;
this._name = key;
this.history = [];
this.onClose = function () { return true; }
}
appendError(message: string, ...args) {
@ -173,7 +176,7 @@ class ChatEntry {
this.history.push(entry);
while(this.history.length > 100) {
let elm = this.history.pop_front();
elm.htmlTag.animate({opacity: 0}, 200, function () {
elm.html_tag.animate({opacity: 0}, 200, function () {
$(this).detach();
});
}
@ -181,66 +184,75 @@ class ChatEntry {
let box = $(this.handle.htmlTag).find(".messages");
let mbox = $(this.handle.htmlTag).find(".message_box");
let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height();
mbox.append(entry.htmlTag);
entry.htmlTag.show().css("opacity", "0").animate({opacity: 1}, 100);
mbox.append(entry.html_tag);
entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100);
if(bottom) box.scrollTop(mbox.height());
} else {
this.unread = true;
this.flag_unread = true;
}
}
displayHistory() {
this.unread = false;
let box = $(this.handle.htmlTag).find(".messages");
let mbox = $(this.handle.htmlTag).find(".message_box");
this.flag_unread = false;
let box = this.handle.htmlTag.find(".messages");
let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */
mbox.empty();
for(let e of this.history) {
mbox.append(e.htmlTag);
if(e.htmlTag.is(":hidden")) e.htmlTag.show();
mbox.append(e.html_tag);
/* TODO Is this really totally useless?
Because its at least a performance bottleneck because is(...) recalculates the page style
if(e.htmlTag.is(":hidden"))
e.htmlTag.show();
*/
}
mbox.appendTo(box);
box.scrollTop(mbox.height());
}
get htmlTag() {
if(this._htmlTag) return this._htmlTag;
get html_tag() {
if(this._html_tag)
return this._html_tag;
let tag = $.spawn("div");
tag.addClass("chat");
if(this._flag_unread)
tag.addClass('unread');
if(this._flag_offline)
tag.addClass('offline');
if(this._flag_closeable)
tag.addClass('closeable');
tag.append("<div class=\"chatIcon icon " + this.chatIcon() + "\"></div>");
tag.append("<a class='name'>" + this._name + "</a>");
tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon()));
tag.append($.spawn("a").addClass("name").text(this._name));
let closeTag = $.spawn("div");
closeTag.addClass("btn_close icon client-tab_close_button");
if(!this._closeable) closeTag.hide();
tag.append(closeTag);
let tag_close = $.spawn("div");
tag_close.addClass("btn_close icon client-tab_close_button");
if(!this._flag_closeable) tag_close.hide();
tag.append(tag_close);
const _this = this;
tag.click(function () {
_this.handle.activeChat = _this;
});
tag.on("contextmenu", function (e) {
tag.click(() => { this.handle.activeChat = this; });
tag.on("contextmenu", (e) => {
e.preventDefault();
let actions = [];
let actions: ContextMenuEntry[] = [];
actions.push({
type: MenuEntryType.ENTRY,
icon: "",
name: tr("Clear"),
callback: () => {
_this.history = [];
_this.displayHistory();
this.history = [];
this.displayHistory();
}
});
if(_this.closeable) {
if(this.flag_closeable) {
actions.push({
type: MenuEntryType.ENTRY,
icon: "client-tab_close_button",
name: tr("Close"),
callback: () => {
chat.deleteChat(_this);
chat.deleteChat(this);
}
});
}
@ -251,18 +263,20 @@ class ChatEntry {
name: tr("Close all private tabs"),
callback: () => {
//TODO Implement this?
}
},
visible: false
});
spawn_context_menu(e.pageX, e.pageY, ...actions);
});
closeTag.click(function () {
if($.isFunction(_this.onClose) && !_this.onClose()) return;
_this.handle.deleteChat(_this);
tag_close.click(() => {
if($.isFunction(this.onClose) && !this.onClose())
return;
this.handle.deleteChat(this);
});
this._htmlTag = tag;
return tag;
return this._html_tag = tag;
}
focus() {
@ -271,33 +285,37 @@ class ChatEntry {
}
set name(newName : string) {
console.log(tr("Change name!"));
this._name = newName;
this.htmlTag.find(".name").text(this._name);
this.html_tag.find(".name").text(this._name);
}
set closeable(flag : boolean) {
if(this._closeable == flag) return;
set flag_closeable(flag : boolean) {
if(this._flag_closeable == flag) return;
this._closeable = flag;
console.log(tr("Set closeable: ") + this._closeable);
if(flag) this.htmlTag.find(".btn_close").show();
else this.htmlTag.find(".btn_close").hide();
this._flag_closeable = flag;
this.html_tag.toggleClass('closeable', flag);
}
set unread(flag : boolean) {
if(this._unread == flag) return;
this._unread = flag;
this.htmlTag.find(".chatIcon").attr("class", "chatIcon icon " + this.chatIcon());
if(flag) {
this.htmlTag.find(".name").css("color", "blue");
} else {
this.htmlTag.find(".name").css("color", "black");
}
set flag_unread(flag : boolean) {
if(this._flag_unread == flag) return;
this._flag_unread = flag;
this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon());
this.html_tag.toggleClass('unread', flag);
}
private chatIcon() : string {
if(this._unread) {
get flag_offline() { return this._flag_offline; }
set flag_offline(flag: boolean) {
if(flag == this._flag_offline)
return;
this._flag_offline = flag;
this.html_tag.toggleClass('offline', flag);
}
private chat_icon() : string {
if(this._flag_unread) {
switch (this.type) {
case ChatType.CLIENT:
return "client-new_chat";
@ -319,6 +337,10 @@ class ChatEntry {
class ChatBox {
//https://regex101.com/r/YQbfcX/2
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
htmlTag: JQuery;
chats: ChatEntry[];
private _activeChat: ChatEntry;
@ -359,10 +381,11 @@ class ChatBox {
return;
chat.serverChat().appendMessage(tr("Failed to send text message."));
console.error(tr("Failed to send server text message: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error);
});
};
this.serverChat().name = tr("Server chat");
this.serverChat().flag_closeable = false;
this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => {
if(!globalClient.serverConnection) {
@ -372,10 +395,11 @@ class ChatBox {
globalClient.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, globalClient.getClient().currentChannel()).catch(error => {
chat.channelChat().appendMessage(tr("Failed to send text message."));
console.error(tr("Failed to send channel text message: %o"), error);
log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error);
});
};
this.channelChat().name = tr("Channel chat");
this.channelChat().flag_closeable = false;
globalClient.permissions.initializedListener.push(flag => {
if(flag) this.activeChat0(this._activeChat);
@ -385,11 +409,15 @@ class ChatBox {
createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry {
let chat = new ChatEntry(this, type, key);
this.chats.push(chat);
this.htmlTag.find(".chats").append(chat.htmlTag);
this.htmlTag.find(".chats").append(chat.html_tag);
if(!this._activeChat) this.activeChat = chat;
return chat;
}
open_chats() : ChatEntry[] {
return this.chats;
}
findChat(key : string) : ChatEntry {
for(let e of this.chats)
if(e.key == key) return e;
@ -398,7 +426,7 @@ class ChatBox {
deleteChat(chat : ChatEntry) {
this.chats.remove(chat);
chat.htmlTag.detach();
chat.html_tag.detach();
if(this._activeChat === chat) {
if(this.chats.length > 0)
this.activeChat = this.chats.last();
@ -414,8 +442,38 @@ class ChatBox {
this._input_message.val("");
this._input_message.trigger("input");
/* preprocessing text */
const words = text.split(/[ \n]/);
for(let index = 0; index < words.length; index++) {
const flag_escaped = words[index].startsWith('!');
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
_try:
try {
const url = new URL(unescaped);
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
if(url.protocol !== 'http:' && url.protocol !== 'https:')
break _try;
if(flag_escaped)
words[index] = unescaped;
else {
text = undefined;
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
}
} catch(e) { /* word isn't an url */ }
if(unescaped.match(ChatBox.URL_REGEX)) {
if(flag_escaped)
words[index] = unescaped;
else {
text = undefined;
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
}
}
}
if(this._activeChat && $.isFunction(this._activeChat.onMessageSend))
this._activeChat.onMessageSend(text);
this._activeChat.onMessageSend(text || words.join(" "));
}
set activeChat(chat : ChatEntry) {
@ -427,27 +485,27 @@ class ChatBox {
private activeChat0(chat: ChatEntry) {
this._activeChat = chat;
for(let e of this.chats)
e.htmlTag.removeClass("active");
e.html_tag.removeClass("active");
let flagAllowSend = false;
let disable_input = !chat;
if(this._activeChat) {
this._activeChat.htmlTag.addClass("active");
this._activeChat.html_tag.addClass("active");
this._activeChat.displayHistory();
if(globalClient && globalClient.permissions && globalClient.permissions.initialized())
if(!disable_input && globalClient && globalClient.permissions && globalClient.permissions.initialized())
switch (this._activeChat.type) {
case ChatType.CLIENT:
flagAllowSend = true;
disable_input = false;
break;
case ChatType.SERVER:
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1);
break;
case ChatType.CHANNEL:
flagAllowSend = globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
disable_input = !globalClient.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1);
break;
}
}
this._input_message.prop("disabled", !flagAllowSend);
this._input_message.prop("disabled", disable_input);
}
get activeChat(){ return this._activeChat; }

View File

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

View File

@ -1,3 +1,4 @@
namespace connection {
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) {
@ -26,6 +27,7 @@ namespace connection {
this["notifychannelmoved"] = this.handleNotifyChannelMoved;
this["notifychanneledited"] = this.handleNotifyChannelEdited;
this["notifytextmessage"] = this.handleNotifyTextMessage;
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
this["notifyclientupdated"] = this.handleNotifyClientUpdated;
this["notifyserveredited"] = this.handleNotifyServerEdited;
this["notifyserverupdated"] = this.handleNotifyServerUpdated;
@ -37,6 +39,9 @@ namespace connection {
this["notifyservergroupclientadded"] = this.handleNotifyServerGroupClientAdd;
this["notifyservergroupclientdeleted"] = this.handleNotifyServerGroupClientRemove;
this["notifyclientchannelgroupchanged"] = this.handleNotifyClientChannelGroupChanged;
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
}
handle_command(command: ServerCommand) : boolean {
@ -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);
}
}
}
}

View File

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

View File

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

View File

@ -7,7 +7,8 @@ enum LogCategory {
GENERAL,
NETWORKING,
VOICE,
I18N
I18N,
IDENTITIES
}
namespace log {
@ -21,14 +22,15 @@ namespace log {
let category_mapping = new Map<number, string>([
[LogCategory.CHANNEL, "Channel "],
[LogCategory.CLIENT, "Channel "],
[LogCategory.CHANNEL_PROPERTIES, "Client "],
[LogCategory.CHANNEL_PROPERTIES, "Channel "],
[LogCategory.CLIENT, "Client "],
[LogCategory.SERVER, "Server "],
[LogCategory.PERMISSIONS, "Permission "],
[LogCategory.GENERAL, "General "],
[LogCategory.NETWORKING, "Network "],
[LogCategory.VOICE, "Voice "],
[LogCategory.I18N, "I18N "]
[LogCategory.I18N, "I18N "],
[LogCategory.IDENTITIES, "IDENTITIES "]
]);
export let enabled_mapping = new Map<number, boolean>([
@ -40,7 +42,8 @@ namespace log {
[LogCategory.GENERAL, true],
[LogCategory.NETWORKING, true],
[LogCategory.VOICE, true],
[LogCategory.I18N, false]
[LogCategory.I18N, false],
[LogCategory.IDENTITIES, true]
]);
loader.register_task(loader.Stage.LOADED, {
@ -109,10 +112,16 @@ namespace log {
name = "[%s] " + name;
optionalParams.unshift(category_mapping.get(category));
return new Group(level, category, name, optionalParams);
return new Group(GroupMode.PREFIX, level, category, name, optionalParams);
}
enum GroupMode {
NATIVE,
PREFIX
}
export class Group {
readonly mode: GroupMode;
readonly level: LogType;
readonly category: LogCategory;
readonly enabled: boolean;
@ -123,9 +132,11 @@ namespace log {
private readonly optionalParams: any[][];
private _collapsed: boolean = true;
private initialized = false;
private _log_prefix: string;
constructor(level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
constructor(mode: GroupMode, level: LogType, category: LogCategory, name: string, optionalParams: any[][], owner: Group = undefined) {
this.level = level;
this.mode = mode;
this.category = category;
this.name = name;
this.optionalParams = optionalParams;
@ -133,7 +144,7 @@ namespace log {
}
group(level: LogType, name: string, ...optionalParams: any[]) : Group {
return new Group(level, this.category, name, optionalParams, this);
return new Group(this.mode, level, this.category, name, optionalParams, this);
}
collapsed(flag: boolean = true) : this {
@ -146,19 +157,43 @@ namespace log {
return this;
if(!this.initialized) {
if(this.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;
}
}
}

View File

@ -238,15 +238,15 @@ function main() {
chat = new ChatBox($("#chat"));
globalClient.setup();
if(settings.static("connect_default", false) && settings.static("connect_address", "")) {
const profile_uuid = settings.static("connect_profile") as string;
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
console.log("UUID: %s", profile_uuid);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static("connect_address", "");
const username = settings.static("connect_username", "Another TeaSpeak user");
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
const password = settings.static("connect_password", "");
const password_hashed = settings.static("connect_password_hashed", false);
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
if(profile && profile.valid()) {
globalClient.startConnection(address, profile, username, password.length > 0 ? {

View File

@ -20,8 +20,7 @@ namespace profiles.identities {
authentication_method: this.identity.type(),
client_nickname: this.identity.name()
}).catch(error => {
console.error(tr("Failed to initialize name based handshake. Error: %o"), error);
log.error(LogCategory.IDENTITIES, tr("Failed to initialize name based handshake. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")");

View File

@ -19,7 +19,7 @@ namespace profiles.identities {
authentication_method: this.identity.type(),
data: this.identity.data_json()
}).catch(error => {
console.error(tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
@ -32,7 +32,7 @@ namespace profiles.identities {
this.connection.send_command("handshakeindentityproof", {
proof: this.identity.data_sign()
}).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error);
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;

View File

@ -214,7 +214,7 @@ namespace profiles.identities {
authentication_method: this.identity.type(),
publicKey: this.identity.public_key
}).catch(error => {
console.error(tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
@ -230,7 +230,7 @@ namespace profiles.identities {
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error);
log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
@ -281,7 +281,7 @@ namespace profiles.identities {
resolve();
};
this._worker.onerror = event => {
console.error("POW Worker error %o", event);
log.error(LogCategory.IDENTITIES, tr("POW Worker error %o"), event);
clearTimeout(timeout_id);
reject("Failed to load worker (" + event.message + ")");
};
@ -394,7 +394,7 @@ namespace profiles.identities {
};
});
} catch(error) {
console.warn("Failed to finalize POW worker! (%o)", error);
log.error(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error);
}
this._worker.terminate();
@ -402,7 +402,7 @@ namespace profiles.identities {
}
private handle_message(message: any) {
console.log("Received message: %o", message);
log.info(LogCategory.IDENTITIES, tr("Received message: %o"), message);
}
}
@ -412,7 +412,7 @@ namespace profiles.identities {
try {
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(e) {
console.error(tr("Could not generate a new key: %o"), e);
log.error(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e);
throw "Failed to generate keypair";
}
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
@ -483,7 +483,7 @@ namespace profiles.identities {
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
this.initialize().catch(error => {
console.error("Failed to initialize TeaSpeakIdentity (%s)", error);
log.error(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error);
this._initialized = false;
});
}
@ -633,7 +633,7 @@ namespace profiles.identities {
try {
await Promise.all(initialize_promise);
} catch(error) {
console.error(error);
log.error(LogCategory.IDENTITIES, error);
throw "failed to initialize";
}
}
@ -688,7 +688,7 @@ namespace profiles.identities {
if(worker.current_level() > best_level) {
this.hash_number = worker.current_hash();
console.log("Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
log.info(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
best_level = worker.current_level();
if(callback_level)
callback_level(best_level);
@ -712,7 +712,7 @@ namespace profiles.identities {
}).catch(error => {
worker_promise.remove(p);
console.warn("POW worker error %o", error);
log.warn(LogCategory.IDENTITIES, "POW worker error %o", error);
reject(error);
return Promise.resolve();
@ -736,7 +736,7 @@ namespace profiles.identities {
try {
await Promise.all(finalize_promise);
} catch(error) {
console.error(error);
log.error(LogCategory.IDENTITIES, error);
throw "failed to finalize";
}
}
@ -761,14 +761,14 @@ namespace profiles.identities {
try {
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
} catch(error) {
console.error(error);
log.error(LogCategory.IDENTITIES, error);
throw "failed to create crypto sign key";
}
try {
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(error) {
console.error(error);
log.error(LogCategory.IDENTITIES, error);
throw "failed to create crypto key";
}
@ -776,7 +776,7 @@ namespace profiles.identities {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
} catch(error) {
console.error(error);
log.error(LogCategory.IDENTITIES, error);
throw "failed to calculate unique id";
}

View File

@ -12,6 +12,15 @@ if(typeof(customElements) !== "undefined") {
}
}
/* T = value type */
interface SettingsKey<T> {
key: string;
fallback_keys?: string | string[];
fallback_imports?: {[key: string]:(value: string) => T};
description?: string;
}
class StaticSettings {
private static _instance: StaticSettings;
static get instance() : StaticSettings {
@ -20,12 +29,14 @@ class StaticSettings {
return this._instance;
}
protected static transformStO?<T>(input?: string, _default?: T) : T {
protected static transformStO?<T>(input?: string, _default?: T, default_type?: string) : T {
default_type = default_type || typeof _default;
if (typeof input === "undefined") return _default;
if (typeof _default === "string") return input as any;
else if (typeof _default === "number") return parseInt(input) as any;
else if (typeof _default === "boolean") return (input == "1" || input == "true") as any;
else if (typeof _default === "undefined") return input as any;
if (default_type === "string") return input as any;
else if (default_type === "number") return parseInt(input) as any;
else if (default_type === "boolean") return (input == "1" || input == "true") as any;
else if (default_type === "undefined") return input as any;
return JSON.parse(input) as any;
}
@ -37,6 +48,35 @@ class StaticSettings {
return JSON.stringify(input);
}
protected static resolveKey<T>(key: SettingsKey<T>, _default: T, resolver: (key: string) => string | boolean, default_type?: string) : T {
let value = resolver(key.key);
if(!value) {
/* trying fallbacks */
for(const fallback of key.fallback_keys || []) {
value = resolver(fallback);
if(typeof(value) === "string") {
/* fallback key succeeded */
const importer = (key.fallback_imports || {})[fallback];
if(importer)
return importer(value);
break;
}
}
}
if(typeof(value) !== 'string')
return _default;
return StaticSettings.transformStO(value as string, _default, default_type);
}
protected static keyify<T>(key: string | SettingsKey<T>) : SettingsKey<T> {
if(typeof(key) === "string")
return {key: key};
if(typeof(key) === "object" && key.key)
return key;
throw "key is not a key";
}
protected _handle: StaticSettings;
protected _staticPropsTag: JQuery;
@ -59,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();

View File

@ -286,7 +286,7 @@ namespace sound {
try {
console.log(tr("Decoding data"));
context.decodeAudioData(buffer, result => {
console.log(tr("Got decoded data"));
log.info(LogCategory.VOICE, tr("Got decoded data"));
file.cached = result;
play(sound, options);
}, error => {

View File

@ -14,6 +14,12 @@ namespace ChannelType {
}
}
enum ChannelSubscribeMode {
SUBSCRIBED,
UNSUBSCRIBED,
INHERITED
}
class ChannelProperties {
channel_order: number = 0;
channel_name: string = "";
@ -71,6 +77,9 @@ class ChannelEntry {
private _cached_channel_description_promise_resolve: any = undefined;
private _cached_channel_description_promise_reject: any = undefined;
private _flag_subscribed: boolean;
private _subscribe_mode: ChannelSubscribeMode;
constructor(channelId, channelName, parent = null) {
this.properties = new ChannelProperties();
this.channelId = channelId;
@ -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

View File

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

View File

@ -19,6 +19,7 @@ class ControlBar {
private _away: boolean;
private _query_visible: boolean;
private _awayMessage: string;
private _channel_subscribe_all: boolean;
private codec_supported: boolean = false;
private support_playback: boolean = false;
@ -40,39 +41,43 @@ class ControlBar {
this.htmlTag.find(".btn_open_settings").on('click', this.onOpenSettings.bind(this));
this.htmlTag.find(".btn_permissions").on('click', this.onPermission.bind(this));
this.htmlTag.find(".btn_banlist").on('click', this.onBanlist.bind(this));
this.htmlTag.find(".button-subscribe-mode").on('click', this.on_toggle_channel_subscribe_all.bind(this));
this.htmlTag.find(".button-playlist-manage").on('click', this.on_playlist_manage.bind(this));
let dropdownify = (tag: JQuery) => {
tag.find(".button-dropdown").on('click', () => {
tag.addClass("displayed");
}).hover(() => {
console.log("Add");
tag.addClass("displayed");
}, () => {
if(tag.find(".dropdown:hover").length > 0)
return;
console.log("Removed");
tag.removeClass("displayed");
});
tag.on('mouseleave', () => {
tag.removeClass("displayed");
});
};
{
let tokens = this.htmlTag.find(".btn_token");
tokens.find(".button-dropdown").on('click', () => {
tokens.find(".dropdown").addClass("displayed");
});
tokens.on('mouseleave', () => {
tokens.find(".dropdown").removeClass("displayed");
});
dropdownify(tokens);
tokens.find(".btn_token_use").on('click', this.on_token_use.bind(this));
tokens.find(".btn_token_list").on('click', this.on_token_list.bind(this));
}
{
let away = this.htmlTag.find(".btn_away");
away.find(".button-dropdown").on('click', () => {
away.find(".dropdown").addClass("displayed");
});
away.on('mouseleave', () => {
away.find(".dropdown").removeClass("displayed");
});
dropdownify(away);
away.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this));
away.find(".btn_away_message").on('click', this.on_away_set_message.bind(this));
}
{
let bookmark = this.htmlTag.find(".btn_bookmark");
bookmark.find(".button-dropdown").on('click', () => {
bookmark.find("> .dropdown").addClass("displayed");
});
bookmark.on('mouseleave', () => {
bookmark.find("> .dropdown").removeClass("displayed");
});
dropdownify(bookmark);
bookmark.find(".btn_bookmark_list").on('click', this.on_bookmark_manage.bind(this));
bookmark.find(".btn_bookmark_add").on('click', this.on_bookmark_server_add.bind(this));
@ -81,22 +86,30 @@ class ControlBar {
}
{
let query = this.htmlTag.find(".btn_query");
query.find(".button-dropdown").on('click', () => {
query.find(".dropdown").addClass("displayed");
});
query.on('mouseleave', () => {
query.find(".dropdown").removeClass("displayed");
});
dropdownify(query);
query.find(".btn_query_toggle").on('click', this.on_query_visibility_toggle.bind(this));
query.find(".btn_query_create").on('click', this.on_query_create.bind(this));
query.find(".btn_query_manage").on('click', this.on_query_manage.bind(this));
}
/* Mobile dropdowns */
{
const dropdown = this.htmlTag.find(".dropdown-audio");
dropdownify(dropdown);
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
}
{
const dropdown = this.htmlTag.find(".dropdown-servertools");
dropdownify(dropdown);
dropdown.find(".button-display").on('click', () => dropdown.addClass("displayed"));
}
//Need an initialise
this.muteInput = settings.static_global("mute_input", false);
this.muteOutput = settings.static_global("mute_output", false);
this.query_visible = settings.static_global("show_server_queries", false);
this.muteInput = settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false);
this.muteOutput = settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false);
this.query_visible = settings.static_global(Settings.KEY_CONTROL_SHOW_QUERIES, false);
this.channel_subscribe_all = settings.static_global(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, true);
}
@ -125,22 +138,20 @@ class ControlBar {
this._muteInput = flag;
let tag = this.htmlTag.find(".btn_mute_input");
if(flag) {
if(!tag.hasClass("activated"))
tag.addClass("activated");
tag.find(".icon_x32").attr("class", "icon_x32 client-input_muted");
} else {
if(tag.hasClass("activated"))
tag.removeClass("activated");
tag.find(".icon_x32").attr("class", "icon_x32 client-capture");
}
const tag_icon = tag.find(".icon_x32, .icon");
tag.toggleClass('activated', flag)
tag_icon
.toggleClass('client-input_muted', flag)
.toggleClass('client-capture', !flag);
if(this.handle.serverConnection.connected)
if(this.handle.serverConnection.connected())
this.handle.serverConnection.send_command("clientupdate", {
client_input_muted: this._muteInput
});
settings.changeGlobal("mute_input", this._muteInput);
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, this._muteInput);
this.updateMicrophoneRecordState();
}
@ -150,22 +161,21 @@ class ControlBar {
if(this._muteOutput == flag) return;
this._muteOutput = flag;
let tag = this.htmlTag.find(".btn_mute_output");
if(flag) {
if(!tag.hasClass("activated"))
tag.addClass("activated");
tag.find(".icon_x32").attr("class", "icon_x32 client-output_muted");
} else {
if(tag.hasClass("activated"))
tag.removeClass("activated");
tag.find(".icon_x32").attr("class", "icon_x32 client-volume");
}
if(this.handle.serverConnection.connected)
let tag = this.htmlTag.find(".btn_mute_output");
const tag_icon = tag.find(".icon_x32, .icon");
tag.toggleClass('activated', flag)
tag_icon
.toggleClass('client-output_muted', flag)
.toggleClass('client-volume', !flag);
if(this.handle.serverConnection.connected())
this.handle.serverConnection.send_command("clientupdate", {
client_output_muted: this._muteOutput
});
settings.changeGlobal("mute_output", this._muteOutput);
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, this._muteOutput);
this.updateMicrophoneRecordState();
}
@ -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;
}
}

View File

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

View File

@ -31,11 +31,11 @@ namespace Modals {
input_nickname.attr("placeholder", "");
let address = input_address.val().toString();
settings.changeGlobal("connect_address", address);
settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address);
let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN);
let nickname = input_nickname.val().toString();
settings.changeGlobal("connect_name", nickname);
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname);
let flag_nickname = (nickname.length == 0 && selected_profile && selected_profile.default_username.length > 0) || nickname.length >= 3 && nickname.length <= 32;
input_address.attr('pattern', flag_address ? null : '^[a]{1000}$').toggleClass('is-invalid', !flag_address);
@ -48,8 +48,8 @@ namespace Modals {
}
};
input_nickname.val(settings.static_global("connect_name", undefined));
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global("connect_address", defaultHost.url));
input_nickname.val(settings.static_global(Settings.KEY_CONNECT_USERNAME, undefined));
input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url));
input_address
.on("keyup", () => updateFields())
.on('keydown', event => {
@ -150,7 +150,7 @@ namespace Modals {
},
width: '70%',
//closeable: false
//flag_closeable: false
});
connectModal.open();
}

View File

@ -755,4 +755,46 @@ class ChannelTree {
get_first_channel?() : ChannelEntry {
return this.channel_first;
}
unsubscribe_all_channels(subscribe_specified?: boolean) {
if(!this.client.serverConnection || !this.client.serverConnection.connected())
return;
this.client.serverConnection.send_command('channelunsubscribeall').then(() => {
const channels: number[] = [];
for(const channel of this.channels) {
if(channel.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED)
channels.push(channel.getChannelId());
}
if(channels.length > 0) {
this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
console.warn(tr("Failed to subscribe to specific channels (%o)"), channels);
});
}
}).catch(error => {
console.warn(tr("Failed to unsubscribe to all channels! (%o)"), error);
});
}
subscribe_all_channels() {
if(!this.client.serverConnection || !this.client.serverConnection.connected())
return;
this.client.serverConnection.send_command('channelsubscribeall').then(() => {
const channels: number[] = [];
for(const channel of this.channels) {
if(channel.subscribe_mode == ChannelSubscribeMode.UNSUBSCRIBED)
channels.push(channel.getChannelId());
}
if(channels.length > 0) {
this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
console.warn(tr("Failed to unsubscribe to specific channels (%o)"), channels);
});
}
}).catch(error => {
console.warn(tr("Failed to subscribe to all channels! (%o)"), error);
});
}
}

View File

@ -40,11 +40,14 @@ var TabFunctions = {
let content = $.spawn("div");
content.addClass("tab-content");
content.append($.spawn("div").addClass("height-watcher"));
let silentContent = $.spawn("div");
silentContent.addClass("tab-content-invisible");
/* add some kind of min height */
const update_height = () => {
const height_watcher = tag.find("> .tab-content .height-watcher");
const entries: JQuery = tag.find("> .tab-content-invisible x-content, > .tab-content x-content");
console.error(entries);
let max_height = 0;
@ -56,13 +59,7 @@ var TabFunctions = {
max_height = height;
});
console.error("HIGHT: " + max_height);
entries.each((_, _e) => {
const entry = $(_e);
entry.animate({
'min-height': max_height + "px"
}, 250);
})
height_watcher.css('min-height', max_height + "px");
};
template.find("x-entry").each( (_, _entry) => {

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

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

File diff suppressed because one or more lines are too long

1
vendor/ua-parser-js vendored Submodule

@ -0,0 +1 @@
Subproject commit 732cf5834e6a8605c75f48db492a14426345d475

View File

@ -19,6 +19,8 @@ html, body {
bottom: 40px;
top: 10px;
transition: all .5s linear;
.app {
width: 100%;
height: 100%;