diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index 4b8fbca..5170b77 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -34,6 +34,7 @@ import { AdvancedSearchPanel } from "@/components/search/advanced-search-panel";
import { isFilterEmpty } from "@/lib/jmap/search-utils";
import { WelcomeBanner } from "@/components/ui/welcome-banner";
import { NavigationRail } from "@/components/layout/navigation-rail";
+import { useFaviconBadge } from "@/hooks/use-favicon-badge";
export default function Home() {
const router = useRouter();
@@ -98,6 +99,9 @@ export default function Home() {
const contactStore = useContactStore();
+ const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
+ useFaviconBadge(inboxUnread);
+
// Keyboard shortcuts handlers
const keyboardHandlers = useMemo(() => ({
onNextEmail: () => {
diff --git a/app/favicon.ico b/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/app/favicon.ico and /dev/null differ
diff --git a/app/icon.svg b/app/icon.svg
new file mode 100644
index 0000000..0da09e0
--- /dev/null
+++ b/app/icon.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/hooks/use-favicon-badge.ts b/hooks/use-favicon-badge.ts
new file mode 100644
index 0000000..a8a4c79
--- /dev/null
+++ b/hooks/use-favicon-badge.ts
@@ -0,0 +1,120 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+const SIZE = 32;
+
+export function useFaviconBadge(count: number) {
+ const canvasRef = useRef(null);
+ const imageRef = useRef(null);
+ const originalHref = useRef(null);
+ const countRef = useRef(count);
+ countRef.current = count;
+
+ useEffect(() => {
+ const link = document.querySelector("link[rel~='icon']");
+ if (!link?.href) return;
+
+ originalHref.current = link.href;
+
+ const canvas = document.createElement("canvas");
+ canvas.width = SIZE;
+ canvas.height = SIZE;
+ canvasRef.current = canvas;
+
+ fetch(link.href)
+ .then((r) => r.blob())
+ .then((blob) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const img = new Image();
+ img.onload = () => {
+ imageRef.current = img;
+ if (countRef.current > 0) {
+ applyBadge(canvas, img, countRef.current);
+ }
+ };
+ img.src = reader.result as string;
+ };
+ reader.readAsDataURL(blob);
+ })
+ .catch(() => {
+ /* favicon not loadable — badge will draw without base icon */
+ });
+ }, []);
+
+ useEffect(() => {
+ if (count <= 0) {
+ if (originalHref.current) {
+ setFavicon(originalHref.current, "image/svg+xml");
+ }
+ return;
+ }
+
+ if (canvasRef.current) {
+ applyBadge(canvasRef.current, imageRef.current, count);
+ }
+ }, [count]);
+}
+
+function applyBadge(
+ canvas: HTMLCanvasElement,
+ image: HTMLImageElement | null,
+ count: number,
+) {
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ return;
+ }
+
+ ctx.clearRect(0, 0, SIZE, SIZE);
+
+ if (image) {
+ ctx.drawImage(image, 0, 0, SIZE, SIZE);
+ }
+
+ const label = count > 9 ? "+" : String(count);
+ const fontSize = SIZE * 0.55;
+ const padding = 2;
+ ctx.font = `bold ${fontSize}px arial,sans-serif`;
+ ctx.textAlign = "left";
+ ctx.textBaseline = "top";
+ const textWidth = ctx.measureText(label).width;
+ const badgeW = textWidth + padding * 2;
+ const badgeH = fontSize + padding;
+ const x = SIZE - badgeW - 1;
+ const y = SIZE - badgeH;
+
+ ctx.fillStyle = "#ef4444";
+ ctx.beginPath();
+ // roundRect is Baseline 2023; fall back to fillRect on Safari 15 and below.
+ if (typeof ctx.roundRect === "function") {
+ ctx.roundRect(x, y, badgeW, badgeH, 3);
+ ctx.fill();
+ } else {
+ ctx.fillRect(x, y, badgeW, badgeH);
+ }
+
+ ctx.strokeStyle = "#b91c1c";
+ ctx.lineWidth = 1;
+ ctx.stroke();
+
+ ctx.fillStyle = "#ffffff";
+ ctx.fillText(label, x + padding, y + padding / 2);
+
+ setFavicon(canvas.toDataURL("image/png"));
+}
+
+function removeCurrentFavicons() {
+ const links = document.querySelectorAll("link[rel~='icon']");
+ links.forEach((link) => link.remove());
+}
+
+export function setFavicon(href: string, type: string = "image/png") {
+ removeCurrentFavicons();
+ const link = document.createElement("link");
+ link.rel = "icon";
+ link.type = type;
+ link.href = href;
+ document.head.appendChild(link);
+}
diff --git a/stores/email-store.ts b/stores/email-store.ts
index 33f67dd..9169c84 100644
--- a/stores/email-store.ts
+++ b/stores/email-store.ts
@@ -1027,14 +1027,16 @@ export const useEmailStore = create((set, get) => ({
const accountChanges = change.changed[accountId];
if (!accountChanges) return;
- // Handle Email state changes - refresh current mailbox
+ // Handle Email state changes - refresh current mailbox and mailbox counts
if (accountChanges.Email) {
await get().refreshCurrentMailbox(client);
get().fetchTagCounts(client);
+ get().fetchMailboxes(client);
}
- // Handle Mailbox state changes - refresh mailbox list
- if (accountChanges.Mailbox) {
+ // Handle Mailbox state changes - refresh mailbox list (skip if already
+ // triggered by an Email change in the same push, to avoid double fetch)
+ if (accountChanges.Mailbox && !accountChanges.Email) {
await get().fetchMailboxes(client);
}