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;
|
||||
|
||||
return {
|
||||
channel: targetChannel ? {target: "/" + targetChannel.channelId, password: targetChannel.cached_password()} : undefined,
|
||||
channel: targetChannel ? {target: "/" + targetChannel.channelId, password: targetChannel.getCachedPasswordHash()} : undefined,
|
||||
nickname: name,
|
||||
password: connectParameters.serverPassword ? {
|
||||
password: connectParameters.serverPassword,
|
||||
|
|
|
@ -14,7 +14,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
function: async () => {
|
||||
clientServices = new ClientServices(new class implements ClientServiceConfig {
|
||||
getServiceHost(): string {
|
||||
//return "localhost:1244";
|
||||
return "localhost:1244";
|
||||
return "client-services.teaspeak.de:27791";
|
||||
}
|
||||
|
||||
|
|
|
@ -441,7 +441,7 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
|||
}
|
||||
|
||||
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 => {
|
||||
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 global_ev_handler from "./events/ClientGlobalControlHandler";
|
||||
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 {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 {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
|
@ -48,6 +48,9 @@ import "./ui/modal/connect/Controller";
|
|||
import "./ui/elements/ContextDivider";
|
||||
import "./ui/elements/Tab";
|
||||
import "./clientservice";
|
||||
import "./text/bbcode/InviteController";
|
||||
import {clientServiceInvite} from "tc-shared/clientservice";
|
||||
import {ActionResult} from "tc-services";
|
||||
|
||||
assertMainApplication();
|
||||
|
||||
|
@ -109,31 +112,102 @@ export function handleNativeConnectRequest(url: URL) {
|
|||
}
|
||||
}
|
||||
|
||||
handleConnectRequest(serverAddress, new UrlParameterParser(url));
|
||||
handleConnectRequest(serverAddress, undefined, new UrlParameterParser(url)).then(undefined);
|
||||
}
|
||||
|
||||
function handleConnectRequest(serverAddress: string, parameters: UrlParameterParser) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
spawnConnectModalNew({
|
||||
selectedAddress: serverAddress,
|
||||
selectedProfile: profile
|
||||
});
|
||||
return;
|
||||
return { status: "profile-invalid" };
|
||||
}
|
||||
|
||||
if(!aplayer.initialized()) {
|
||||
/* Trick the client into clicking somewhere on the site */
|
||||
spawnYesNo(tra("Connect to {}", serverAddress), tra("Would you like to connect to {}?", serverAddress), result => {
|
||||
if(result) {
|
||||
aplayer.on_ready(() => handleConnectRequest(serverAddress, parameters));
|
||||
} else {
|
||||
/* Well... the client don't want to... */
|
||||
}
|
||||
}).open();
|
||||
return;
|
||||
/* Trick the client into clicking somewhere on the site to initialize audio */
|
||||
const resultPromise = new Promise<boolean>(resolve => {
|
||||
spawnYesNo(tra("Connect to {}", serverAddress), tra("Would you like to connect to {}?", serverAddress), resolve).open();
|
||||
});
|
||||
|
||||
if(!(await resultPromise)) {
|
||||
/* Well... the client don't want to... */
|
||||
return { status: "client-aborted" };
|
||||
}
|
||||
|
||||
await new Promise(resolve => aplayer.on_ready(resolve));
|
||||
}
|
||||
|
||||
const clientNickname = parameters.getValue(AppParameters.KEY_CONNECT_NICKNAME, undefined);
|
||||
|
@ -144,28 +218,82 @@ function handleConnectRequest(serverAddress: string, parameters: UrlParameterPar
|
|||
const channel = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL, undefined);
|
||||
const channelPassword = parameters.getValue(AppParameters.KEY_CONNECT_CHANNEL_PASSWORD, undefined);
|
||||
|
||||
let connection = server_connections.getActiveConnectionHandler();
|
||||
if(connection.connected) {
|
||||
connection = server_connections.spawnConnectionHandler();
|
||||
if(!targetServerConnection) {
|
||||
targetServerConnection = server_connections.getActiveConnectionHandler();
|
||||
if(targetServerConnection.connected) {
|
||||
targetServerConnection = server_connections.spawnConnectionHandler();
|
||||
}
|
||||
}
|
||||
|
||||
connection.startConnectionNew({
|
||||
targetAddress: serverAddress,
|
||||
server_connections.setActiveConnectionHandler(targetServerConnection);
|
||||
if(isCurrentServerConnection) {
|
||||
/* Just join the new channel and may use the token (before) */
|
||||
|
||||
nickname: clientNickname,
|
||||
nicknameSpecified: false,
|
||||
/* TODO: Use the token! */
|
||||
let containsToken = false;
|
||||
|
||||
profile: profile,
|
||||
token: undefined,
|
||||
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 */
|
||||
}
|
||||
|
||||
serverPassword: serverPassword,
|
||||
serverPasswordHashed: passwordsHashed,
|
||||
return { status: "server-already-joined" };
|
||||
}
|
||||
|
||||
defaultChannel: channel,
|
||||
defaultChannelPassword: channelPassword,
|
||||
defaultChannelPasswordHashed: passwordsHashed
|
||||
}, false).then(undefined);
|
||||
server_connections.setActiveConnectionHandler(connection);
|
||||
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,
|
||||
|
||||
nickname: clientNickname,
|
||||
nicknameSpecified: false,
|
||||
|
||||
profile: profile,
|
||||
token: undefined,
|
||||
|
||||
serverPassword: serverPassword,
|
||||
serverPasswordHashed: passwordsHashed,
|
||||
|
||||
defaultChannel: channel,
|
||||
defaultChannelPassword: channelPassword,
|
||||
defaultChannelPasswordHashed: passwordsHashed
|
||||
}, false);
|
||||
|
||||
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 */
|
||||
|
@ -178,7 +306,7 @@ export function handle_connect_request(properties: ConnectRequestData, _connecti
|
|||
urlBuilder.setValue(AppParameters.KEY_CONNECT_PASSWORDS_HASHED, properties.password?.hashed);
|
||||
|
||||
const url = new URL(`https://localhost/?${urlBuilder.build()}`);
|
||||
handleConnectRequest(properties.address, new UrlParameterParser(url));
|
||||
handleConnectRequest(properties.address, undefined, new UrlParameterParser(url));
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
@ -339,7 +467,7 @@ const task_connect_handler: loader.Task = {
|
|||
preventWelcomeUI = true;
|
||||
loader.register_task(loader.Stage.LOADED, {
|
||||
priority: 0,
|
||||
function: async () => handleConnectRequest(address, AppParameters.Instance),
|
||||
function: async () => handleConnectRequest(address, undefined, AppParameters.Instance),
|
||||
name: tr("default url connect")
|
||||
});
|
||||
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
|
||||
|
|
|
@ -180,6 +180,12 @@ export namespace AppParameters {
|
|||
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> = {
|
||||
key: "cnsi",
|
||||
|
@ -249,7 +255,7 @@ export namespace AppParameters {
|
|||
export const KEY_IPC_REMOTE_ADDRESS: RegistryKey<string> = {
|
||||
key: "ipc-address",
|
||||
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> = {
|
||||
|
|
|
@ -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 "./url";
|
||||
import "./image";
|
||||
import {ElementRenderer, Renderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TextElement} from "vendor/xbbcode/elements";
|
||||
|
||||
export let BBCodeHandlerContext: Context<string>;
|
||||
|
||||
export const rendererText = new TextRenderer();
|
||||
export const rendererReact = new ReactRenderer();
|
||||
export const rendererReact = new ReactRenderer(true);
|
||||
export const rendererHTML = new HTMLRenderer(rendererReact);
|
||||
|
||||
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 {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||
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) {
|
||||
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;
|
||||
|
||||
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, {
|
||||
name: "XBBCode code tag init",
|
||||
function: async () => {
|
||||
|
@ -65,23 +77,26 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
|||
const clientDatabaseId = parseInt(clientData[1]);
|
||||
const clientUniqueId = clientDatabaseId[2];
|
||||
|
||||
return <ClientTag
|
||||
key={"er-" + ++reactId}
|
||||
clientName={rendererText.renderContent(element).join("")}
|
||||
clientUniqueId={clientUniqueId}
|
||||
clientDatabaseId={clientDatabaseId > 0 ? clientDatabaseId : undefined}
|
||||
handlerId={handlerId}
|
||||
/>;
|
||||
return (
|
||||
<ClientTag
|
||||
key={"er-" + ++reactId}
|
||||
clientName={rendererText.renderContent(element).join("")}
|
||||
clientUniqueId={clientUniqueId}
|
||||
clientDatabaseId={clientDatabaseId > 0 ? clientDatabaseId : undefined}
|
||||
handlerId={handlerId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if(isInviteLink(target)) {
|
||||
return <InviteLinkRenderer key={"er-" + ++reactId} handlerId={handlerId} url={target} />;
|
||||
}
|
||||
}
|
||||
|
||||
const body = (
|
||||
<a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
||||
}}>
|
||||
<SimpleUrlRenderer key={"er-" + ++reactId} target={target}>
|
||||
{renderer.renderContent(element)}
|
||||
</a>
|
||||
</SimpleUrlRenderer>
|
||||
);
|
||||
|
||||
if(isYoutubeLink(target)) {
|
||||
|
|
|
@ -693,42 +693,50 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
return ChannelType.TEMPORARY;
|
||||
}
|
||||
|
||||
joinChannel(ignorePasswordFlag?: boolean) {
|
||||
if(this.channelTree.client.getClient().currentChannel() === this)
|
||||
return;
|
||||
async joinChannel(ignorePasswordFlag?: boolean) : Promise<boolean> {
|
||||
if(this.channelTree.client.getClient().currentChannel() === this) {
|
||||
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", {
|
||||
"clid": this.channelTree.client.getClientId(),
|
||||
"cid": this.getChannelId(),
|
||||
"cpw": this.cachedPasswordHash || ""
|
||||
}).then(() => {
|
||||
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
|
||||
}).catch(error => {
|
||||
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(),
|
||||
"cid": this.getChannelId(),
|
||||
"cpw": this.cachedPasswordHash || ""
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id == ErrorCode.CHANNEL_INVALID_PASSWORD) { //Invalid password
|
||||
this.invalidateCachedPassword();
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async requestChannelPassword(ignorePermission: PermissionType) : Promise<{ hash: string } | undefined> {
|
||||
if(this.cachedPasswordHash)
|
||||
if(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" };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const hash = await hashPassword(password);
|
||||
this.cachedPasswordHash = hash;
|
||||
|
@ -741,7 +749,11 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
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() {
|
||||
let shouldBeSubscribed = false;
|
||||
|
@ -845,7 +857,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
}
|
||||
|
||||
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;
|
||||
} else if (!this.properties.channel_flag_maxclients_unlimited && this.clients().length >= this.properties.channel_maxclients) {
|
||||
return subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed;
|
||||
|
|
|
@ -270,6 +270,19 @@ export class ChannelTree {
|
|||
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 {
|
||||
for(let index = 0; index < this.channels.length; index++)
|
||||
if(this.channels[index].channelName() == name && (!force_parent || parent == this.channels[index].parent))
|
||||
|
|
|
@ -182,7 +182,7 @@ class InviteController {
|
|||
|
||||
this.targetChannelId = channel.channelId;
|
||||
this.targetChannelName = channel.channelName();
|
||||
this.targetChannelPasswordHashed = channel.cached_password();
|
||||
this.targetChannelPasswordHashed = channel.getCachedPasswordHash();
|
||||
this.targetChannelPasswordRaw = undefined;
|
||||
} else if(this.targetChannelId === 0) {
|
||||
return;
|
||||
|
@ -281,7 +281,7 @@ class InviteController {
|
|||
const inviteLink = result.unwrap();
|
||||
this.linkAdminToken = inviteLink.adminToken;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -533,7 +533,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
return {
|
||||
path: e.info.path,
|
||||
cid: e.info.channelId,
|
||||
cpw: e.info.channel?.cached_password(),
|
||||
cpw: e.info.channel?.getCachedPasswordHash(),
|
||||
name: e.name
|
||||
}
|
||||
})).then(async result => {
|
||||
|
@ -647,7 +647,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
//ftcreatedir cid=4 cpw dirname=\/TestDir return_code=1:17
|
||||
connection.serverConnection.send_command("ftcreatedir", {
|
||||
cid: path.channelId,
|
||||
cpw: path.channel.cached_password(),
|
||||
cpw: path.channel.getCachedPasswordHash(),
|
||||
dirname: path.path + event.name
|
||||
}).then(() => {
|
||||
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,
|
||||
path: info.type === "channel" ? info.path : "",
|
||||
name: info.type === "channel" ? file.name : "/" + file.name,
|
||||
channelPassword: info.channel?.cached_password(),
|
||||
channelPassword: info.channel?.getCachedPasswordHash(),
|
||||
targetSupplier: targetSupplier
|
||||
});
|
||||
transfer.awaitFinished().then(() => {
|
||||
|
@ -752,7 +752,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
const fileName = file.name;
|
||||
const transfer = connection.fileManager.initializeFileUpload({
|
||||
channel: pathInfo.channelId,
|
||||
channelPassword: pathInfo.channel?.cached_password(),
|
||||
channelPassword: pathInfo.channel?.getCachedPasswordHash(),
|
||||
name: file.name,
|
||||
path: pathInfo.path,
|
||||
source: async () => TransferProvider.provider().createBrowserFileSource(file)
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
PopoutIPCMessage
|
||||
} from "../../../ui/react-elements/external-modal/IPCMessage";
|
||||
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 {
|
||||
public readonly modalType: string;
|
||||
|
@ -26,7 +27,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
|||
|
||||
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.documentUnloadListener = () => this.destroy();
|
||||
|
|
Loading…
Reference in New Issue