diff --git a/shared/js/file/Icons.ts b/shared/js/file/Icons.ts index 9ea34319..67d2f722 100644 --- a/shared/js/file/Icons.ts +++ b/shared/js/file/Icons.ts @@ -1,6 +1,6 @@ import {Registry} from "tc-shared/events"; -export const kIPCIconChannel = "avatars"; +export const kIPCIconChannel = "icons"; export const kGlobalIconHandlerId = "global"; export interface RemoteIconEvents { @@ -22,8 +22,8 @@ export abstract class RemoteIcon { private state: RemoteIconState; - private imageUrl: string; - private errorMessage: string; + protected imageUrl: string; + protected errorMessage: string; protected constructor(serverUniqueId: string, iconId: number) { this.events = new Registry(); @@ -55,6 +55,10 @@ export abstract class RemoteIcon { this.events.fire("notify_state_changed", { newState: state, oldState: oldState }); } + hasImageUrl() : boolean { + return !!this.imageUrl; + } + /** * Will throw an string if the icon isn't in loaded state */ @@ -64,7 +68,7 @@ export abstract class RemoteIcon { } if(!this.imageUrl) { - throw tr("remote icon is missing an image url"); + throw tra("remote {} icon is missing an image url", this.iconId); } return this.imageUrl; @@ -112,6 +116,10 @@ export abstract class RemoteIcon { } export abstract class AbstractIconManager { + protected static iconUniqueKey(iconId: number, serverUniqueId: string) : string { + return "v2-" + serverUniqueId + "-" + iconId; + } + /** * @param iconId The requested icon * @param serverUniqueId The server unique id for the icon diff --git a/shared/js/file/LocalIcons.ts b/shared/js/file/LocalIcons.ts index 48b83f93..67864698 100644 --- a/shared/js/file/LocalIcons.ts +++ b/shared/js/file/LocalIcons.ts @@ -1,7 +1,7 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import {ImageCache, ImageType, imageType2MediaType, responseImageType} from "tc-shared/file/ImageCache"; -import {AbstractIconManager, RemoteIcon, RemoteIconState, setIconManager} from "tc-shared/file/Icons"; +import {AbstractIconManager, kIPCIconChannel, RemoteIcon, RemoteIconState, setIconManager} from "tc-shared/file/Icons"; import * as log from "tc-shared/log"; import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log"; import {server_connections} from "tc-shared/ConnectionManager"; @@ -10,6 +10,9 @@ import {FileTransferState, ResponseTransferTarget, TransferProvider, TransferTar import {tr} from "tc-shared/i18n/localize"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; +import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC"; +import * as ipc from "tc-shared/ipc/BrowserIPC"; +import {kIPCAvatarChannel} from "tc-shared/file/Avatars"; /* TODO: Retry icon download after some time */ /* TODO: Download icon when we're connected to the server were we want the icon from and update the icon */ @@ -38,6 +41,13 @@ class LocalRemoteIcon extends RemoteIcon { super(serverUniqueId, iconId); } + destroy() { + super.destroy(); + if(this.imageUrl && "revokeObjectURL" in URL) { + URL.revokeObjectURL(this.imageUrl); + } + } + public setImageUrl(url: string) { super.setImageUrl(url); } @@ -55,14 +65,14 @@ export let localIconCache: ImageCache; class IconManager extends AbstractIconManager { private cachedIcons: {[key: string]: LocalRemoteIcon} = {}; private connectionStateChangeListener: {[key: string]: (handlerId: string, event: ConnectionEvents["notify_connection_state_changed"]) => void} = {}; - - private static iconUniqueKey(iconId: number, serverUniqueId: string) : string { - return "v2-" + serverUniqueId + "-" + iconId; - } + private ipcChannel: IPCChannel; constructor() { super(); + this.ipcChannel = ipc.getInstance().createChannel(undefined, kIPCIconChannel); + this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); + server_connections.events().on("notify_handler_created", event => { this.connectionStateChangeListener[event.handlerId] = this.handleHandlerStateChange.bind(this, event.handlerId); event.handler.events().on("notify_connection_state_changed", this.connectionStateChangeListener[event.handlerId] as any); @@ -76,6 +86,13 @@ class IconManager extends AbstractIconManager { }); } + destroy() { + Object.values(this.cachedIcons).forEach(icon => icon.destroy()); + this.cachedIcons = {}; + + /* TODO: Unregister server handler events */ + } + private handleHandlerStateChange(handlerId: string, event: ConnectionEvents["notify_connection_state_changed"]) { const connection = server_connections.findConnection(handlerId); if(!connection) { @@ -99,6 +116,40 @@ class IconManager extends AbstractIconManager { }); } + private handleIconStateChanged(icon: RemoteIcon) { + this.sendIconStateChange(icon); + } + + private sendIconStateChange(icon: RemoteIcon, remoteId?: string) { + let data = {} as any; + + data.iconUniqueId = IconManager.iconUniqueKey(icon.iconId, icon.serverUniqueId); + data.status = icon.getState(); + + switch (icon.getState()) { + case "loaded": + data.url = icon.hasImageUrl() ? icon.getImageUrl() : undefined; + break; + + case "error": + data.errorMessage = icon.getErrorMessage(); + break; + } + + this.ipcChannel.sendMessage("notify-icon-status", data, remoteId); + } + + + private handleIpcMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) { + if(broadcast) { return; } + if(message.type === "initialize") { + this.ipcChannel.sendMessage("initialized", {}, remoteId); + return; + } else if(message.type === "icon-resolve") { + this.sendIconStateChange(this.resolveIcon(message.data.iconId, message.data.serverUniqueId, message.data.handlerId), remoteId); + } + } + resolveIcon(iconId: number, serverUniqueId: string, handlerIdHint: string): RemoteIcon { /* just to ensure */ iconId = iconId >>> 0; @@ -111,6 +162,8 @@ class IconManager extends AbstractIconManager { let icon = new LocalRemoteIcon(serverUniqueId, iconId); this.cachedIcons[iconUniqueId] = icon; + icon.events.on("notify_state_changed", () => this.handleIconStateChanged(icon)); + if(iconId >= 0 && iconId <= 1000) { icon.setState("loaded"); } else { diff --git a/shared/js/file/RemoteIcons.ts b/shared/js/file/RemoteIcons.ts new file mode 100644 index 00000000..2ba3eb0e --- /dev/null +++ b/shared/js/file/RemoteIcons.ts @@ -0,0 +1,126 @@ +import {AbstractIconManager, kIPCIconChannel, RemoteIcon, RemoteIconState, setIconManager} from "tc-shared/file/Icons"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC"; +import * as ipc from "tc-shared/ipc/BrowserIPC"; +import {Settings} from "tc-shared/settings"; +import {LogCategory, logWarn} from "tc-shared/log"; + +class RemoteRemoteIcon extends RemoteIcon { + constructor(serverUniqueId: string, iconId: number) { + super(serverUniqueId, iconId); + } + + public setState(state: RemoteIconState) { + super.setState(state); + } + + public setErrorMessage(message: string) { + super.setErrorMessage(message); + } + + public setImageUrl(url: string) { + super.setImageUrl(url); + } +} + +class RemoteIconManager extends AbstractIconManager { + private readonly ipcChannel: IPCChannel; + private callbackInitialized: () => void; + + private cachedIcons: {[key: string]: RemoteRemoteIcon} = {}; + + constructor() { + super(); + + this.ipcChannel = ipc.getInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCIconChannel); + this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); + } + + async initialize() { + this.ipcChannel.sendMessage("initialize", {}); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.callbackInitialized = undefined; + reject(tr("initialize timeout")); + }, 5000); + + this.callbackInitialized = () => { + clearTimeout(timeout); + resolve(); + } + }); + } + + resolveIcon(iconId: number, serverUniqueId: string, handlerId?: string): RemoteIcon { + iconId = iconId >>> 0; + + const uniqueId = RemoteIconManager.iconUniqueKey(iconId, serverUniqueId); + if(this.cachedIcons[uniqueId]) { return this.cachedIcons[uniqueId]; } + + const icon = new RemoteRemoteIcon(serverUniqueId, iconId); + this.cachedIcons[uniqueId] = icon; + + this.ipcChannel.sendMessage("icon-resolve", { + iconId: iconId, + serverUniqueId: serverUniqueId, + handlerId: handlerId + }); + return icon; + } + + + private handleIpcMessage(_remoteId: string, broadcast: boolean, message: ChannelMessage) { + if(!broadcast) { + if(message.type === "initialized") { + if(this.callbackInitialized) + this.callbackInitialized(); + } + } + + if(message.type === "notify-icon-status") { + const icon = this.cachedIcons[message.data.iconUniqueId]; + if(!icon) { return; } + + switch (message.data.status as RemoteIconState) { + case "destroyed": + delete this.cachedIcons[message.data.iconUniqueId]; + icon.destroy(); + break; + + case "empty": + icon.setState("empty"); + break; + + case "error": + icon.setErrorMessage(message.data.errorMessage); + icon.setState("error"); + break; + + case "loaded": + icon.setImageUrl(message.data.url); + icon.setState("loaded"); + break; + + case "loading": + icon.setState("loading"); + break; + + default: + logWarn(LogCategory.FILE_TRANSFER, tr("Received remote icon state change with an unknown state %s"), message.data.state); + break; + } + } + } +} + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 10, + name: "IPC icon init", + function: async () => { + let instance = new RemoteIconManager(); + await instance.initialize(); + setIconManager(instance); + } +}); \ No newline at end of file