Recoded the client info ui in React and increased responsibility to variable changes

canary
WolverinDEV 2020-12-07 19:37:06 +01:00
parent bb3283a6c9
commit 2041ed5fc3
20 changed files with 1432 additions and 694 deletions

View File

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

View File

View File

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

View File

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

View File

@ -273,7 +273,8 @@ if(typeof ($) !== "undefined") {
}
}
if(!Object.values)
if(!Object.values) {
Object.values = object => Object.keys(object).map(e => object[e]);
}
export = {};

View File

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

View File

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

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

View 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 })
}
}

View 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
}
}

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

View File

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

View File

@ -177,7 +177,7 @@
flex-shrink: 1;
}
.containerIcon {
.icon {
padding: 0 2em;
flex-grow: 0;

View File

@ -50,7 +50,7 @@ html:root {
flex-grow: 0;
flex-shrink: 0;
.containerIcon {
.icon {
margin: auto .25em;
padding: .2em;

View File

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

View 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];
}

View File

@ -1,5 +1,4 @@
.empty {
/* legacy values, we're using em now */
width: 16px;
height: 16px;
.container {
width: 1em;
height: 1em;
}

View File

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

View File

@ -20,6 +20,8 @@ html:root {
.statusIcon {
flex-shrink: 0;
font-size: 16px;
}
.clientName {

View File

@ -81,6 +81,7 @@
.icon {
margin-right: 4px;
font-size: 16px;
}
.arrow {