diff --git a/src/public/images/tray/darwin/offline.png b/src/public/images/tray/darwin/offline.png new file mode 100644 index 0000000000..fc5d17596f Binary files /dev/null and b/src/public/images/tray/darwin/offline.png differ diff --git a/src/public/images/tray/darwin/offline@2x.png b/src/public/images/tray/darwin/offline@2x.png new file mode 100644 index 0000000000..8d75341c2c Binary files /dev/null and b/src/public/images/tray/darwin/offline@2x.png differ diff --git a/src/public/images/tray/linux/offline.png b/src/public/images/tray/linux/offline.png new file mode 100644 index 0000000000..c12af62be2 Binary files /dev/null and b/src/public/images/tray/linux/offline.png differ diff --git a/src/public/images/tray/linux/offline@2x.png b/src/public/images/tray/linux/offline@2x.png new file mode 100644 index 0000000000..d377410bed Binary files /dev/null and b/src/public/images/tray/linux/offline@2x.png differ diff --git a/src/public/images/tray/win32/offline.ico b/src/public/images/tray/win32/offline.ico new file mode 100644 index 0000000000..c94256880c Binary files /dev/null and b/src/public/images/tray/win32/offline.ico differ diff --git a/src/ui/main/icons.ts b/src/ui/main/icons.ts index 19b809d78c..d213141e60 100644 --- a/src/ui/main/icons.ts +++ b/src/ui/main/icons.ts @@ -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') || @@ -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') || @@ -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})`); diff --git a/src/ui/main/rootWindow.ts b/src/ui/main/rootWindow.ts index bdc40ca3f3..e2689d6599 100644 --- a/src/ui/main/rootWindow.ts +++ b/src/ui/main/rootWindow.ts @@ -438,6 +438,7 @@ export const setupRootWindow = (): void => { getTrayIconPath({ platform: process.platform, badge: globalBadge, + isLoggedIn: true, }) ) ); diff --git a/src/ui/main/trayIcon.ts b/src/ui/main/trayIcon.ts index 2baca3d90e..e840cc8b61 100644 --- a/src/ui/main/trayIcon.ts +++ b/src/ui/main/trayIcon.ts @@ -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'; @@ -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, }); trayIcon.setImage(nativeImage.createFromPath(image)); }; @@ -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( @@ -175,6 +207,7 @@ const manageTrayIcon = async (): Promise<() => void> => { return () => { unwatchGlobalBadge(); + unwatchUserLoggedIn(); unwatchIsRootWindowVisible(); trayIcon.destroy(); };