Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 123 additions & 36 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ global.__dirname = path.dirname(__filename);
global.__filename = __filename;

import electron from "electron";
const { BrowserWindow, app, shell, Notification, protocol, net } = electron;
const { BrowserWindow, app, shell, Notification, Tray, Menu, nativeImage, protocol, net } = electron;
import {
getLanguagePreference,
getWindowBounds,
Expand Down Expand Up @@ -70,7 +70,7 @@ let pendingDeepLinkUrl: string | null = null;
* Supported routes:
* chatons://extensions/install/<npm-package-id>
* chatons://cloud/connect?base_url=...
* chatons://cloud/auth/callback?...
* chatons://cloud/auth/callback?...
*/
function handleDeepLink(url: string) {
const win = getMainWindow();
Expand Down Expand Up @@ -137,33 +137,91 @@ const appIconPath = path.join(__dirname, "../build/icons/icon.png");
// Variable to keep track of the main window
let mainWindow: electron.BrowserWindow | null = null;
let isQuitting = false;
let tray: electron.Tray | null = null;
let windowIpcRegistered = false;

function createSystemTray() {
if (tray || process.platform === "darwin") {
return;
}

const iconPath = path.join(__dirname, "../build/icons/icon.png");
let trayIcon: electron.NativeImage;

try {
trayIcon = nativeImage.createFromPath(iconPath);
if (trayIcon.isEmpty()) {
trayIcon = nativeImage.createEmpty();
}
} catch {
trayIcon = nativeImage.createEmpty();
}

trayIcon = trayIcon.resize({ width: 16, height: 16 });

tray = new Tray(trayIcon);
tray.setToolTip("Chatons");

const contextMenu = Menu.buildFromTemplate([
{
label: "Show Chatons",
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
mainWindow.focus();
}
},
},
{
type: "separator",
},
{
label: "Quit",
click: () => {
isQuitting = true;
app.quit();
},
},
]);

tray.setContextMenu(contextMenu);
tray.on("click", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}
});
}

