343 lines
No EOL
13 KiB
TypeScript
343 lines
No EOL
13 KiB
TypeScript
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
|
import {Registry} from "tc-events";
|
|
import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/modal/avatar-upload/Definitions";
|
|
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
|
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
|
import {server_connections} from "tc-shared/ConnectionManager";
|
|
import PermissionType from "tc-shared/permission/PermissionType";
|
|
import {getOwnAvatarStorage, LocalAvatarInfo} from "tc-shared/file/OwnAvatarStorage";
|
|
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
|
|
import {Mutex} from "tc-shared/Mutex";
|
|
import {tr, tra, trJQuery} from "tc-shared/i18n/localize";
|
|
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
|
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
|
import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer";
|
|
|
|
class Controller {
|
|
readonly connection: ConnectionHandler;
|
|
readonly serverUniqueId: string;
|
|
readonly events: Registry<ModalAvatarUploadEvents>;
|
|
readonly variables: IpcUiVariableProvider<ModalAvatarUploadVariables>;
|
|
|
|
private registeredListener: (() => void)[];
|
|
|
|
private rendererUploading = false;
|
|
private controllerLoading = false;
|
|
private serverAvatarLoading = false;
|
|
|
|
private avatarInfo: LocalAvatarInfo | undefined;
|
|
|
|
/* TODO: Update the UI so the client can't upload multiple avatars at the same time (maybe even server wide?) */
|
|
private serverAvatarMutex: Mutex<void>;
|
|
private serverAvatarUrl: string;
|
|
|
|
constructor(connection: ConnectionHandler) {
|
|
this.connection = connection;
|
|
this.serverUniqueId = connection.getCurrentServerUniqueId();
|
|
this.registeredListener = [];
|
|
this.serverAvatarMutex = new Mutex<void>(void 0);
|
|
|
|
this.events = new Registry<ModalAvatarUploadEvents>();
|
|
this.variables = new IpcUiVariableProvider<ModalAvatarUploadVariables>();
|
|
|
|
this.variables.setVariableProvider("maxAvatarSize", () => {
|
|
const permission = this.connection.permissions.neededPermission(PermissionType.I_CLIENT_MAX_AVATAR_FILESIZE);
|
|
return permission.valueOr(-1);
|
|
});
|
|
|
|
this.variables.setVariableProvider("currentAvatar", () => {
|
|
if(this.rendererUploading || this.controllerLoading || this.serverAvatarLoading) {
|
|
return { status: "loading" };
|
|
}
|
|
|
|
if(this.avatarInfo) {
|
|
const maxSize = this.variables.getVariableSync("maxAvatarSize");
|
|
return {
|
|
status: maxSize >= 0 && maxSize < this.avatarInfo.fileSize ? "exceeds-max-size" : "available",
|
|
|
|
fileHashMD5: this.avatarInfo.fileHashMD5,
|
|
fileName: this.avatarInfo.fileName,
|
|
fileSize: this.avatarInfo.fileSize,
|
|
|
|
resourceUrl: this.avatarInfo.resourceUrl,
|
|
|
|
serverHasAvatar: this.serverAvatarUrl !== undefined
|
|
};
|
|
} else if(this.serverAvatarUrl) {
|
|
return { status: "server", resourceUrl: this.serverAvatarUrl };
|
|
} else {
|
|
return { status: "unset" };
|
|
}
|
|
});
|
|
|
|
this.registeredListener.push(this.connection.permissions.register_needed_permission(PermissionType.I_CLIENT_MAX_AVATAR_FILESIZE, () => {
|
|
this.variables.sendVariable("maxAvatarSize");
|
|
this.variables.sendVariable("currentAvatar");
|
|
}));
|
|
|
|
this.events.on("action_file_cache_loading", () => {
|
|
this.rendererUploading = true;
|
|
this.variables.sendVariable("currentAvatar");
|
|
});
|
|
|
|
this.events.on("action_file_cache_loading_finished", event => {
|
|
this.rendererUploading = false;
|
|
if(!event.success) {
|
|
/* Failed to update local avatar. Send the last one. */
|
|
this.variables.sendVariable("currentAvatar");
|
|
return;
|
|
}
|
|
|
|
this.loadLocalAvatar();
|
|
});
|
|
|
|
this.loadServerAvatar();
|
|
}
|
|
|
|
destroy() {
|
|
this.registeredListener.forEach(callback => callback());
|
|
this.registeredListener = [];
|
|
|
|
this.events.destroy();
|
|
this.variables.destroy();
|
|
|
|
this.setAvatarInfo(undefined);
|
|
|
|
this.serverAvatarMutex.execute(async () => {
|
|
/* Cleanup the cache so the next upload will be fresh */
|
|
await getOwnAvatarStorage().removeAvatar(this.serverUniqueId, "uploading");
|
|
}).then(undefined);
|
|
}
|
|
|
|
private loadLocalAvatar() {
|
|
if(this.controllerLoading) {
|
|
return;
|
|
}
|
|
|
|
this.loadLocalAvatar0().catch(error => {
|
|
logError(LogCategory.GENERAL, tr("Failed to load local cached avatar: %o"), error);
|
|
this.events.fire("notify_avatar_load_error", { error: tr("Failed to load local cached avatar") });
|
|
}).then(() => {
|
|
this.controllerLoading = false;
|
|
this.variables.sendVariable("currentAvatar");
|
|
});
|
|
}
|
|
|
|
private async loadLocalAvatar0() {
|
|
const result = await getOwnAvatarStorage().loadAvatar(this.serverUniqueId, "uploading", true);
|
|
let info: LocalAvatarInfo;
|
|
switch (result.status) {
|
|
case "success":
|
|
info = result.result;
|
|
break;
|
|
|
|
case "error":
|
|
this.events.fire("notify_avatar_load_error", {error: result.reason});
|
|
return;
|
|
|
|
case "cache-unavailable":
|
|
this.events.fire("notify_avatar_load_error", {error: tr("Avatar cache unavailable")});
|
|
return;
|
|
|
|
case "empty-result":
|
|
this.setAvatarInfo(undefined);
|
|
return;
|
|
|
|
default:
|
|
throw tr("invalid state");
|
|
}
|
|
|
|
this.setAvatarInfo(info);
|
|
}
|
|
|
|
private loadServerAvatar() {
|
|
if(this.serverAvatarLoading) {
|
|
return;
|
|
}
|
|
|
|
this.serverAvatarUrl = undefined;
|
|
this.loadServerAvatar0().catch(error => {
|
|
logError(LogCategory.GENERAL, tr("Failed to load server avatar: %o"), error);
|
|
}).then(() => {
|
|
this.serverAvatarLoading = false;
|
|
this.variables.sendVariable("currentAvatar");
|
|
})
|
|
}
|
|
|
|
private async loadServerAvatar0() {
|
|
const ownClientAvatar = this.connection.fileManager.avatars.resolveAvatar(this.connection.getClient().avatarId());
|
|
await ownClientAvatar.awaitLoaded(5000);
|
|
if(ownClientAvatar.getState() !== "loaded") {
|
|
return;
|
|
}
|
|
|
|
this.serverAvatarUrl = ownClientAvatar.getTypedStateData("loaded").url;
|
|
}
|
|
|
|
/**
|
|
* Note: This will not trigger the "currentAvatar" variable resend!
|
|
* @param newInfo
|
|
* @private
|
|
*/
|
|
private setAvatarInfo(newInfo: LocalAvatarInfo) {
|
|
if(this.avatarInfo?.resourceUrl) {
|
|
URL.revokeObjectURL(this.avatarInfo.resourceUrl);
|
|
}
|
|
|
|
this.avatarInfo = newInfo;
|
|
}
|
|
|
|
resetAvatar() {
|
|
this.serverAvatarMutex.execute(async () => {
|
|
this.setAvatarInfo(undefined);
|
|
await getOwnAvatarStorage().removeAvatar(this.serverUniqueId, "uploading");
|
|
|
|
const serverConnection = this.connection.serverConnection;
|
|
if(!serverConnection.connected()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await serverConnection.send_command('ftdeletefile', {
|
|
name: "/avatar_", /* delete own avatar */
|
|
path: "",
|
|
cid: 0
|
|
});
|
|
|
|
createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open();
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to reset avatar flag: %o"), error);
|
|
|
|
let message;
|
|
if(error instanceof CommandResult) {
|
|
message = formatMessage(tr("Failed to delete avatar.\nError: {0}"), error.formattedMessage());
|
|
}
|
|
|
|
if(!message) {
|
|
message = formatMessage(tr("Failed to delete avatar.\nLookup the console for more details"));
|
|
}
|
|
|
|
createErrorModal(tr("Failed to delete avatar"), message).open();
|
|
return;
|
|
}
|
|
|
|
this.loadServerAvatar();
|
|
});
|
|
}
|
|
|
|
uploadAvatar() {
|
|
/* copy the avatar info */
|
|
const avatarInfo = this.avatarInfo;
|
|
this.serverAvatarMutex.execute(async() => {
|
|
const serverConnection = this.connection.serverConnection;
|
|
if(!serverConnection.connected()) {
|
|
return;
|
|
}
|
|
|
|
if(!avatarInfo) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logInfo(LogCategory.CLIENT, tr("Uploading new avatar"));
|
|
const loadResult = await getOwnAvatarStorage().loadAvatarImage(this.serverUniqueId, "uploading");
|
|
if (loadResult.status !== "success") {
|
|
logError(LogCategory.GENERAL, tr("Failed to load cached avatar image: %o"), loadResult);
|
|
throw tr("failed to load avatar image");
|
|
}
|
|
|
|
const transfer = this.connection.fileManager.initializeFileUpload({
|
|
name: "/avatar",
|
|
path: "",
|
|
|
|
channel: 0,
|
|
channelPassword: undefined,
|
|
|
|
source: async () => await TransferProvider.provider().createBufferSource(loadResult.result)
|
|
});
|
|
|
|
await transfer.awaitFinished();
|
|
|
|
if (transfer.transferState() !== FileTransferState.FINISHED) {
|
|
if (transfer.transferState() === FileTransferState.ERRORED) {
|
|
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to upload clients avatar: %o"), transfer.currentError());
|
|
createErrorModal(tr("Failed to upload avatar"), tra("Failed to upload avatar:\n{0}", transfer.currentErrorMessage())).open();
|
|
return;
|
|
} else if (transfer.transferState() === FileTransferState.CANCELED) {
|
|
createErrorModal(tr("Failed to upload avatar"), tr("Your avatar upload has been canceled.")).open();
|
|
return;
|
|
} else {
|
|
createErrorModal(tr("Failed to upload avatar"), tr("Avatar upload finished with an unknown finished state.")).open();
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to upload avatar: %o"), error);
|
|
createErrorModal(tr("Failed to upload avatar"), tr("Avatar upload failed. Lookup the console for more details.")).open();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.connection.serverConnection.send_command('clientupdate', {
|
|
client_flag_avatar: avatarInfo.fileHashMD5
|
|
});
|
|
} catch(error) {
|
|
logError(LogCategory.GENERAL, tr("Failed to update avatar flag: %o"), error);
|
|
|
|
let message;
|
|
if(error instanceof CommandResult) {
|
|
message = formatMessage(tr("Failed to update avatar flag.\nError: {0}"), error.formattedMessage());
|
|
}
|
|
|
|
if(!message) {
|
|
message = formatMessage(tr("Failed to update avatar flag.\nLookup the console for more details"));
|
|
}
|
|
|
|
createErrorModal(tr("Failed to set avatar"), message).open();
|
|
return;
|
|
}
|
|
|
|
createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open();
|
|
this.loadServerAvatar();
|
|
});
|
|
}
|
|
}
|
|
|
|
export function spawnAvatarUpload(connection: ConnectionHandler) {
|
|
const controller = new Controller(connection);
|
|
|
|
const modal = spawnModal("modal-avatar-upload", [
|
|
controller.events.generateIpcDescription(),
|
|
controller.variables.generateConsumerDescription(),
|
|
connection.getCurrentServerUniqueId()
|
|
], {
|
|
popoutable: true
|
|
});
|
|
|
|
controller.events.on("action_avatar_upload", event => {
|
|
controller.uploadAvatar();
|
|
if(event.closeWindow) {
|
|
modal.destroy();
|
|
}
|
|
});
|
|
|
|
controller.events.on("action_avatar_delete", event => {
|
|
controller.resetAvatar();
|
|
if(event.closeWindow) {
|
|
modal.destroy();
|
|
}
|
|
});
|
|
|
|
modal.getEvents().on("destroy", () => controller.destroy());
|
|
modal.getEvents().on("destroy", connection.events().on("notify_connection_state_changed", event => {
|
|
if(event.newState !== ConnectionState.CONNECTED) {
|
|
modal.destroy();
|
|
}
|
|
}));
|
|
|
|
modal.show().then(undefined);
|
|
|
|
/* Trying to prompt the user */
|
|
controller.events.fire("action_open_select");
|
|
} |