Updating the channel edit modal and some minor bugfixing

master
WolverinDEV 2020-12-22 13:32:56 +01:00
parent 218a9d0600
commit 810f50f336
31 changed files with 2304 additions and 2424 deletions

View File

@ -1,4 +1,14 @@
# Changelog:
* **22.12.20**
- Fixed missing channel status icon update on channel type edit
- Improved channel edit UI experience and fixed some bugs
- Fixed the invalid flags bug
- Show "Not supported" for options which the server does not support
- Added the option to edit the channel sidebar mode
- Remove the phonetic name and the channel title (Both are not used)
- Improved property validation
- Adjusting property editibility according to the clients permissions
* **18.12.20**
- Added the ability to send private messages to multiple clients
- Channel client count now updates within the side bar header

View File

@ -1,7 +1,6 @@
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/properties.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/main-layout.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/general.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/context_menu.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/frame-chat.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/server-log.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/scroll.scss"
@ -16,7 +15,6 @@ import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/m
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-banclient.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-banlist.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-bookmarks.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-channel.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-channelinfo.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-clientinfo.scss"
import "!style-loader!css-loader?url=false!sass-loader?sourceMap=true!./static/modal-connect.scss"

View File

@ -1,153 +0,0 @@
.context-menu {
overflow: visible;
display: none;
z-index: 120000;
position: absolute;
.context-menu-container {
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
padding: 3px;
&.left {
margin-left: -100%;
width: 100%;
}
* {
font-family: Arial, serif;
font-size: 12px;
white-space: pre;
line-height: 1;
vertical-align: middle;
}
hr {
margin-top: 8px;
margin-bottom: 8px;
}
.entry {
/*padding: 8px 12px;*/
padding-right: 12px;
cursor: pointer;
list-style-type: none;
transition: all .3s ease;
user-select: none;
align-items: center;
display: flex;
&.disabled {
pointer-events: none;
background-color: lightgray;
cursor: not-allowed;
}
&:hover:not(.disabled) {
background-color: #DEF;
}
}
.icon_empty, .icon {
margin-right: 4px;
}
.arrow {
cursor: pointer;
pointer-events: all;
width: 7px;
height: 7px;
padding: 0;
margin-right: 5px;
margin-left: 5px;
position: absolute;
right: 3px;
}
.sub-container {
margin-right: -3px;
padding-right: 24px;
position: relative;
&:hover {
.sub-menu {
display: block;
}
}
}
.sub-menu {
display: none;
left: 100%;
top: -4px;
position: absolute;
margin-left: 0;
}
}
}
/* we call it "ccheckbox" else it will be messed up the the global checkbox */
.ccheckbox {
margin-top: 1px;
margin-left: 1px;
display: block;
position: relative;
padding-left: 14px;
margin-bottom: 12px;
cursor: pointer;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Hide the browser's default checkbox */
input {
position: absolute;
opacity: 0;
cursor: pointer;
display: none;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 11px;
width: 11px;
background-color: #eee;
&:after {
content: "";
position: absolute;
display: none;
left: 4px;
top: 1px;
width: 3px;
height: 7px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
}
&:hover input ~ .checkmark {
background-color: #ccc;
}
input:checked ~ .checkmark {
background-color: #2196F3;
}
input:checked ~ .checkmark:after {
display: block;
}
}

View File

@ -1,815 +0,0 @@
$required_notab_height: 800px;
@import "mixin";
@import "properties";
.modal-body.modal-channel {
display: flex!important;
flex-direction: column!important;
justify-content: stretch!important;
max-height: calc(100vh - 10em)!important;
padding: 1em!important;
input, textarea, select {
width: 100%;
}
select {
margin-left: 0!important;
height: 2.5em!important;
}
textarea {
padding: .5em;
}
.container-general {
display: flex;
flex-direction: column;
justify-content: stretch;
flex-shrink: 0;
> div:not(:first-of-type) {
flex-grow: 0;
flex-shrink: 0;
margin-top: 1em;
}
.container-name-icon {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
.container-icon-select {
position: relative;
height: 2.5em;
border-radius: .2em;
margin-left: 1em;
display: flex;
flex-direction: row;
justify-content: flex-start;
cursor: pointer;
background-color: #121213;
border: 1px solid #0d0d0d;
.icon-preview {
height: 100%;
width: 3em;
border: none;
border-right: 1px solid #0d0d0d;
display: flex;
flex-direction: column;
justify-content: space-around;
> div {
align-self: center;
}
@include transition(border-color $button_hover_animation_time ease-in-out);
}
.container-dropdown {
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
width: 1.5em;
.button {
text-align: center;
.arrow {
border-color: #999999;
}
}
.dropdown {
display: none;
position: absolute;
width: max-content;
top: calc(2.5em - 1px);
flex-direction: column;
justify-content: flex-start;
background-color: #121213;
border: 1px solid #0d0d0d;
border-radius: .2em 0 .2em .2em;
right: -1px;
.entry {
padding: .5em;
&:not(:last-of-type) {
border: none;
border-bottom: 1px solid #0d0d0d;
}
&:hover {
background-color: #17171a;
}
}
}
&:hover {
border-bottom-right-radius: 0;
.dropdown {
display: flex;
}
}
}
&:hover {
background-color: #17171a;
border-color: hsla(0, 0%, 20%, 1);
.icon-preview {
border-color: hsla(0, 0%, 20%, 1);
}
}
@include transition(border-color $button_hover_animation_time ease-in-out);
}
}
.container-description {
position: relative;
flex-grow: 1!important;
flex-shrink: 1!important;
min-height: 5em;
max-height: 22.5em;
border-radius: .2em;
border: 1px solid #111112;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: stretch;
.toolbar {
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
height: 2.5em;
background-color: #17171a;
font-size: .8em;
padding: .25em;
.button {
cursor: pointer;
padding: .5em;
&:not(:first-child) {
margin-left: .25em;
}
border-radius: .2em;
border: 1px solid #111112;
background-color: #121213;
height: 2em;
width: 2em;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-self: center;
&.button-bold {
font-weight: bold;
}
&.button-italic {
font-style: italic;
}
&.button-underline {
text-decoration: underline;
}
&.button-color {
input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
}
}
&:hover {
background-color: #0f0f0f;
@include transition(background-color $button_hover_animation_time);
}
}
}
> .input-boxed {
flex-shrink: 1;
flex-grow: 1;
min-height: 2.5em;
height: 5em;
max-height: 20em;
border: none;
border-radius: 0;
border-top: 1px solid #111112;
overflow-x: hidden;;
overflow-y: auto;
resize: vertical;
@include chat-scrollbar-vertical();
}
&:focus-within {
background-color: #131b22;
//border-color: #284262;
}
}
}
.mode-container {
flex-grow: 1;
flex-shrink: 1;
min-height: min-content;
display: flex;
position: relative;
@include transition(.25s ease-in-out);
}
.container-advanced, .container-simple {
flex-grow: 1;
flex-shrink: 1;
margin-top: 1em;
min-width: 20em;
width: 50em;
&.hidden {
position: absolute;
top: 0;
}
&.container-simple.hidden {
transform: translate(-100%, -100%);
}
&.container-advanced.hidden {
transform: translate(100%, 100%);
}
@include transition(.25s ease-in-out);
.header {
text-align: center;
color: #548abc;
}
fieldset {
padding: 0;
width: 100%;
}
label {
display: flex;
flex-direction: row;
justify-content: stretch;
/* total height 2.5em */
margin-top: .5em;
margin-bottom: .5em;
height: 1.5em;
cursor: pointer;
* {
align-self: center;
}
a {
margin-left: .5em;
margin-right: .5em;
}
.form-group {
margin: -.5em 0!important;
padding: 0!important;
input {
height: 1.5em!important;
}
}
}
/* radio buttons */
$icon_width: 1.7em; /* equal to the label height */
.input-boxed {
position: relative;
height: 1.7em;
margin-left: 2.5em;
flex-grow: 1;
flex-shrink: 1;
min-width: 4em;
display: flex;
flex-direction: row;
justify-content: stretch;
.container-tooltip {
flex-shrink: 0;
flex-grow: 0;
position: relative;
width: $icon_width;
display: flex;
flex-direction: column;
justify-content: center;
img {
height: 1em;
width: 1em;
align-self: center;
font-size: 1.2em;
}
.tooltip {
display: none;
}
}
}
.container-type, .container-codec, .container-sort {
padding-top: .5em;
}
.container-talk {
.input-boxed {
margin-left: 0!important;
height: 2.5em;
.container-tooltip {
width: 2.5em!important;
}
}
}
}
.container-advanced {
display: flex;
flex-direction: column;
justify-content: stretch;
min-height: 5em;
border-radius: .2em;
border: 1px solid #111112;
background-color: #17171a;
.categories {
height: 2.5em;
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
border-bottom: 1px solid #1d1d1d;
.entry {
padding: .5em;
text-align: center;
flex-grow: 1;
flex-shrink: 1;
cursor: pointer;
&:hover {
color: #b6c4d6;
}
&.selected {
border-bottom: 3px solid #245184;
margin-bottom: -1px;
color: #245184;
}
@include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time);
}
}
.bodies {
position: relative;
flex-shrink: 1;
flex-grow: 1;
display: flex;
justify-content: stretch;
min-height: 12em;
height: 20em;
.body {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: .5em;
display: flex;
justify-content: stretch;
overflow: auto;
@include chat-scrollbar-vertical();
&.hidden {
display: none;
}
&.container-standard {
flex-direction: column;
overflow: visible;
.container-top, .container-bottom {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: row;
justify-content: stretch;
min-height: 5em;
}
.container-right, .container-left {
flex-shrink: 1;
flex-grow: 1;
min-width: 3em;
width: 50%;
display: flex;
flex-direction: column;
justify-content: start;
}
.container-top {
border-bottom: 2px solid #111113;
.container-left, .container-right {
padding-bottom: .5em;
}
}
.container-bottom {
.container-left, .container-right {
padding-top: .5em;
}
}
.container-left {
border-right: 2px solid #111113;
padding-right: .5em;
}
.container-right {
border: none;
padding-left: .5em;
}
.container-perm-default {
display: flex;
flex-direction: row;
justify-content: space-between;
> * {
margin-bottom: 0;
margin-top: 0;
align-self: center;
}
.container-default-channel {
display: inline-flex;
flex-direction: row;
justify-content: flex-end;
}
}
}
&.container-permissions {
flex-direction: row;
overflow: visible;
.container-right, .container-left {
flex-shrink: 1;
flex-grow: 1;
min-width: 3em;
width: 50%;
display: flex;
flex-direction: column;
justify-content: start;
}
.container-left {
padding-right: .5em;
border-right: 2px solid #111113;
}
.container-right {
padding-left: .5em;
}
.container-permission {
display: flex;
flex-direction: row;
justify-content: stretch;
margin-top: .5em;
margin-bottom: .5em;
.name {
flex-grow: 0;
flex-shrink: 0;
width: 8em;
align-self: center;
}
.input-boxed {
align-self: center;
margin-left: 0!important;
}
}
}
&.container-audio {
overflow: visible;
flex-direction: column;
.container-top {
width: 100%;
display: flex;
flex-direction: row;
justify-content: stretch;
.container-right, .container-left {
border-bottom: 2px solid #111113;
padding-bottom: .5em;
}
.container- {
border-right: 2px solid #111113;
}
}
.container-bottom {
width: 100%;
padding-top: .5em;
display: flex;
flex-direction: column;
justify-content: flex-start;
text-align: center;
.container-needed-bandwidth {
padding-left: .5em;
font-weight: bold;
}
.hint {
color: #383838;
font-size: .8em;
}
}
.container-right, .container-left {
flex-shrink: 1;
flex-grow: 1;
width: 50%;
min-width: 3em;
height: unset;
display: flex;
flex-direction: column;
justify-content: start;
}
.container-left {
padding-right: .5em;
border-right: 2px solid #111113;
}
.container-right {
border: none;
padding-left: .5em;
}
}
&.container-misc {
flex-direction: column;
overflow: visible;
.container-other {
display: flex;
flex-direction: column;
justify-content: flex-start;
.container-phonetic, .container-delay, .container-encrypt {
flex-grow: 0;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: stretch;
padding-top: .5em;
padding-bottom: .5em;
> a {
flex-grow: 0;
flex-shrink: 0;
width: 10em;
align-self: center;
}
> button {
flex-grow: 0;
flex-shrink: 0;
width: 5em;
/* results in a height of 1.7em */
height: 2em;
font-size: .85em;
align-self: center;
margin-left: 1em;
}
> input, .input-boxed {
flex-grow: 1;
flex-shrink: 1;
align-self: center;
margin-left: 0;
}
}
}
}
}
}
}
.container-simple {
display: flex;
flex-direction: row;
justify-content: stretch;
min-height: 5em;
border-radius: 0.2em;
border: 1px solid #111112;
background-color: #17171a;
padding: .5em;
.container-left, .container-right {
flex-grow: 1;
flex-shrink: 1;
width: 50%;
}
.container-left {
padding-right: .5em;
border-right: 2px solid #111113;
}
.container-right {
padding-left: .5em;
}
.container-perm-default {
display: flex;
flex-direction: row;
justify-content: space-between;
> * {
margin-bottom: 0;
margin-top: 0;
align-self: center;
}
.container-default-channel {
display: inline-flex;
flex-direction: row;
justify-content: flex-end;
}
}
.container-talk {
padding-top: .5em;
}
}
.container-buttons {
margin-top: 1em;
display: flex;
flex-direction: row;
justify-content: stretch;
flex-shrink: 0;
flex-grow: 0;
.spacer {
flex-grow: 1;
flex-shrink: 1;
}
> *:not(.spacer) {
flex-grow: 0;
flex-shrink: 0;
}
label {
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: flex-start;
> * {
align-self: center;
}
}
button {
&:not(:last-of-type) {
margin-right: 1em;
}
}
a {
padding-left: .25em;
}
}
}

