Recoded the client info ui in React and increased responsibility to variable changes
This commit is contained in:
parent
bb3283a6c9
commit
2041ed5fc3
20 changed files with 1432 additions and 694 deletions
|
@ -10,6 +10,7 @@
|
||||||
- Fixed BBCode inline code style
|
- Fixed BBCode inline code style
|
||||||
- URL tags can not contain any other tags
|
- URL tags can not contain any other tags
|
||||||
- Correctly parsing the "lazy close tag" `[/]`
|
- Correctly parsing the "lazy close tag" `[/]`
|
||||||
|
- The client info modal now updates its values accordingly on client changes
|
||||||
|
|
||||||
* **05.12.20**
|
* **05.12.20**
|
||||||
- Fixed the webclient for Firefox in incognito mode
|
- Fixed the webclient for Firefox in incognito mode
|
||||||
|
|
|
@ -261,364 +261,6 @@ html:root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.container-client-info {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
padding-right: 5px;
|
|
||||||
padding-left: 5px;
|
|
||||||
|
|
||||||
.heading {
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
.container-avatar {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
margin: calc(#{$client_info_avatar_size} / -2) .75em .5em .5em;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
width: $client_info_avatar_size;
|
|
||||||
height: $client_info_avatar_size;
|
|
||||||
|
|
||||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-avatar-edit {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> img {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
width: $client_info_avatar_size;
|
|
||||||
height: $client_info_avatar_size;
|
|
||||||
|
|
||||||
padding: calc(#{$client_info_avatar_size} / 6);
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.editable {
|
|
||||||
.container-avatar-edit {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-avatar-edit:hover + .avatar {
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-name {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.htmltag-client {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
color: $color_client_normal;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-description {
|
|
||||||
padding-right: calc(#{$client_info_avatar_size} / 2);
|
|
||||||
padding-left: calc(#{$client_info_avatar_size} / 2);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
.client-description {
|
|
||||||
color: #6f6f6f;
|
|
||||||
|
|
||||||
max-width: 100%;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.general-info {
|
|
||||||
padding-top: 1em;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
@include chat-scrollbar-vertical();
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
.block {
|
|
||||||
display: inline-block;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
|
|
||||||
min-width: 6em;
|
|
||||||
|
|
||||||
&.block-right {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.container-property {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
|
|
||||||
.icon_em, .container-icon {
|
|
||||||
margin-left: .2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.block-left {
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.container-property {
|
|
||||||
.icon_em, .container-icon {
|
|
||||||
margin-right: .2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-property {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
> .icon_em, > .container-icon {
|
|
||||||
margin-bottom: .1em;
|
|
||||||
|
|
||||||
font-size: 2em;
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.list {
|
|
||||||
> .icon_em {
|
|
||||||
margin-top: 0; /* for lists the .1em patting on the top looks odd */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.property {
|
|
||||||
line-height: 1.1em;
|
|
||||||
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
min-width: 4em; /* 2em for the icon the last 4 for the text */
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
.title, .value {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: #636363;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: #d9d9d9;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.country {
|
|
||||||
margin-right: .2em;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-container {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
|
|
||||||
.icon-container, .icon_empty, .icon {
|
|
||||||
margin-left: .5em;
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
& > img {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-entry {
|
|
||||||
> .icon_em {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.away-message {
|
|
||||||
margin-left: .25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.client-teaforo-account {
|
|
||||||
a, a:visited {
|
|
||||||
color: #d9d9d9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.list {
|
|
||||||
.property {
|
|
||||||
.value {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(first-of-type) {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-close {
|
|
||||||
font-size: 4em;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
opacity: 0.3;
|
|
||||||
|
|
||||||
width: .5em;
|
|
||||||
height: .5em;
|
|
||||||
|
|
||||||
margin-right: .1em;
|
|
||||||
margin-top: .1em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
@include transition(opacity $button_hover_animation_time ease-in-out);
|
|
||||||
|
|
||||||
&:before, &:after {
|
|
||||||
position: absolute;
|
|
||||||
left: .25em;
|
|
||||||
content: ' ';
|
|
||||||
height: .5em;
|
|
||||||
width: .05em;
|
|
||||||
background-color: #5a5a5a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-more {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
height: 1.5em;
|
|
||||||
font-size: 1.25em;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
color: #999999;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
margin-left: -5px;
|
|
||||||
margin-right: -5px;
|
|
||||||
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #393939;
|
|
||||||
}
|
|
||||||
@include transition($button_hover_animation_time ease-in-out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-music-info {
|
.container-music-info {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
@ -268,23 +268,6 @@ $animation_seperator_length: .1s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
> img {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_empty {
|
|
||||||
display: inline-block;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,7 +273,8 @@ if(typeof ($) !== "undefined") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Object.values)
|
if(!Object.values) {
|
||||||
Object.values = object => Object.keys(object).map(e => object[e]);
|
Object.values = object => Object.keys(object).map(e => object[e]);
|
||||||
|
}
|
||||||
|
|
||||||
export = {};
|
export = {};
|
|
@ -54,6 +54,8 @@ export class ClientProperties {
|
||||||
client_servergroups: string = "";
|
client_servergroups: string = "";
|
||||||
|
|
||||||
client_channel_group_id: number = 0;
|
client_channel_group_id: number = 0;
|
||||||
|
client_channel_group_inherited_channel_id: number = 0;
|
||||||
|
|
||||||
client_lastconnected: number = 0;
|
client_lastconnected: number = 0;
|
||||||
client_created: number = 0;
|
client_created: number = 0;
|
||||||
client_totalconnections: number = 0;
|
client_totalconnections: number = 0;
|
||||||
|
@ -799,16 +801,9 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||||
update_avatar = true;
|
update_avatar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* process updates after variables have been set */
|
if(update_avatar) {
|
||||||
const side_bar = this.channelTree?.client?.side_bar;
|
|
||||||
if(side_bar) {
|
|
||||||
const client_info = side_bar.client_info();
|
|
||||||
if(client_info.current_client() === this)
|
|
||||||
client_info.set_current_client(this, true); /* force an update */
|
|
||||||
}
|
|
||||||
|
|
||||||
if(update_avatar)
|
|
||||||
this.channelTree.client?.fileManager?.avatars.updateCache(this.avatarId(), this.properties.client_flag_avatar);
|
this.channelTree.client?.fileManager?.avatars.updateCache(this.avatarId(), this.properties.client_flag_avatar);
|
||||||
|
}
|
||||||
|
|
||||||
/* devel-block(log-client-property-updates) */
|
/* devel-block(log-client-property-updates) */
|
||||||
group.end();
|
group.end();
|
||||||
|
@ -823,8 +818,9 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClientVariables(force_update?: boolean) : Promise<void> {
|
updateClientVariables(force_update?: boolean) : Promise<void> {
|
||||||
if(Date.now() - 10 * 60 * 1000 < this.promiseClientInfoTimestamp && this.promiseClientInfo && (typeof(force_update) !== "boolean" || force_update))
|
if(Date.now() - 10 * 60 * 1000 < this.promiseClientInfoTimestamp && this.promiseClientInfo && (typeof(force_update) !== "boolean" || force_update)) {
|
||||||
return this.promiseClientInfo;
|
return this.promiseClientInfo;
|
||||||
|
}
|
||||||
|
|
||||||
this.promiseClientInfoTimestamp = Date.now();
|
this.promiseClientInfoTimestamp = Date.now();
|
||||||
return (this.promiseClientInfo = new Promise<void>((resolve, reject) => {
|
return (this.promiseClientInfo = new Promise<void>((resolve, reject) => {
|
||||||
|
@ -931,8 +927,9 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setAudioVolume(value: number) {
|
setAudioVolume(value: number) {
|
||||||
if(this.voiceVolume == value)
|
if(this.voiceVolume == value) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.voiceVolume = value;
|
this.voiceVolume = value;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {ConversationManager} from "../../ui/frames/side/ConversationManager";
|
||||||
import {PrivateConversationManager} from "../../ui/frames/side/PrivateConversationManager";
|
import {PrivateConversationManager} from "../../ui/frames/side/PrivateConversationManager";
|
||||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
import { tr } from "tc-shared/i18n/localize";
|
||||||
|
import {ClientInfoController} from "tc-shared/ui/frames/side/ClientInfoController";
|
||||||
|
|
||||||
export enum InfoFrameMode {
|
export enum InfoFrameMode {
|
||||||
NONE = "none",
|
NONE = "none",
|
||||||
|
@ -60,7 +61,7 @@ export class InfoFrame {
|
||||||
this._value_ping = this._html_tag.find(".value-ping");
|
this._value_ping = this._html_tag.find(".value-ping");
|
||||||
this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations());
|
this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations());
|
||||||
this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => {
|
this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => {
|
||||||
const selected_client = this.handle.client_info().current_client();
|
const selected_client = this.handle.getClientInfo().getClient();
|
||||||
if(!selected_client) return;
|
if(!selected_client) return;
|
||||||
|
|
||||||
const conversation = selected_client ? this.handle.private_conversations().findOrCreateConversation(selected_client) : undefined;
|
const conversation = selected_client ? this.handle.private_conversations().findOrCreateConversation(selected_client) : undefined;
|
||||||
|
@ -248,7 +249,7 @@ export class InfoFrame {
|
||||||
|
|
||||||
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
|
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
|
||||||
//Will be called every time a client is shown
|
//Will be called every time a client is shown
|
||||||
const selected_client = this.handle.client_info().current_client();
|
const selected_client = this.handle.getClientInfo().getClient();
|
||||||
const conversation = selected_client ? this.handle.private_conversations().findConversation(selected_client) : undefined;
|
const conversation = selected_client ? this.handle.private_conversations().findConversation(selected_client) : undefined;
|
||||||
|
|
||||||
const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.getClientId()) ? "visible" : "hidden";
|
const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.getClientId()) ? "visible" : "hidden";
|
||||||
|
@ -281,7 +282,7 @@ export class Frame {
|
||||||
private containerChannelChat: JQuery;
|
private containerChannelChat: JQuery;
|
||||||
private _content_type: FrameContent;
|
private _content_type: FrameContent;
|
||||||
|
|
||||||
private clientInfo: ClientInfo;
|
private clientInfo: ClientInfoController;
|
||||||
private musicInfo: MusicInfo;
|
private musicInfo: MusicInfo;
|
||||||
private channelConversations: ConversationManager;
|
private channelConversations: ConversationManager;
|
||||||
private privateConversations: PrivateConversationManager;
|
private privateConversations: PrivateConversationManager;
|
||||||
|
@ -293,7 +294,7 @@ export class Frame {
|
||||||
this.infoFrame = new InfoFrame(this);
|
this.infoFrame = new InfoFrame(this);
|
||||||
this.privateConversations = new PrivateConversationManager(handle);
|
this.privateConversations = new PrivateConversationManager(handle);
|
||||||
this.channelConversations = new ConversationManager(handle);
|
this.channelConversations = new ConversationManager(handle);
|
||||||
this.clientInfo = new ClientInfo(this);
|
this.clientInfo = new ClientInfoController(handle);
|
||||||
this.musicInfo = new MusicInfo(this);
|
this.musicInfo = new MusicInfo(this);
|
||||||
|
|
||||||
this._build_html_tag();
|
this._build_html_tag();
|
||||||
|
@ -313,7 +314,7 @@ export class Frame {
|
||||||
this.infoFrame && this.infoFrame.destroy();
|
this.infoFrame && this.infoFrame.destroy();
|
||||||
this.infoFrame = undefined;
|
this.infoFrame = undefined;
|
||||||
|
|
||||||
this.clientInfo && this.clientInfo.destroy();
|
this.clientInfo?.destroy();
|
||||||
this.clientInfo = undefined;
|
this.clientInfo = undefined;
|
||||||
|
|
||||||
this.musicInfo && this.musicInfo.destroy();
|
this.musicInfo && this.musicInfo.destroy();
|
||||||
|
@ -349,7 +350,7 @@ export class Frame {
|
||||||
return this.channelConversations;
|
return this.channelConversations;
|
||||||
}
|
}
|
||||||
|
|
||||||
client_info() : ClientInfo {
|
getClientInfo() : ClientInfoController {
|
||||||
return this.clientInfo;
|
return this.clientInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,16 +387,15 @@ export class Frame {
|
||||||
}
|
}
|
||||||
|
|
||||||
show_client_info(client: ClientEntry) {
|
show_client_info(client: ClientEntry) {
|
||||||
this.clientInfo.set_current_client(client);
|
this.clientInfo.setClient(client);
|
||||||
this.infoFrame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
|
this.infoFrame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
|
||||||
|
|
||||||
if(this._content_type === FrameContent.CLIENT_INFO)
|
if(this._content_type === FrameContent.CLIENT_INFO)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.clientInfo.previous_frame_content = this._content_type;
|
|
||||||
this._clear();
|
this._clear();
|
||||||
this._content_type = FrameContent.CLIENT_INFO;
|
this._content_type = FrameContent.CLIENT_INFO;
|
||||||
this.containerChannelChat.append(this.clientInfo.html_tag());
|
this.containerChannelChat.append(this.clientInfo.getHtmlTag());
|
||||||
}
|
}
|
||||||
|
|
||||||
show_music_player(client: MusicClientEntry) {
|
show_music_player(client: MusicClientEntry) {
|
||||||
|
|
378
shared/js/ui/frames/side/ClientInfo.scss
Normal file
378
shared/js/ui/frames/side/ClientInfo.scss
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
$color_client_normal: #cccccc;
|
||||||
|
$client_info_avatar_size: 10em;
|
||||||
|
$bot_thumbnail_width: 16em;
|
||||||
|
$bot_thumbnail_height: 9em;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.clientName {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.htmltag {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: $color_client_normal;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerDescription {
|
||||||
|
padding-right: calc(#{$client_info_avatar_size} / 2);
|
||||||
|
padding-left: calc(#{$client_info_avatar_size} / 2);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #6f6f6f;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonClose {
|
||||||
|
font-size: 4em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
opacity: 0.3;
|
||||||
|
|
||||||
|
width: .5em;
|
||||||
|
height: .5em;
|
||||||
|
|
||||||
|
margin-right: .1em;
|
||||||
|
margin-top: .1em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
&:before, &:after {
|
||||||
|
position: absolute;
|
||||||
|
left: .25em;
|
||||||
|
content: ' ';
|
||||||
|
height: .5em;
|
||||||
|
width: .05em;
|
||||||
|
background-color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonMore {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
height: 1.5em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
color: #999999;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
margin-left: -5px;
|
||||||
|
margin-right: -5px;
|
||||||
|
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #393939;
|
||||||
|
}
|
||||||
|
@include transition($button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerAvatar {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
margin: calc(#{$client_info_avatar_size} / -2) .75em .5em .5em;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
-moz-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||||
|
-webkit-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||||
|
box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
width: $client_info_avatar_size;
|
||||||
|
height: $client_info_avatar_size;
|
||||||
|
|
||||||
|
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.avatarImage {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
height: $client_info_avatar_size * .8;
|
||||||
|
width: $client_info_avatar_size * .8;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
width: $client_info_avatar_size;
|
||||||
|
height: $client_info_avatar_size;
|
||||||
|
|
||||||
|
padding: calc(#{$client_info_avatar_size} / 6);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@include transition(opacity $button_hover_animation_time ease-in-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.editable {
|
||||||
|
.edit {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit:hover + .avatar {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalInfo {
|
||||||
|
padding-top: 1em;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 6em;
|
||||||
|
|
||||||
|
&.blockRight {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.containerProperty {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-left: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blockLeft {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.containerProperty {
|
||||||
|
.icon {
|
||||||
|
margin-right: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.containerProperty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
margin-bottom: .1em;
|
||||||
|
|
||||||
|
font-size: 2em;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.list {
|
||||||
|
> .icon_em {
|
||||||
|
margin-top: 0; /* for lists the .1em patting on the top looks odd */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.property {
|
||||||
|
line-height: 1.1em;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-width: 4em; /* 2em for the icon the last 4 for the text */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.title, .value {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #636363;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #d9d9d9;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.country {
|
||||||
|
margin-right: .2em;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status, &.groups {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.statusEntry, .groupEntry {
|
||||||
|
.icon {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.awayMessage {
|
||||||
|
margin-left: .25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupEntry {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clientTeaforoAccount {
|
||||||
|
a, a:visited {
|
||||||
|
color: #d9d9d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.list {
|
||||||
|
.property {
|
||||||
|
.value {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(first-of-type) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
406
shared/js/ui/frames/side/ClientInfoController.ts
Normal file
406
shared/js/ui/frames/side/ClientInfoController.ts
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
|
import {ClientEntry, ClientType, LocalClientEntry} from "tc-shared/tree/Client";
|
||||||
|
import {
|
||||||
|
ClientForumInfo,
|
||||||
|
ClientGroupInfo,
|
||||||
|
ClientInfoEvents,
|
||||||
|
ClientStatusInfo, ClientVersionInfo
|
||||||
|
} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
||||||
|
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {ClientInfoRenderer} from "tc-shared/ui/frames/side/ClientInfoRenderer";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as i18nc from "../../../i18n/country";
|
||||||
|
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
|
||||||
|
|
||||||
|
type CurrentClientInfo = {
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
joinTimestamp: number,
|
||||||
|
leaveTimestamp: number,
|
||||||
|
country: { name: string, flag: string },
|
||||||
|
volume: { volume: number, muted: boolean },
|
||||||
|
status: ClientStatusInfo,
|
||||||
|
forumAccount: ClientForumInfo | undefined,
|
||||||
|
channelGroup: number,
|
||||||
|
serverGroups: number[],
|
||||||
|
version: ClientVersionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientInfoController {
|
||||||
|
private readonly connection: ConnectionHandler;
|
||||||
|
private readonly listenerConnection: (() => void)[];
|
||||||
|
|
||||||
|
private readonly uiEvents: Registry<ClientInfoEvents>;
|
||||||
|
private readonly htmlContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
private listenerClient: (() => void)[];
|
||||||
|
private currentClient: ClientEntry | undefined;
|
||||||
|
private currentClientStatus: CurrentClientInfo | undefined;
|
||||||
|
|
||||||
|
constructor(connection: ConnectionHandler) {
|
||||||
|
this.connection = connection;
|
||||||
|
this.uiEvents = new Registry<ClientInfoEvents>();
|
||||||
|
this.uiEvents.enableDebug("client-info");
|
||||||
|
|
||||||
|
this.listenerConnection = [];
|
||||||
|
this.listenerClient = [];
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
|
||||||
|
this.htmlContainer = document.createElement("div");
|
||||||
|
this.htmlContainer.style.display = "flex";
|
||||||
|
this.htmlContainer.style.flexDirection = "column";
|
||||||
|
this.htmlContainer.style.justifyContent = "strech";
|
||||||
|
this.htmlContainer.style.height = "100%";
|
||||||
|
ReactDOM.render(React.createElement(ClientInfoRenderer, { events: this.uiEvents }), this.htmlContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtmlTag() : HTMLDivElement {
|
||||||
|
return this.htmlContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize() {
|
||||||
|
this.listenerConnection.push(this.connection.groups.events.on("notify_groups_updated", event => {
|
||||||
|
if(!this.currentClientStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const update of event.updates) {
|
||||||
|
if(update.group.id === this.currentClientStatus.channelGroup) {
|
||||||
|
this.sendChannelGroup();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const update of event.updates) {
|
||||||
|
if(this.currentClientStatus.serverGroups.indexOf(update.group.id) !== -1) {
|
||||||
|
this.sendServerGroups();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.listenerConnection.push(this.connection.channelTree.events.on("notify_client_leave_view", event => {
|
||||||
|
if(event.client !== this.currentClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentClientStatus.leaveTimestamp = Date.now() / 1000;
|
||||||
|
this.currentClient = undefined;
|
||||||
|
this.unregisterClientEvents();
|
||||||
|
this.sendOnline();
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.uiEvents.on("query_client_name", () => this.sendClientName());
|
||||||
|
this.uiEvents.on("query_client_description", () => this.sendClientDescription());
|
||||||
|
this.uiEvents.on("query_channel_group", () => this.sendChannelGroup());
|
||||||
|
this.uiEvents.on("query_server_groups", () => this.sendServerGroups());
|
||||||
|
this.uiEvents.on("query_status", () => this.sendClientStatus());
|
||||||
|
this.uiEvents.on("query_online", () => this.sendOnline());
|
||||||
|
this.uiEvents.on("query_country", () => this.sendCountry());
|
||||||
|
this.uiEvents.on("query_volume", () => this.sendVolume());
|
||||||
|
this.uiEvents.on("query_version", () => this.sendVersion());
|
||||||
|
this.uiEvents.on("query_forum", () => this.sendForum());
|
||||||
|
|
||||||
|
this.uiEvents.on("action_edit_avatar", () => this.connection.update_avatar());
|
||||||
|
this.uiEvents.on("action_show_full_info", () => this.currentClient && openClientInfo(this.currentClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
private unregisterClientEvents() {
|
||||||
|
this.listenerClient.forEach(callback => callback());
|
||||||
|
this.listenerClient = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerClientEvents(client: ClientEntry) {
|
||||||
|
const events = this.listenerClient;
|
||||||
|
|
||||||
|
events.push(client.events.on("notify_properties_updated", event => {
|
||||||
|
if('client_nickname' in event.updated_properties) {
|
||||||
|
this.currentClientStatus.name = event.client_properties.client_nickname;
|
||||||
|
this.sendClientName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if('client_description' in event.updated_properties) {
|
||||||
|
this.currentClientStatus.description = event.client_properties.client_description;
|
||||||
|
this.sendClientDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
if('client_channel_group_id' in event.updated_properties) {
|
||||||
|
this.currentClientStatus.channelGroup = event.client_properties.client_channel_group_id;
|
||||||
|
this.sendChannelGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if('client_servergroups' in event.updated_properties) {
|
||||||
|
this.currentClientStatus.serverGroups = client.assignedServerGroupIds();
|
||||||
|
this.sendServerGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Can happen since that variable isn't in view on client appearance */
|
||||||
|
if('client_lastconnected' in event.updated_properties) {
|
||||||
|
this.currentClientStatus.joinTimestamp = event.client_properties.client_lastconnected;
|
||||||
|
this.sendOnline();
|
||||||
|
}
|
||||||
|
|
||||||
|
if('client_country' in event.updated_properties) {
|
||||||
|
this.updateCachedCountry(client);
|
||||||
|
this.sendCountry();
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const key of ["client_away", "client_away_message", "client_input_muted", "client_input_hardware", "client_output_muted", "client_output_hardware"]) {
|
||||||
|
if(key in event.updated_properties) {
|
||||||
|
this.updateCachedClientStatus(client);
|
||||||
|
this.sendClientStatus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if('client_platform' in event.updated_properties || 'client_version' in event.updated_properties) {
|
||||||
|
this.currentClientStatus.version = {
|
||||||
|
platform: client.properties.client_platform,
|
||||||
|
version: client.properties.client_version
|
||||||
|
};
|
||||||
|
this.sendVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
if('client_teaforo_flags' in event.updated_properties || 'client_teaforo_name' in event.updated_properties || 'client_teaforo_id' in event.updated_properties) {
|
||||||
|
this.updateForumAccount(client);
|
||||||
|
this.sendForum();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
events.push(client.events.on("notify_audio_level_changed", () => {
|
||||||
|
this.updateCachedVolume(client);
|
||||||
|
this.sendVolume();
|
||||||
|
}));
|
||||||
|
|
||||||
|
events.push(client.events.on("notify_mute_state_change", () => {
|
||||||
|
this.updateCachedVolume(client);
|
||||||
|
this.sendVolume();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCachedClientStatus(client: ClientEntry) {
|
||||||
|
this.currentClientStatus.status = {
|
||||||
|
away: client.properties.client_away ? client.properties.client_away_message ? client.properties.client_away_message : true : false,
|
||||||
|
microphoneMuted: client.properties.client_input_muted,
|
||||||
|
microphoneDisabled: !client.properties.client_input_hardware,
|
||||||
|
speakerMuted: client.properties.client_output_muted,
|
||||||
|
speakerDisabled: client.properties.client_output_hardware
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCachedCountry(client: ClientEntry) {
|
||||||
|
this.currentClientStatus.country = {
|
||||||
|
flag: client.properties.client_country,
|
||||||
|
name: i18nc.country_name(client.properties.client_country.toUpperCase()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCachedVolume(client: ClientEntry) {
|
||||||
|
this.currentClientStatus.volume = {
|
||||||
|
volume: client.getAudioVolume(),
|
||||||
|
muted: client.isMuted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateForumAccount(client: ClientEntry) {
|
||||||
|
if(client.properties.client_teaforo_id) {
|
||||||
|
this.currentClientStatus.forumAccount = {
|
||||||
|
flags: client.properties.client_teaforo_flags,
|
||||||
|
nickname: client.properties.client_teaforo_name,
|
||||||
|
userId: client.properties.client_teaforo_id
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.currentClientStatus.forumAccount = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeClientInfo(client: ClientEntry) {
|
||||||
|
this.currentClientStatus = {
|
||||||
|
name: client.properties.client_nickname,
|
||||||
|
description: client.properties.client_description,
|
||||||
|
channelGroup: client.properties.client_channel_group_id,
|
||||||
|
serverGroups: client.assignedServerGroupIds(),
|
||||||
|
country: undefined,
|
||||||
|
forumAccount: undefined,
|
||||||
|
joinTimestamp: client.properties.client_lastconnected,
|
||||||
|
leaveTimestamp: 0,
|
||||||
|
status: undefined,
|
||||||
|
volume: undefined,
|
||||||
|
version: {
|
||||||
|
platform: client.properties.client_platform,
|
||||||
|
version: client.properties.client_version
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.updateCachedClientStatus(client);
|
||||||
|
this.updateCachedCountry(client);
|
||||||
|
this.updateCachedVolume(client);
|
||||||
|
this.updateForumAccount(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.listenerConnection.forEach(callback => callback());
|
||||||
|
this.listenerConnection.splice(0, this.listenerConnection.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClient(client: ClientEntry | undefined) {
|
||||||
|
if(this.currentClient === client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unregisterClientEvents();
|
||||||
|
this.currentClient = client;
|
||||||
|
if(this.currentClient) {
|
||||||
|
this.currentClient.updateClientVariables().then(undefined);
|
||||||
|
this.registerClientEvents(this.currentClient);
|
||||||
|
this.initializeClientInfo(this.currentClient);
|
||||||
|
this.uiEvents.fire("notify_client", {
|
||||||
|
info: {
|
||||||
|
handlerId: this.connection.handlerId,
|
||||||
|
type: client instanceof LocalClientEntry ? "self" : client.properties.client_type === ClientType.CLIENT_QUERY ? "query" : "voice",
|
||||||
|
clientDatabaseId: client.properties.client_database_id,
|
||||||
|
clientId: client.clientId(),
|
||||||
|
clientUniqueId: client.properties.client_unique_identifier
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.currentClientStatus = undefined;
|
||||||
|
this.uiEvents.fire("notify_client", {
|
||||||
|
info: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient() : ClientEntry | undefined {
|
||||||
|
return this.currentClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateGroupInfo(groupId: number, type: "channel" | "server") : ClientGroupInfo {
|
||||||
|
const uniqueServerId = this.connection.channelTree.server.properties.virtualserver_unique_identifier;
|
||||||
|
const group = type === "channel" ? this.connection.groups.findChannelGroup(groupId) : this.connection.groups.findServerGroup(groupId);
|
||||||
|
|
||||||
|
if(!group) {
|
||||||
|
return {
|
||||||
|
groupId: groupId,
|
||||||
|
groupIcon: { iconId: 0, serverUniqueId: uniqueServerId, handlerId: this.connection.handlerId },
|
||||||
|
groupName: tra("Unknown group {}", groupId),
|
||||||
|
groupSortOrder: 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
groupId: group.id,
|
||||||
|
groupName: group.name,
|
||||||
|
groupIcon: {
|
||||||
|
handlerId: this.connection.handlerId,
|
||||||
|
serverUniqueId: uniqueServerId,
|
||||||
|
iconId: group.properties.iconid
|
||||||
|
},
|
||||||
|
groupSortOrder: group.properties.sortid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendChannelGroup() {
|
||||||
|
if(typeof this.currentClientStatus === "undefined") {
|
||||||
|
this.uiEvents.fire_react("notify_channel_group", { group: undefined });
|
||||||
|
} else {
|
||||||
|
this.uiEvents.fire_react("notify_channel_group", { group: this.generateGroupInfo(this.currentClientStatus.channelGroup, "channel") });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendServerGroups() {
|
||||||
|
if(this.currentClientStatus === undefined) {
|
||||||
|
this.uiEvents.fire_react("notify_server_groups", { groups: [] });
|
||||||
|
} else {
|
||||||
|
this.uiEvents.fire_react("notify_server_groups", {
|
||||||
|
groups: this.currentClientStatus.serverGroups.map(group => this.generateGroupInfo(group, "server"))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.groupSortOrder < b.groupSortOrder)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (a.groupSortOrder > b.groupSortOrder)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (a.groupId > b.groupId)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (a.groupId < b.groupId)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}).reverse()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendClientStatus() {
|
||||||
|
this.uiEvents.fire_react("notify_status", {
|
||||||
|
status: this.currentClientStatus?.status || {
|
||||||
|
away: false,
|
||||||
|
speakerDisabled: false,
|
||||||
|
speakerMuted: false,
|
||||||
|
microphoneDisabled: false,
|
||||||
|
microphoneMuted: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendClientName() {
|
||||||
|
this.uiEvents.fire_react("notify_client_name", { name: this.currentClientStatus?.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendClientDescription() {
|
||||||
|
this.uiEvents.fire_react("notify_client_description", { description: this.currentClientStatus?.description });
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendOnline() {
|
||||||
|
this.uiEvents.fire_react("notify_online", {
|
||||||
|
status: {
|
||||||
|
leaveTimestamp: this.currentClientStatus ? this.currentClientStatus.leaveTimestamp : 0,
|
||||||
|
joinTimestamp: this.currentClientStatus ? this.currentClientStatus.joinTimestamp : 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendCountry() {
|
||||||
|
this.uiEvents.fire_react("notify_country", {
|
||||||
|
country: this.currentClientStatus ? {
|
||||||
|
name: this.currentClientStatus.country.name,
|
||||||
|
flag: this.currentClientStatus.country.flag
|
||||||
|
} : {
|
||||||
|
name: tr("Unknown"),
|
||||||
|
flag: "xx"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendVolume() {
|
||||||
|
this.uiEvents.fire_react("notify_volume", {
|
||||||
|
volume: this.currentClientStatus ? {
|
||||||
|
volume: this.currentClientStatus.volume.volume,
|
||||||
|
muted: this.currentClientStatus.volume.muted
|
||||||
|
} : {
|
||||||
|
volume: -1,
|
||||||
|
muted: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendVersion() {
|
||||||
|
this.uiEvents.fire_react("notify_version", {
|
||||||
|
version: this.currentClientStatus ? {
|
||||||
|
platform: this.currentClientStatus.version.platform,
|
||||||
|
version: this.currentClientStatus.version.version
|
||||||
|
} : {
|
||||||
|
platform: tr("Unknown"),
|
||||||
|
version: tr("Unknown")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendForum() {
|
||||||
|
this.uiEvents.fire_react("notify_forum", { forum: this.currentClientStatus?.forumAccount })
|
||||||
|
}
|
||||||
|
}
|
90
shared/js/ui/frames/side/ClientInfoDefinitions.ts
Normal file
90
shared/js/ui/frames/side/ClientInfoDefinitions.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||||
|
|
||||||
|
export type ClientInfoType = "query" | "voice" | "self";
|
||||||
|
|
||||||
|
export type ClientInfoInfo = {
|
||||||
|
type: ClientInfoType,
|
||||||
|
handlerId: string,
|
||||||
|
|
||||||
|
/* might be zero if the client has been disconnected */
|
||||||
|
clientId: number | 0,
|
||||||
|
clientUniqueId: string,
|
||||||
|
clientDatabaseId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OptionalClientInfoInfo = { contextHash: string } & (ClientInfoInfo | { type: "none" });
|
||||||
|
|
||||||
|
export type ClientStatusInfo = {
|
||||||
|
microphoneMuted: boolean,
|
||||||
|
microphoneDisabled: boolean,
|
||||||
|
|
||||||
|
speakerMuted: boolean,
|
||||||
|
speakerDisabled: boolean,
|
||||||
|
|
||||||
|
away: boolean | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientForumInfo = {
|
||||||
|
userId: number,
|
||||||
|
nickname: string,
|
||||||
|
flags: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientInfoOnline = {
|
||||||
|
joinTimestamp: number,
|
||||||
|
leaveTimestamp: number | 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientGroupInfo = {
|
||||||
|
groupId: number,
|
||||||
|
groupIcon: RemoteIconInfo,
|
||||||
|
groupName: string,
|
||||||
|
groupSortOrder: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientCountryInfo = {
|
||||||
|
flag: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientVolumeInfo = {
|
||||||
|
volume: number,
|
||||||
|
muted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientVersionInfo = {
|
||||||
|
platform: string,
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientInfoEvents {
|
||||||
|
action_show_full_info: {},
|
||||||
|
action_edit_avatar: {},
|
||||||
|
|
||||||
|
query_channel_group: {},
|
||||||
|
query_server_groups: {},
|
||||||
|
query_client_name: {},
|
||||||
|
query_client_description: {},
|
||||||
|
query_status: {},
|
||||||
|
query_online: {},
|
||||||
|
query_country: {},
|
||||||
|
query_volume: {},
|
||||||
|
query_version: {},
|
||||||
|
query_forum: {},
|
||||||
|
|
||||||
|
notify_client_name: { name: string },
|
||||||
|
notify_client_description: { description: string }
|
||||||
|
notify_channel_group: { group: ClientGroupInfo | undefined },
|
||||||
|
notify_server_groups: { groups: ClientGroupInfo[] },
|
||||||
|
notify_status: { status: ClientStatusInfo },
|
||||||
|
notify_online: { status: ClientInfoOnline },
|
||||||
|
notify_country: { country: ClientCountryInfo },
|
||||||
|
notify_volume: { volume: ClientVolumeInfo },
|
||||||
|
notify_version: { version: ClientVersionInfo },
|
||||||
|
notify_forum: { forum: ClientForumInfo },
|
||||||
|
|
||||||
|
/* reset all fields into "loading" state */
|
||||||
|
notify_client: {
|
||||||
|
info: ClientInfoInfo | undefined
|
||||||
|
}
|
||||||
|
}
|
490
shared/js/ui/frames/side/ClientInfoRenderer.tsx
Normal file
490
shared/js/ui/frames/side/ClientInfoRenderer.tsx
Normal file
|
@ -0,0 +1,490 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {useContext, useEffect, useState} from "react";
|
||||||
|
import {
|
||||||
|
ClientCountryInfo, ClientForumInfo, ClientGroupInfo,
|
||||||
|
ClientInfoEvents,
|
||||||
|
ClientInfoOnline,
|
||||||
|
ClientStatusInfo,
|
||||||
|
ClientVersionInfo,
|
||||||
|
ClientVolumeInfo,
|
||||||
|
OptionalClientInfoInfo
|
||||||
|
} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {ClientAvatar, getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
|
||||||
|
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {format_online_time} from "tc-shared/utils/TimeUtils";
|
||||||
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
|
import {getIconManager} from "tc-shared/file/Icons";
|
||||||
|
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
|
|
||||||
|
const cssStyle = require("./ClientInfo.scss");
|
||||||
|
|
||||||
|
const EventsContext = React.createContext<Registry<ClientInfoEvents>>(undefined);
|
||||||
|
const ClientContext = React.createContext<OptionalClientInfoInfo>(undefined);
|
||||||
|
|
||||||
|
const Avatar = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
|
||||||
|
let avatar: "loading" | ClientAvatar;
|
||||||
|
if(client.type === "none") {
|
||||||
|
avatar = "loading";
|
||||||
|
} else {
|
||||||
|
avatar = getGlobalAvatarManagerFactory().getManager(client.handlerId).resolveClientAvatar({ id: client.clientId, clientUniqueId: client.clientUniqueId, database_id: client.clientDatabaseId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerAvatar + " " + (client.type === "self" ? cssStyle.editable : undefined)}>
|
||||||
|
<div className={cssStyle.avatar}>
|
||||||
|
<AvatarRenderer avatar={avatar} className={cssStyle.avatarImage + " " + (avatar === "loading" ? cssStyle.loading : "")} />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.edit} onClick={() => events.fire("action_edit_avatar")}>
|
||||||
|
<img src="img/photo-camera.svg" alt={tr("Upload avatar")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientName = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
|
||||||
|
const [ name, setName ] = useDependentState<string | null>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_client_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_client_name", event => setName(event.name), undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.clientName}>
|
||||||
|
{name === null || client.type === "none" ?
|
||||||
|
<div key={"loading"} className={cssStyle.htmltag}><Translatable>loading</Translatable> <LoadingDots /></div> :
|
||||||
|
<ClientTag className={cssStyle.htmltag} clientName={name} clientUniqueId={client.clientUniqueId} handlerId={client.handlerId} key={"info-" + client.clientUniqueId + "-" + name} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientDescription = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ description, setDescription ] = useDependentState<string | null | undefined>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_client_description");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_client_description", event => {
|
||||||
|
setDescription(event.description ? event.description : null);
|
||||||
|
}, undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerDescription}>
|
||||||
|
{description === undefined || description === null ?
|
||||||
|
null :
|
||||||
|
<div key={"description"} className={cssStyle.description}>{description}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const InfoBlock = (props: { imageUrl?: string, clientIcon?: ClientIcon, children: [React.ReactElement, React.ReactElement], valueClass?: string }) => {
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.containerProperty}>
|
||||||
|
<div className={cssStyle.icon}>
|
||||||
|
{props.imageUrl ? <img alt={""} src={props.imageUrl} /> : <ClientIconRenderer icon={props.clientIcon} />}
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.property}>
|
||||||
|
<div className={cssStyle.title}>{props.children[0]}</div>
|
||||||
|
<div className={cssStyle.value + " " + props.valueClass}>{props.children[1]}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientOnlineSince = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ onlineInfo, setOnlineInfo ] = useDependentState<ClientInfoOnline>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_online");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
const [ revision, setRevision ] = useState(0);
|
||||||
|
|
||||||
|
events.reactUse("notify_online", event => setOnlineInfo(event.status), undefined, []);
|
||||||
|
|
||||||
|
let onlineBody;
|
||||||
|
if(client.type === "none" || !onlineInfo) {
|
||||||
|
onlineBody = <React.Fragment key={"loading"}><Translatable>loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||||
|
} else if(onlineInfo.joinTimestamp === 0) {
|
||||||
|
onlineBody = <React.Fragment key={"invalid"}><Translatable>Join timestamp not logged</Translatable></React.Fragment>;
|
||||||
|
} else if(onlineInfo.leaveTimestamp === 0) {
|
||||||
|
const onlineTime = Date.now() / 1000 - onlineInfo.joinTimestamp;
|
||||||
|
onlineBody = <React.Fragment key={"value-live"}>{format_online_time(onlineTime)}</React.Fragment>;
|
||||||
|
} else {
|
||||||
|
const onlineTime = onlineInfo.leaveTimestamp - onlineInfo.joinTimestamp;
|
||||||
|
onlineBody = <React.Fragment key={"value-disconnected"}>{format_online_time(onlineTime)} (<Translatable>left view</Translatable>)</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!onlineInfo || onlineInfo.leaveTimestamp !== 0 || onlineInfo.joinTimestamp === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => setRevision(revision + 1), 900);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock imageUrl={"img/icon_client_online_time.svg"}>
|
||||||
|
<Translatable>Online since</Translatable>
|
||||||
|
{onlineBody}
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientCountry = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ country, setCountry ] = useDependentState<ClientCountryInfo>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_country");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_country", event => setCountry(event.country), undefined, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock imageUrl={"img/icon_client_country.svg"}>
|
||||||
|
<Translatable>Country</Translatable>
|
||||||
|
<>
|
||||||
|
<div className={cssStyle.country + " country flag-" + country?.flag.toLocaleLowerCase()} />
|
||||||
|
<a>{country?.name || tr("Unknown")}</a>
|
||||||
|
</>
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientVolume = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ volume, setVolume ] = useDependentState<ClientVolumeInfo>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_volume");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_volume", event => setVolume(event.volume), undefined, []);
|
||||||
|
|
||||||
|
if(client.type === "self" || client.type === "none") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if(volume) {
|
||||||
|
let text = (volume.volume * 100).toFixed(0) + "%";
|
||||||
|
if(volume.muted) {
|
||||||
|
text += " (" + tr("Muted") + ")";
|
||||||
|
}
|
||||||
|
body = <React.Fragment key={"value"}>{text}</React.Fragment>;
|
||||||
|
} else {
|
||||||
|
body = <React.Fragment key={"loading"}><Translatable>loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock imageUrl={"img/icon_client_volume.svg"} key={"volume"}>
|
||||||
|
<Translatable>Volume</Translatable>
|
||||||
|
{body}
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientForumAccount = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ forum, setForum ] = useDependentState<ClientForumInfo>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_forum");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_forum", event => setForum(event.forum), undefined, []);
|
||||||
|
|
||||||
|
if(!forum) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = forum.nickname;
|
||||||
|
if((forum.flags & 0x01) > 0) {
|
||||||
|
text += " (" + tr("Banned") + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if((forum.flags & 0x02) > 0) {
|
||||||
|
text += " (" + tr("Stuff") + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if((forum.flags & 0x04) > 0) {
|
||||||
|
text += " (" + tr("Premium") + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock imageUrl={"img/icon_client_forum_account.svg"} valueClass={cssStyle.clientTeaforoAccount}>
|
||||||
|
<Translatable>TeaSpeak Forum account</Translatable>
|
||||||
|
<a href={"https://forum.teaspeak.de/index.php?members/" + forum.userId} target={"_blank"}>{text}</a>
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientVersion = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ version, setVersion ] = useDependentState<ClientVersionInfo>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_version");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_version", event => setVersion(event.version), undefined, []);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if(version) {
|
||||||
|
let platform = version.platform;
|
||||||
|
if(platform.indexOf("Win32") != 0 && (version.version.indexOf("Win64") != -1 || version.version.indexOf("WOW64") != -1)) {
|
||||||
|
platform = platform.replace("Win32", "Win64");
|
||||||
|
}
|
||||||
|
|
||||||
|
body = <span title={version.version} key={"value"}>{version.version.split(" ")[0]} on {platform}</span>;
|
||||||
|
} else {
|
||||||
|
body = <React.Fragment key={"loading"}><Translatable>loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock imageUrl={"img/icon_client_version.svg"}>
|
||||||
|
<Translatable>Version</Translatable>
|
||||||
|
{body}
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClientStatusEntry = (props: { icon: ClientIcon, children: React.ReactElement }) => (
|
||||||
|
<div className={cssStyle.statusEntry}>
|
||||||
|
<ClientIconRenderer icon={props.icon} className={cssStyle.icon} />
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ClientStatus = React.memo(() => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ status, setStatus ] = useDependentState<ClientStatusInfo>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_status");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_status", event => setStatus(event.status), undefined, []);
|
||||||
|
|
||||||
|
let elements = [];
|
||||||
|
if(status) {
|
||||||
|
if(status.away) {
|
||||||
|
let message = typeof status.away === "string" ? " (" + status.away + ")" : undefined;
|
||||||
|
elements.push(<ClientStatusEntry key={"away"} icon={ClientIcon.Away}><><Translatable>Away</Translatable> {message}</></ClientStatusEntry>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(status.speakerDisabled) {
|
||||||
|
elements.push(<ClientStatusEntry key={"hardwareoutputmuted"} icon={ClientIcon.HardwareOutputMuted}><Translatable>Speakers/Headphones disabled</Translatable></ClientStatusEntry>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(status.microphoneDisabled) {
|
||||||
|
elements.push(<ClientStatusEntry key={"hardwareinputmuted"} icon={ClientIcon.HardwareInputMuted}><Translatable>Microphone disabled</Translatable></ClientStatusEntry>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(status.speakerMuted) {
|
||||||
|
elements.push(<ClientStatusEntry key={"outputmuted"} icon={ClientIcon.OutputMuted}><Translatable>Speakers/Headphones Muted</Translatable></ClientStatusEntry>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(status.microphoneMuted) {
|
||||||
|
elements.push(<ClientStatusEntry key={"inputmuted"} icon={ClientIcon.InputMuted}><Translatable>Microphone Muted</Translatable></ClientStatusEntry>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(elements.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock imageUrl={"img/icon_client_status.svg"} key={"status"} valueClass={cssStyle.status}>
|
||||||
|
<Translatable>Status</Translatable>
|
||||||
|
<>{elements}</>
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const FullInfoButton = () => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ onlineInfo, setOnlineInfo ] = useDependentState<ClientInfoOnline>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_online");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_online", event => setOnlineInfo(event.status), undefined, []);
|
||||||
|
|
||||||
|
if(!onlineInfo || onlineInfo.leaveTimestamp !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.buttonMore} onClick={() => events.fire("action_show_full_info")} key={"button"}>
|
||||||
|
<Translatable>Full Info</Translatable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupRenderer = (props: { group: ClientGroupInfo }) => {
|
||||||
|
const icon = getIconManager().resolveIcon(props.group.groupIcon.iconId, props.group.groupIcon.serverUniqueId, props.group.groupIcon.handlerId);
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.groupEntry} title={tra("Group {}", props.group.groupId)}>
|
||||||
|
<RemoteIconRenderer icon={icon} className={cssStyle.icon} />
|
||||||
|
{props.group.groupName}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChannelGroupRenderer = () => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ channelGroup, setChannelGroup ] = useDependentState<ClientGroupInfo>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_channel_group");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_channel_group", event => setChannelGroup(event.group), undefined, []);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if(channelGroup) {
|
||||||
|
body = <GroupRenderer group={channelGroup} key={"group-" + channelGroup.groupId} />;
|
||||||
|
} else {
|
||||||
|
body = <React.Fragment key={"loading"}><Translatable>loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock clientIcon={ClientIcon.PermissionServerGroups} valueClass={cssStyle.groups}>
|
||||||
|
<Translatable>Channel group</Translatable>
|
||||||
|
<>{body}</>
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerGroupRenderer = () => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
const client = useContext(ClientContext);
|
||||||
|
const [ serverGroups, setServerGroups ] = useDependentState<ClientGroupInfo[]>(() => {
|
||||||
|
if(client.type !== "none") {
|
||||||
|
events.fire("query_server_groups");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [ client.contextHash ]);
|
||||||
|
|
||||||
|
events.reactUse("notify_server_groups", event => setServerGroups(event.groups), undefined, []);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if(serverGroups) {
|
||||||
|
body = serverGroups.map(group => <GroupRenderer group={group} key={"group-" + group.groupId} />);
|
||||||
|
} else {
|
||||||
|
body = <React.Fragment key={"loading"}><Translatable>loading</Translatable> <LoadingDots /></React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoBlock clientIcon={ClientIcon.PermissionChannel} valueClass={cssStyle.groups}>
|
||||||
|
<Translatable>Channel group</Translatable>
|
||||||
|
<>{body}</>
|
||||||
|
</InfoBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientInfoProvider = () => {
|
||||||
|
const events = useContext(EventsContext);
|
||||||
|
|
||||||
|
const [ client, setClient ] = useState<OptionalClientInfoInfo>({ type: "none", contextHash: guid() });
|
||||||
|
events.reactUse("notify_client", event => {
|
||||||
|
if(event.info) {
|
||||||
|
setClient({
|
||||||
|
contextHash: guid(),
|
||||||
|
type: event.info.type,
|
||||||
|
handlerId: event.info.handlerId,
|
||||||
|
clientUniqueId: event.info.clientUniqueId,
|
||||||
|
clientId: event.info.clientId,
|
||||||
|
clientDatabaseId: event.info.clientDatabaseId
|
||||||
|
});
|
||||||
|
} else if(client.type !== "none") {
|
||||||
|
setClient({ type: "none", contextHash: guid() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider value={client} >
|
||||||
|
<div className={cssStyle.container}>
|
||||||
|
<div className={cssStyle.heading}>
|
||||||
|
<Avatar />
|
||||||
|
<ClientName />
|
||||||
|
<ClientDescription />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.generalInfo}>
|
||||||
|
<div className={cssStyle.block + " " + cssStyle.blockLeft}>
|
||||||
|
<ClientOnlineSince />
|
||||||
|
<ClientCountry />
|
||||||
|
<ClientForumAccount />
|
||||||
|
<ClientVolume />
|
||||||
|
<ClientVersion />
|
||||||
|
<ClientStatus />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.block + " " + cssStyle.blockRight}>
|
||||||
|
<ChannelGroupRenderer />
|
||||||
|
<ServerGroupRenderer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FullInfoButton />
|
||||||
|
</div>
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClientInfoRenderer = (props: { events: Registry<ClientInfoEvents> }) => (
|
||||||
|
<EventsContext.Provider value={props.events}>
|
||||||
|
<ClientInfoProvider />
|
||||||
|
</EventsContext.Provider>
|
||||||
|
);
|
|
@ -1,281 +0,0 @@
|
||||||
import {GroupManager} from "../../../permission/GroupManager";
|
|
||||||
import {Frame, FrameContent} from "../../../ui/frames/chat_frame";
|
|
||||||
import {openClientInfo} from "../../../ui/modal/ModalClientInfo";
|
|
||||||
import * as htmltags from "../../../ui/htmltags";
|
|
||||||
import * as image_preview from "../image_preview";
|
|
||||||
import * as i18nc from "../../../i18n/country";
|
|
||||||
import {ClientEntry, LocalClientEntry} from "../../../tree/Client";
|
|
||||||
import {format_online_time} from "../../../utils/TimeUtils";
|
|
||||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
|
||||||
import { tr } from "tc-shared/i18n/localize";
|
|
||||||
|
|
||||||
export class ClientInfo {
|
|
||||||
readonly handle: Frame;
|
|
||||||
private _html_tag: JQuery;
|
|
||||||
private _current_client: ClientEntry | undefined;
|
|
||||||
private _online_time_updater: number;
|
|
||||||
previous_frame_content: FrameContent;
|
|
||||||
|
|
||||||
constructor(handle: Frame) {
|
|
||||||
this.handle = handle;
|
|
||||||
this._build_html_tag();
|
|
||||||
}
|
|
||||||
|
|
||||||
html_tag() : JQuery {
|
|
||||||
return this._html_tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
clearInterval(this._online_time_updater);
|
|
||||||
|
|
||||||
this._html_tag && this._html_tag.remove();
|
|
||||||
this._html_tag = undefined;
|
|
||||||
|
|
||||||
this._current_client = undefined;
|
|
||||||
this.previous_frame_content = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _build_html_tag() {
|
|
||||||
this._html_tag = $("#tmpl_frame_chat_client_info").renderTag();
|
|
||||||
this._html_tag.find(".button-close").on('click', () => {
|
|
||||||
if(this.previous_frame_content === FrameContent.CLIENT_INFO)
|
|
||||||
this.previous_frame_content = FrameContent.NONE;
|
|
||||||
|
|
||||||
this.handle.set_content(this.previous_frame_content);
|
|
||||||
});
|
|
||||||
this._html_tag.find(".button-more").on('click', () => {
|
|
||||||
if(!this._current_client)
|
|
||||||
return;
|
|
||||||
|
|
||||||
openClientInfo(this._current_client);
|
|
||||||
});
|
|
||||||
this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar());
|
|
||||||
}
|
|
||||||
|
|
||||||
current_client() : ClientEntry {
|
|
||||||
return this._current_client;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_current_client(client: ClientEntry | undefined, enforce?: boolean) {
|
|
||||||
if(client) client.updateClientVariables(); /* just to ensure */
|
|
||||||
if(client === this._current_client && (typeof(enforce) === "undefined" || !enforce))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._current_client = client;
|
|
||||||
|
|
||||||
/* updating the header */
|
|
||||||
{
|
|
||||||
const client_name = this._html_tag.find(".client-name");
|
|
||||||
client_name.children().remove();
|
|
||||||
htmltags.generate_client_object({
|
|
||||||
add_braces: false,
|
|
||||||
client_name: client ? client.clientNickName() : "undefined",
|
|
||||||
client_unique_id: client ? client.clientUid() : "",
|
|
||||||
client_id: client ? client.clientId() : 0
|
|
||||||
}).appendTo(client_name);
|
|
||||||
|
|
||||||
const client_description = this._html_tag.find(".client-description");
|
|
||||||
client_description.text(client ? client.properties.client_description : "").toggle(!!client.properties.client_description);
|
|
||||||
|
|
||||||
const is_local_entry = client instanceof LocalClientEntry;
|
|
||||||
const container_avatar = this._html_tag.find(".container-avatar");
|
|
||||||
container_avatar.find(".avatar").remove();
|
|
||||||
if(client) {
|
|
||||||
const avatar = this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid());
|
|
||||||
if(!is_local_entry) {
|
|
||||||
avatar.css("cursor", "pointer").on('click', event => {
|
|
||||||
image_preview.preview_image_tag(this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
avatar.appendTo(container_avatar);
|
|
||||||
} else
|
|
||||||
this.handle.handle.fileManager.avatars.generate_chat_tag(undefined, undefined).appendTo(container_avatar);
|
|
||||||
|
|
||||||
container_avatar.toggleClass("editable", is_local_entry);
|
|
||||||
}
|
|
||||||
/* updating the info fields */
|
|
||||||
{
|
|
||||||
const online_time = this._html_tag.find(".client-online-time");
|
|
||||||
online_time.text(format_online_time(client ? client.calculateOnlineTime() : 0));
|
|
||||||
if(this._online_time_updater) {
|
|
||||||
clearInterval(this._online_time_updater);
|
|
||||||
this._online_time_updater = 0;
|
|
||||||
}
|
|
||||||
if(client) {
|
|
||||||
this._online_time_updater = setInterval(() => {
|
|
||||||
const client = this._current_client;
|
|
||||||
if(!client) {
|
|
||||||
clearInterval(this._online_time_updater);
|
|
||||||
this._online_time_updater = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(client.currentChannel()) /* If he has no channel then he might be disconnected */
|
|
||||||
online_time.text(format_online_time(client.calculateOnlineTime()));
|
|
||||||
else {
|
|
||||||
online_time.text(online_time.text() + tr(" (left view)"));
|
|
||||||
clearInterval(this._online_time_updater);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const country = this._html_tag.find(".client-country");
|
|
||||||
country.children().detach();
|
|
||||||
const country_code = (client ? client.properties.client_country : undefined) || "xx";
|
|
||||||
$.spawn("div").addClass("country flag-" + country_code.toLowerCase()).appendTo(country);
|
|
||||||
$.spawn("a").text(i18nc.country_name(country_code.toUpperCase())).appendTo(country);
|
|
||||||
|
|
||||||
|
|
||||||
const version = this._html_tag.find(".client-version");
|
|
||||||
version.children().detach();
|
|
||||||
if(client) {
|
|
||||||
let platform = client.properties.client_platform;
|
|
||||||
if(platform.indexOf("Win32") != 0 && (client.properties.client_version.indexOf("Win64") != -1 || client.properties.client_version.indexOf("WOW64") != -1))
|
|
||||||
platform = platform.replace("Win32", "Win64");
|
|
||||||
$.spawn("a").attr("title", client.properties.client_version).text(
|
|
||||||
client.properties.client_version.split(" ")[0] + " on " + platform
|
|
||||||
).appendTo(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
const volume = this._html_tag.find(".client-local-volume");
|
|
||||||
volume.text((client && client.getVoiceClient() ? (client.getVoiceClient().getVolume() * 100) : -1).toFixed(0) + "%");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* teaspeak forum */
|
|
||||||
{
|
|
||||||
const container_forum = this._html_tag.find(".container-teaforo");
|
|
||||||
if(client && client.properties.client_teaforo_id) {
|
|
||||||
container_forum.show();
|
|
||||||
|
|
||||||
const container_data = container_forum.find(".client-teaforo-account");
|
|
||||||
container_data.children().remove();
|
|
||||||
|
|
||||||
let text = client.properties.client_teaforo_name;
|
|
||||||
if((client.properties.client_teaforo_flags & 0x01) > 0)
|
|
||||||
text += " (" + tr("Banned") + ")";
|
|
||||||
if((client.properties.client_teaforo_flags & 0x02) > 0)
|
|
||||||
text += " (" + tr("Stuff") + ")";
|
|
||||||
if((client.properties.client_teaforo_flags & 0x04) > 0)
|
|
||||||
text += " (" + tr("Premium") + ")";
|
|
||||||
|
|
||||||
$.spawn("a")
|
|
||||||
.attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id)
|
|
||||||
.attr("target", "_blank")
|
|
||||||
.text(text)
|
|
||||||
.appendTo(container_data);
|
|
||||||
} else {
|
|
||||||
container_forum.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* update the client status */
|
|
||||||
{
|
|
||||||
//TODO Implement client status!
|
|
||||||
const container_status = this._html_tag.find(".container-client-status");
|
|
||||||
const container_status_entries = container_status.find(".client-status");
|
|
||||||
container_status_entries.children().detach();
|
|
||||||
if(client) {
|
|
||||||
if(client.properties.client_away) {
|
|
||||||
container_status_entries.append(
|
|
||||||
$.spawn("div").addClass("status-entry").append(
|
|
||||||
$.spawn("div").addClass("icon_em client-away"),
|
|
||||||
$.spawn("a").text(tr("Away")),
|
|
||||||
client.properties.client_away_message ?
|
|
||||||
$.spawn("a").addClass("away-message").text("(" + client.properties.client_away_message + ")") :
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(client.isMuted()) {
|
|
||||||
container_status_entries.append(
|
|
||||||
$.spawn("div").addClass("status-entry").append(
|
|
||||||
$.spawn("div").addClass("icon_em client-input_muted_local"),
|
|
||||||
$.spawn("a").text(tr("Client local muted"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(!client.properties.client_output_hardware) {
|
|
||||||
container_status_entries.append(
|
|
||||||
$.spawn("div").addClass("status-entry").append(
|
|
||||||
$.spawn("div").addClass("icon_em client-hardware_output_muted"),
|
|
||||||
$.spawn("a").text(tr("Speakers/Headphones disabled"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(!client.properties.client_input_hardware) {
|
|
||||||
container_status_entries.append(
|
|
||||||
$.spawn("div").addClass("status-entry").append(
|
|
||||||
$.spawn("div").addClass("icon_em client-hardware_input_muted"),
|
|
||||||
$.spawn("a").text(tr("Microphone disabled"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(client.properties.client_output_muted) {
|
|
||||||
container_status_entries.append(
|
|
||||||
$.spawn("div").addClass("status-entry").append(
|
|
||||||
$.spawn("div").addClass("icon_em client-output_muted"),
|
|
||||||
$.spawn("a").text(tr("Speakers/Headphones Muted"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if(client.properties.client_input_muted) {
|
|
||||||
container_status_entries.append(
|
|
||||||
$.spawn("div").addClass("status-entry").append(
|
|
||||||
$.spawn("div").addClass("icon_em client-input_muted"),
|
|
||||||
$.spawn("a").text(tr("Microphone Muted"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container_status.toggle(container_status_entries.children().length > 0);
|
|
||||||
}
|
|
||||||
/* update client server groups */
|
|
||||||
{
|
|
||||||
const container_groups = this._html_tag.find(".client-group-server");
|
|
||||||
container_groups.children().detach();
|
|
||||||
if(client) {
|
|
||||||
const invalid_groups = [];
|
|
||||||
const groups = client.assignedServerGroupIds().map(group_id => {
|
|
||||||
const result = this.handle.handle.groups.findServerGroup(group_id);
|
|
||||||
if(!result)
|
|
||||||
invalid_groups.push(group_id);
|
|
||||||
return result;
|
|
||||||
}).filter(e => !!e).sort(GroupManager.sorter());
|
|
||||||
for(const invalid_id of invalid_groups) {
|
|
||||||
container_groups.append($.spawn("a").text("{" + tr("server group ") + invalid_groups + "}").attr("title", tr("Missing server group id!") + " (" + invalid_groups + ")"));
|
|
||||||
}
|
|
||||||
for(let group of groups) {
|
|
||||||
container_groups.append(
|
|
||||||
$.spawn("div").addClass("group-container")
|
|
||||||
.append(
|
|
||||||
generateIconJQueryTag(getIconManager().resolveIcon(group.properties.iconid, this.handle.handle.getCurrentServerUniqueId(), this.handle.handle.handlerId))
|
|
||||||
).append(
|
|
||||||
$.spawn("a").text(group.name).attr("title", tr("Group id: ") + group.id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* update client channel group */
|
|
||||||
{
|
|
||||||
const container_group = this._html_tag.find(".client-group-channel");
|
|
||||||
container_group.children().detach();
|
|
||||||
if(client) {
|
|
||||||
const group_id = client.assignedChannelGroup();
|
|
||||||
let group = this.handle.handle.groups.findChannelGroup(group_id);
|
|
||||||
if(group) {
|
|
||||||
container_group.append(
|
|
||||||
$.spawn("div").addClass("group-container")
|
|
||||||
.append(
|
|
||||||
generateIconJQueryTag(getIconManager().resolveIcon(group.properties.iconid, this.handle.handle.getCurrentServerUniqueId(), this.handle.handle.handlerId))
|
|
||||||
).append(
|
|
||||||
$.spawn("a").text(group.name).attr("title", tr("Group id: ") + group_id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
container_group.append($.spawn("a").text(tr("Invalid channel group!")).attr("title", tr("Missing channel group id!") + " (" + group_id + ")"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -177,7 +177,7 @@
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerIcon {
|
.icon {
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
|
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
|
@ -50,7 +50,7 @@ html:root {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.containerIcon {
|
.icon {
|
||||||
margin: auto .25em;
|
margin: auto .25em;
|
||||||
padding: .2em;
|
padding: .2em;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ClientAvatar, kDefaultAvatarImage, kLoadingAvatarImage} from "tc-shared/file/Avatars";
|
import {ClientAvatar, kDefaultAvatarImage, kLoadingAvatarImage} from "tc-shared/file/Avatars";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import * as image_preview from "tc-shared/ui/frames/image_preview";
|
import * as image_preview from "tc-shared/ui/frames/image_preview";
|
||||||
|
|
||||||
const ImageStyle = { height: "100%", width: "100%", cursor: "pointer" };
|
const ImageStyle = { height: "100%", width: "100%", cursor: "pointer" };
|
||||||
|
@ -8,6 +8,7 @@ export const AvatarRenderer = React.memo((props: { avatar: ClientAvatar | "loadi
|
||||||
let [ revision, setRevision ] = useState(0);
|
let [ revision, setRevision ] = useState(0);
|
||||||
|
|
||||||
let image;
|
let image;
|
||||||
|
let avatar: ClientAvatar;
|
||||||
if(props.avatar === "loading") {
|
if(props.avatar === "loading") {
|
||||||
image = <img draggable={false} src={kLoadingAvatarImage} alt={tr("loading")}/>;
|
image = <img draggable={false} src={kLoadingAvatarImage} alt={tr("loading")}/>;
|
||||||
} else if(props.avatar === "default") {
|
} else if(props.avatar === "default") {
|
||||||
|
@ -63,9 +64,11 @@ export const AvatarRenderer = React.memo((props: { avatar: ClientAvatar | "loadi
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
props.avatar?.events.reactUse("avatar_state_changed", () => setRevision(revision + 1));
|
avatar = props.avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => avatar && avatar.events.on("avatar_state_changed", () => setRevision(revision + 1)), [ props.avatar ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={props.className} style={{ overflow: "hidden" }}>
|
<div className={props.className} style={{ overflow: "hidden" }}>
|
||||||
{image}
|
{image}
|
||||||
|
|
26
shared/js/ui/react-elements/Helper.ts
Normal file
26
shared/js/ui/react-elements/Helper.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {Dispatch, SetStateAction, useEffect, useMemo, useState} from "react";
|
||||||
|
|
||||||
|
export function useDependentState<S>(
|
||||||
|
factory: (prevState?: S) => S,
|
||||||
|
inputs: ReadonlyArray<any>,
|
||||||
|
): [S, Dispatch<SetStateAction<S>>] {
|
||||||
|
let skipCalculation = false;
|
||||||
|
|
||||||
|
let [state, setState] = useState<S>(() => {
|
||||||
|
skipCalculation = true;
|
||||||
|
return factory(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
if(skipCalculation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = factory(state);
|
||||||
|
if (newState !== state) {
|
||||||
|
setState(state = newState);
|
||||||
|
}
|
||||||
|
}, inputs);
|
||||||
|
|
||||||
|
return [state, setState];
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
.empty {
|
.container {
|
||||||
/* legacy values, we're using em now */
|
width: 1em;
|
||||||
width: 16px;
|
height: 1em;
|
||||||
height: 16px;
|
|
||||||
}
|
}
|
|
@ -10,9 +10,9 @@ export const IconRenderer = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if(!props.icon) {
|
if(!props.icon) {
|
||||||
return <div className={cssStyle.empty + " icon-container icon-empty " + props.className} title={props.title} />;
|
return <div className={cssStyle.container + " icon-container icon-empty " + props.className} title={props.title} />;
|
||||||
} else if(typeof props.icon === "string") {
|
} else if(typeof props.icon === "string") {
|
||||||
return <div className={"icon " + props.icon + " " + props.className} title={props.title} />;
|
return <div className={cssStyle.container + " icon " + props.icon + " " + props.className} title={props.title} />;
|
||||||
} else {
|
} else {
|
||||||
throw "JQuery icons are not longer supported";
|
throw "JQuery icons are not longer supported";
|
||||||
}
|
}
|
||||||
|
@ -26,27 +26,27 @@ export const RemoteIconRenderer = (props: { icon: RemoteIcon, className?: string
|
||||||
switch (props.icon.getState()) {
|
switch (props.icon.getState()) {
|
||||||
case "empty":
|
case "empty":
|
||||||
case "destroyed":
|
case "destroyed":
|
||||||
return <div key={"empty"} className={"icon-container icon-empty " + props.className} title={props.title} />;
|
return <div key={"empty"} className={cssStyle.container + " icon-container icon-empty " + props.className} title={props.title} />;
|
||||||
|
|
||||||
case "loaded":
|
case "loaded":
|
||||||
if(props.icon.iconId >= 0 && props.icon.iconId <= 1000) {
|
if(props.icon.iconId >= 0 && props.icon.iconId <= 1000) {
|
||||||
if(props.icon.iconId === 0) {
|
if(props.icon.iconId === 0) {
|
||||||
return <div key={"loaded-empty"} className={"icon-container icon-empty " + props.className} title={props.title} />;
|
return <div key={"loaded-empty"} className={cssStyle.container + " icon-container icon-empty " + props.className} title={props.title} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div key={"loaded"} className={"icon_em client-group_" + props.icon.iconId + " " + props.className} title={props.title} />;
|
return <div key={"loaded"} className={cssStyle.container + " icon_em client-group_" + props.icon.iconId + " " + props.className} title={props.title} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={"icon-" + props.icon.iconId} className={"icon-container " + props.className}>
|
<div key={"icon-" + props.icon.iconId} className={cssStyle.container + "icon-container " + props.className}>
|
||||||
<img style={{ maxWidth: "100%", maxHeight: "100%" }} src={props.icon.getImageUrl()} alt={props.title || ("icon " + props.icon.iconId)} draggable={false} />
|
<img style={{ maxWidth: "100%", maxHeight: "100%" }} src={props.icon.getImageUrl()} alt={props.title || ("icon " + props.icon.iconId)} draggable={false} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "loading":
|
case "loading":
|
||||||
return <div key={"loading"} className={"icon-container " + props.className} title={props.title}><div className={"icon_loading"} /></div>;
|
return <div key={"loading"} className={cssStyle.container + " icon-container " + props.className} title={props.title}><div className={"icon_loading"} /></div>;
|
||||||
|
|
||||||
case "error":
|
case "error":
|
||||||
return <div key={"error"} className={"icon client-warning " + props.className} title={props.icon.getErrorMessage() || tr("Failed to load icon")} />;
|
return <div key={"error"} className={cssStyle.container + " icon client-warning " + props.className} title={props.icon.getErrorMessage() || tr("Failed to load icon")} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw "invalid icon state";
|
throw "invalid icon state";
|
||||||
|
|
|
@ -20,6 +20,8 @@ html:root {
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clientName {
|
.clientName {
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
|
|
Loading…
Add table
Reference in a new issue