TeaWeb/shared/js/text/bbcode/InviteRenderer.tsx
2023-11-17 21:47:09 +01:00

226 lines
No EOL
7.9 KiB
TypeScript

import * as React from "react";
import { IpcInviteInfo, IpcInviteInfoLoaded } from "tc-shared/text/bbcode/InviteDefinitions";
import { ChannelMessage, getIpcInstance, IPCChannel } from "tc-shared/ipc/BrowserIPC";
import * as loader from "tc-loader";
import { useEffect, useState } from "react";
import _ = require("lodash");
import { Translatable } from "tc-shared/ui/react-elements/i18n";
import { Button } from "tc-shared/ui/react-elements/Button";
import { SimpleUrlRenderer } from "tc-shared/text/bbcode/url";
import { LoadingDots } from "tc-shared/ui/react-elements/LoadingDots";
import { ClientIconRenderer } from "tc-shared/ui/react-elements/Icons";
import { ClientIcon } from "svg-sprites/client-icons";
const cssStyle = require("./InviteRenderer.scss");
const kInviteUrlRegex = /^(https:\/\/)?(teaspeak.de\/|join.teaspeak.de\/(invite\/)?)([a-zA-Z0-9]{4})$/gm;
export function isInviteLink(url: string): boolean {
kInviteUrlRegex.lastIndex = 0;
return !!url.match(kInviteUrlRegex);
}
type LocalInviteInfo = IpcInviteInfo | { status: "loading" };
type InviteCacheEntry = { status: LocalInviteInfo, timeout: number };
const localInviteCache: { [key: string]: InviteCacheEntry } = {};
const localInviteCallbacks: { [key: string]: (() => void)[] } = {};
const useInviteLink = (linkId: string): LocalInviteInfo => {
if (!localInviteCache[linkId]) {
localInviteCache[linkId] = { status: { status: "loading" }, timeout: setTimeout(() => delete localInviteCache[linkId], 60 * 1000) };
ipcChannel?.sendMessage("query", { linkId });
}
const [value, setValue] = useState(localInviteCache[linkId].status);
useEffect(() => {
if (typeof localInviteCache[linkId]?.status === "undefined") {
return;
}
if (!_.isEqual(value, localInviteCache[linkId].status)) {
setValue(localInviteCache[linkId].status);
}
const callback = () => setValue(localInviteCache[linkId].status);
(localInviteCallbacks[linkId] || (localInviteCallbacks[linkId] = [])).push(callback);
return () => { localInviteCallbacks[linkId]?.remove(callback); }
}, [linkId]);
return value;
}
const LoadedInviteRenderer = React.memo((props: { info: IpcInviteInfoLoaded }) => {
let joinButton = (
<div className={cssStyle.right}>
<Button
color={"green"}
type={"small"}
onClick={() => {
ipcChannel?.sendMessage("connect", {
connectParameters: props.info.connectParameters,
serverAddress: props.info.serverAddress,
serverUniqueId: props.info.serverUniqueId,
});
}}
>
<Translatable>Join Now!</Translatable>
</Button>
</div>
);
const [, setRevision] = useState(0);
useEffect(() => {
if (props.info.expireTimestamp === 0) {
return;
}
const timeout = props.info.expireTimestamp - (Date.now() / 1000);
if (timeout <= 0) {
return;
}
const timeoutId = setTimeout(() => setRevision(Date.now()));
return () => clearTimeout(timeoutId);
});
if (props.info.expireTimestamp > 0 && Date.now() / 1000 >= props.info.expireTimestamp) {
return (
<InviteErrorRenderer noTitle={true} key={"expired"}>
<Translatable>Link expired</Translatable>
</InviteErrorRenderer>
);
}
if (props.info.channelName) {
return (
<div className={cssStyle.container + " " + cssStyle.info} key={"with-channel"}>
<div className={cssStyle.left}>
<div className={cssStyle.channelName} title={props.info.channelName}>
<ClientIconRenderer icon={ClientIcon.ChannelGreenSubscribed} />
<div className={cssStyle.name}>{props.info.channelName}</div>
</div>
<div className={cssStyle.serverName + " " + cssStyle.short} title={props.info.serverName}>{props.info.serverName}</div>
</div>
{joinButton}
</div>
);
} else {
return (
<div className={cssStyle.container + " " + cssStyle.info} key={"without-channel"}>
<div className={cssStyle.left}>
<div className={cssStyle.joinServer}><Translatable>Join server</Translatable></div>
<div className={cssStyle.serverName + " " + cssStyle.large} title={props.info.serverName}>{props.info.serverName}</div>
</div>
{joinButton}
</div>
);
}
});
const InviteErrorRenderer = (props: { children, noTitle?: boolean }) => {
return (
<div className={cssStyle.container + " " + cssStyle.error}>
<div className={cssStyle.containerError + " " + (props.noTitle ? cssStyle.noTitle : "")}>
<div className={cssStyle.title}>
<Translatable>Failed to load invite key:</Translatable>
</div>
<div className={cssStyle.message}>
{props.children}
</div>
</div>
</div>
);
}
const InviteLoadingRenderer = () => {
return (
<div className={cssStyle.container + " " + cssStyle.loading}>
<div className={cssStyle.left}>
<div className={cssStyle.loading}>
<Translatable>Loading,<br /> please wait</Translatable> <LoadingDots />
</div>
</div>
<div className={cssStyle.right}>
<Button
color={"green"}
type={"small"}
disabled={true}
>
<Translatable>Join now!</Translatable>
</Button>
</div>
</div>
);
}
export const InviteLinkRenderer = (props: { url: string, handlerId: string }) => {
kInviteUrlRegex.lastIndex = 0;
const inviteLinkId = kInviteUrlRegex.exec(props.url)[4];
const linkInfo = useInviteLink(inviteLinkId);
let body;
switch (linkInfo.status) {
case "success":
body = <LoadedInviteRenderer info={linkInfo} key={"loaded"} />;
break;
case "loading":
body = <InviteLoadingRenderer key={"loading"} />;
break;
case "error":
body = (
<InviteErrorRenderer key={"error"}>
{linkInfo.message}
</InviteErrorRenderer>
);
break;
case "expired":
body = (
<InviteErrorRenderer key={"expired"} noTitle={true}>
<Translatable>Invite link expired</Translatable>
</InviteErrorRenderer>
);
break;
case "not-found":
body = (
<InviteErrorRenderer key={"expired"} noTitle={true}>
<Translatable>Unknown invite link</Translatable>
</InviteErrorRenderer>
);
break;
}
return (
<React.Fragment>
<SimpleUrlRenderer target={props.url}>{props.url}</SimpleUrlRenderer>
{body}
</React.Fragment>
);
}
let ipcChannel: IPCChannel;
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "Invite controller init",
function: async () => {
ipcChannel = getIpcInstance().createCoreControlChannel("invite-info");
ipcChannel.messageHandler = handleIpcMessage;
},
priority: 10
});
function handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
if (message.type === "query-result") {
if (!localInviteCache[message.data.linkId]) {
return;
}
localInviteCache[message.data.linkId].status = message.data.result;
localInviteCallbacks[message.data.linkId]?.forEach(callback => callback());
}
}