Recoded the client info ui in React and increased responsibility to variable changes
parent
bb3283a6c9
commit
2041ed5fc3
|
@ -10,7 +10,8 @@
|
|||
- Fixed BBCode inline code style
|
||||
- URL tags can not contain any other tags
|
||||
- Correctly parsing the "lazy close tag" `[/]`
|
||||
|
||||
- The client info modal now updates its values accordingly on client changes
|
||||
|
||||
* **05.12.20**
|
||||
- Fixed the webclient for Firefox in incognito mode
|
||||
|
||||
|
|
|
@ -261,364 +261,6 @@ html:root {
|
|||
display: flex;
|
||||
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 {
|
||||
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 {
|
||||
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]);
|
||||
}
|
||||
|
||||
export = {};
|
|
@ -54,6 +54,8 @@ export class ClientProperties {
|
|||
client_servergroups: string = "";
|
||||
|
||||
client_channel_group_id: number = 0;
|
||||
client_channel_group_inherited_channel_id: number = 0;
|
||||
|
||||
client_lastconnected: number = 0;
|
||||
client_created: number = 0;
|
||||
client_totalconnections: number = 0;
|
||||
|
@ -799,16 +801,9 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
update_avatar = true;
|
||||
}
|
||||
|
||||
/* process updates after variables have been set */
|
||||
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)
|
||||
if(update_avatar) {
|
||||
this.channelTree.client?.fileManager?.avatars.updateCache(this.avatarId(), this.properties.client_flag_avatar);
|
||||
}
|
||||
|
||||
/* devel-block(log-client-property-updates) */
|
||||
group.end();
|
||||
|
@ -823,8 +818,9 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.promiseClientInfoTimestamp = Date.now();
|
||||
return (this.promiseClientInfo = new Promise<void>((resolve, reject) => {
|
||||
|
@ -931,8 +927,9 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
}
|
||||
|
||||
setAudioVolume(value: number) {
|
||||
if(this.voiceVolume == value)
|
||||
if(this.voiceVolume == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.voiceVolume = value;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {ConversationManager} from "../../ui/frames/side/ConversationManager";
|
|||
import {PrivateConversationManager} from "../../ui/frames/side/PrivateConversationManager";
|
||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {ClientInfoController} from "tc-shared/ui/frames/side/ClientInfoController";
|
||||
|
||||
export enum InfoFrameMode {
|
||||
NONE = "none",
|
||||
|
@ -60,7 +61,7 @@ export class InfoFrame {
|
|||
this._value_ping = this._html_tag.find(".value-ping");
|
||||
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 => {
|
||||
const selected_client = this.handle.client_info().current_client();
|
||||
const selected_client = this.handle.getClientInfo().getClient();
|
||||
if(!selected_client) return;
|
||||
|
||||
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) {
|
||||
//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 visibility = (selected_client && selected_client.clientId() !== this.handle.handle.getClientId()) ? "visible" : "hidden";
|
||||
|
@ -281,7 +282,7 @@ export class Frame {
|
|||
private containerChannelChat: JQuery;
|
||||
private _content_type: FrameContent;
|
||||
|
||||
private clientInfo: ClientInfo;
|
||||
private clientInfo: ClientInfoController;
|
||||
private musicInfo: MusicInfo;
|
||||
private channelConversations: ConversationManager;
|
||||
private privateConversations: PrivateConversationManager;
|
||||
|
@ -293,7 +294,7 @@ export class Frame {
|
|||
this.infoFrame = new InfoFrame(this);
|
||||
this.privateConversations = new PrivateConversationManager(handle);
|
||||
this.channelConversations = new ConversationManager(handle);
|
||||
this.clientInfo = new ClientInfo(this);
|
||||
this.clientInfo = new ClientInfoController(handle);
|
||||
this.musicInfo = new MusicInfo(this);
|
||||
|
||||
this._build_html_tag();
|
||||
|
@ -313,7 +314,7 @@ export class Frame {
|
|||
this.infoFrame && this.infoFrame.destroy();
|
||||
this.infoFrame = undefined;
|
||||
|
||||
this.clientInfo && this.clientInfo.destroy();
|
||||
this.clientInfo?.destroy();
|
||||
this.clientInfo = undefined;
|
||||
|
||||
this.musicInfo && this.musicInfo.destroy();
|
||||
|
@ -349,7 +350,7 @@ export class Frame {
|
|||
return this.channelConversations;
|
||||
}
|
||||
|
||||
client_info() : ClientInfo {
|
||||
getClientInfo() : ClientInfoController {
|
||||
return this.clientInfo;
|
||||
}
|
||||
|
||||
|
@ -386,16 +387,15 @@ export class Frame {
|
|||
}
|
||||
|
||||
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 */
|
||||
|
||||
if(this._content_type === FrameContent.CLIENT_INFO)
|
||||
return;
|
||||
|
||||
this.clientInfo.previous_frame_content = this._content_type;
|
||||
this._clear();
|
||||
this._content_type = FrameContent.CLIENT_INFO;
|
||||
this.containerChannelChat.append(this.clientInfo.html_tag());
|
||||
this.containerChannelChat.append(this.clientInfo.getHtmlTag());
|
||||
}
|
||||
|
||||
show_music_player(client: MusicClientEntry) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
.containerIcon {
|
||||
.icon {
|
||||
padding: 0 2em;
|
||||
|
||||
flex-grow: 0;
|
||||
|
|
|
@ -50,7 +50,7 @@ html:root {
|
|||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.containerIcon {
|
||||
.icon {
|
||||
margin: auto .25em;
|
||||
padding: .2em;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
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";
|
||||
|
||||
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 image;
|
||||
let avatar: ClientAvatar;
|
||||
if(props.avatar === "loading") {
|
||||
image = <img draggable={false} src={kLoadingAvatarImage} alt={tr("loading")}/>;
|
||||
} else if(props.avatar === "default") {
|
||||
|
@ -63,9 +64,11 @@ export const AvatarRenderer = React.memo((props: { avatar: ClientAvatar | "loadi
|
|||
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 (
|
||||
<div className={props.className} style={{ overflow: "hidden" }}>
|
||||
{image}
|
||||
|
|
|
@ -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 {
|
||||
/* legacy values, we're using em now */
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
.container {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
|
@ -10,9 +10,9 @@ export const IconRenderer = (props: {
|
|||
className?: string;
|
||||
}) => {
|
||||
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") {
|
||||
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 {
|
||||
throw "JQuery icons are not longer supported";
|
||||
}
|
||||
|
@ -26,27 +26,27 @@ export const RemoteIconRenderer = (props: { icon: RemoteIcon, className?: string
|
|||
switch (props.icon.getState()) {
|
||||
case "empty":
|
||||
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":
|
||||
if(props.icon.iconId >= 0 && props.icon.iconId <= 1000) {
|
||||
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 (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
|
||||
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":
|
||||
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:
|
||||
throw "invalid icon state";
|
||||
|
|
|
@ -20,6 +20,8 @@ html:root {
|
|||
|
||||
.statusIcon {
|
||||
flex-shrink: 0;
|
||||
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.clientName {
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
|
Loading…
Reference in New Issue