Skip to content
Binary file added src/public/images/tray/darwin/offline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/images/tray/darwin/offline@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/images/tray/linux/offline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/images/tray/linux/offline@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/images/tray/win32/offline.ico
Binary file not shown.
38 changes: 31 additions & 7 deletions src/ui/main/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,28 @@ export const getAppIconPath = ({
return `${app.getAppPath()}/app/images/icon.ico`;
};

const getMacOSTrayIconPath = (badge: Server['badge']): string =>
path.join(
const getMacOSTrayIconPath = (
badge: Server['badge'],
isLoggedIn: boolean
): string => {
if (!isLoggedIn) {
return path.join(app.getAppPath(), 'app/images/tray/darwin/offline.png');
}

return path.join(
app.getAppPath(),
`app/images/tray/darwin/${badge ? 'notification' : 'default'}Template.png`
);
};

const getWindowsTrayIconPath = (
badge: Server['badge'],
isLoggedIn: boolean
): string => {
if (!isLoggedIn) {
return path.join(app.getAppPath(), 'app/images/tray/win32/offline.ico');
}

const getWindowsTrayIconPath = (badge: Server['badge']): string => {
const name =
(!badge && 'default') ||
(badge === '•' && 'notification-dot') ||
Expand All @@ -31,7 +46,14 @@ const getWindowsTrayIconPath = (badge: Server['badge']): string => {
return path.join(app.getAppPath(), `app/images/tray/win32/${name}.ico`);
};

const getLinuxTrayIconPath = (badge: Server['badge']): string => {
const getLinuxTrayIconPath = (
badge: Server['badge'],
isLoggedIn: boolean
): string => {
if (!isLoggedIn) {
return path.join(app.getAppPath(), 'app/images/tray/linux/offline.png');
}

const name =
(!badge && 'default') ||
(badge === '•' && 'notification-dot') ||
Expand All @@ -43,19 +65,21 @@ const getLinuxTrayIconPath = (badge: Server['badge']): string => {
export const getTrayIconPath = ({
badge,
platform,
isLoggedIn = false,
}: {
badge?: Server['badge'];
platform: NodeJS.Platform;
isLoggedIn?: boolean;
}): string => {
switch (platform ?? process.platform) {
case 'darwin':
return getMacOSTrayIconPath(badge);
return getMacOSTrayIconPath(badge, isLoggedIn);

case 'win32':
return getWindowsTrayIconPath(badge);
return getWindowsTrayIconPath(badge, isLoggedIn);

case 'linux':
return getLinuxTrayIconPath(badge);
return getLinuxTrayIconPath(badge, isLoggedIn);

default:
throw Error(`unsupported platform (${platform})`);
Expand Down
1 change: 1 addition & 0 deletions src/ui/main/rootWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ export const setupRootWindow = (): void => {
getTrayIconPath({
platform: process.platform,
badge: globalBadge,
isLoggedIn: true,
})
)
);
Expand Down
35 changes: 34 additions & 1 deletion src/ui/main/trayIcon.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, Menu, nativeImage, Tray } from 'electron';
import { app, Menu, nativeImage, Tray, Notification } from 'electron';
import i18next from 'i18next';

import type { Server } from '../../servers/common';
Expand Down Expand Up @@ -63,9 +63,12 @@ const createTrayIcon = (): Tray => {
};

const updateTrayIconImage = (trayIcon: Tray, badge: Server['badge']): void => {
const servers = select(({ servers }) => servers || []);
const isLoggedIn = servers.every((server) => server.userLoggedIn);
const image = getTrayIconPath({
platform: process.platform,
badge,
isLoggedIn,
});
Comment thread
tyroneyeh marked this conversation as resolved.
trayIcon.setImage(nativeImage.createFromPath(image));
};
Expand Down Expand Up @@ -128,6 +131,35 @@ const manageTrayIcon = async (): Promise<() => void> => {
updateTrayIconToolTip(trayIcon, globalBadge);
});

const unwatchUserLoggedIn = watch(
(state: RootState) => {
const servers = state.servers || [];
return (
servers.length === 0 || servers.some((server) => !server.userLoggedIn)
);
},
async (isLoggedOut) => {
if (isLoggedOut) {
try {
const rootWindow = await getRootWindow();
if (rootWindow.isMinimized()) rootWindow.restore();
rootWindow.show();
rootWindow.focus();
} catch {
// Root window may not be ready/destroyed; continue with notification/icon update.
}
new Notification({
title: t('tray.balloon.stillRunning.title', { appName: app.name }),
body: t('error.authNeeded', { auth: '' }).replace(/<\/?strong>/g, ''),
timeoutType: 'never',
urgency: 'critical',
}).show();
}
const globalBadge = select(selectGlobalBadge);
updateTrayIconImage(trayIcon, globalBadge);
}
);

let firstTrayIconBalloonShown = false;

const unwatchIsRootWindowVisible = watch(
Expand Down Expand Up @@ -175,6 +207,7 @@ const manageTrayIcon = async (): Promise<() => void> => {

return () => {
unwatchGlobalBadge();
unwatchUserLoggedIn();
unwatchIsRootWindowVisible();
trayIcon.destroy();
};
Expand Down