View File

@ -68,6 +68,11 @@
padding: .5em;
flex-direction: row;
display: flex;
flex-wrap: wrap;
align-content: baseline;
&.container-icons-local {
font-size: 16px;
}

View File

@ -583,594 +583,6 @@
<!-- Template for channel create & edit-->
<script class="jsrender-template" id="tmpl_channel_edit" type="text/html">
<div> <!-- only for rendering -->
<div class="container-general">
<div class="container-name-icon">
<input type="text" class="input-boxed channel_name" placeholder="{{tr 'Channel name' /}}"
value="{{>channel_name}}"/>
<div class="container-icon-select">
<div class="button-select-icon icon-preview">
<node key="channel_icon_general"/>
</div>
<div class="container-dropdown">
<div class="button">
<div class="arrow down"></div>
</div>
<div class="dropdown">
<div class="entry button-select-icon">{{tr "Edit icon" /}}</div>
<div class="entry button-icon-remove">{{tr "Remove icon" /}}</div>
</div>
</div>
</div>
</div>
<div class="container-password">
<input class="input-boxed channel_password" placeholder="{{tr 'Password' /}}" type="password"
id="field_channel_password_{{rnd '0~13377331'/}}" {{if channel_flag_password}}
value="WolverinDEV" {{/if}}/>
</div>
<div class="container-topic">
<input class="input-boxed channel_topic" placeholder="{{tr 'Topic' /}}"
value="{{>channel_topic}}"/>
</div>
<div class="container-description">
<div class="toolbar">
<div class="button button-bold">B</div>
<div class="button button-italic">I</div>
<div class="button button-underline">U</div>
<label class="button button-color">
<input type="color" value="#FF0000">
<a class="rainbow-letter">C</a>
</label>
</div>
<textarea class="input-boxed channel_description" placeholder="{{tr 'Description' /}}">{{>channel_description}}</textarea>
</div>
</div>
<div class="mode-container">
<div class="container-simple">
<div class="container-left border">
<div class="header">{{tr "Channel Options" /}}</div>
<div class="content">
<div class="container-type">
<a>{{tr "Channel Type"/}}</a>
<select name="channel-type" class="input-boxed channel-type">
<option name="channel-type" value="temp">{{tr "Temporary" /}}</option>
<option name="channel-type" value="semi">{{tr "Semi-Permanent" /}}</option>
<option name="channel-type" value="perm">{{tr "Permanent" /}}</option>
<option name="channel-type" value="def">{{tr "Default Channel" /}}</option>
</select>
</div>
<div class="container-codec">
<a>{{tr "Channel Codec"/}}</a>
<select name="voice_template" class="input-boxed channel-codec">
<option name="voice_template" value="custom" style="display: none">{{tr "Custom (Advanced)" /}}
</option>
<option name="voice_template" value="voice_mobile">{{tr "Mobile" /}}</option>
<option name="voice_template" value="voice_desktop">{{tr "Voice" /}}</option>
<option name="voice_template" value="music">{{tr "Music" /}}</option>
</select>
</div>
</div>
</div>
<div class="container-right">
<div class="header">{{tr "Sorting and Talk power" /}}</div>
<div class="content">
<div class="container-sort">
<a>{{tr "Sort this channel after:"/}}</a>
<select class="input-boxed order_id">
</select>
</div>
<div class="container-talk">
<a>&nbsp;</a>
<div class="input-boxed">
<a class="prefix">{{tr "Talk power:" /}}</a>
<input type="number" type="number" min="0" name="talk_power"
value="{{if channel_needed_talk_power}}{{:channel_needed_talk_power}}{{else}}0{{/if}}">
<div class="container-tooltip tooltip-permission-subscribe">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to talk in this channel" /}}</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-advanced">
<div class="categories">
<div class="entry" container="container-standard">{{tr "Standard" /}}</div>
<div class="entry" container="container-audio">{{tr "Audio" /}}</div>
<div class="entry" container="container-permissions">{{tr "Permissions" /}}</div>
<div class="entry" container="container-misc">{{tr "Misc" /}}</div>
</div>
<div class="bodies">
<div class="body container-standard">
<div class="container-top">
<div class="container-left border">
<div class="header">{{tr "Channel Type" /}}</div>
<div class="content">
<fieldset class="container-channel-type">
<label class="type type-temp">
<div class="ratio-button">
<input type="radio" name="channel_type" value="temp"/>
<div class="mark"></div>
</div>
<a>{{tr "Temporary" /}}</a>
</label>
<label class="type type-semi">
<div class="ratio-button">
<input type="radio" name="channel_type" value="semi"/>
<div class="mark"></div>
</div>
<a>{{tr "Semi-Permanent" /}}</a>
</label>
<div class="container-perm-default">
<label class="type type-perm">
<div class="ratio-button">
<input type="radio" name="channel_type" value="perm"/>
<div class="mark"></div>
</div>
<a>{{tr "Permanent" /}}</a>
</label>
<label class="container-default-channel">
<a>{{tr "Default Channel" /}}</a>
<div class="checkbox">
<input type="checkbox" class="input-flag-default">
<div class="mark"></div>
</div>
</label>
</div>
</fieldset>
</div>
</div>
<div class="container-right">
<div class="header">{{tr "Sorting and Talk power" /}}</div>
<div class="content">
<div class="container-sort">
<a>{{tr "Sort this channel after:"/}}</a>
<select class="input-boxed order_id">
</select>
</div>
<div class="container-talk">
<a>&nbsp;</a>
<div class="input-boxed">
<a class="prefix">{{tr "Talk power:" /}}</a>
<input type="number" type="number" min="0" name="talk_power"
value="{{if channel_needed_talk_power}}{{:channel_needed_talk_power}}{{else}}0{{/if}}">
<div class="container-tooltip tooltip-permission-subscribe">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to talk in this channel" /}}</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-bottom">
<div class="container-left container-max-users">
<div class="header">{{tr "Max users" /}}</div>
<div class="content">
<fieldset>
<label class="container-unlimited">
<div class="ratio-button">
<input type="radio" name="max_users" value="unlimited" {{if
channel_flag_maxclients_unlimited}}checked{{/if}}/>
<div class="mark"></div>
</div>
<a>{{tr "Unlimited" /}}</a>
</label>
<label class="container-limited">
<div class="ratio-button">
<input type="radio" name="max_users" value="limited" {{if
!channel_flag_maxclients_unlimited}}checked{{/if}}/>
<div class="mark"></div>
</div>
<a>{{tr "Limited" /}}</a>
<div class="input-boxed">
<input class="channel_maxclients"
value="{{if channel_maxclients}}{{:channel_maxclients}}{{else}}0{{/if}}"
type="number" min="0" max="99999999">
<div class="container-tooltip tooltip-max-users">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Max users which could join the channel" /}}</a>
</div>
</div>
</div>
</label>
</fieldset>
</div>
</div>
<div class="container-right container-max-family-users">
<div class="header">{{tr "Family Max users" /}}</div>
<div class="content">
<fieldset>
<label class="container-inherited">
<div class="ratio-button">
<input type="radio" name="max_family_users" value="inherited"
{{if channel_flag_maxfamilyclients_inherited}}checked{{/if}}/>
<div class="mark"></div>
</div>
<a>{{tr "Inherited" /}}</a>
</label>
<label class="container-unlimited">
<div class="ratio-button">
<input type="radio" name="max_family_users" value="unlimited"
{{if channel_flag_maxfamilyclients_unlimited}}checked{{/if}}/>
<div class="mark"></div>
</div>
<a>{{tr "Unlimited" /}}</a>
</label>
<label class="container-limited">
<div class="ratio-button">
<input type="radio" name="max_family_users" value="limited" {{if
!channel_flag_maxfamilyclients_unlimited &&
!channel_flag_maxfamilyclients_inherited}}checked{{/if}}/>
<div class="mark"></div>
</div>
<a>{{tr "Limited" /}}</a>
<div class="input-boxed">
<input class="channel_maxfamilyclients"
value="{{if channel_maxfamilyclients}}{{:channel_maxfamilyclients}}{{else}}0{{/if}}"
type="number" min="0" max="99999999">
<div class="container-tooltip tooltip-max-family-users">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Max users which could join the channel family"
/}}</a>
</div>
</div>
</div>
</label>
</fieldset>
</div>
</div>
</div>
</div>
<div class="body container-audio">
<div class="container-top">
<div class="container-left container-presets">
<div class="header">{{tr "Presets" /}}</div>
<div class="content">
<fieldset style="">
<label>
<div class="ratio-button">
<input type="radio" name="voice_template" value="voice_mobile"/>
<div class="mark"></div>
</div>
<a>{{tr "Voice Mobile" /}}</a>
</label>
<label>
<div class="ratio-button">
<input type="radio" name="voice_template"
value="voice_desktop"/>
<div class="mark"></div>
</div>
<a>{{tr "Voice Desktop" /}}</a>
</label>
<label>
<div class="ratio-button">
<input type="radio" name="voice_template" value="music"/>
<div class="mark"></div>
</div>
<a>{{tr "Music" /}}</a>
</label>
<label>
<div class="ratio-button">
<input type="radio" name="voice_template" value="custom"/>
<div class="mark"></div>
</div>
<a>{{tr "Custom" /}}</a>
</label>
</fieldset>
</div>
</div>
<div class="container-right border container-custom">
<div class="header">{{tr "Custom Settings" /}}</div>
<div class="content">
<div class="custom">
<div class="contianer-codec-type">
<a>{{tr "Codec:" /}}</a>
<select class="input-boxed voice_codec">
<option value="speex_narrow">
{{tr "Speex Narrowband" /}}
</option>
<option value="speex_wide">
{{tr "Speex Wideband" /}}
</option>
<option value="speex_ultra_wide">
{{tr "Speex Ultra-Wideband" /}}
</option>
<option value="celt">
{{tr "CELT Mono" /}}
</option>
<option value="opus_voice">
{{tr "Opus Voice" /}}
</option>
<option value="opus_music">
{{tr "Opus Music" /}}
</option>
</select>
</div>
<div class="container-quality">
<label class="bmd-label-static">
{{tr "Quality:" /}}
<a class="container-value"></a>
</label>
<div class="container-slider">
<div class="filler" style="width: 30%"></div>
<div class="thumb container-tooltip" style="left: 30%">
<div class="tooltip">
<a>86%</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-bottom">
<div class="header">{{tr "Information" /}}</div>
<div class="content">
<p>
{{tr "Estimated needed bandwidth:" /}}<a class="container-needed-bandwidth">0.00 KiB/s</a>
</p>
<p class="hint">
{{tr "For bad internet connection, lower settings are recommend to reduce bandwidth." /}}
</p>
</div>
</div>
</div>
<div class="body container-permissions">
<div class="container-left">
<div class="header">{{tr "Regular needed powers:" /}}</div>
<div class="content">
<div class="container-permission">
<a class="name">{{tr "Join" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_channel_needed_join_power">
<div class="container-tooltip tooltip-permission-join">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to join this channel" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "View" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_channel_needed_view_power">
<div class="container-tooltip tooltip-permission-view">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to see this channel" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Description view" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_channel_needed_description_view_power">
<div class="container-tooltip tooltip-permission-view">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to see the channel description" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Subscribe" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_channel_needed_subscribe_power">
<div class="container-tooltip tooltip-permission-subscribe">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to subscribe to this channel" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Modify" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_channel_needed_modify_power">
<div class="container-tooltip tooltip-permission-modify">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to modify this channel permissions"
/}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Delete" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_channel_needed_delete_power">
<div class="container-tooltip tooltip-permission-delete">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to delete this channel" /}}</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-right border">
<div class="header">{{tr "File transfer needed powers:" /}}</div>
<div class="content">
<div class="container-permission">
<a class="name">{{tr "Browse" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_ft_needed_file_browse_power">
<div class="container-tooltip tooltip-permission-ft-browse">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to browse all files and directories"
/}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Upload" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_ft_needed_file_upload_power">
<div class="container-tooltip tooltip-permission-ft-upload">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to upload files" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Download" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_ft_needed_file_download_power">
<div class="container-tooltip tooltip-permission-ft-download">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to download files" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Rename" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_ft_needed_file_rename_power">
<div class="container-tooltip tooltip-permission-ft-rename">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to rename files within this channel"
/}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Directory create" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_ft_needed_directory_create_power">
<div class="container-tooltip tooltip-permission-ft-create">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to create a directory" /}}</a>
</div>
</div>
</div>
</div>
<div class="container-permission">
<a class="name">{{tr "Delete" /}}</a>
<div class="input-boxed">
<input type="number" min="0" value="0" class="value"
permission="i_ft_needed_file_delete_power">
<div class="container-tooltip tooltip-permission-ft-delete">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Required power to delete a directory or file" /}}</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="body container-misc">
<div class="container-other">
<div class="header">{{tr "Other Settings" /}}</div>
<div class="content">
<div class="container-phonetic">
<a>{{tr "Phonetic Name:" /}}</a>
<input class="input-boxed channel_name_phonetic"
value="{{>channel_name_phonetic}}">
</div>
<div class="container-delay">
<a>{{tr "Delete delay:" /}}</a>
<div class="input-boxed">
<input class="channel_delete_delay" type="number" min="0"
max="99999999"
value="{{if channel_delete_delay}}{{:channel_delete_delay}}{{else}}0{{/if}}">
<div class="container-tooltip tooltip-misc-delete-delay">
<img src="img/icon_tooltip.svg"/>
<div class="tooltip">
<a>{{tr "Time in seconds before the channel gets deleted when its empty." /}}</a>
</div>
</div>
</div>
<button class="btn btn-info button-delete-max">{{tr "Max" /}}</button>
</div>
<div class="container-encrypt">
<a>{{tr "Encrypt voice data:" /}}</a>
<label>
<div class="switch">
<input class="channel_codec_is_unencrypted" type="checkbox" {{if
!channel_codec_is_unencrypted}}checked{{/if}}/>
<span class="slider">
<div class="dot"></div>
</span>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-buttons">
<label>
<div class="switch">
<input class="input-advanced-mode" type="checkbox"/>
<span class="slider">
<div class="dot"></div>
</span>
</div>
<a>{{tr "Advanced mode" /}}</a>
</label>
<div class="spacer"></div>
<button class="btn btn-danger button_cancel">{{tr "Cancel" /}}</button>
<button class="btn btn-success button_ok">{{if create}}{{tr "Create" /}}{{else}}{{tr "Ok"
/}}{{/if}}
</button>
</div>
<!-- button_cancel, button_ok -->
</div>
</script>
<script class="jsrender-template" id="tmpl_server_edit" type="text/html">
<div> <!-- Only for rendering -->
<div class="container-general">

View File

@ -9,11 +9,12 @@ import { tr } from "tc-shared/i18n/localize";
export type ServerFeatureSupport = "unsupported" | "supported" | "experimental" | "deprecated";
export enum ServerFeature {
ERROR_BULKS= "error-bulks", /* Current version is 1 */
ADVANCED_CHANNEL_CHAT= "advanced-channel-chat", /* Current version is 1 */
LOG_QUERY= "log-query", /* Current version is 1 */
ERROR_BULKS = "error-bulks", /* Current version is 1 */
ADVANCED_CHANNEL_CHAT = "advanced-channel-chat", /* Current version is 1 */
LOG_QUERY = "log-query", /* Current version is 1 */
WHISPER_ECHO = "whisper-echo", /* Current version is 1 */
VIDEO = "video"
VIDEO = "video",
SIDEBAR_MODE = "sidebar-mode"
}
export interface ServerFeatureEvents {

View File

@ -126,7 +126,7 @@ export abstract class AbstractIconManager {
* @param serverUniqueId The server unique id for the icon
* @param handlerId Hint which connection handler should be used if we're downloading the icon
*/
abstract resolveIcon(iconId: number, serverUniqueId: string, handlerId?: string) : RemoteIcon;
abstract resolveIcon(iconId: number, serverUniqueId?: string, handlerId?: string) : RemoteIcon;
}
let globalIconManager: AbstractIconManager;

View File

@ -150,6 +150,8 @@ class IconManager extends AbstractIconManager {
}
resolveIcon(iconId: number, serverUniqueId: string, handlerIdHint: string): RemoteIcon {
serverUniqueId = serverUniqueId || "global";
/* just to ensure */
iconId = iconId >>> 0;

View File

@ -53,7 +53,9 @@ class RemoteIconManager extends AbstractIconManager {
});
}
resolveIcon(iconId: number, serverUniqueId: string, handlerId?: string): RemoteIcon {
resolveIcon(iconId: number, serverUniqueId?: string, handlerId?: string): RemoteIcon {
serverUniqueId = serverUniqueId || "global";
iconId = iconId >>> 0;
const uniqueId = RemoteIconManager.iconUniqueKey(iconId, serverUniqueId);

View File

@ -459,8 +459,8 @@ export class PermissionManager extends AbstractCommandHandler {
}, "success", PermissionManager.parse_permission_bulk(json, this.handle.permissions));
}
private execute_channel_permission_request(request: PermissionRequestKeys) {
this.handle.serverConnection.send_command("channelpermlist", {"cid": request.channel_id}).catch(error => {
private execute_channel_permission_request(request: PermissionRequestKeys, processResult?: boolean) {
this.handle.serverConnection.send_command("channelpermlist", {"cid": request.channel_id}, { process_result: !!processResult }).catch(error => {
if(error instanceof CommandResult && error.id == ErrorCode.DATABASE_EMPTY_RESULT)
this.fullfill_permission_request("requests_channel_permissions", request, "success", []);
else
@ -468,11 +468,11 @@ export class PermissionManager extends AbstractCommandHandler {
});
}
requestChannelPermissions(channelId: number) : Promise<PermissionValue[]> {
requestChannelPermissions(channelId: number, processResult?: boolean) : Promise<PermissionValue[]> {
const keys: PermissionRequestKeys = {
channel_id: channelId
};
return this.execute_permission_request("requests_channel_permissions", keys, this.execute_channel_permission_request.bind(this));
return this.execute_permission_request("requests_channel_permissions", keys, criteria => this.execute_channel_permission_request(criteria, processResult));
}
/* client permission request */
@ -768,4 +768,16 @@ export class PermissionManager extends AbstractCommandHandler {
result = result + "}";
return result;
}
getFailedPermission(command: CommandResult, index?: number) {
const json = command.bulks[typeof index === "number" ? index : 0] || {};
if("failed_permsid" in json) {
return json["failed_permsid"];
} else if("failed_permid" in json) {
const info = this.resolveInfo(parseInt(json["failed_permid"]));
return info ? info.name : "permission id " + json["failed_permid"];
} else {
return tr("unknown permission");
}
}
}

View File

@ -12,7 +12,6 @@ import {CommandResult} from "../connection/ServerConnectionDeclaration";
import * as htmltags from "../ui/htmltags";
import {hashPassword} from "../utils/helpers";
import {openChannelInfo} from "../ui/modal/ModalChannelInfo";
import {createChannelModal} from "../ui/modal/ModalCreateChannel";
import {formatMessage} from "../ui/frames/chat";
import {Registry} from "../events";
@ -22,6 +21,7 @@ import {ErrorCode} from "../connection/ErrorCode";
import {ClientIcon} from "svg-sprites/client-icons";
import { tr } from "tc-shared/i18n/localize";
import {EventChannelData} from "tc-shared/connectionlog/Definitions";
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
export enum ChannelType {
PERMANENT,
@ -66,7 +66,7 @@ export class ChannelProperties {
channel_password: string = "";
channel_codec: number = 4;
channel_codec_quality: number = 0;
channel_codec_quality: number = 6;
channel_codec_is_unencrypted: boolean = false;
channel_maxclients: number = -1;
@ -78,9 +78,9 @@ export class ChannelProperties {
channel_flag_semi_permanent: boolean = false;
channel_flag_default: boolean = false;
channel_flag_password: boolean = false;
channel_flag_maxclients_unlimited: boolean = false;
channel_flag_maxclients_unlimited: boolean = true;
channel_flag_maxfamilyclients_inherited: boolean = false;
channel_flag_maxfamilyclients_unlimited: boolean = false;
channel_flag_maxfamilyclients_unlimited: boolean = true;
channel_icon_id: number = 0;
channel_delete_delay: number = 0;
@ -275,6 +275,10 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
return promise;
}
isDescriptionCached() {
return this.channelDescriptionCached;
}
private async doGetChannelDescription() {
if(!this.channelDescriptionCached) {
await this.channelTree.client.serverConnection.send_command("channelgetdescription", {
@ -497,21 +501,24 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
name: tr("Edit channel"),
invalidPermission: !channelModify,
callback: () => {
createChannelModal(this.channelTree.client, this, this.parent, this.channelTree.client.permissions, (changes?, permissions?) => {
if(changes) {
changes["cid"] = this.channelId;
this.channelTree.client.serverConnection.send_command("channeledit", changes);
log.info(LogCategory.CHANNEL, tr("Changed channel properties of channel %s: %o"), this.channelName(), changes);
spawnChannelEditNew(this.channelTree.client, this, this.parent, (properties, permissions) => {
const changedProperties = Object.keys(properties);
if(changedProperties.length > 0) {
properties["cid"] = this.channelId;
this.channelTree.client.serverConnection.send_command("channeledit", properties).then(() => {
this.channelTree.client.sound.play(Sound.CHANNEL_EDITED_SELF);
});
log.info(LogCategory.CHANNEL, tr("Changed channel properties of channel %s: %o"), this.channelName(), properties);
}
if(permissions && permissions.length > 0) {
if(permissions.length > 0) {
let perms = [];
for(let perm of permissions) {
perms.push({
permvalue: perm.value,
permnegated: false,
permskip: false,
permid: perm.type.id
permsid: perm.permission
});
}

View File

@ -10,7 +10,6 @@ import {ChannelEntry, ChannelProperties, ChannelSubscribeMode} from "./Channel";
import {ClientEntry, LocalClientEntry, MusicClientEntry} from "./Client";
import {ChannelTreeEntry} from "./ChannelTreeEntry";
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel";
import {Registry} from "tc-shared/events";
import * as ReactDOM from "react-dom";
import * as React from "react";
@ -20,13 +19,14 @@ import {createInputModal} from "tc-shared/ui/elements/Modal";
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {tra} from "tc-shared/i18n/localize";
import {tr, tra} from "tc-shared/i18n/localize";
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
import {Settings, settings} from "tc-shared/settings";
import {ClientIcon} from "svg-sprites/client-icons";
import "./EntryTagsHandler";
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
export interface ChannelTreeEvents {
/* general tree notified */
@ -888,9 +888,7 @@ export class ChannelTree {
}
spawnCreateChannel(parent?: ChannelEntry) {
createChannelModal(this.client, undefined, parent, this.client.permissions, (properties?, permissions?) => {
if(!properties) return;
spawnChannelEditNew(this.client, undefined, parent, (properties, permissions) => {
properties["cpid"] = parent ? parent.channelId : 0;
log.debug(LogCategory.CHANNEL, tr("Creating a new channel.\nProperties: %o\nPermissions: %o"), properties);
this.client.serverConnection.send_command("channelcreate", properties).then(() => {
@ -899,6 +897,7 @@ export class ChannelTree {
log.error(LogCategory.CHANNEL, tr("Failed to resolve channel after creation. Could not apply permissions!"));
return;
}
if(permissions && permissions.length > 0) {
let perms = [];
for(let perm of permissions) {
@ -906,7 +905,7 @@ export class ChannelTree {
permvalue: perm.value,
permnegated: false,
permskip: false,
permid: perm.type.id
permsid: perm.permission
});
}

View File

@ -1,756 +0,0 @@
import PermissionType from "../../permission/PermissionType";
import {ConnectionHandler} from "../../ConnectionHandler";
import {ChannelEntry, ChannelProperties} from "../../tree/Channel";
import {PermissionManager, PermissionValue} from "../../permission/PermissionManager";
import {LogCategory} from "../../log";
import {createModal} from "../../ui/elements/Modal";
import * as log from "../../log";
import {Settings, settings} from "../../settings";
import * as tooltip from "../../ui/elements/Tooltip";
import {spawnIconSelect} from "../../ui/modal/ModalIconSelect";
import {hashPassword} from "../../utils/helpers";
import {sliderfy} from "../../ui/elements/Slider";
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
import { tr } from "tc-shared/i18n/localize";
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
export function createChannelModal(connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, permissions: PermissionManager, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => any) {
//spawnChannelEditNew(connection, channel, parent, callback);
//return;
let properties: ChannelProperties = { } as ChannelProperties; //The changes properties
const modal = createModal({
header: channel ? tr("Edit channel") : tr("Create channel"),
body: () => {
const render_properties = {};
Object.assign(render_properties, channel ? channel.properties : {
channel_flag_maxfamilyclients_unlimited: true,
channel_flag_maxclients_unlimited: true,
});
render_properties["channel_icon_tab"] = generateIconJQueryTag(getIconManager().resolveIcon(channel ? channel.properties.channel_icon_id : 0, connection.getCurrentServerUniqueId(), connection.handlerId));
render_properties["channel_icon_general"] = generateIconJQueryTag(getIconManager().resolveIcon(channel ? channel.properties.channel_icon_id : 0, connection.getCurrentServerUniqueId(), connection.handlerId));
render_properties["create"] = !channel;
let template = $("#tmpl_channel_edit").renderTag(render_properties);
/* the tab functionality */
{
const container_tabs = template.find(".container-advanced");
container_tabs.find(".categories .entry").on('click', event => {
const entry = $(event.target);
container_tabs.find(".bodies > .body").addClass("hidden");
container_tabs.find(".categories > .selected").removeClass("selected");
entry.addClass("selected");
container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden");
});
container_tabs.find(".entry").first().trigger('click');
}
/* Advanced/normal switch */
{
const input = template.find(".input-advanced-mode");
const container_mode = template.find(".mode-container");
const container_advanced = container_mode.find(".container-advanced");
const container_simple = container_mode.find(".container-simple");
input.on('change', event => {
const advanced = input.prop("checked");
settings.changeGlobal(Settings.KEY_CHANNEL_EDIT_ADVANCED, advanced);
container_mode.css("overflow", "hidden");
container_advanced.show().toggleClass("hidden", !advanced);
container_simple.show().toggleClass("hidden", advanced);
setTimeout(() => {
container_advanced.toggle(advanced);
container_simple.toggle(!advanced);
container_mode.css("overflow", "visible");
}, 300);
}).prop("checked", settings.static_global(Settings.KEY_CHANNEL_EDIT_ADVANCED)).trigger('change');
}
return template.tabify().children(); /* the "render" div */
},
footer: null,
width: 500
});
modal.htmlTag.find(".modal-body").addClass("modal-channel modal-blue");
applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel);
applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel);
applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel);
applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel);
applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel);
let updated: PermissionValue[] = [];
modal.htmlTag.find(".button_ok").click(() => {
modal.htmlTag.find(".container-permissions").find("input[permission]").each((index, _element) => {
let element = $(_element);
if(element.val() == element.attr("original-value")) return;
let permission = permissions.resolveInfo(element.attr("permission"));
if(!permission) {
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
element.prop("disabled", true);
return;
}
updated.push(new PermissionValue(permission, element.val()));
});
console.log(tr("Updated permissions %o"), updated);
}).click(() => {
modal.close();
for(const key of Object.keys(channel ? channel.properties : {})) {
if(channel.properties[key] == properties[key]) {
delete properties[key];
}
}
if(!properties["channel_flag_default"]) {
delete properties["channel_flag_default"];
}
if(!channel) {
/* Delete the default values */
if(properties["channel_flag_maxfamilyclients_unlimited"]) {
delete properties["channel_flag_maxfamilyclients_unlimited"];
delete properties["channel_flag_maxfamilyclients_inherited"];
delete properties["channel_maxfamilyclients"];
}
if(properties["channel_flag_maxclients_unlimited"]) {
delete properties["channel_flag_maxclients_unlimited"];
delete properties["channel_maxclients"];
}
}
callback(properties, updated); //First may create the channel
});
tooltip.initialize(modal.htmlTag);
modal.htmlTag.find(".button_cancel").click(() => {
modal.close();
callback();
});
modal.open();
if(!channel)
modal.htmlTag.find(".channel_name").focus();
}
function applyGeneralListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel: ChannelEntry | undefined) {
let updateButton = () => {
const status = tag.find(".input_error").length != 0;
console.log("Disabled: %o", status);
button.prop("disabled", status);
};
{
const channel_name = tag.find(".channel_name");
tag.find(".channel_name").on('change keyup', function (this: HTMLInputElement) {
properties.channel_name = this.value;
channel_name.toggleClass("input_error", this.value.length < 1 || this.value.length > 40);
updateButton();
}).prop("disabled", channel && !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_NAME).granted(1));
}
tag.find(".button-select-icon").on('click', event => {
spawnIconSelect(connection, id => {
const icon_node = tag.find(".icon-preview");
icon_node.children().remove();
icon_node.append(generateIconJQueryTag(getIconManager().resolveIcon(id, connection.getCurrentServerUniqueId(), connection.handlerId)));
console.log("Selected icon ID: %d", id);
properties.channel_icon_id = id;
}, channel ? channel.properties.channel_icon_id : 0);
});
tag.find(".button-icon-remove").on('click', event => {
const icon_node = tag.find(".icon-preview");
icon_node.children().remove();
icon_node.append(generateIconJQueryTag(getIconManager().resolveIcon(0, connection.getCurrentServerUniqueId(), connection.handlerId)));
console.log("Remove channel icon");
properties.channel_icon_id = 0;
});
{
const channel_password = tag.find(".channel_password");
tag.find(".channel_password").change(function (this: HTMLInputElement) {
properties.channel_flag_password = this.value.length != 0;
if(properties.channel_flag_password)
hashPassword(this.value).then(pass => properties.channel_password = pass);
channel_password.removeClass("input_error");
if(!properties.channel_flag_password)
if(connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1))
channel_password.addClass("input_error");
updateButton();
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_PASSWORD : PermissionType.B_CHANNEL_MODIFY_PASSWORD).granted(1));
}
tag.find(".channel_topic").change(function (this: HTMLInputElement) {
properties.channel_topic = this.value;
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1));
{
const container = tag.find(".container-description");
const input = container.find("textarea");
const insert_tag = (open: string, close: string) => {
if(input.prop("disabled"))
return;
const node = input[0] as HTMLTextAreaElement;
if (node.selectionStart || node.selectionStart == 0) {
const startPos = node.selectionStart;
const endPos = node.selectionEnd;
node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos);
node.selectionEnd = endPos + open.length;
node.selectionStart = node.selectionEnd;
} else {
node.value += open + close;
node.selectionEnd = node.value.length - close.length;
node.selectionStart = node.selectionEnd;
}
input.focus().trigger('change');
};
input.on('change', event => {
console.log(tr("Channel description edited: %o"), input.val());
properties.channel_description = input.val() as string;
});
container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]'));
container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]'));
container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]'));
container.find(".button-color input").on('change', event => {
insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]')
})
}
tag.find(".channel_description").change(function (this: HTMLInputElement) {
properties.channel_description = this.value;
}).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1));
if(!channel) {
setTimeout(() => {
tag.find(".channel_name").trigger("change");
tag.find(".channel_password").trigger('change');
}, 0);
}
}
function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) {
/* Channel type */
{
const input_advanced_type = tag.find("input[name='channel_type']");
let _in_update = false;
const update_simple_type = () => {
if(_in_update)
return;
let type;
if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default)) {
type = "def";
} else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent)) {
type = "perm";
} else if(properties.channel_flag_semi_permanent || (typeof(properties.channel_flag_semi_permanent) === "undefined" && channel && channel.properties.channel_flag_semi_permanent)) {
type = "semi";
} else {
type = "temp";
}
simple.find("option[name='channel-type'][value='" + type + "']").prop("selected", true);
};
input_advanced_type.on('change', event => {
const value = [...input_advanced_type as JQuery<HTMLInputElement>].find(e => e.checked).value;
switch(value) {
case "semi":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = true;
break;
case "perm":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
break;
default:
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = false;
break;
}
update_simple_type();
});
const permission_temp = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1);
const permission_semi = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1);
const permission_perm = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1);
const permission_default = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) &&
connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1);
/* advanced type listeners */
const container_types = tag.find(".container-channel-type");
const tag_type_temp = container_types.find(".type-temp");
const tag_type_semi = container_types.find(".type-semi");
const tag_type_perm = container_types.find(".type-perm");
const select_default = tag.find(".input-flag-default");
{
select_default.on('change', event => {
const node = select_default[0] as HTMLInputElement;
properties.channel_flag_default = node.checked;
if(node.checked)
tag_type_perm.find("input").prop("checked", true);
tag_type_temp
.toggleClass("disabled", node.checked || !permission_temp)
.find("input").prop("disabled", node.checked || !permission_temp);
tag_type_semi
.toggleClass("disabled", node.checked || !permission_semi)
.find("input").prop("disabled", node.checked || !permission_semi);
tag_type_perm
.toggleClass("disabled", node.checked || !permission_perm)
.find("input").prop("disabled", node.checked || !permission_perm);
update_simple_type();
}).prop("disabled", !permission_default).trigger('change').parent().toggleClass("disabled", !permission_default);
}
/* simple */
{
simple.find("option[name='channel-type'][value='def']").prop("disabled", !permission_default);
simple.find("option[name='channel-type'][value='perm']").prop("disabled", !permission_perm);
simple.find("option[name='channel-type'][value='semi']").prop("disabled", !permission_semi);
simple.find("option[name='channel-type'][value='temp']").prop("disabled", !permission_temp);
simple.find("select[name='channel-type']").on('change', event => {
try {
_in_update = true;
switch ((event.target as HTMLSelectElement).value) {
case "temp":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = false;
properties.channel_flag_default = false;
select_default.prop("checked", false).trigger('change');
tag_type_temp.trigger('click');
break;
case "semi":
properties.channel_flag_permanent = false;
properties.channel_flag_semi_permanent = true;
properties.channel_flag_default = false;
select_default.prop("checked", false).trigger('change');
tag_type_semi.trigger('click');
break;
case "perm":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
properties.channel_flag_default = false;
select_default.prop("checked", false).trigger('change');
tag_type_perm.trigger('click');
break;
case "def":
properties.channel_flag_permanent = true;
properties.channel_flag_semi_permanent = false;
properties.channel_flag_default = true;
select_default.prop("checked", true).trigger('change');
break;
}
} finally {
_in_update = false;
/* We dont need to update the simple type because we changed the advanced part to the just changed simple part */
//update_simple_type();
}
});
}
/* init */
setTimeout(() => {
if(!channel) {
if(permission_perm)
tag_type_perm.find("input").trigger('click');
else if(permission_semi)
tag_type_semi.find("input").trigger('click');
else
tag_type_temp.find("input").trigger('click');
} else {
if(channel.properties.channel_flag_permanent)
tag_type_perm.find("input").trigger('click');
else if(channel.properties.channel_flag_semi_permanent)
tag_type_semi.find("input").trigger('click');
else
tag_type_temp.find("input").trigger('click');
}
}, 0);
}
/* Talk power */
{
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1);
const input_advanced = tag.find("input[name='talk_power']").prop("disabled", !permission);
const input_simple = simple.find("input[name='talk_power']").prop("disabled", !permission);
input_advanced.on('change', event => {
properties.channel_needed_talk_power = parseInt(input_advanced.val() as string);
input_simple.val(input_advanced.val());
});
input_simple.on('change', event => {
properties.channel_needed_talk_power = parseInt(input_simple.val() as string);
input_advanced.val(input_simple.val());
});
}
/* Channel order */
{
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1);
const advanced_order_id = tag.find(".order_id").prop("disabled", !permission) as JQuery<HTMLSelectElement>;
const simple_order_id = simple.find(".order_id").prop("disabled", !permission) as JQuery<HTMLSelectElement>;
for(let previous_channel of (parent ? parent.children() : connection.channelTree.rootChannel())) {
let selected = channel && channel.properties.channel_order == previous_channel.channelId;
$.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(advanced_order_id);
$.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(simple_order_id);
}
advanced_order_id.on('change', event => {
simple_order_id[0].selectedIndex = advanced_order_id[0].selectedIndex;
const selected = $(advanced_order_id[0].options.item(advanced_order_id[0].selectedIndex));
properties.channel_order = parseInt(selected.attr("channelId"));
});
simple_order_id.on('change', event => {
advanced_order_id[0].selectedIndex = simple_order_id[0].selectedIndex;
const selected = $(simple_order_id[0].options.item(simple_order_id[0].selectedIndex));
properties.channel_order = parseInt(selected.attr("channelId"));
});
}
/* Advanced only */
{
const container_max_users = tag.find(".container-max-users");
const container_unlimited = container_max_users.find(".container-unlimited");
const container_limited = container_max_users.find(".container-limited");
const input_unlimited = container_unlimited.find("input[value='unlimited']");
const input_limited = container_limited.find("input[value='limited']");
const input_limit = container_limited.find(".channel_maxclients");
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1);
if(!permission) {
input_unlimited.prop("disabled", true);
input_limited.prop("disabled", true);
input_limit.prop("disabled", true);
container_limited.addClass("disabled");
container_unlimited.addClass("disabled");
} else {
container_max_users.find("input[name='max_users']").on('change', event => {
const node = event.target as HTMLInputElement;
console.log(tr("Channel max user mode: %o"), node.value);
const flag = node.value === "unlimited";
input_limit
.prop("disabled", flag)
.parent().toggleClass("disabled", flag);
properties.channel_flag_maxclients_unlimited = flag;
if(flag) {
input_limit.trigger("change");
} else {
properties.channel_maxclients = -1;
}
});
input_limit.on('change', event => {
properties.channel_maxclients = parseInt(input_limit.val() as string);
console.log(tr("Changed max user limit to %o"), properties.channel_maxclients);
});
setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100);
}
}
{
const container_max_users = tag.find(".container-max-family-users");
const container_unlimited = container_max_users.find(".container-unlimited");
const container_inherited = container_max_users.find(".container-inherited");
const container_limited = container_max_users.find(".container-limited");
const input_unlimited = container_unlimited.find("input[value='unlimited']");
const input_inherited = container_inherited.find("input[value='inherited']");
const input_limited = container_limited.find("input[value='limited']");
const input_limit = container_limited.find(".channel_maxfamilyclients");
const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1);
if(!permission) {
input_unlimited.prop("disabled", true);
input_inherited.prop("disabled", true);
input_limited.prop("disabled", true);
input_limit.prop("disabled", true);
container_limited.addClass("disabled");
container_unlimited.addClass("disabled");
container_inherited.addClass("disabled");
} else {
container_max_users.find("input[name='max_family_users']").on('change', event => {
const node = event.target as HTMLInputElement;
console.log(tr("Channel max family user mode: %o"), node.value);
const flag_unlimited = node.value === "unlimited";
const flag_inherited = node.value === "inherited";
input_limit
.prop("disabled", flag_unlimited || flag_inherited)
.parent().toggleClass("disabled", flag_unlimited || flag_inherited);
properties.channel_flag_maxfamilyclients_unlimited = flag_unlimited;
properties.channel_flag_maxfamilyclients_inherited = flag_inherited;
if(!flag_inherited && !flag_inherited) {
input_limit.trigger("change");
} else {
properties.channel_maxfamilyclients = -1;
}
});
input_limit.on('change', event => {
properties.channel_maxfamilyclients = parseInt(input_limit.val() as string);
console.log(tr("Changed max family user limit to %o"), properties.channel_maxfamilyclients);
});
setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100);
}
}
}
function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) {
let apply_permissions = (channel_permissions: PermissionValue[]) => {
log.trace(LogCategory.CHANNEL, tr("Received channel permissions: %o"), channel_permissions);
let required_power = -2;
for(let cperm of channel_permissions)
if(cperm.type.name == PermissionType.I_CHANNEL_NEEDED_MODIFY_POWER) {
required_power = cperm.value;
break;
}
tag.find("input[permission]").each((index, _element) => {
let element = $(_element);
element.attr("original-value", 0);
element.val(0);
let permission = permissions.resolveInfo(element.attr("permission"));
if(!permission) {
log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission"));
element.prop("disabled", true);
return;
}
for(let cperm of channel_permissions)
if(cperm.type == permission) {
element.val(cperm.value);
element.attr("original-value", cperm.value);
return;
}
});
const permission = permissions.neededPermission(PermissionType.I_CHANNEL_PERMISSION_MODIFY_POWER).granted(required_power, false);
tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions
};
if(channel) {
permissions.requestChannelPermissions(channel.getChannelId()).then(apply_permissions).catch((error) => {
tag.find("input[permission]").prop("disabled", true);
console.log("Failed to receive channel permissions (%o)", error);
});
} else apply_permissions([]);
}
function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, channel?: ChannelEntry) {
const bandwidth_mapping = [
/* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22],
/* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37],
/* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57],
/* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92],
/* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71],
/* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87]
];
let update_template = () => {
let codec = properties.channel_codec;
if(!codec && channel)
codec = channel.properties.channel_codec;
if(!codec) return;
let quality = properties.channel_codec_quality;
if(!quality && channel)
quality = channel.properties.channel_codec_quality;
if(!quality) return;
let template_name = "custom";
{
if(codec == 4 && quality == 4)
template_name = "voice_mobile";
else if(codec == 4 && quality == 6)
template_name = "voice_desktop";
else if(codec == 5 && quality == 6)
template_name = "music";
}
tag.find("input[name='voice_template'][value='" + template_name + "']").prop("checked", true);
simple.find("option[name='voice_template'][value='" + template_name + "']").prop("selected", true);
let bandwidth;
if(codec < 0 || codec > bandwidth_mapping.length)
bandwidth = 0;
else
bandwidth = bandwidth_mapping[codec][quality] || 0; /* OOB access results in undefined, but is allowed */
tag.find(".container-needed-bandwidth").text(bandwidth.toFixed(2) + " KiB/s");
};
let change_codec = codec => {
if(properties.channel_codec == codec) return;
tag.find(".voice_codec option").prop("selected", false).eq(codec).prop("selected", true);
properties.channel_codec = codec;
update_template();
};
const container_quality = tag.find(".container-quality");
const slider_quality = sliderfy(container_quality.find(".container-slider"), {
initial_value: properties.channel_codec_quality || 6,
unit: "",
min_value: 1,
max_value: 10,
step: 1,
value_field: container_quality.find(".container-value")
});
let change_quality = (quality: number) => {
if(properties.channel_codec_quality == quality) return;
properties.channel_codec_quality = quality;
slider_quality.value(quality);
update_template();
};
container_quality.find(".container-slider").on('change', event => {
properties.channel_codec_quality = slider_quality.value();
update_template();
});
tag.find("input[name='voice_template']").change(function (this: HTMLInputElement) {
switch(this.value) {
case "custom":
break;
case "music":
change_codec(5);
change_quality(6);
break;
case "voice_desktop":
change_codec(4);
change_quality(6);
break;
case "voice_mobile":
change_codec(4);
change_quality(4);
break;
}
});
simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) {
switch(this.value) {
case "custom":
break;
case "music":
change_codec(5);
change_quality(6);
break;
case "voice_desktop":
change_codec(4);
change_quality(6);
break;
case "voice_mobile":
change_codec(4);
change_quality(4);
break;
}
});
/* disable not granted templates */
{
tag.find("input[name='voice_template'][value='voice_mobile']")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
simple.find("option[name='voice_template'][value='voice_mobile']")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name='voice_template'][value=\"voice_desktop\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
simple.find("option[name='voice_template'][value=\"voice_desktop\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
tag.find("input[name='voice_template'][value=\"music\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
simple.find("option[name='voice_template'][value=\"music\"]")
.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
}
let codecs = tag.find(".voice_codec option");
codecs.eq(4).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1));
codecs.eq(5).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1));
tag.find(".voice_codec").change(function (this: HTMLSelectElement) {
if($(this.item(this.selectedIndex)).prop("disabled")) return false;
change_codec(this.selectedIndex);
});
if(!channel) {
change_codec(4);
change_quality(6);
} else {
change_codec(channel.properties.channel_codec);
change_quality(channel.properties.channel_codec_quality);
}
update_template();
}
function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) {
tag.find(".channel_name_phonetic").change(function (this: HTMLInputElement) {
properties.channel_topic = this.value;
});
{
const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1);
tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) {
properties.channel_delete_delay = parseInt(this.value);
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
}
{
tag.find(".button-delete-max").on('click', event => {
const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value;
let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power;
tag.find(".channel_delete_delay").val(value).trigger('change');
});
}
{
const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1);
tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) {
properties.channel_codec_is_unencrypted = parseInt(this.value) == 0;
}).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission);
}
}

