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 = (
); 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 ( Link expired ); } if (props.info.channelName) { return (
{props.info.channelName}
{props.info.serverName}
{joinButton}
); } else { return (
Join server
{props.info.serverName}
{joinButton}
); } }); const InviteErrorRenderer = (props: { children, noTitle?: boolean }) => { return (
Failed to load invite key:
{props.children}
); } const InviteLoadingRenderer = () => { return (
Loading,
please wait
); } 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 = ; break; case "loading": body = ; break; case "error": body = ( {linkInfo.message} ); break; case "expired": body = ( Invite link expired ); break; case "not-found": body = ( Unknown invite link ); break; } return ( {props.url} {body} ); } 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()); } }