Files
ChatApp/apps/desktop/src/lib/trayBadge.ts
T
byGalax f9e340dbec feat(branding): Netralax rebrand + Discord-style taskbar unread badge
App productName becomes Netralax (driving exe name and window title);
existing installs keep their %APPDATA%\ChatApp profile via an explicit
app.setPath('userData', appData/ChatApp) so no Login/Sounds/Secret store
data is lost.

The Windows taskbar overlay now renders a red bubble with the actual
unread count (Discord parity) instead of just a static red dot. Renderer
paints a 64×64 PNG via canvas — full-bleed red circle, white bold count
with a "99+" cap, no outer ring — and passes the data URL through the
existing setTrayUnread IPC. Main decodes via nativeImage and applies it
as the BrowserWindow overlay icon. Falls back to the static dot if the
renderer canvas pipeline is unavailable.

Also: app.setName('Netralax') + setAppUserModelId('cloud.netralax.desktop')
for Windows taskbar grouping and notification source attribution, and
release.mjs now reads productName dynamically from package.json so the
artifact lookup stays correct after the rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:30:54 +02:00

64 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isTauriRuntime } from './globalShortcut';
// Pushes the current aggregate unread count to main, which updates the
// Tray tooltip and (on Windows) the taskbar overlay icon. We render the
// badge here in the renderer because main has no Canvas2D — painting a
// red bubble with the count, encoding to PNG, and handing the buffer
// to main keeps the implementation free of a native canvas dependency.
//
// No-op outside the Electron runtime (e.g. browser dev preview) so no
// guards needed at call-sites.
export async function updateTrayUnread(count: number): Promise<void> {
if (!isTauriRuntime()) return;
const n = Math.max(0, Math.floor(count));
const badgeDataUrl = n > 0 ? renderBadgePng(n) : null;
try {
await window.electronAPI.setTrayUnread(n, badgeDataUrl);
} catch (err: unknown) {
console.warn('updateTrayUnread failed', err);
}
}
// Discord-style red bubble with white count.
//
// Source is rendered at 64×64 so Windows' high-quality downsample to the
// 16×16 taskbar overlay slot retains crisp edges (4× supersampling).
// Earlier 32×32 + 2px white ring blurred badly: the ring became a half
// pixel at the target size, and the bigger font fell into AA mush. Now
// no outer ring (Discord doesn't use one either) and a full-bleed circle.
function renderBadgePng(count: number): string | null {
if (typeof document === 'undefined') return null;
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// Solid red bubble, full bleed.
const center = size / 2;
ctx.beginPath();
ctx.arc(center, center, center, 0, Math.PI * 2);
ctx.fillStyle = '#ef4444';
ctx.fill();
// Count label — Discord parity: cap at 99 with a "+" once we cross it.
// Sizes are tuned per glyph count so each variant fills the bubble
// without clipping when Windows downsamples to 16×16.
const label = count > 99 ? '99+' : String(count);
const fontPx = label.length >= 3 ? 30 : label.length === 2 ? 40 : 48;
ctx.fillStyle = '#fff';
// Segoe UI is the Windows system font; explicit weight 800 keeps the
// glyph chunky after downsampling. Fallbacks cover macOS/Linux.
ctx.font = `800 ${fontPx}px "Segoe UI", system-ui, -apple-system, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// +2 vertical nudge: most system fonts render numerals visually high
// relative to the baseline mid-point; the offset re-centres them.
ctx.fillText(label, center, center + 2);
return canvas.toDataURL('image/png');
}