TeaWeb/shared/js/ui/frames/side/ClientInfoRenderer.tsx

554 lines
19 KiB
TypeScript

import * as React from "react";
import {useContext, useEffect, useRef, useState} from "react";
import {
ClientCountryInfo,
ClientForumInfo,
ClientGroupInfo,
ClientInfoEvents,
ClientInfoOnline,
ClientStatusInfo,
ClientVersionInfo,
ClientVolumeInfo, InheritedChannelInfo,
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";
import {CountryCode} from "tc-shared/ui/react-elements/CountryCode";
import {getKeyBoard} from "tc-shared/PPTListener";
import {tra} from "tc-shared/i18n/localize";
const cssStyle = require("./ClientInfoRenderer.scss");
const EventsContext = React.createContext<Registry<ClientInfoEvents>>(undefined);
const ClientContext = React.createContext<OptionalClientInfoInfo>(undefined);
const EditOverlay = React.memo(() => {
const events = useContext(EventsContext);
const refContainer = useRef<HTMLDivElement>();
useEffect(() => {
const keyboard = getKeyBoard();
return keyboard.registerHook({
keyShift: true,
callbackPress: () => {
refContainer.current?.classList.add(cssStyle.disabled);
},
callbackRelease: () => {
refContainer.current?.classList.remove(cssStyle.disabled);
}
});
}, []);
return (
<div
ref={refContainer}
className={cssStyle.edit}
onClick={() => events.fire("action_edit_avatar")}
>
<ClientIconRenderer icon={ClientIcon.AvatarUpload} className={cssStyle.icon} />
</div>
);
});
const Avatar = React.memo(() => {
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 : "")} key={avatar === "loading" ? "loading" : avatar.clientAvatarId} />
</div>
<EditOverlay />
</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 clientIcon={ClientIcon.ClientInfoOnlineTime}>
<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 clientIcon={ClientIcon.ClientInfoCountry}>
<Translatable>Country</Translatable>
<CountryCode alphaCode={country?.flag} className={cssStyle.country} />
</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 clientIcon={ClientIcon.ClientInfoVolume} 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 clientIcon={ClientIcon.ClientInfoForumAccount} 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 clientIcon={ClientIcon.ClientInfoVersion}>
<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 clientIcon={ClientIcon.ClientInfoStatus} 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 ]);
const [ inheritedChannel, setInheritedChannel ] = useDependentState<InheritedChannelInfo>(() => undefined, [ client.contextHash ]);
events.reactUse("notify_channel_group", event => {
setChannelGroup(event.group);
setInheritedChannel(event.inheritedChannel);
}, undefined, []);
let body;
if(channelGroup) {
let groupRendered = <GroupRenderer group={channelGroup} key={"group-" + channelGroup.groupId} />;
if(inheritedChannel) {
body = (
<React.Fragment key={"inherited"}>
{groupRendered}
<div className={cssStyle.channelGroupInherited}>
<Translatable>Inherited from</Translatable>&nbsp;
{inheritedChannel.channelName}
</div>
</React.Fragment>
)
} else {
body = groupRendered;
}
} 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>Server groups</Translatable>
<>{body}</>
</InfoBlock>
);
};
const ConnectedClientInfoBlock = () => {
const client = useContext(ClientContext);
if(client.type === "query" || client.type === "none") {
return null;
}
return (
<React.Fragment key={"info"}>
<ClientOnlineSince />
<ClientCountry />
<ClientForumAccount />
<ClientVolume />
<ClientVersion />
<ClientStatus />
</React.Fragment>
);
}
const ClientInfoProvider = () => {
const events = useContext(EventsContext);
const [ client, setClient ] = useState<OptionalClientInfoInfo>(() => {
events.fire("query_client");
return { 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}>
<ConnectedClientInfoBlock />
</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>
);