Added a chat invite link resolver which allows to easily connect within the client
parent
2b96dc91aa
commit
a1e1df6a2d
|
@ -984,7 +984,7 @@ export class ConnectionHandler {
|
||||||
const connectParameters = this.serverConnection.handshake_handler().parameters;
|
const connectParameters = this.serverConnection.handshake_handler().parameters;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channel: targetChannel ? {target: "/" + targetChannel.channelId, password: targetChannel.cached_password()} : undefined,
|
channel: targetChannel ? {target: "/" + targetChannel.channelId, password: targetChannel.getCachedPasswordHash()} : undefined,
|
||||||
nickname: name,
|
nickname: name,
|
||||||
password: connectParameters.serverPassword ? {
|
password: connectParameters.serverPassword ? {
|
||||||
password: connectParameters.serverPassword,
|
password: connectParameters.serverPassword,
|
||||||
|
|
|
@ -14,7 +14,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
function: async () => {
|
function: async () => {
|
||||||
clientServices = new ClientServices(new class implements ClientServiceConfig {
|
clientServices = new ClientServices(new class implements ClientServiceConfig {
|
||||||
getServiceHost(): string {
|
getServiceHost(): string {
|
||||||
//return "localhost:1244";
|
return "localhost:1244";
|
||||||
return "client-services.teaspeak.de:27791";
|
return "client-services.teaspeak.de:27791";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -441,7 +441,7 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
||||||
}
|
}
|
||||||
|
|
||||||
queryUnreadFlags() {
|
queryUnreadFlags() {
|
||||||
const commandData = this.connection.channelTree.channels.map(e => { return { cid: e.channelId, cpw: e.cached_password() }});
|
const commandData = this.connection.channelTree.channels.map(e => { return { cid: e.channelId, cpw: e.getCachedPasswordHash() }});
|
||||||
this.connection.serverConnection.send_command("conversationfetch", commandData).catch(error => {
|
this.connection.serverConnection.send_command("conversationfetch", commandData).catch(error => {
|
||||||
logWarn(LogCategory.CHAT, tr("Failed to query conversation indexes: %o"), error);
|
logWarn(LogCategory.CHAT, tr("Failed to query conversation indexes: %o"), error);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,9 +9,9 @@ import * as aplayer from "tc-backend/audio/player";
|
||||||
import * as ppt from "tc-backend/ppt";
|
import * as ppt from "tc-backend/ppt";
|
||||||
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
||||||
import {AppParameters, settings, Settings, UrlParameterBuilder, UrlParameterParser} from "tc-shared/settings";
|
import {AppParameters, settings, Settings, UrlParameterBuilder, UrlParameterParser} from "tc-shared/settings";
|
||||||
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
|
import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log";
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {createInfoModal} from "tc-shared/ui/elements/Modal";
|
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {RecorderProfile, setDefaultRecorder} from "tc-shared/voice/RecorderProfile";
|
import {RecorderProfile, setDefaultRecorder} from "tc-shared/voice/RecorderProfile";
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
|
@ -48,6 +48,9 @@ import "./ui/modal/connect/Controller";
|
||||||
import "./ui/elements/ContextDivider";
|
import "./ui/elements/ContextDivider";
|
||||||
import "./ui/elements/Tab";
|
import "./ui/elements/Tab";
|
||||||
import "./clientservice";
|
import "./clientservice";
|
||||||
|
import "./text/bbcode/InviteController";
|
||||||
|
import {clientServiceInvite} from "tc-shared/clientservice";
|
||||||
|
import {ActionResult} from "tc-services";
|
||||||
|
|
||||||
assertMainApplication();
|
assertMainApplication();
|
||||||
|
|
||||||
|
@ -109,31 +112,102 @@ export function handleNativeConnectRequest(url: URL) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnectRequest(serverAddress, new UrlParameterParser(url));
|
handleConnectRequest(serverAddress, undefined, new UrlParameterParser(url)).then(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleConnectRequest(serverAddress: string, serverUniqueId: string | undefined, parameters: UrlParameterParser) {
|
||||||
|
const inviteLinkId = parameters.getValue(AppParameters.KEY_CONNECT_INVITE_REFERENCE, undefined);
|
||||||
|
logDebug(LogCategory.STATISTICS, tr("Executing connect request with invite key reference: %o"), inviteLinkId);
|
||||||
|
|
||||||
|
if(inviteLinkId) {
|
||||||
|
clientServiceInvite.logAction(inviteLinkId, "ConnectAttempt").then(result => {
|
||||||
|
if(result.status !== "success") {
|
||||||
|
logWarn(LogCategory.STATISTICS, tr("Failed to register connect attempt: %o"), result.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await doHandleConnectRequest(serverAddress, serverUniqueId, parameters);
|
||||||
|
if(inviteLinkId) {
|
||||||
|
let promise: Promise<ActionResult<void>>;
|
||||||
|
switch (result.status) {
|
||||||
|
case "success":
|
||||||
|
promise = clientServiceInvite.logAction(inviteLinkId, "ConnectSuccess");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "channel-already-joined":
|
||||||
|
case "server-already-joined":
|
||||||
|
promise = clientServiceInvite.logAction(inviteLinkId, "ConnectNoAction", { reason: result.status });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
promise = clientServiceInvite.logAction(inviteLinkId, "ConnectFailure", { reason: result.status });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(result => {
|
||||||
|
if(result.status !== "success") {
|
||||||
|
logWarn(LogCategory.STATISTICS, tr("Failed to register connect result: %o"), result.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectRequestResult = {
|
||||||
|
status:
|
||||||
|
"success" |
|
||||||
|
"profile-invalid" |
|
||||||
|
"client-aborted" |
|
||||||
|
"server-join-failed" |
|
||||||
|
"server-already-joined" |
|
||||||
|
"channel-already-joined" |
|
||||||
|
"channel-not-visible" |
|
||||||
|
"channel-join-failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param serverAddress The target address to connect to
|
||||||
|
* @param serverUniqueId If given a server unique id. If any of our current connections matches it, such connection will be used
|
||||||
|
* @param parameters General connect parameters from the connect URL
|
||||||
|
*/
|
||||||
|
async function doHandleConnectRequest(serverAddress: string, serverUniqueId: string | undefined, parameters: UrlParameterParser) : Promise<ConnectRequestResult> {
|
||||||
|
|
||||||
|
let targetServerConnection: ConnectionHandler;
|
||||||
|
let isCurrentServerConnection: boolean;
|
||||||
|
|
||||||
|
if(serverUniqueId) {
|
||||||
|
if(server_connections.getActiveConnectionHandler()?.getCurrentServerUniqueId() === serverUniqueId) {
|
||||||
|
targetServerConnection = server_connections.getActiveConnectionHandler();
|
||||||
|
isCurrentServerConnection = true;
|
||||||
|
} else {
|
||||||
|
targetServerConnection = server_connections.getAllConnectionHandlers().find(connection => connection.getCurrentServerUniqueId() === serverUniqueId);
|
||||||
|
isCurrentServerConnection = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnectRequest(serverAddress: string, parameters: UrlParameterParser) {
|
|
||||||
const profileId = parameters.getValue(AppParameters.KEY_CONNECT_PROFILE, undefined);
|
const profileId = parameters.getValue(AppParameters.KEY_CONNECT_PROFILE, undefined);
|
||||||
const profile = findConnectProfile(profileId) || defaultConnectProfile();
|
const profile = findConnectProfile(profileId) || targetServerConnection?.serverConnection.handshake_handler()?.parameters.profile || defaultConnectProfile();
|
||||||
|
|
||||||
if(!profile || !profile.valid()) {
|
if(!profile || !profile.valid()) {
|
||||||
spawnConnectModalNew({
|
spawnConnectModalNew({
|
||||||
selectedAddress: serverAddress,
|
selectedAddress: serverAddress,
|
||||||
selectedProfile: profile
|
selectedProfile: profile
|
||||||
});
|
});
|
||||||
return;
|
return { status: "profile-invalid" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!aplayer.initialized()) {
|
if(!aplayer.initialized()) {
|
||||||
/* Trick the client into clicking somewhere on the site */
|
/* Trick the client into clicking somewhere on the site to initialize audio */
|
||||||
spawnYesNo(tra("Connect to {}", serverAddress), tra("Would you like to connect to {}?", serverAddress), result => {
|
const resultPromise = new Promise<boolean>(resolve => {
|
||||||
if(result) {
|
spawnYesNo(tra("Connect to {}", serverAddress), tra("Would you like to connect to {}?", serverAddress), resolve).open();
|
||||||
aplayer.on_ready(() => handleConnectRequest(serverAddress, parameters));
|
});
|
||||||
} else {
|
|
||||||
|
if(!(await resultPromise)) {
|
||||||
/* Well... the client don't want to... */
|
/* Well... the client don't want to... */
|
||||||
|
return { status: "client-aborted" };
|
||||||
}
|
}
|
||||||
}).open();
|
|
||||||
return;
|
await new Promise(resolve => aplayer.on_ready(resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientNickname = parameters.getValue(AppParameters.KEY_CONNECT_NICKNAME, undefined);
|
const clientNickname = parameters.getValue(AppParameters.KEY_CONNECT_NICKNAME, undefined);
|
||||||
|
@ -144,12 +218,59 @@ function handleConnectRequest(serverAddress: string, parameters: UrlParameterPar
|
||||||
const channel = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL, undefined);
|
const channel = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL, undefined);
|
||||||
const channelPassword = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL_PASSWORD, undefined);
|
const channelPassword = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL_PASSWORD, undefined);
|
||||||
|
|
||||||
let connection = server_connections.getActiveConnectionHandler();
|
if(!targetServerConnection) {
|
||||||
if(connection.connected) {
|
targetServerConnection = server_connections.getActiveConnectionHandler();
|
||||||
connection = server_connections.spawnConnectionHandler();
|
if(targetServerConnection.connected) {
|
||||||
|
targetServerConnection = server_connections.spawnConnectionHandler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.startConnectionNew({
|
server_connections.setActiveConnectionHandler(targetServerConnection);
|
||||||
|
if(isCurrentServerConnection) {
|
||||||
|
/* Just join the new channel and may use the token (before) */
|
||||||
|
|
||||||
|
/* TODO: Use the token! */
|
||||||
|
let containsToken = false;
|
||||||
|
|
||||||
|
if(!channel) {
|
||||||
|
/* No need to join any channel */
|
||||||
|
if(!containsToken) {
|
||||||
|
createInfoModal(tr("Already connected"), tr("You're already connected to the target server.")).open();
|
||||||
|
} else {
|
||||||
|
/* Don't show a message since a token has been used */
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: "server-already-joined" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetChannel = targetServerConnection.channelTree.resolveChannelPath(channel);
|
||||||
|
if(!targetChannel) {
|
||||||
|
createErrorModal(tr("Missing target channel"), tr("Failed to join channel since it is not visible.")).open();
|
||||||
|
return { status: "channel-not-visible" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if(targetServerConnection.getClient().currentChannel() === targetChannel) {
|
||||||
|
createErrorModal(tr("Channel already joined"), tr("You already joined the channel.")).open();
|
||||||
|
return { status: "channel-already-joined" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if(targetChannel.getCachedPasswordHash()) {
|
||||||
|
const succeeded = await targetChannel.joinChannel();
|
||||||
|
if(succeeded) {
|
||||||
|
/* Successfully joined channel with a password we already knew */
|
||||||
|
return { status: "success" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetChannel.setCachedHashedPassword(channelPassword);
|
||||||
|
if(await targetChannel.joinChannel()) {
|
||||||
|
return { status: "success" };
|
||||||
|
} else {
|
||||||
|
/* TODO: More detail? */
|
||||||
|
return { status: "channel-join-failed" };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await targetServerConnection.startConnectionNew({
|
||||||
targetAddress: serverAddress,
|
targetAddress: serverAddress,
|
||||||
|
|
||||||
nickname: clientNickname,
|
nickname: clientNickname,
|
||||||
|
@ -164,8 +285,15 @@ function handleConnectRequest(serverAddress: string, parameters: UrlParameterPar
|
||||||
defaultChannel: channel,
|
defaultChannel: channel,
|
||||||
defaultChannelPassword: channelPassword,
|
defaultChannelPassword: channelPassword,
|
||||||
defaultChannelPasswordHashed: passwordsHashed
|
defaultChannelPasswordHashed: passwordsHashed
|
||||||
}, false).then(undefined);
|
}, false);
|
||||||
server_connections.setActiveConnectionHandler(connection);
|
|
||||||
|
if(targetServerConnection.connected) {
|
||||||
|
return { status: "success" };
|
||||||
|
} else {
|
||||||
|
/* TODO: More detail? */
|
||||||
|
return { status: "server-join-failed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Used by the old native clients (an within the multi instance handler). Delete it later */
|
/* Used by the old native clients (an within the multi instance handler). Delete it later */
|
||||||
|
@ -178,7 +306,7 @@ export function handle_connect_request(properties: ConnectRequestData, _connecti
|
||||||
urlBuilder.setValue(AppParameters.KEY_CONNECT_PASSWORDS_HASHED, properties.password?.hashed);
|
urlBuilder.setValue(AppParameters.KEY_CONNECT_PASSWORDS_HASHED, properties.password?.hashed);
|
||||||
|
|
||||||
const url = new URL(`https://localhost/?${urlBuilder.build()}`);
|
const url = new URL(`https://localhost/?${urlBuilder.build()}`);
|
||||||
handleConnectRequest(properties.address, new UrlParameterParser(url));
|
handleConnectRequest(properties.address, undefined, new UrlParameterParser(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
@ -339,7 +467,7 @@ const task_connect_handler: loader.Task = {
|
||||||
preventWelcomeUI = true;
|
preventWelcomeUI = true;
|
||||||
loader.register_task(loader.Stage.LOADED, {
|
loader.register_task(loader.Stage.LOADED, {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
function: async () => handleConnectRequest(address, AppParameters.Instance),
|
function: async () => handleConnectRequest(address, undefined, AppParameters.Instance),
|
||||||
name: tr("default url connect")
|
name: tr("default url connect")
|
||||||
});
|
});
|
||||||
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
|
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
|
||||||
|
|
|
@ -180,6 +180,12 @@ export namespace AppParameters {
|
||||||
description: "A target address to automatically connect to."
|
description: "A target address to automatically connect to."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const KEY_CONNECT_INVITE_REFERENCE: RegistryKey<string> = {
|
||||||
|
key: "cir",
|
||||||
|
fallbackKeys: ["connect-invite-reference"],
|
||||||
|
valueType: "string",
|
||||||
|
description: "The invite link used to generate the connect parameters"
|
||||||
|
};
|
||||||
|
|
||||||
export const KEY_CONNECT_NO_SINGLE_INSTANCE: ValuedRegistryKey<boolean> = {
|
export const KEY_CONNECT_NO_SINGLE_INSTANCE: ValuedRegistryKey<boolean> = {
|
||||||
key: "cnsi",
|
key: "cnsi",
|
||||||
|
@ -249,7 +255,7 @@ export namespace AppParameters {
|
||||||
export const KEY_IPC_REMOTE_ADDRESS: RegistryKey<string> = {
|
export const KEY_IPC_REMOTE_ADDRESS: RegistryKey<string> = {
|
||||||
key: "ipc-address",
|
key: "ipc-address",
|
||||||
valueType: "string",
|
valueType: "string",
|
||||||
description: "Address of the owner for IPC communication."
|
description: "Address of the apps IPC channel"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KEY_IPC_REMOTE_POPOUT_CHANNEL: RegistryKey<string> = {
|
export const KEY_IPC_REMOTE_POPOUT_CHANNEL: RegistryKey<string> = {
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
import * as loader from "tc-loader";
|
||||||
|
import {ChannelMessage, getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
|
import {AppParameters, UrlParameterParser} from "tc-shared/settings";
|
||||||
|
import {IpcInviteInfo} from "tc-shared/text/bbcode/InviteDefinitions";
|
||||||
|
import {LogCategory, logError} from "tc-shared/log";
|
||||||
|
import {clientServiceInvite, clientServices} from "tc-shared/clientservice";
|
||||||
|
import {handleConnectRequest} from "tc-shared/main";
|
||||||
|
|
||||||
|
let ipcChannel: IPCChannel;
|
||||||
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
|
name: "Invite controller init",
|
||||||
|
function: async () => {
|
||||||
|
ipcChannel = getIpcInstance().createChannel(AppParameters.getValue(AppParameters.KEY_IPC_REMOTE_ADDRESS, undefined), "invite-info");
|
||||||
|
ipcChannel.messageHandler = handleIpcMessage;
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
type QueryCacheEntry = { result, finished: boolean, timeout: number };
|
||||||
|
let queryCache: {[key: string]: QueryCacheEntry} = {};
|
||||||
|
|
||||||
|
let executingPendingInvites = false;
|
||||||
|
const pendingInviteQueries: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
function handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
if(message.type === "query") {
|
||||||
|
const linkId = message.data["linkId"];
|
||||||
|
|
||||||
|
if(queryCache[linkId]) {
|
||||||
|
if(queryCache[linkId].finished) {
|
||||||
|
ipcChannel?.sendMessage("query-result", { linkId, result: queryCache[linkId].result }, remoteId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Query already enqueued. */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = queryCache[linkId] = {
|
||||||
|
finished: false,
|
||||||
|
result: undefined,
|
||||||
|
timeout: 0
|
||||||
|
} as QueryCacheEntry;
|
||||||
|
|
||||||
|
entry.timeout = setTimeout(() => {
|
||||||
|
if(queryCache[linkId] === entry) {
|
||||||
|
delete queryCache[linkId];
|
||||||
|
}
|
||||||
|
}, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
pendingInviteQueries.push(() => queryInviteLink(linkId));
|
||||||
|
invokeLinkQueries();
|
||||||
|
} else if(message.type === "connect") {
|
||||||
|
const connectParameterString = message.data.connectParameters;
|
||||||
|
const serverAddress = message.data.serverAddress;
|
||||||
|
const serverUniqueId = message.data.serverUniqueId;
|
||||||
|
|
||||||
|
handleConnectRequest(serverAddress, serverUniqueId, new UrlParameterParser(new URL(`https://localhost/?${connectParameterString}`))).then(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeLinkQueries() {
|
||||||
|
if(executingPendingInvites) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executingPendingInvites = true;
|
||||||
|
executePendingInvites().catch(error => {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to execute pending invite queries: %o"), error);
|
||||||
|
executingPendingInvites = false;
|
||||||
|
if(pendingInviteQueries.length > 0) {
|
||||||
|
invokeLinkQueries();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePendingInvites() {
|
||||||
|
while(pendingInviteQueries.length > 0) {
|
||||||
|
const invite = pendingInviteQueries.pop_front();
|
||||||
|
await invite();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
}
|
||||||
|
|
||||||
|
executingPendingInvites = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryInviteLink(linkId: string) {
|
||||||
|
let result: IpcInviteInfo;
|
||||||
|
try {
|
||||||
|
result = await doQueryInviteLink(linkId);
|
||||||
|
} catch (error) {
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to query invite link info: %o"), error);
|
||||||
|
result = {
|
||||||
|
status: "error",
|
||||||
|
message: tr("lookup the console for details")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if(queryCache[linkId]) {
|
||||||
|
queryCache[linkId].finished = true;
|
||||||
|
queryCache[linkId].result = result;
|
||||||
|
} else {
|
||||||
|
const entry = queryCache[linkId] = {
|
||||||
|
finished: true,
|
||||||
|
result: result,
|
||||||
|
timeout: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
entry.timeout = setTimeout(() => {
|
||||||
|
if(queryCache[linkId] === entry) {
|
||||||
|
delete queryCache[linkId];
|
||||||
|
}
|
||||||
|
}, 30 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcChannel?.sendMessage("query-result", { linkId, result });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doQueryInviteLink(linkId: string) : Promise<IpcInviteInfo> {
|
||||||
|
if(!clientServices.isSessionInitialized()) {
|
||||||
|
const connectAwait = new Promise(resolve => {
|
||||||
|
clientServices.awaitSession().then(() => resolve(true));
|
||||||
|
setTimeout(() => resolve(false), 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!await connectAwait) {
|
||||||
|
return { status: "error", message: tr("Client service not connected") };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: Cache if the client has ever seen the view! */
|
||||||
|
const result = await clientServiceInvite.queryInviteLink(linkId, true);
|
||||||
|
if(result.status === "error") {
|
||||||
|
switch (result.result.type) {
|
||||||
|
case "InviteKeyExpired":
|
||||||
|
return { status: "expired" };
|
||||||
|
|
||||||
|
case "InviteKeyNotFound":
|
||||||
|
return { status: "not-found" };
|
||||||
|
|
||||||
|
default:
|
||||||
|
logError(LogCategory.GENERAL, tr("Failed to query invite link info for %s: %o"), linkId, result.result);
|
||||||
|
return { status: "error", message: tra("Server query error ({})", result.result.type) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteInfo = result.unwrap();
|
||||||
|
|
||||||
|
const serverName = inviteInfo.propertiesInfo["server-name"];
|
||||||
|
if(typeof serverName !== "string") {
|
||||||
|
return { status: "error", message: tr("Missing server name") };
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverUniqueId = inviteInfo.propertiesInfo["server-unique-id"];
|
||||||
|
if(typeof serverUniqueId !== "string") {
|
||||||
|
return { status: "error", message: tr("Missing server unique id") };
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverAddress = inviteInfo.propertiesConnect["server-address"];
|
||||||
|
if(typeof serverAddress !== "string") {
|
||||||
|
return { status: "error", message: tr("Missing server address") };
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParameters = {};
|
||||||
|
{
|
||||||
|
urlParameters["cir"] = linkId;
|
||||||
|
|
||||||
|
urlParameters["cn"] = inviteInfo.propertiesConnect["nickname"];
|
||||||
|
urlParameters["ctk"] = inviteInfo.propertiesConnect["token"];
|
||||||
|
urlParameters["cc"] = inviteInfo.propertiesConnect["channel"];
|
||||||
|
|
||||||
|
urlParameters["cph"] = inviteInfo.propertiesConnect["passwords-hashed"];
|
||||||
|
urlParameters["csp"] = inviteInfo.propertiesConnect["server-password"];
|
||||||
|
urlParameters["ccp"] = inviteInfo.propertiesConnect["channel-password"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParameterString = Object.keys(urlParameters)
|
||||||
|
.filter(key => typeof urlParameters[key] === "string" && urlParameters[key].length > 0)
|
||||||
|
.map(key => `${key}=${encodeURIComponent(urlParameters[key])}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
return {
|
||||||
|
linkId: linkId,
|
||||||
|
|
||||||
|
status: "success",
|
||||||
|
expireTimestamp: inviteInfo.timestampExpired,
|
||||||
|
|
||||||
|
serverUniqueId: serverUniqueId,
|
||||||
|
serverName: serverName,
|
||||||
|
serverAddress: serverAddress,
|
||||||
|
|
||||||
|
channelName: inviteInfo.propertiesInfo["channel-name"],
|
||||||
|
|
||||||
|
connectParameters: urlParameterString,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
export type IpcInviteInfoLoaded = {
|
||||||
|
linkId: string,
|
||||||
|
|
||||||
|
serverAddress: string,
|
||||||
|
serverUniqueId: string,
|
||||||
|
serverName: string,
|
||||||
|
|
||||||
|
connectParameters: string,
|
||||||
|
|
||||||
|
channelId?: number,
|
||||||
|
channelName?: string,
|
||||||
|
|
||||||
|
expireTimestamp: number | 0
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IpcInviteInfo = (
|
||||||
|
{
|
||||||
|
status: "success",
|
||||||
|
} & IpcInviteInfoLoaded
|
||||||
|
) | {
|
||||||
|
status: "error",
|
||||||
|
message: string
|
||||||
|
} | {
|
||||||
|
status: "not-found" | "expired"
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
@import "../../../css/static/mixin";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-top: .25em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 3em;
|
||||||
|
|
||||||
|
background: #454545;
|
||||||
|
border-radius: .2em;
|
||||||
|
|
||||||
|
padding: .2em .3em;
|
||||||
|
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info, &.loading {
|
||||||
|
.left, .right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
margin-left: 1em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
height: 2.4em;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
min-width: 1em;
|
||||||
|
max-width: 20em;
|
||||||
|
|
||||||
|
line-height: 1.2em;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinServer {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
min-height: 0;
|
||||||
|
height: 1em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelName {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
color: #b3b3b3;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
max-height: 1em;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-left: .25em;
|
||||||
|
align-self: center;
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.serverName {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #b3b3b3;
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
max-height: 2.4em;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.short {
|
||||||
|
max-height: 1.2em;
|
||||||
|
@include text-dotdotdot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.containerError {
|
||||||
|
height: 2.4em;
|
||||||
|
color: #cf1717;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
|
||||||
|
&.noTitle {
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
max-height: 2.4em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
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 {AppParameters} from "tc-shared/settings";
|
||||||
|
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().createChannel(AppParameters.getValue(AppParameters.KEY_IPC_REMOTE_ADDRESS, undefined), "invite-info");
|
||||||
|
ipcChannel.messageHandler = handleIpcMessage;
|
||||||
|
},
|
||||||
|
priority: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
|
||||||
|
console.error(message);
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,11 +13,13 @@ import "./highlight";
|
||||||
import "./youtube";
|
import "./youtube";
|
||||||
import "./url";
|
import "./url";
|
||||||
import "./image";
|
import "./image";
|
||||||
|
import {ElementRenderer, Renderer} from "vendor/xbbcode/renderer/base";
|
||||||
|
import {TextElement} from "vendor/xbbcode/elements";
|
||||||
|
|
||||||
export let BBCodeHandlerContext: Context<string>;
|
export let BBCodeHandlerContext: Context<string>;
|
||||||
|
|
||||||
export const rendererText = new TextRenderer();
|
export const rendererText = new TextRenderer();
|
||||||
export const rendererReact = new ReactRenderer();
|
export const rendererReact = new ReactRenderer(true);
|
||||||
export const rendererHTML = new HTMLRenderer(rendererReact);
|
export const rendererHTML = new HTMLRenderer(rendererReact);
|
||||||
|
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|
@ -8,6 +8,7 @@ import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||||
import {rendererReact, rendererText, BBCodeHandlerContext} from "tc-shared/text/bbcode/renderer";
|
import {rendererReact, rendererText, BBCodeHandlerContext} from "tc-shared/text/bbcode/renderer";
|
||||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||||
import {isYoutubeLink, YoutubeRenderer} from "tc-shared/text/bbcode/youtube";
|
import {isYoutubeLink, YoutubeRenderer} from "tc-shared/text/bbcode/youtube";
|
||||||
|
import {InviteLinkRenderer, isInviteLink} from "tc-shared/text/bbcode/InviteRenderer";
|
||||||
|
|
||||||
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
||||||
contextmenu.spawn_context_menu(pageX, pageY, {
|
contextmenu.spawn_context_menu(pageX, pageY, {
|
||||||
|
@ -35,6 +36,17 @@ function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
||||||
|
|
||||||
const ClientUrlRegex = /client:\/\/([0-9]+)\/([-A-Za-z0-9+/=]+)~/g;
|
const ClientUrlRegex = /client:\/\/([0-9]+)\/([-A-Za-z0-9+/=]+)~/g;
|
||||||
|
|
||||||
|
export const SimpleUrlRenderer = (props: { target: string, children }) => {
|
||||||
|
return (
|
||||||
|
<a className={"xbbcode xbbcode-tag-url"} href={props.target} target={"_blank"} onContextMenu={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
spawnUrlContextMenu(event.pageX, event.pageY, props.target);
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "XBBCode code tag init",
|
name: "XBBCode code tag init",
|
||||||
function: async () => {
|
function: async () => {
|
||||||
|
@ -65,23 +77,26 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
const clientDatabaseId = parseInt(clientData[1]);
|
const clientDatabaseId = parseInt(clientData[1]);
|
||||||
const clientUniqueId = clientDatabaseId[2];
|
const clientUniqueId = clientDatabaseId[2];
|
||||||
|
|
||||||
return <ClientTag
|
return (
|
||||||
|
<ClientTag
|
||||||
key={"er-" + ++reactId}
|
key={"er-" + ++reactId}
|
||||||
clientName={rendererText.renderContent(element).join("")}
|
clientName={rendererText.renderContent(element).join("")}
|
||||||
clientUniqueId={clientUniqueId}
|
clientUniqueId={clientUniqueId}
|
||||||
clientDatabaseId={clientDatabaseId > 0 ? clientDatabaseId : undefined}
|
clientDatabaseId={clientDatabaseId > 0 ? clientDatabaseId : undefined}
|
||||||
handlerId={handlerId}
|
handlerId={handlerId}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isInviteLink(target)) {
|
||||||
|
return <InviteLinkRenderer key={"er-" + ++reactId} handlerId={handlerId} url={target} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
<SimpleUrlRenderer key={"er-" + ++reactId} target={target}>
|
||||||
event.preventDefault();
|
|
||||||
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
|
||||||
}}>
|
|
||||||
{renderer.renderContent(element)}
|
{renderer.renderContent(element)}
|
||||||
</a>
|
</SimpleUrlRenderer>
|
||||||
);
|
);
|
||||||
|
|
||||||
if(isYoutubeLink(target)) {
|
if(isYoutubeLink(target)) {
|
||||||
|
|
|
@ -693,42 +693,50 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
return ChannelType.TEMPORARY;
|
return ChannelType.TEMPORARY;
|
||||||
}
|
}
|
||||||
|
|
||||||
joinChannel(ignorePasswordFlag?: boolean) {
|
async joinChannel(ignorePasswordFlag?: boolean) : Promise<boolean> {
|
||||||
if(this.channelTree.client.getClient().currentChannel() === this)
|
if(this.channelTree.client.getClient().currentChannel() === this) {
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
if(this.properties.channel_flag_password === true && !this.cachedPasswordHash && !ignorePasswordFlag) {
|
|
||||||
this.requestChannelPassword(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD).then(() => {
|
|
||||||
this.joinChannel(true);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.channelTree.client.serverConnection.send_command("clientmove", {
|
if(this.properties.channel_flag_password === true && !this.cachedPasswordHash && !ignorePasswordFlag) {
|
||||||
|
const password = await this.requestChannelPassword(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD);
|
||||||
|
if(typeof password === "undefined") {
|
||||||
|
/* aborted */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.channelTree.client.serverConnection.send_command("clientmove", {
|
||||||
"clid": this.channelTree.client.getClientId(),
|
"clid": this.channelTree.client.getClientId(),
|
||||||
"cid": this.getChannelId(),
|
"cid": this.getChannelId(),
|
||||||
"cpw": this.cachedPasswordHash || ""
|
"cpw": this.cachedPasswordHash || ""
|
||||||
}).then(() => {
|
});
|
||||||
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
|
return true;
|
||||||
}).catch(error => {
|
} catch (error) {
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
if(error.id == ErrorCode.CHANNEL_INVALID_PASSWORD) { //Invalid password
|
if(error.id == ErrorCode.CHANNEL_INVALID_PASSWORD) { //Invalid password
|
||||||
this.invalidateCachedPassword();
|
this.invalidateCachedPassword();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestChannelPassword(ignorePermission: PermissionType) : Promise<{ hash: string } | undefined> {
|
async requestChannelPassword(ignorePermission: PermissionType) : Promise<{ hash: string } | undefined> {
|
||||||
if(this.cachedPasswordHash)
|
if(this.cachedPasswordHash) {
|
||||||
return { hash: this.cachedPasswordHash };
|
return { hash: this.cachedPasswordHash };
|
||||||
|
}
|
||||||
|
|
||||||
if(this.channelTree.client.permissions.neededPermission(ignorePermission).granted(1))
|
if(this.channelTree.client.permissions.neededPermission(ignorePermission).granted(1)) {
|
||||||
return { hash: "having ignore permission" };
|
return { hash: "having ignore permission" };
|
||||||
|
}
|
||||||
|
|
||||||
const password = await new Promise(resolve => createInputModal(tr("Channel password"), tr("Channel password:"), () => true, resolve).open())
|
const password = await new Promise(resolve => createInputModal(tr("Channel password"), tr("Channel password:"), () => true, resolve).open())
|
||||||
if(typeof(password) !== "string" || !password)
|
if(typeof(password) !== "string" || !password) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hash = await hashPassword(password);
|
const hash = await hashPassword(password);
|
||||||
this.cachedPasswordHash = hash;
|
this.cachedPasswordHash = hash;
|
||||||
|
@ -741,7 +749,11 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
this.events.fire("notify_cached_password_updated", { reason: "password-miss-match" });
|
this.events.fire("notify_cached_password_updated", { reason: "password-miss-match" });
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_password() { return this.cachedPasswordHash; }
|
setCachedHashedPassword(passwordHash: string) {
|
||||||
|
this.cachedPasswordHash = passwordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCachedPasswordHash() { return this.cachedPasswordHash; }
|
||||||
|
|
||||||
async updateSubscribeMode() {
|
async updateSubscribeMode() {
|
||||||
let shouldBeSubscribed = false;
|
let shouldBeSubscribed = false;
|
||||||
|
@ -845,7 +857,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscribed = this.isSubscribed();
|
const subscribed = this.isSubscribed();
|
||||||
if (this.properties.channel_flag_password === true && !this.cached_password()) {
|
if (this.properties.channel_flag_password === true && !this.getCachedPasswordHash()) {
|
||||||
return subscribed ? ClientIcon.ChannelYellowSubscribed : ClientIcon.ChannelYellow;
|
return subscribed ? ClientIcon.ChannelYellowSubscribed : ClientIcon.ChannelYellow;
|
||||||
} else if (!this.properties.channel_flag_maxclients_unlimited && this.clients().length >= this.properties.channel_maxclients) {
|
} else if (!this.properties.channel_flag_maxclients_unlimited && this.clients().length >= this.properties.channel_maxclients) {
|
||||||
return subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed;
|
return subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed;
|
||||||
|
|
|
@ -270,6 +270,19 @@ export class ChannelTree {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a channel by its path
|
||||||
|
*/
|
||||||
|
resolveChannelPath(target: string) : ChannelEntry | undefined {
|
||||||
|
if(target.match(/^\/[0-9]+$/)) {
|
||||||
|
const channelId = parseInt(target.substring(1));
|
||||||
|
return this.findChannel(channelId);
|
||||||
|
} else {
|
||||||
|
/* TODO: Resolve the whole channel path */
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
find_channel_by_name(name: string, parent?: ChannelEntry, force_parent: boolean = true) : ChannelEntry | undefined {
|
find_channel_by_name(name: string, parent?: ChannelEntry, force_parent: boolean = true) : ChannelEntry | undefined {
|
||||||
for(let index = 0; index < this.channels.length; index++)
|
for(let index = 0; index < this.channels.length; index++)
|
||||||
if(this.channels[index].channelName() == name && (!force_parent || parent == this.channels[index].parent))
|
if(this.channels[index].channelName() == name && (!force_parent || parent == this.channels[index].parent))
|
||||||
|
|
|
@ -182,7 +182,7 @@ class InviteController {
|
||||||
|
|
||||||
this.targetChannelId = channel.channelId;
|
this.targetChannelId = channel.channelId;
|
||||||
this.targetChannelName = channel.channelName();
|
this.targetChannelName = channel.channelName();
|
||||||
this.targetChannelPasswordHashed = channel.cached_password();
|
this.targetChannelPasswordHashed = channel.getCachedPasswordHash();
|
||||||
this.targetChannelPasswordRaw = undefined;
|
this.targetChannelPasswordRaw = undefined;
|
||||||
} else if(this.targetChannelId === 0) {
|
} else if(this.targetChannelId === 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -281,7 +281,7 @@ class InviteController {
|
||||||
const inviteLink = result.unwrap();
|
const inviteLink = result.unwrap();
|
||||||
this.linkAdminToken = inviteLink.adminToken;
|
this.linkAdminToken = inviteLink.adminToken;
|
||||||
this.inviteLinkShort = `https://teaspeak.de/${inviteLink.linkId}`;
|
this.inviteLinkShort = `https://teaspeak.de/${inviteLink.linkId}`;
|
||||||
this.inviteLinkLong = `https://join.teaspeak.de/invite/${inviteLink.linkId}`;
|
this.inviteLinkLong = `https://join.teaspeak.de/${inviteLink.linkId}`;
|
||||||
this.inviteLinkExpireDate = this.linkExpiresAfter;
|
this.inviteLinkExpireDate = this.linkExpiresAfter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -533,7 +533,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
||||||
return {
|
return {
|
||||||
path: e.info.path,
|
path: e.info.path,
|
||||||
cid: e.info.channelId,
|
cid: e.info.channelId,
|
||||||
cpw: e.info.channel?.cached_password(),
|
cpw: e.info.channel?.getCachedPasswordHash(),
|
||||||
name: e.name
|
name: e.name
|
||||||
}
|
}
|
||||||
})).then(async result => {
|
})).then(async result => {
|
||||||
|
@ -647,7 +647,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
||||||
//ftcreatedir cid=4 cpw dirname=\/TestDir return_code=1:17
|
//ftcreatedir cid=4 cpw dirname=\/TestDir return_code=1:17
|
||||||
connection.serverConnection.send_command("ftcreatedir", {
|
connection.serverConnection.send_command("ftcreatedir", {
|
||||||
cid: path.channelId,
|
cid: path.channelId,
|
||||||
cpw: path.channel.cached_password(),
|
cpw: path.channel.getCachedPasswordHash(),
|
||||||
dirname: path.path + event.name
|
dirname: path.path + event.name
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
events.fire("action_create_directory_result", {path: event.path, name: event.name, status: "success"});
|
events.fire("action_create_directory_result", {path: event.path, name: event.name, status: "success"});
|
||||||
|
@ -709,7 +709,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
||||||
channel: info.channelId,
|
channel: info.channelId,
|
||||||
path: info.type === "channel" ? info.path : "",
|
path: info.type === "channel" ? info.path : "",
|
||||||
name: info.type === "channel" ? file.name : "/" + file.name,
|
name: info.type === "channel" ? file.name : "/" + file.name,
|
||||||
channelPassword: info.channel?.cached_password(),
|
channelPassword: info.channel?.getCachedPasswordHash(),
|
||||||
targetSupplier: targetSupplier
|
targetSupplier: targetSupplier
|
||||||
});
|
});
|
||||||
transfer.awaitFinished().then(() => {
|
transfer.awaitFinished().then(() => {
|
||||||
|
@ -752,7 +752,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
||||||
const fileName = file.name;
|
const fileName = file.name;
|
||||||
const transfer = connection.fileManager.initializeFileUpload({
|
const transfer = connection.fileManager.initializeFileUpload({
|
||||||
channel: pathInfo.channelId,
|
channel: pathInfo.channelId,
|
||||||
channelPassword: pathInfo.channel?.cached_password(),
|
channelPassword: pathInfo.channel?.getCachedPasswordHash(),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: pathInfo.path,
|
path: pathInfo.path,
|
||||||
source: async () => TransferProvider.provider().createBrowserFileSource(file)
|
source: async () => TransferProvider.provider().createBrowserFileSource(file)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
PopoutIPCMessage
|
PopoutIPCMessage
|
||||||
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
||||||
import {ModalController, ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions";
|
import {ModalController, ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions";
|
||||||
|
import {guid} from "tc-shared/crypto/uid";
|
||||||
|
|
||||||
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController {
|
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController {
|
||||||
public readonly modalType: string;
|
public readonly modalType: string;
|
||||||
|
@ -26,7 +27,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
|
|
||||||
this.modalEvents = new Registry<ModalEvents>();
|
this.modalEvents = new Registry<ModalEvents>();
|
||||||
|
|
||||||
this.ipcChannel = ipc.getIpcInstance().createChannel();
|
this.ipcChannel = ipc.getIpcInstance().createChannel(undefined, "modal-" + guid());
|
||||||
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
|
||||||
|
|
||||||
this.documentUnloadListener = () => this.destroy();
|
this.documentUnloadListener = () => this.destroy();
|
||||||
|
|
Loading…
Reference in New Issue