Updating the channel edit modal and some minor bugfixing
parent
218a9d0600
commit
810f50f336
10
ChangeLog.md
10
ChangeLog.md
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,6 +68,11 @@
|
|||
|
||||
padding: .5em;
|
||||
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: baseline;
|
||||
|
||||
&.container-icons-local {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
|
@ -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> </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> </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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,59 +1,140 @@
|
|||
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
|
||||
ChannelPropertyProviders[event.property as any].applier(event.value, this.currentProperties, this.channel);
|
||||
this.notifyProperty(event.property);
|
||||
this.validateProperty(event.property);
|
||||
});
|
||||
}).catch(error => {
|
||||
logError(LogCategory.CHANNEL, tr("Failed to get property value for %s: %o"), event.property, error);
|
||||
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)) {
|
||||
const provider = ChannelPropertyPermissionsProviders[key];
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
@ -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(" ");
|
||||
}
|
|
@ -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"}
|
||||
type={this.props.type || "text"}
|
||||
ref={this.refInput}
|
||||
value={this.props.value || this.state.value}
|
||||
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
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -46,3 +46,9 @@ html:root {
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.iconTooltip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
|
@ -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
|
||||
return (
|
||||
<span
|
||||
ref={this.refContainer}
|
||||
onMouseEnter={event => this.onMouseEnter(event)}
|
||||
onMouseLeave={() => this.setState({ hovered: false })}
|
||||
>{this.props.children}</span>;
|
||||
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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit b884828db27025abf0802a015b2fa46bf2c2e44c
|
||||
Subproject commit 336077435bbb09bb25f6efdcdac36956288fd3ca
|
Loading…
Reference in New Issue