Skip to content

Commit

Permalink
Fix error when setting custom tray icon
Browse files Browse the repository at this point in the history
  • Loading branch information
zcbenz committed Aug 20, 2023
1 parent e95dc60 commit a151e7f
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"eslint": "8.36.0",
"fontello-cli": "0.6.2",
"mocha": "10.2.0",
"tempy": "1.0.1",
"ts-node": "10.9.1",
"typescript": "5.0.2",
"yackage": "0.9.x"
Expand Down
18 changes: 13 additions & 5 deletions src/model/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ export default class Assistant {
this.id = id;
this.service = service;
this.viewClass = viewClass;
if (service.icon)
// When icon is from somewhere not managed, copy it to users dir.
if (service.icon &&
!service.icon.filePath.startsWith(Icon.builtinIconsPath) &&
!service.icon.filePath.startsWith(Icon.userIconsPath)) {
service.icon = this.#copyIcon(service.icon);
}
}

destructor() {
Expand All @@ -38,6 +42,10 @@ export default class Assistant {
}

setIcon(icon: Icon) {
if (!(icon instanceof Icon))
throw new Error('Must pass Icon to the setIcon method.');
if (this.service.icon?.filePath == icon.filePath)
return;
this.#removeIcon(this.service.icon);
this.service.setIcon(this.#copyIcon(icon));
}
Expand All @@ -55,7 +63,8 @@ export default class Assistant {
}

setTrayIcon(trayIcon: Icon | null) {
if (this.trayIcon == trayIcon) // ignore when icon is not changed
if (this.trayIcon == trayIcon ||
this.trayIcon?.filePath == trayIcon?.filePath)
return;
if (this.tray) { // remove existing tray
this.tray.remove();
Expand Down Expand Up @@ -113,8 +122,7 @@ export default class Assistant {

// If the icon file is located outside app's bundle, copy it to user data dir.
#copyIcon(icon: Icon) {
if (icon.filePath.startsWith(Icon.builtinIconsPath) ||
icon.filePath.startsWith(Icon.userIconsPath))
if (icon.filePath.startsWith(Icon.builtinIconsPath))
return icon;
const filename = crypto.randomUUID() + path.extname(icon.filePath);
const filePath = path.join(Icon.userIconsPath, filename);
Expand All @@ -125,6 +133,6 @@ export default class Assistant {
// If the icon file is managed by us, remove it.
#removeIcon(icon: Icon) {
if (icon.filePath.startsWith(Icon.userIconsPath))
fs.remove(icon.filePath);
fs.removeSync(icon.filePath);
}
}
6 changes: 3 additions & 3 deletions src/model/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export default class Icon {
const u = new URL(options.chieURL);
if (u.host == 'app-file')
this.filePath = path.join(__dirname, `../../${u.pathname}`);
else if (u.host == 'user-file')
this.filePath = path.join(config.dir, u.pathname);
else if (u.host == 'user-icon')
this.filePath = path.join(Icon.userIconsPath, u.pathname);
else
throw new Error(`Invalid chie URL: ${options.chieURL}`);
this.#chieURL = options.chieURL;
Expand All @@ -41,7 +41,7 @@ export default class Icon {
if (this.filePath.startsWith(Icon.builtinIconsPath))
this.#chieURL = 'chie://app-file/assets/icons' + this.filePath.substr(Icon.builtinIconsPath.length).replaceAll('\\', '/');
else if (this.filePath.startsWith(Icon.userIconsPath))
this.#chieURL = 'chie://user-file/icons' + this.filePath.substr(Icon.userIconsPath.length).replaceAll('\\', '/');
this.#chieURL = 'chie://user-icon' + this.filePath.substr(Icon.userIconsPath.length).replaceAll('\\', '/');
else
throw new Error('Can not convert arbitrary file path to chie url.');
}
Expand Down
7 changes: 5 additions & 2 deletions src/util/object-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ export function deepAssign<T extends AnyObject, S extends AnyObject>(target: T,
const value = source[key];
if (typeof value == 'object' && value !== null) {
// eslint-disable-next-line no-prototype-builtins
if (target.hasOwnProperty(key))
if (target.hasOwnProperty(key) &&
typeof target[key as keyof T] == 'object' &&
target[key as keyof T] !== null) {
deepAssign(target[key], value);
else
} else {
target[key as keyof T] = value;
}
} else {
target[key as keyof T] = value;
}
Expand Down
8 changes: 4 additions & 4 deletions src/view/messages-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {realpathSync} from 'node:fs';

import BaseChatService from '../model/base-chat-service';
import BrowserView, {style} from '../view/browser-view';
import Icon from '../model/icon';
import StreamedMarkdown, {escapeText, highlightCode} from '../util/streamed-markdown';
import basicStyle from '../view/basic-style';
import toolManager from '../controller/tool-manager';
import {ChatRole, ChatMessage, ChatLink, ChatStep} from '../model/chat-api';
import {config} from '../controller/configs';

const actionsMap = {
refresh: {
Expand Down Expand Up @@ -180,9 +180,9 @@ gui.Browser.registerProtocol('chie', (url) => {
// Load file inside app bundle.
const p = realpathSync(`${__dirname}/../..${u.pathname}`);
return gui.ProtocolFileJob.create(p);
} else if (u.host == 'user-file') {
// Load file inside user data dir.
return gui.ProtocolFileJob.create(path.join(config.dir, u.pathname));
} else if (u.host == 'user-icon') {
// Load icons inside user data dir.
return gui.ProtocolFileJob.create(path.join(Icon.userIconsPath, u.pathname));
} else if (u.host == 'chat') {
// Recieve chat service from URL.
const [, chatServiceId, title] = u.pathname.split('/');
Expand Down
57 changes: 57 additions & 0 deletions test/assistant-manager-test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import fs from 'fs-extra';
import path from 'path';
import {assert} from 'chai';
import {directory} from 'tempy';

import APICredential from '../src/model/api-credential';
import ChatView from '../src/view/chat-view';
import ChatService from '../src/model/chat-service';
import Icon from '../src/model/icon';
import apiManager from '../src/controller/api-manager';
import {AssistantManager} from '../src/controller/assistant-manager';

describe('AssistantManager', () => {
let assistantManager: AssistantManager;
let currentUserIconsPath: string;
const defaultUserIconsPath = Icon.userIconsPath;
const fixtureIcon = path.resolve(__dirname, '../node_modules/yackage/resources/icon.png');

beforeEach(() => {
assistantManager = new AssistantManager();
Icon.userIconsPath = currentUserIconsPath = directory();
});

afterEach(() => {
Icon.userIconsPath = defaultUserIconsPath;
fs.removeSync(currentUserIconsPath);
});

it('createAssistant checks API compatibility', () => {
Expand All @@ -37,4 +50,48 @@ describe('AssistantManager', () => {
delete (assistantManager.getAssistants()[0].service as ChatService).id;
assert.deepEqual(assistant, assistantManager.getAssistants()[0]);
});

it('does not copy icon for builtin icons', () => {
const assistant = assistantManager.createAssistant('TestChat', 'ChatService', apiManager.getCredentials()[0], ChatView);
assert.isOk(assistant.service.icon.filePath.startsWith(Icon.builtinIconsPath));
assert.isEmpty(fs.readdirSync(currentUserIconsPath));
});

it('copy icon for user icons', async () => {
const options = {icon: new Icon({filePath: fixtureIcon})};
const assistant = assistantManager.createAssistant('TestChat', 'ChatService', apiManager.getCredentials()[0], ChatView, options);
assert.isOk(assistant.service.icon.filePath.startsWith(currentUserIconsPath));
assert.isNotEmpty(fs.readdirSync(currentUserIconsPath));
assistant.setIcon(new Icon({name: 'bot'}));
assert.isEmpty(fs.readdirSync(currentUserIconsPath));
});

it('create tray icon for builtin icons', async () => {
const assistant = assistantManager.createAssistant('TestChat', 'ChatService', apiManager.getCredentials()[0], ChatView);
assistant.setTrayIcon(assistant.service.icon);
assert.isOk(assistant.trayIcon.filePath.startsWith(currentUserIconsPath));
assert.isNotEmpty(fs.readdirSync(currentUserIconsPath));
assistant.setTrayIcon(null);
assert.isEmpty(fs.readdirSync(currentUserIconsPath));
});

it('create tray icon for user icons', async () => {
const options = {icon: new Icon({filePath: fixtureIcon})};
const assistant = assistantManager.createAssistant('TestChat', 'ChatService', apiManager.getCredentials()[0], ChatView, options);
assistant.setTrayIcon(assistant.service.icon);
assert.isOk(assistant.trayIcon.filePath.startsWith(currentUserIconsPath));
assert.equal(fs.readdirSync(currentUserIconsPath).length, 2);
assistant.setTrayIcon(null);
assert.equal(fs.readdirSync(currentUserIconsPath).length, 1);
});

it('serialize and restore tray icons', async () => {
const assistant = assistantManager.createAssistant('TestChat', 'ChatService', apiManager.getCredentials()[0], ChatView);
assistant.setTrayIcon(new Icon({filePath: fixtureIcon}));
assert.equal(fs.readdirSync(currentUserIconsPath).length, 1);
assistantManager.deserialize(assistantManager.serialize());
const newAssistant = assistantManager.getAssistants()[0];
assert.deepEqual(assistant.trayIcon, newAssistant.trayIcon);
assert.equal(fs.readdirSync(currentUserIconsPath).length, 1);
});
});

0 comments on commit a151e7f

Please sign in to comment.