View File

@ -104,7 +104,7 @@ export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id:
if (!chunk) return;
for (const icon of chunk) {
const iconId = parseInt(icon.name.substr("icon_".length));
const iconId = parseInt((icon.name || "").substr("icon_".length));
if (Number.isNaN(iconId)) {
log.warn(LogCategory.GENERAL, tr("Received an unparsable icon within icon list (%o)"), icon);
continue;

View File

@ -1,58 +1,139 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
import {ChannelEditEvents, ChannelPropertyPermission} from "tc-shared/ui/modal/channel-edit/Definitions";
import {
ChannelEditablePermissions,
ChannelEditableProperty,
ChannelEditEvents,
ChannelPropertyPermission
} from "tc-shared/ui/modal/channel-edit/Definitions";
import {Registry} from "tc-shared/events";
import {ChannelPropertyProviders} from "tc-shared/ui/modal/channel-edit/ControllerProperties";
import {LogCategory, logError} from "tc-shared/log";
import {LogCategory, logDebug, logError, logInfo} from "tc-shared/log";
import {ChannelPropertyPermissionsProviders} from "tc-shared/ui/modal/channel-edit/ControllerPermissions";
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
import {ChannelEditModal} from "tc-shared/ui/modal/channel-edit/Renderer";
import {PermissionValue} from "tc-shared/permission/PermissionManager";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import PermissionType from "tc-shared/permission/PermissionType";
import {ChannelPropertyValidators} from "tc-shared/ui/modal/channel-edit/ControllerValidation";
import {hashPassword} from "tc-shared/utils/helpers";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {spawnIconSelect} from "tc-shared/ui/modal/ModalIconSelect";
export const spawnChannelEditNew = (connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, callback: (properties?: ChannelProperties, permissions?: PermissionValue[]) => void) => {
const controller = new ChannelEditController(connection, channel);
const modal = spawnReactModal(ChannelEditModal, controller.uiEvents, typeof channel === "number");
export type ChannelEditCallback = (properties: Partial<ChannelProperties>, permissions: ChannelEditChangedPermission[]) => void;
export type ChannelEditChangedPermission = { permission: PermissionType, value: number };
export const spawnChannelEditNew = (connection: ConnectionHandler, channel: ChannelEntry | undefined, parent: ChannelEntry | undefined, callback: ChannelEditCallback) => {
const controller = new ChannelEditController(connection, channel, parent);
const modal = spawnReactModal(ChannelEditModal, controller.uiEvents, typeof channel !== "object");
modal.show().then(undefined);
modal.events.on("destroy", () => {
controller.destroy();
});
controller.uiEvents.one("action_cancel", () => modal.destroy());
controller.uiEvents.on("action_apply", async () => {
if(!controller.validateAllProperties()) {
return;
}
const changedProperties = controller.getChangedProperties();
if("channel_password" in changedProperties) {
changedProperties.channel_password = await hashPassword(changedProperties.channel_password);
}
logDebug(LogCategory.CHANNEL, tr("Updating channel properties: %o"), changedProperties);
logDebug(LogCategory.CHANNEL, tr("Updating channel permissions: %o"), controller.getChangedPermissions());
callback(changedProperties, controller.getChangedPermissions());
modal.destroy();
});
};
type PermissionCacheState = {
state: "loaded",
permissions: PermissionValue[]
} | {
state: "loading",
promise: Promise<void>
} | {
state: "uninitialized",
} | {
state: "error",
reason: string
} | {
state: "no-permissions",
failedPermission: string
};
function permissionFromEditablePermission(permission: ChannelEditablePermissions) : PermissionType {
switch (permission) {
case "join": return PermissionType.I_CHANNEL_NEEDED_JOIN_POWER;
case "view": return PermissionType.I_CHANNEL_NEEDED_VIEW_POWER;
case "view-description": return PermissionType.I_CHANNEL_NEEDED_DESCRIPTION_VIEW_POWER;
case "subscribe": return PermissionType.I_CHANNEL_NEEDED_SUBSCRIBE_POWER;
case "modify": return PermissionType.I_CHANNEL_NEEDED_MODIFY_POWER;
case "delete": return PermissionType.I_CHANNEL_NEEDED_DELETE_POWER;
case "browse": return PermissionType.I_FT_NEEDED_FILE_BROWSE_POWER;
case "upload": return PermissionType.I_FT_NEEDED_FILE_UPLOAD_POWER;
case "download": return PermissionType.I_FT_NEEDED_FILE_DOWNLOAD_POWER;
case "rename": return PermissionType.I_FT_NEEDED_FILE_RENAME_POWER;
case "directory-create": return PermissionType.I_FT_NEEDED_DIRECTORY_CREATE_POWER;
case "file-delete": return PermissionType.I_FT_NEEDED_FILE_DELETE_POWER;
default:
throw tr("invalid editable permission");
}
}
class ChannelEditController {
readonly uiEvents: Registry<ChannelEditEvents>;
private readonly listenerPermissions: (() => void)[];
private readonly connection: ConnectionHandler;
private readonly channelParent: ChannelEntry | undefined;
private readonly channel: ChannelEntry | undefined;
private readonly originalProperties: ChannelProperties;
private currentProperties: ChannelProperties;
private readonly currentProperties: ChannelProperties;
constructor(connection: ConnectionHandler, channel: ChannelEntry | undefined) {
private propertyValidationStates: {[T in keyof ChannelEditableProperty]?: boolean} = {};
private cachedChannelPermissions: PermissionCacheState;
private currentChannelPermissions: {[T in keyof ChannelEditablePermissions]?: number} = {};
constructor(connection: ConnectionHandler, channel: ChannelEntry | undefined, channelParent: ChannelEntry | undefined) {
this.connection = connection;
this.channel = channel;
this.channelParent = channelParent;
this.uiEvents = new Registry<ChannelEditEvents>();
this.uiEvents.on("query_property", event => {
this.uiEvents.on("query_property", event => this.notifyProperty(event.property));
this.uiEvents.on("query_property_permission", event => this.notifyPropertyPermission(event.permission));
this.uiEvents.on("query_permission", event => this.notifyPermission(event.permission));
this.uiEvents.on("query_permissions", () => this.notifyPermissions());
this.uiEvents.on("action_change_property", event => {
if (typeof ChannelPropertyProviders[event.property] !== "object") {
logError(LogCategory.CHANNEL, tr("Channel edit controller missing property provider %s."), event.property);
logError(LogCategory.CHANNEL, tr("Channel edit ui tried to change an unknown property %s."), event.property);
return;
}
ChannelPropertyProviders[event.property as any].provider(this.currentProperties, this.channel, this.channel?.parent_channel(), this.connection.channelTree).then(value => {
this.uiEvents.fire_react("notify_property", {
property: event.property,
value: value
});
}).catch(error => {
logError(LogCategory.CHANNEL, tr("Failed to get property value for %s: %o"), event.property, error);
});
ChannelPropertyProviders[event.property as any].applier(event.value, this.currentProperties, this.channel);
this.notifyProperty(event.property);
this.validateProperty(event.property);
});
this.uiEvents.on("action_change_permission", event => {
this.currentChannelPermissions[event.permission] = event.value;
this.notifyPermission(event.permission);
});
this.uiEvents.on("action_icon_select", () => {
spawnIconSelect(this.connection, id => {
this.uiEvents.fire("action_change_property", { property: "icon", value: { iconId: id } });
}, this.currentProperties.channel_icon_id);
});
this.uiEvents.on("query_property_permission", event => this.notifyPropertyPermission(event.permission));
this.listenerPermissions = [];
for(const key of Object.keys(ChannelPropertyPermissionsProviders)) {
@ -64,12 +145,17 @@ class ChannelEditController {
if(channel) {
this.originalProperties = channel.properties;
this.cachedChannelPermissions = { state: "uninitialized" };
} else {
this.originalProperties = new ChannelProperties();
/* TODO: Get the default channel delete/modify power? */
this.cachedChannelPermissions = { state: "loaded", permissions: [] }; /* The channel has no default values */
}
/* FIXME: Correctly setup the currentProperties! */
this.currentProperties = new ChannelProperties();
Object.keys(this.originalProperties).forEach(key => this.currentProperties[key] = this.originalProperties[key]);
this.uiEvents.enableDebug("channel-edit");
}
destroy() {
@ -79,6 +165,88 @@ class ChannelEditController {
this.uiEvents.destroy();
}
validateAllProperties() : boolean {
for(const property of Object.keys(ChannelPropertyValidators)) {
this.validateProperty(property as any);
}
return Object.keys(ChannelPropertyValidators).map(key => this.propertyValidationStates[key])
.findIndex(entry => entry === false) === -1;
}
private validateProperty(property: keyof ChannelEditableProperty) {
const validator = ChannelPropertyValidators[property];
if(!validator) { return; }
const newState = validator(
this.currentProperties, this.originalProperties,
this.channel, this.channelParent,
this.connection.permissions,
this.connection.channelTree
);
if(this.propertyValidationStates[property] !== newState) {
this.propertyValidationStates[property] = newState;
this.notifyValidStatus(property);
}
}
getChangedProperties() : Partial<ChannelProperties> {
const properties: Partial<ChannelProperties> = {};
for(const key of Object.keys(this.currentProperties)) {
if(this.currentProperties[key] !== this.originalProperties[key]) {
properties[key] = this.currentProperties[key];
}
}
for(const key of Object.keys(this.originalProperties)) {
if(this.currentProperties[key] !== this.originalProperties[key]) {
properties[key] = this.currentProperties[key];
}
}
return properties;
}
getChangedPermissions() : { permission: PermissionType, value: number }[] {
const changes = [];
for(const key of Object.keys(this.currentChannelPermissions)) {
const neededPermission = permissionFromEditablePermission(key as any);
const permissionInfo = this.connection.permissions.resolveInfo(neededPermission);
if(!permissionInfo) {
continue;
}
if(this.cachedChannelPermissions.state === "loaded") {
const defaultValue = this.cachedChannelPermissions.permissions.find(permission => permission.type === permissionInfo);
if(defaultValue?.valueOr(0) === this.currentChannelPermissions[key]) {
continue;
}
}
changes.push({ permission: neededPermission, value: this.currentChannelPermissions[key] });
}
return changes;
}
private notifyProperty(property: keyof ChannelEditableProperty) {
if (typeof ChannelPropertyProviders[property] !== "object") {
logError(LogCategory.CHANNEL, tr("Channel edit controller missing property provider %s."), property);
return;
}
ChannelPropertyProviders[property as any].provider(this.currentProperties, this.channel, this.channelParent, this.connection.channelTree).then(value => {
this.uiEvents.fire_react("notify_property", {
property: property,
value: value
});
}).catch(error => {
logError(LogCategory.CHANNEL, tr("Failed to get property value for %s: %o"), property, error);
});
}
private notifyPropertyPermission(permission: keyof ChannelPropertyPermission) {
if (typeof ChannelPropertyPermissionsProviders[permission] !== "object") {
logError(LogCategory.CHANNEL, tr("Channel edit controller missing property permission provider %s."), permission);
@ -91,4 +259,110 @@ class ChannelEditController {
value: value
});
}
private async notifyPermissions() {
switch(this.cachedChannelPermissions.state) {
case "error":
this.uiEvents.fire_react("notify_permissions", {
state: {
state: "error",
reason: this.cachedChannelPermissions.reason
}
});
break;
case "loading":
/* will be notified later */
break;
case "loaded":
this.uiEvents.fire_react("notify_permissions", {
state: {
state: "editable"
}
});
break;
case "no-permissions":
this.uiEvents.fire_react("notify_permissions", {
state: {
state: "no-permissions",
failedPermission: this.cachedChannelPermissions.failedPermission
}
});
break;
case "uninitialized":
const promise = this.connection.permissions.requestChannelPermissions(this.channel.channelId, false).then(permissions => {
this.cachedChannelPermissions = {
state: "loaded",
permissions: permissions
};
}).catch(error => {
if(error instanceof CommandResult) {
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
console.error(error);
this.cachedChannelPermissions = {
state: "no-permissions",
failedPermission: this.connection.permissions.getFailedPermission(error)
};
return;
}
error = error.formattedMessage();
} else if(typeof error !== "string") {
logError(LogCategory.PERMISSIONS, tr("Failed to request channel permissions: %o"), error);
error = tr("Lookup the console");
}
this.cachedChannelPermissions = {
state: "error",
reason: error
};
}).then(() => this.notifyPermissions());
this.cachedChannelPermissions = { state: "loading", promise: promise };
break;
}
}
private notifyPermission(permission: ChannelEditablePermissions) {
switch(this.cachedChannelPermissions.state) {
case "error":
case "uninitialized":
case "loading":
case "no-permissions":
this.uiEvents.fire_react("notify_permission", {permission: permission, value: {state: "loading"}});
break;
case "loaded":
const neededPermission = permissionFromEditablePermission(permission);
const permissionInfo = this.connection.permissions.resolveInfo(neededPermission);
if(permissionInfo) {
//const neededModifyPower = this.cachedChannelPermissions.permissions.find(permission => permission.type.name === PermissionType.I_CHANNEL_NEEDED_PERMISSION_MODIFY_POWER);
const defaultValue = this.cachedChannelPermissions.permissions.find(permission => permission.type === permissionInfo);
let value: number = 0;
if(defaultValue?.hasValue()) {
value = defaultValue.value;
}
if(this.currentChannelPermissions[permission]) {
value = this.currentChannelPermissions[permission];
}
/* FIXME: Test if permissions are really editable! */
this.uiEvents.fire_react("notify_permission", { permission: permission, value: {state: "editable", value: value} });
} else {
this.uiEvents.fire_react("notify_permission", { permission: permission, value: {state: "unsupported"} });
}
break;
}
}
private notifyValidStatus(property: keyof ChannelEditableProperty) {
this.uiEvents.fire_react("notify_property_validate_state", {
property: property,
valid: typeof this.propertyValidationStates[property] === "boolean" ? this.propertyValidationStates[property] : true
});
}
}

View File

@ -30,7 +30,7 @@ ChannelPropertyPermissionsProviders["name"] = {
permissions.register_needed_permission(PermissionType.B_CHANNEL_MODIFY_NAME, callback)
]
}
ChannelPropertyPermissionsProviders["icon"] = SimplePermissionProvider(PermissionType.B_ICON_MANAGE, PermissionType.B_ICON_MANAGE);
ChannelPropertyPermissionsProviders["sortingOrder"] = SimplePermissionProvider(PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER, PermissionType.B_CHANNEL_MODIFY_SORTORDER);
ChannelPropertyPermissionsProviders["description"] = SimplePermissionProvider(PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION, PermissionType.B_CHANNEL_MODIFY_DESCRIPTION);
ChannelPropertyPermissionsProviders["topic"] = SimplePermissionProvider(PermissionType.B_CHANNEL_CREATE_WITH_TOPIC, PermissionType.B_CHANNEL_MODIFY_TOPIC);
@ -66,6 +66,7 @@ ChannelPropertyPermissionsProviders["channelType"] = {
permissions.register_needed_permission(channel ? PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT : PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT, callback),
]
};
ChannelPropertyPermissionsProviders["sidebarMode"] = SimplePermissionProvider(PermissionType.B_CHANNEL_CREATE_MODIFY_SIDEBAR_MODE, PermissionType.B_CHANNEL_CREATE_MODIFY_SIDEBAR_MODE);
ChannelPropertyPermissionsProviders["codec"] = {
provider: (permissions) => {
return {
@ -78,6 +79,7 @@ ChannelPropertyPermissionsProviders["codec"] = {
permissions.register_needed_permission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE, callback),
]
};
ChannelPropertyPermissionsProviders["codecQuality"] = SimplePermissionProvider(PermissionType.B_CHANNEL_MODIFY_CODEC_QUALITY, PermissionType.B_CHANNEL_MODIFY_CODEC_QUALITY);
ChannelPropertyPermissionsProviders["deleteDelay"] = {
provider: permissions => {
return {

View File

@ -1,6 +1,7 @@
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
import {ChannelEntry, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
import {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
import {ChannelTree} from "tc-shared/tree/ChannelTree";
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
export type ChannelPropertyProvider<T extends keyof ChannelEditableProperty> = {
provider: (properties: ChannelProperties, channel: ChannelEntry | undefined, parentChannel: ChannelEntry | undefined, channelTree: ChannelTree) => Promise<ChannelEditableProperty[T]>,
@ -18,24 +19,43 @@ const SimplePropertyProvider = <P extends keyof ChannelProperties>(channelProper
ChannelPropertyProviders["name"] = SimplePropertyProvider("channel_name", "");
ChannelPropertyProviders["phoneticName"] = SimplePropertyProvider("channel_name_phonetic", "");
ChannelPropertyProviders["type"] = {
provider: async properties => {
if(properties.channel_flag_default) {
return "default";
} else if(properties.channel_flag_permanent) {
return "permanent";
} else if(properties.channel_flag_semi_permanent) {
return "semi-permanent";
} else {
return "temporary";
ChannelPropertyProviders["icon"] = {
provider: async (properties, _channel, _parentChannel, channelTree) => {
return {
iconId: properties.channel_icon_id,
remoteIcon: {
iconId: properties.channel_icon_id,
serverUniqueId: channelTree.server.properties.virtualserver_unique_identifier,
handlerId: channelTree.client.handlerId
}
}
},
applier: (value, properties) => properties.channel_icon_id = value.iconId
};
ChannelPropertyProviders["type"] = {
provider: async (properties, channel) => {
let type;
if(properties.channel_flag_default) {
type = "default";
} else if(properties.channel_flag_permanent) {
type = "permanent";
} else if(properties.channel_flag_semi_permanent) {
type = "semi-permanent";
} else {
type = "temporary";
}
return {
type: type,
originallyDefault: !!channel?.properties.channel_flag_default
};
},
applier: (value, properties) => {
properties["channel_flag_default"] = false;
properties["channel_flag_permanent"] = false;
properties["channel_flag_semi_permanent"] = false;
switch (value) {
switch (value.type) {
case "default":
properties["channel_flag_permanent"] = true;
properties["channel_flag_default"] = true;
@ -54,6 +74,42 @@ ChannelPropertyProviders["type"] = {
}
}
}
ChannelPropertyProviders["sideBar"] = {
provider: async (properties, channel, parentChannel, channelTree) => {
const features = channelTree.client.serverFeatures;
if(!features.supportsFeature(ServerFeature.SIDEBAR_MODE)) {
return "not-supported";
}
switch (properties.channel_sidebar_mode) {
case ChannelSidebarMode.FileTransfer:
return "file-transfer";
case ChannelSidebarMode.Description:
return "description";
case ChannelSidebarMode.Conversation:
return "conversation";
case ChannelSidebarMode.Unknown:
default:
return "unknown";
}
},
applier: (value, properties) => {
switch (value) {
case "file-transfer":
properties.channel_sidebar_mode = ChannelSidebarMode.FileTransfer;
break;
case "conversation":
properties.channel_sidebar_mode = ChannelSidebarMode.Conversation;
break;
case "description":
properties.channel_sidebar_mode = ChannelSidebarMode.Description;
break;
}
}
}
ChannelPropertyProviders["password"] = {
provider: async properties => properties.channel_flag_password ? { state: "set" } : { state: "clear" },
applier: (value, properties) => {
@ -86,7 +142,9 @@ ChannelPropertyProviders["sortingOrder"] = {
ChannelPropertyProviders["topic"] = SimplePropertyProvider("channel_topic", "");
ChannelPropertyProviders["description"] = {
provider: async (properties, channel) => {
if(channel) {
if(typeof properties.channel_description !== "undefined" && (properties.channel_description.length !== 0 || channel?.isDescriptionCached())) {
return properties.channel_description;
} else if(channel) {
return await channel.getChannelDescription();
} else {
return "";
@ -107,7 +165,30 @@ ChannelPropertyProviders["codec"] = {
}
}
ChannelPropertyProviders["talkPower"] = SimplePropertyProvider("channel_needed_talk_power", 0);
ChannelPropertyProviders["encryptedVoiceData"] = SimplePropertyProvider("channel_codec_is_unencrypted", true);
ChannelPropertyProviders["encryptedVoiceData"] = {
provider: async (properties, channel, parentChannel, channelTree) => {
let serverSetting: "encrypted" | "unencrypted" | "individual";
switch (channelTree.server.properties.virtualserver_codec_encryption_mode) {
case 1:
serverSetting = "unencrypted";
break;
case 2:
serverSetting = "encrypted";
break;
default:
serverSetting = "individual";
break;
}
return {
encrypted: properties.channel_codec_is_unencrypted,
serverSetting: serverSetting
};
},
applier: (value, properties) => properties.channel_codec_is_unencrypted = value.encrypted
}
ChannelPropertyProviders["maxUsers"] = {
provider: async properties => {
if(properties.channel_flag_maxclients_unlimited) {

View File

@ -0,0 +1,25 @@
import {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
import {ChannelTree} from "tc-shared/tree/ChannelTree";
import {PermissionManager} from "tc-shared/permission/PermissionManager";
import PermissionType from "tc-shared/permission/PermissionType";
export const ChannelPropertyValidators: {[T in keyof ChannelEditableProperty]?: (
currentProperties: ChannelProperties,
originalProperties: ChannelProperties,
channel: ChannelEntry | undefined,
parent: ChannelEntry | undefined,
permissions: PermissionManager,
channelTree: ChannelTree
) => boolean} = {};
ChannelPropertyValidators["name"] = properties => properties.channel_name.length > 0 && properties.channel_name.length <= 30;
ChannelPropertyValidators["phoneticName"] = properties => properties.channel_name_phonetic.length >= 0 && properties.channel_name_phonetic.length <= 30;
ChannelPropertyValidators["password"] = (currentProperties, originalProperties, _channel, _parent, permissions) => {
if(!currentProperties.channel_flag_password) {
if(permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1)) {
return false;
}
}
return true;
}

View File

@ -1,8 +1,50 @@
import {RemoteIconInfo} from "tc-shared/file/Icons";
export type ChannelEditablePermissions =
"join" |
"view" |
"view-description" |
"subscribe" |
"modify" |
"delete" |
"browse" |
"upload" |
"download" |
"rename" |
"directory-create" |
"file-delete";
export type ChannelEditablePermissionValue = {
state: "editable" | "readonly" | "applying",
value: number
} | {
state: "loading" | "unsupported",
};
export type ChannelEditPermissionsState = {
state: "editable" | "loading"
} | {
state: "no-permissions",
failedPermission: string
} | {
state: "error",
reason: string
};
export interface ChannelEditableProperty {
"name": string,
"phoneticName": string,
"type": "default" | "permanent" | "semi-permanent" | "temporary",
"icon": {
iconId: number,
remoteIcon?: RemoteIconInfo
},
"type": {
type: "default" | "permanent" | "semi-permanent" | "temporary",
originallyDefault?: boolean
},
sideBar: "conversation" | "description" | "file-transfer" | "unknown" | "not-supported",
"password": { state: "set", password?: string } | { state: "clear" },
"sortingOrder": { previousChannelId: number, availableChannels: { channelName: string, channelId: number }[] | undefined },
@ -11,7 +53,10 @@ export interface ChannelEditableProperty {
"codec": { type: number, quality: number },
"talkPower": number,
"encryptedVoiceData": number
"encryptedVoiceData": {
encrypted: boolean,
serverSetting?: "encrypted" | "unencrypted" | "individual"
},
"maxUsers": "unlimited" | number,
"maxFamilyUsers": "unlimited" | "inherited" | number,
@ -21,6 +66,7 @@ export interface ChannelEditableProperty {
export interface ChannelPropertyPermission {
name: boolean,
icon: boolean,
password: { editable: boolean, enforced: boolean },
talkPower: boolean,
sortingOrder: boolean,
@ -32,12 +78,14 @@ export interface ChannelPropertyPermission {
temporary: boolean,
default: boolean
},
sidebarMode: boolean,
maxUsers: boolean,
maxFamilyUsers: boolean,
codec: {
opusVoice: boolean,
opusMusic: boolean
},
codecQuality: boolean,
deleteDelay: {
editable: boolean,
maxDelay: number | -1,
@ -62,18 +110,43 @@ export type ChannelEditPermissionEvent<T extends keyof ChannelPropertyPermission
}
export interface ChannelEditEvents {
change_property: {
action_cancel: {},
action_apply: {},
action_change_property: {
property: keyof ChannelEditableProperty
value: ChannelEditableProperty[keyof ChannelEditableProperty]
},
action_change_permission: {
permission: ChannelEditablePermissions,
value: number
},
action_icon_select: {},
query_property: {
property: keyof ChannelEditableProperty
},
query_property_permission: {
permission: keyof ChannelPropertyPermission
}
},
query_permission: {
permission: ChannelEditablePermissions
},
query_permissions: {},
query_property_valid_state: {
property: keyof ChannelEditableProperty,
},
notify_property: ChannelEditPropertyEvent<keyof ChannelEditableProperty>,
notify_property_permission: ChannelEditPermissionEvent<keyof ChannelPropertyPermission>
notify_property_permission: ChannelEditPermissionEvent<keyof ChannelPropertyPermission>,
notify_permission: {
permission: ChannelEditablePermissions,
value: ChannelEditablePermissionValue
},
notify_permissions: {
state: ChannelEditPermissionsState
},
notify_property_validate_state: {
property: keyof ChannelEditableProperty,
valid: boolean
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,10 @@ export function useDependentState<S>(
return [state, setState];
}
export function useTr(message: string) : string {
return /* @tr-ignore */ tr(message);
}
export function joinClassList(...classes: any[]) : string {
return classes.filter(value => typeof value === "string" && value.length > 0).join(" ");
}

View File

@ -1,6 +1,5 @@
import * as React from "react";
import {ReactElement} from "react";
import {AST_Export} from "terser";
const cssStyle = require("./InputField.scss");
@ -25,6 +24,7 @@ export interface BoxedInputFieldProperties {
className?: string;
size?: "normal" | "large" | "small";
type?: "text" | "password" | "number";
onFocus?: (event: React.FocusEvent | React.MouseEvent) => void;
onBlur?: () => void;
@ -71,17 +71,22 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
{this.props.leftIcon ? this.props.leftIcon() : ""}
{this.props.prefix ? <a key={"prefix"} className={cssStyle.prefix}>{this.props.prefix}</a> : undefined}
{this.props.inputBox ?
<span key={"custom-input"} className={cssStyle.inputBox + " " + (this.props.editable ? cssStyle.editable : "")} onClick={this.props.onFocus}>{this.props.inputBox()}</span> :
<span key={"custom-input"}
className={cssStyle.inputBox + " " + (this.props.editable ? cssStyle.editable : "")}
onClick={this.props.onFocus}>{this.props.inputBox()}</span> :
<input key={"input"}
ref={this.refInput}
value={this.props.value || this.state.value}
defaultValue={this.state.defaultValue || this.props.defaultValue}
placeholder={this.props.placeholder}
readOnly={typeof this.props.editable === "boolean" ? !this.props.editable : false}
disabled={this.state.disabled || this.props.disabled}
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
onKeyDown={e => this.onKeyDown(e)}
/>}
type={this.props.type || "text"}
ref={this.refInput}
value={typeof this.props.value === "undefined" ? this.state.value : this.props.value}
defaultValue={this.state.defaultValue || this.props.defaultValue}
placeholder={this.props.placeholder}
readOnly={typeof this.props.editable === "boolean" ? !this.props.editable : false}
disabled={this.state.disabled || this.props.disabled}
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
onKeyDown={e => this.onKeyDown(e)}
/>
}
{this.props.suffix ? <a key={"suffix"} className={cssStyle.suffix}>{this.props.suffix}</a> : undefined}
{this.props.rightIcon ? this.props.rightIcon() : ""}
</div>
@ -271,7 +276,7 @@ export class Select extends React.Component<SelectProperties, SelectFieldState>
render() {
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
return (
<div className={(this.props.type === "boxed" ? cssStyle.containerBoxed : cssStyle.containerFlat) + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.props.className || "") + " " + cssStyle.noLeftIcon + " " + cssStyle.noRightIcon}>
<div className={(this.props.type === "boxed" ? cssStyle.containerBoxed : cssStyle.containerFlat) + " " + cssStyle["size-normal"] + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.props.className || "") + " " + cssStyle.noLeftIcon + " " + cssStyle.noRightIcon}>
{this.props.label ?
<label className={cssStyle["type-static"] + " " + (this.props.labelClassName || "")}>{this.props.label}</label> : undefined}
<select

View File

@ -44,6 +44,7 @@ export abstract class AbstractModal {
/* only valid for the "inline" modals */
type() : ModalType { return "none"; }
color() : "none" | "blue" { return "none"; }
protected onInitialize() {}
protected onDestroy() {}

View File

@ -2,7 +2,7 @@ import * as React from "react";
const cssStyle = require("./RadioButton.scss");
export const RadioButton = (props: {
children?: React.ReactNode | React.ReactNode[],
children?: React.ReactNode | string | React.ReactNode[],
name: string,
selected: boolean,

View File

@ -45,4 +45,10 @@ html:root {
&.shown {
opacity: 1;
}
}
.iconTooltip {
display: flex;
flex-direction: column;
justify-content: center;
}

View File

@ -2,6 +2,7 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import {ReactElement} from "react";
import {guid} from "tc-shared/crypto/uid";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
const cssStyle = require("./Tooltip.scss");
@ -72,7 +73,7 @@ export interface TooltipState {
}
export interface TooltipProperties {
tooltip: () => ReactElement | string;
tooltip: () => ReactElement | ReactElement[] | string;
}
export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
@ -96,11 +97,17 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
}
render() {
return <span
ref={this.refContainer}
onMouseEnter={event => this.onMouseEnter(event)}
onMouseLeave={() => this.setState({ hovered: false })}
>{this.props.children}</span>;
return (
<span
ref={this.refContainer}
onMouseEnter={event => this.onMouseEnter(event)}
onMouseLeave={() => this.setState({ hovered: false })}
onClick={() => this.setState({ hovered: !this.state.hovered })}
style={{ cursor: "pointer" }}
>
{this.props.children}
</span>
);
}
componentDidUpdate(prevProps: Readonly<TooltipProperties>, prevState: Readonly<TooltipState>, snapshot?: any): void {
@ -142,6 +149,15 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
});
}
}
export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string }) => (
<Tooltip tooltip={() => props.children}>
<div className={cssStyle.tooltip + " " + props.className}>
<img src="img/icon_tooltip.svg"/>
</div>
</Tooltip>
);
const globalTooltipRef = React.createRef<GlobalTooltip>();
const tooltipContainer = document.createElement("div");
document.body.appendChild(tooltipContainer);

View File

@ -67,7 +67,7 @@ export class InternalModalRenderer extends React.PureComponent<{ modal: Abstract
onClose={this.props.onClose}
containerClass={cssStyle.contentInternal}
bodyClass={cssStyle.body}
bodyClass={cssStyle.body + " " + cssStyle["modal-" + this.props.modal.color()]}
/>
</div>
</div>

View File

@ -287,9 +287,13 @@ class ChannelTreeController {
}));
events.push(channel.events.on("notify_properties_updated", event => {
if("channel_name" in event.updated_properties) {
this.sendChannelInfo(channel);
}
for (const key of ChannelIconUpdateKeys) {
if (key in event.updated_properties) {
this.sendChannelInfo(channel);
this.sendChannelStatusIcon(channel);
break;
}
}

2
vendor/xbbcode vendored

@ -1 +1 @@
Subproject commit b884828db27025abf0802a015b2fa46bf2c2e44c
Subproject commit 336077435bbb09bb25f6efdcdac36956288fd3ca