function registerWindowIpc() {
if (windowIpcRegistered) {
return;
}

windowIpcRegistered = true;

electron.ipcMain.handle('window:isFocused', () => {
electron.ipcMain.handle("window:isFocused", () => {
return mainWindow?.isFocused() ?? false;
});

electron.ipcMain.handle('window:showNotification', (_event, title: string, body: string, conversationId?: string) => {
electron.ipcMain.handle("window:showNotification", (_event, title: string, body: string, conversationId?: string) => {
if (!mainWindow || mainWindow.isDestroyed()) return false;

if (mainWindow.isFocused()) {
return false;
}

const notification = new Notification({
title: title,
body: body,
title,
body,
icon: appIconPath,
});

notification.on('click', () => {
notification.on("click", () => {
if (!mainWindow || mainWindow.isDestroyed()) {
return;
}
Expand All @@ -173,35 +231,54 @@ function registerWindowIpc() {
mainWindow.show();
mainWindow.focus();
if (conversationId) {
mainWindow.webContents.send('desktop:notification-clicked', { conversationId });
mainWindow.webContents.send("desktop:notification-clicked", { conversationId });
}
});

notification.show();
return true;
});

electron.ipcMain.handle('window:close', () => {
electron.ipcMain.handle("window:close", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
if (process.platform !== "darwin") {
mainWindow.hide();
} else {
mainWindow.close();
}
}
});

electron.ipcMain.handle('window:minimize', () => {
electron.ipcMain.handle("window:minimize", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});

electron.ipcMain.handle('window:maximize', () => {
electron.ipcMain.handle("window:maximize", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
mainWindow.webContents.send("window:unmaximized");
} else {
mainWindow.maximize();
mainWindow.webContents.send("window:maximized");
}
}
});

electron.ipcMain.handle("window:isMaximized", () => {
return mainWindow?.isMaximized() ?? false;
});

electron.ipcMain.handle("window:quit", () => {
isQuitting = true;
app.quit();
});

electron.ipcMain.handle("window:showTrayMenu", () => {
tray?.popUpContextMenu();
});
}

const isTelemetryEnabled = () => {
Expand Down Expand Up @@ -262,16 +339,13 @@ async function createWindow() {
console.error('Error setting main window for shortcut manager:', error);
}

const indexPath = path.join(__dirname, "../../dist/index.html");
console.log(`[DEBUG] isDev=${isDev}, __dirname=${__dirname}, indexPath=${indexPath}`);
if (isDev && process.env.VITE_DEV_SERVER_URL) {
mainWindow.loadURL(
`${process.env.VITE_DEV_SERVER_URL}?language=${languagePreference}`,
);
mainWindow.webContents.openDevTools({ mode: "detach" });
} else {
console.log(`[DEBUG] Loading index.html from: ${indexPath}`);
mainWindow.loadFile(indexPath, {
mainWindow.loadFile(path.join(__dirname, "../dist/index.html"), {
query: { language: languagePreference },
});
}
Expand Down Expand Up @@ -331,7 +405,7 @@ async function createWindow() {
}

// Keep app alive when user closes the window on macOS, unless a real app quit is in progress.
if (process.platform === 'darwin' && !isQuitting) {
if (process.platform === "darwin" && !isQuitting) {
e.preventDefault();
mainWindow!.hide();
}
Expand All @@ -342,9 +416,17 @@ async function createWindow() {
mainWindow = null;
}
});

// Setup status bar after window is created
setupStatusBar(mainWindow);

// Update launch at startup setting
updateLaunchAtStartup(appSettings.launchAtStartup);

// Create system tray icon for Windows/Linux
if (process.platform !== "darwin") {
createSystemTray();
}
}

// macOS: handle chatons:// links when the app is already running
Expand Down Expand Up @@ -378,31 +460,31 @@ if (!gotTheLock) {
app.whenReady().then(async () => {
protocol.handle(EXTENSION_PROTOCOL_PREFIX, async (request) => {
try {
if (request.method === 'OPTIONS') {
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': '*',
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
});
}

const url = new URL(request.url);
const extensionId = decodeURIComponent(url.host || '');
const rawPath = decodeURIComponent(url.pathname || '/');
const relativePath = rawPath.replace(/^\/+/, '');
const extensionId = decodeURIComponent(url.host || "");
const rawPath = decodeURIComponent(url.pathname || "/");
const relativePath = rawPath.replace(/^\/+/, "");
if (!extensionId || !relativePath) {
return new Response('Not found', {
return new Response("Not found", {
status: 404,
headers: {
'Access-Control-Allow-Origin': '*',
"Access-Control-Allow-Origin": "*",
},
});
}

const { getExtensionRootCandidates } = await import('./extensions/runtime/manifest.js');
const { getExtensionRootCandidates } = await import("./extensions/runtime/manifest.js");
const roots = getExtensionRootCandidates(extensionId);
for (const root of roots) {
const candidate = path.resolve(root, relativePath);
Expand All @@ -413,10 +495,10 @@ app.whenReady().then(async () => {
const response = await net.fetch(`file://${candidate}`);
if (response.ok) {
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
headers.set('Access-Control-Allow-Headers', '*');
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
headers.set("Access-Control-Allow-Headers", "*");
headers.set("Cross-Origin-Resource-Policy", "cross-origin");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
Expand All @@ -427,18 +509,18 @@ app.whenReady().then(async () => {
// Continue trying other candidate roots.
}
}
return new Response('Not found', {
return new Response("Not found", {
status: 404,
headers: {
'Access-Control-Allow-Origin': '*',
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error('Failed to resolve extension asset request:', request.url, error);
return new Response('Bad request', {
console.error("Failed to resolve extension asset request:", request.url, error);
return new Response("Bad request", {
status: 400,
headers: {
'Access-Control-Allow-Origin': '*',
"Access-Control-Allow-Origin": "*",
},
});
}
Expand Down Expand Up @@ -606,6 +688,11 @@ app.whenReady().then(async () => {
app.on("before-quit", () => {
isQuitting = true;
void stopPiRuntimes();
// Cleanup tray icon
if (tray) {
tray.destroy();
tray = null;
}
});

app.on("window-all-closed", () => {
Expand Down
Loading
Loading