@agent-native/core 0.168.1 → 0.168.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/netlify.toml +4 -4
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +32 -5
- package/corpus/templates/analytics/server/lib/scoped-settings.ts +2 -3
- package/corpus/templates/assets/netlify.toml +4 -1
- package/corpus/templates/brain/netlify.toml +4 -1
- package/corpus/templates/calendar/app/root.tsx +2 -0
- package/corpus/templates/calendar/netlify.toml +4 -1
- package/corpus/templates/chat/netlify.toml +4 -1
- package/corpus/templates/clips/desktop/src/app.tsx +4 -8
- package/corpus/templates/clips/netlify.toml +4 -1
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +17 -11
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -5
- package/corpus/templates/content/netlify.toml +4 -1
- package/corpus/templates/crm/netlify.toml +6 -1
- package/corpus/templates/design/app/root.tsx +5 -1
- package/corpus/templates/design/netlify.toml +4 -1
- package/corpus/templates/dispatch/netlify.toml +4 -1
- package/corpus/templates/factory/netlify.toml +4 -1
- package/corpus/templates/forms/netlify.toml +4 -1
- package/corpus/templates/macros/netlify.toml +3 -3
- package/corpus/templates/mail/actions/view-screen.ts +80 -8
- package/corpus/templates/mail/app/components/email/EmailList.tsx +3 -1
- package/corpus/templates/mail/app/components/email/EmailListItem.tsx +1 -35
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +16 -8
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +146 -87
- package/corpus/templates/mail/app/global.css +21 -0
- package/corpus/templates/mail/app/hooks/use-emails.ts +6 -0
- package/corpus/templates/mail/app/hooks/use-navigation-state.ts +2 -0
- package/corpus/templates/mail/app/lib/inbox-tabs.ts +5 -0
- package/corpus/templates/mail/app/lib/label-colors.ts +47 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +25 -8
- package/corpus/templates/mail/netlify.toml +4 -1
- package/corpus/templates/mail/server/handlers/emails.ts +27 -2
- package/corpus/templates/plan/netlify.toml +4 -1
- package/corpus/templates/slides/netlify.toml +4 -1
- package/corpus/templates/tasks/netlify.toml +6 -1
- package/dist/app-config/migration.d.ts +5 -0
- package/dist/app-config/migration.js +11 -0
- package/dist/app-config/schema.d.ts +4 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/app-config/store.d.ts +8 -0
- package/dist/app-config/store.js +19 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/migrations.js +10 -2
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/netlify-static-headers.d.ts +3 -0
- package/dist/deploy/netlify-static-headers.js +60 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +17 -17
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js +1 -1
- package/dist/settings/org-settings.js +7 -3
- package/dist/templates/chat/netlify.toml +4 -1
- package/dist/vite/client.js +2 -0
- package/package.json +2 -1
- package/src/templates/chat/netlify.toml +4 -1
|
@@ -5,6 +5,12 @@ import { getSetting } from "@agent-native/core/settings";
|
|
|
5
5
|
import { emailMessageMatchesSearch } from "@shared/search.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
augmentSelfSentLabels,
|
|
10
|
+
filterInboxTabEmails,
|
|
11
|
+
OTHER_INBOX_TAB_PARAM,
|
|
12
|
+
resolvePinnedLabels,
|
|
13
|
+
} from "../app/lib/inbox-tabs.js";
|
|
8
14
|
import { buildGmailEmailSearchQuery } from "../server/lib/gmail-query.js";
|
|
9
15
|
import { gmailGetThread } from "../server/lib/google-api.js";
|
|
10
16
|
import {
|
|
@@ -16,10 +22,12 @@ import {
|
|
|
16
22
|
fetchGmailLabelMap,
|
|
17
23
|
} from "../server/lib/google-auth.js";
|
|
18
24
|
import { getSyntheticEmailsForView } from "../server/lib/jobs.js";
|
|
25
|
+
import { readSettings } from "../server/lib/mail-settings.js";
|
|
19
26
|
import {
|
|
20
27
|
listQueuedDrafts,
|
|
21
28
|
requireQueuedDraft,
|
|
22
29
|
} from "../server/lib/queued-drafts.js";
|
|
30
|
+
import type { EmailMessage } from "../shared/types.js";
|
|
23
31
|
import { getAccessTokens, fetchLabelMap } from "./helpers.js";
|
|
24
32
|
|
|
25
33
|
function latestPerThread(emails: any[]): any[] {
|
|
@@ -43,10 +51,33 @@ async function fetchEmailList(
|
|
|
43
51
|
view: string,
|
|
44
52
|
search?: string,
|
|
45
53
|
_label?: string,
|
|
54
|
+
activeInboxTab?: string,
|
|
55
|
+
activeAccounts?: string[],
|
|
46
56
|
): Promise<any[]> {
|
|
47
57
|
try {
|
|
48
58
|
const ownerEmail = getRequestUserEmail();
|
|
49
59
|
if (!ownerEmail) throw new Error("no authenticated user");
|
|
60
|
+
const shouldFilterOther =
|
|
61
|
+
view === "inbox" && !search && activeInboxTab === OTHER_INBOX_TAB_PARAM;
|
|
62
|
+
const selectedAccountEmails = Array.isArray(activeAccounts)
|
|
63
|
+
? [
|
|
64
|
+
...new Set(
|
|
65
|
+
activeAccounts
|
|
66
|
+
.filter((email): email is string => typeof email === "string")
|
|
67
|
+
.map((email) => email.toLowerCase()),
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
: [];
|
|
71
|
+
const selectedAccountSet = new Set(selectedAccountEmails);
|
|
72
|
+
const filterSelectedAccounts = (emails: any[]) => {
|
|
73
|
+
if (selectedAccountEmails.length === 0) return emails;
|
|
74
|
+
return emails.filter(
|
|
75
|
+
(email) =>
|
|
76
|
+
typeof email.accountEmail === "string" &&
|
|
77
|
+
selectedAccountSet.has(email.accountEmail.toLowerCase()),
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
50
81
|
if (view === "snoozed" || view === "scheduled") {
|
|
51
82
|
let emails = await getSyntheticEmailsForView(ownerEmail, view);
|
|
52
83
|
if (search) {
|
|
@@ -54,10 +85,37 @@ async function fetchEmailList(
|
|
|
54
85
|
emailMessageMatchesSearch(e, search),
|
|
55
86
|
);
|
|
56
87
|
}
|
|
57
|
-
return emails.slice(0, 50);
|
|
88
|
+
return filterSelectedAccounts(emails).slice(0, 50);
|
|
58
89
|
}
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
|
|
91
|
+
const googleConnected = await isConnected(ownerEmail);
|
|
92
|
+
const settings =
|
|
93
|
+
googleConnected || shouldFilterOther
|
|
94
|
+
? await readSettings(ownerEmail)
|
|
95
|
+
: undefined;
|
|
96
|
+
const userPinnedLabels = settings?.pinnedLabels;
|
|
97
|
+
const pinnedLabels =
|
|
98
|
+
userPinnedLabels === undefined
|
|
99
|
+
? resolvePinnedLabels([], googleConnected)
|
|
100
|
+
: resolvePinnedLabels(userPinnedLabels, googleConnected);
|
|
101
|
+
const hasNoteToSelf = pinnedLabels.includes("note-to-self");
|
|
102
|
+
const clients = googleConnected ? await getClients(ownerEmail) : [];
|
|
103
|
+
const connectedEmails = new Set(
|
|
104
|
+
clients.map(({ email }) => email.toLowerCase()),
|
|
105
|
+
);
|
|
106
|
+
const prepareEmails = (emails: any[]) => {
|
|
107
|
+
const augmented = augmentSelfSentLabels(emails as EmailMessage[], {
|
|
108
|
+
isGoogleConnected: googleConnected,
|
|
109
|
+
connectedEmails,
|
|
110
|
+
hasNoteToSelf,
|
|
111
|
+
});
|
|
112
|
+
return filterSelectedAccounts(augmented);
|
|
113
|
+
};
|
|
114
|
+
const applyActiveInboxTab = (emails: any[]) =>
|
|
115
|
+
shouldFilterOther
|
|
116
|
+
? filterInboxTabEmails(prepareEmails(emails), null, pinnedLabels)
|
|
117
|
+
: prepareEmails(emails);
|
|
118
|
+
if (googleConnected) {
|
|
61
119
|
const labelMap = new Map<string, string>();
|
|
62
120
|
await Promise.all(
|
|
63
121
|
clients.map(async ({ accessToken }) => {
|
|
@@ -79,6 +137,10 @@ async function fetchEmailList(
|
|
|
79
137
|
{
|
|
80
138
|
mode: "threads",
|
|
81
139
|
threadFormat: "metadata",
|
|
140
|
+
accountEmails:
|
|
141
|
+
selectedAccountEmails.length > 0
|
|
142
|
+
? selectedAccountEmails
|
|
143
|
+
: undefined,
|
|
82
144
|
threadCandidateLimit: search ? 500 : undefined,
|
|
83
145
|
threadRecentMessageCandidateLimit:
|
|
84
146
|
!search && (view === "inbox" || view === "unread")
|
|
@@ -87,9 +149,11 @@ async function fetchEmailList(
|
|
|
87
149
|
},
|
|
88
150
|
);
|
|
89
151
|
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
152
|
+
return applyActiveInboxTab(
|
|
153
|
+
latestPerThread(
|
|
154
|
+
messages.map((m: any) =>
|
|
155
|
+
gmailToEmailMessage(m, m._accountEmail, labelMap),
|
|
156
|
+
),
|
|
93
157
|
),
|
|
94
158
|
).slice(0, 50);
|
|
95
159
|
}
|
|
@@ -136,7 +200,7 @@ async function fetchEmailList(
|
|
|
136
200
|
emailMessageMatchesSearch(e, search),
|
|
137
201
|
);
|
|
138
202
|
}
|
|
139
|
-
return emails.slice(0, 50);
|
|
203
|
+
return applyActiveInboxTab(emails).slice(0, 50);
|
|
140
204
|
}
|
|
141
205
|
return [];
|
|
142
206
|
} catch {
|
|
@@ -255,7 +319,13 @@ export default defineAction({
|
|
|
255
319
|
};
|
|
256
320
|
}
|
|
257
321
|
} else if (nav?.view) {
|
|
258
|
-
const emails = await fetchEmailList(
|
|
322
|
+
const emails = await fetchEmailList(
|
|
323
|
+
nav.view,
|
|
324
|
+
nav.search,
|
|
325
|
+
nav.label,
|
|
326
|
+
nav.activeInboxTab,
|
|
327
|
+
nav.activeAccounts,
|
|
328
|
+
);
|
|
259
329
|
const selectedThreadIds = Array.isArray(nav.selectedThreadIds)
|
|
260
330
|
? new Set(
|
|
261
331
|
nav.selectedThreadIds.filter(
|
|
@@ -279,6 +349,8 @@ export default defineAction({
|
|
|
279
349
|
screen.emailList = {
|
|
280
350
|
view: nav.view,
|
|
281
351
|
label: nav.label ?? null,
|
|
352
|
+
activeInboxTab: nav.activeInboxTab ?? null,
|
|
353
|
+
activeAccounts: nav.activeAccounts ?? [],
|
|
282
354
|
search: nav.search ?? null,
|
|
283
355
|
selectedThreadIds: Array.from(selectedThreadIds),
|
|
284
356
|
count: compact.length,
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
useBulkToggleStar,
|
|
49
49
|
useBulkMarkRead,
|
|
50
50
|
useLabels,
|
|
51
|
+
EMPTY_LABELS,
|
|
51
52
|
useMoveEmail,
|
|
52
53
|
unsuppressThread,
|
|
53
54
|
} from "@/hooks/use-emails";
|
|
@@ -435,7 +436,8 @@ export function EmailList({
|
|
|
435
436
|
const bulkTrashEmails = useBulkTrashEmails();
|
|
436
437
|
const bulkToggleStar = useBulkToggleStar();
|
|
437
438
|
const bulkMarkRead = useBulkMarkRead();
|
|
438
|
-
const { data:
|
|
439
|
+
const { data: labelsData } = useLabels();
|
|
440
|
+
const labels = labelsData ?? EMPTY_LABELS;
|
|
439
441
|
const moveEmail = useMoveEmail();
|
|
440
442
|
const cancelScheduledJob = useDeleteScheduledJob();
|
|
441
443
|
const sendScheduledJobNow = useSendScheduledJobNow();
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
TooltipTrigger,
|
|
22
22
|
} from "@/components/ui/tooltip";
|
|
23
23
|
import { useAccountFilter } from "@/hooks/use-account-filter";
|
|
24
|
+
import { getLabelStyle } from "@/lib/label-colors";
|
|
24
25
|
import type { ThreadSummary } from "@/lib/threads";
|
|
25
26
|
import { cn, formatEmailDate, truncate } from "@/lib/utils";
|
|
26
27
|
|
|
@@ -96,29 +97,6 @@ function formatParticipants(participants: string[], maxWidth = 3): string {
|
|
|
96
97
|
return `${firstNames[0]} .. ${firstNames.slice(-(maxWidth - 1)).join(", ")}`;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
// Map common label IDs to display colors
|
|
100
|
-
const labelColors: Record<string, { bg: string; text: string }> = {
|
|
101
|
-
automated: { bg: "bg-pink-500/20", text: "text-pink-700 dark:text-pink-300" },
|
|
102
|
-
social: { bg: "bg-blue-500/20", text: "text-blue-700 dark:text-blue-300" },
|
|
103
|
-
updates: {
|
|
104
|
-
bg: "bg-yellow-500/20",
|
|
105
|
-
text: "text-yellow-700 dark:text-yellow-300",
|
|
106
|
-
},
|
|
107
|
-
promotions: {
|
|
108
|
-
bg: "bg-green-500/20",
|
|
109
|
-
text: "text-green-700 dark:text-green-300",
|
|
110
|
-
},
|
|
111
|
-
forums: {
|
|
112
|
-
bg: "bg-sky-500/20",
|
|
113
|
-
text: "text-sky-700 dark:text-sky-300",
|
|
114
|
-
},
|
|
115
|
-
finance: {
|
|
116
|
-
bg: "bg-emerald-500/20",
|
|
117
|
-
text: "text-emerald-700 dark:text-emerald-300",
|
|
118
|
-
},
|
|
119
|
-
travel: { bg: "bg-cyan-500/20", text: "text-cyan-700 dark:text-cyan-300" },
|
|
120
|
-
};
|
|
121
|
-
|
|
122
100
|
/** Stable dot colors for distinguishing accounts */
|
|
123
101
|
const accountDotColors = [
|
|
124
102
|
"bg-blue-400",
|
|
@@ -139,18 +117,6 @@ function getAccountColor(
|
|
|
139
117
|
return accountDotColors[(idx >= 0 ? idx : 0) % accountDotColors.length];
|
|
140
118
|
}
|
|
141
119
|
|
|
142
|
-
function getLabelStyle(labelId: string): { bg: string; text: string } {
|
|
143
|
-
const normalized = labelId.toLowerCase().replace(/^label:/, "");
|
|
144
|
-
if (labelColors[normalized]) return labelColors[normalized];
|
|
145
|
-
// Fallback: hash to a color
|
|
146
|
-
let hash = 0;
|
|
147
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
148
|
-
hash = normalized.charCodeAt(i) + ((hash << 5) - hash);
|
|
149
|
-
}
|
|
150
|
-
const options = Object.values(labelColors);
|
|
151
|
-
return options[Math.abs(hash) % options.length];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
120
|
export const EmailListItem = memo(function EmailListItem({
|
|
155
121
|
email,
|
|
156
122
|
thread,
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
decodeHtmlEntities,
|
|
65
65
|
processHtmlImages,
|
|
66
66
|
} from "@/lib/email-image-policy";
|
|
67
|
+
import { getLabelStyle } from "@/lib/label-colors";
|
|
67
68
|
import { isMcpEmbedSurface } from "@/lib/mcp-embed";
|
|
68
69
|
import { getResolvedTheme } from "@/lib/theme";
|
|
69
70
|
import { ensureThread, warmThreads } from "@/lib/thread-cache";
|
|
@@ -1138,14 +1139,21 @@ export function EmailThread({
|
|
|
1138
1139
|
<h1 className="text-base sm:text-lg font-semibold leading-tight text-foreground line-clamp-2">
|
|
1139
1140
|
{threadSubject}
|
|
1140
1141
|
</h1>
|
|
1141
|
-
{displayLabels.map((labelId) =>
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1142
|
+
{displayLabels.map((labelId) => {
|
|
1143
|
+
const style = getLabelStyle(labelId);
|
|
1144
|
+
return (
|
|
1145
|
+
<span
|
|
1146
|
+
key={labelId}
|
|
1147
|
+
className={cn(
|
|
1148
|
+
"label-badge shrink-0 mt-1",
|
|
1149
|
+
style.bg,
|
|
1150
|
+
style.text,
|
|
1151
|
+
)}
|
|
1152
|
+
>
|
|
1153
|
+
{labelId}
|
|
1154
|
+
</span>
|
|
1155
|
+
);
|
|
1156
|
+
})}
|
|
1149
1157
|
{/* Action bar */}
|
|
1150
1158
|
<div className="hidden sm:flex items-center gap-0.5 ml-auto shrink-0">
|
|
1151
1159
|
<Tooltip>
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
IconMailForward,
|
|
35
35
|
IconStar,
|
|
36
36
|
IconTrash,
|
|
37
|
+
IconAlertCircle,
|
|
37
38
|
} from "@tabler/icons-react";
|
|
38
39
|
import { useQueryClient } from "@tanstack/react-query";
|
|
39
40
|
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
useBlockSender,
|
|
69
70
|
useMuteThread,
|
|
70
71
|
markExternalEmailRefresh,
|
|
72
|
+
EMPTY_LABELS,
|
|
71
73
|
} from "@/hooks/use-emails";
|
|
72
74
|
import {
|
|
73
75
|
useGoogleAuthStatus,
|
|
@@ -81,6 +83,8 @@ import {
|
|
|
81
83
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
82
84
|
import { runUndo } from "@/hooks/use-undo";
|
|
83
85
|
import {
|
|
86
|
+
OTHER_INBOX_TAB_ID,
|
|
87
|
+
OTHER_INBOX_TAB_PARAM,
|
|
84
88
|
qualifiesForInboxTab,
|
|
85
89
|
pinnedTriageLabels,
|
|
86
90
|
augmentSelfSentLabels,
|
|
@@ -256,6 +260,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
256
260
|
const [searchParams] = useSearchParams();
|
|
257
261
|
const activeSearchQuery = searchParams.get("q");
|
|
258
262
|
const activeLabel = searchParams.get("label");
|
|
263
|
+
const activeInboxTab = searchParams.get("tab");
|
|
259
264
|
const composeInitialExpanded =
|
|
260
265
|
searchParams.get(COMPOSE_FULLSCREEN_PARAM) === "1";
|
|
261
266
|
const clearComposeInitialExpanded = useCallback(() => {
|
|
@@ -271,22 +276,31 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
271
276
|
{ replace: true },
|
|
272
277
|
);
|
|
273
278
|
}, [location.pathname, navigate, searchParams]);
|
|
274
|
-
// Remember which view (
|
|
279
|
+
// Remember which view (label or inbox tab) the user was in before searching —
|
|
275
280
|
// SearchBar always routes searches through /all?q=..., so on clear we'd
|
|
276
281
|
// otherwise drop a user searching from Starred/Sent/Archive or from a
|
|
277
282
|
// label-filtered tab back into plain Inbox.
|
|
278
|
-
const preSearchViewRef = useRef<{
|
|
279
|
-
view
|
|
280
|
-
label:
|
|
281
|
-
|
|
283
|
+
const preSearchViewRef = useRef<{
|
|
284
|
+
view: string;
|
|
285
|
+
label: string | null;
|
|
286
|
+
tab: string | null;
|
|
287
|
+
}>({ view, label: activeLabel, tab: activeInboxTab });
|
|
282
288
|
useEffect(() => {
|
|
283
289
|
if (!activeSearchQuery) {
|
|
284
|
-
preSearchViewRef.current = {
|
|
290
|
+
preSearchViewRef.current = {
|
|
291
|
+
view,
|
|
292
|
+
label: activeLabel,
|
|
293
|
+
tab: activeInboxTab,
|
|
294
|
+
};
|
|
285
295
|
}
|
|
286
|
-
}, [view, activeLabel, activeSearchQuery]);
|
|
296
|
+
}, [view, activeLabel, activeInboxTab, activeSearchQuery]);
|
|
287
297
|
const restorePreSearchPath = useCallback(() => {
|
|
288
|
-
const { view: v, label: l } = preSearchViewRef.current;
|
|
289
|
-
|
|
298
|
+
const { view: v, label: l, tab } = preSearchViewRef.current;
|
|
299
|
+
const params = new URLSearchParams();
|
|
300
|
+
if (l) params.set("label", l);
|
|
301
|
+
if (tab) params.set("tab", tab);
|
|
302
|
+
const search = params.toString();
|
|
303
|
+
return `/${v}${search ? `?${search}` : ""}`;
|
|
290
304
|
}, []);
|
|
291
305
|
// When the search param is cleared externally (browser back/forward,
|
|
292
306
|
// agent navigation), drop the searchFocused flag — otherwise the bar
|
|
@@ -300,7 +314,15 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
300
314
|
}
|
|
301
315
|
prevSearchQueryRef.current = activeSearchQuery;
|
|
302
316
|
}, [activeSearchQuery]);
|
|
303
|
-
const {
|
|
317
|
+
const {
|
|
318
|
+
data: labelsData,
|
|
319
|
+
isLoading: labelsLoading,
|
|
320
|
+
isError: labelsError,
|
|
321
|
+
error: labelsQueryError,
|
|
322
|
+
isFetching: labelsFetching,
|
|
323
|
+
refetch: refetchLabels,
|
|
324
|
+
} = useLabels();
|
|
325
|
+
const labels = labelsData ?? EMPTY_LABELS;
|
|
304
326
|
const { data: settings, isLoading: settingsLoading } = useSettings();
|
|
305
327
|
const updateSettings = useUpdateSettings();
|
|
306
328
|
const googleStatus = useGoogleAuthStatus();
|
|
@@ -400,7 +422,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
400
422
|
localStorage.removeItem(SIDEBAR_COLLAPSE_KEY);
|
|
401
423
|
}
|
|
402
424
|
}, [sidebarCollapsed]);
|
|
403
|
-
const showSidebar = sidebarOpen ||
|
|
425
|
+
const showSidebar = isMobile ? sidebarOpen : sidebarOpen || sidebarPinned;
|
|
404
426
|
const showCollapsedSidebar =
|
|
405
427
|
!isMobile &&
|
|
406
428
|
showSidebar &&
|
|
@@ -559,7 +581,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
559
581
|
id: "inbox",
|
|
560
582
|
label: t("mail.views.inbox"),
|
|
561
583
|
href: "/inbox",
|
|
562
|
-
isActive:
|
|
584
|
+
isActive:
|
|
585
|
+
view === "inbox" &&
|
|
586
|
+
!activeLabel &&
|
|
587
|
+
activeInboxTab !== OTHER_INBOX_TAB_PARAM,
|
|
563
588
|
type: "system",
|
|
564
589
|
});
|
|
565
590
|
}
|
|
@@ -615,10 +640,13 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
615
640
|
|
|
616
641
|
if (hasPinnedFilters) {
|
|
617
642
|
tabs.push({
|
|
618
|
-
id:
|
|
643
|
+
id: OTHER_INBOX_TAB_ID,
|
|
619
644
|
label: t("mail.views.other"),
|
|
620
|
-
href:
|
|
621
|
-
isActive:
|
|
645
|
+
href: `/inbox?tab=${OTHER_INBOX_TAB_PARAM}`,
|
|
646
|
+
isActive:
|
|
647
|
+
view === "inbox" &&
|
|
648
|
+
!activeLabel &&
|
|
649
|
+
activeInboxTab === OTHER_INBOX_TAB_PARAM,
|
|
622
650
|
type: "system",
|
|
623
651
|
});
|
|
624
652
|
}
|
|
@@ -630,6 +658,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
630
658
|
labelAliases,
|
|
631
659
|
view,
|
|
632
660
|
activeLabel,
|
|
661
|
+
activeInboxTab,
|
|
633
662
|
hasPinnedFilters,
|
|
634
663
|
t,
|
|
635
664
|
]);
|
|
@@ -661,6 +690,15 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
661
690
|
[pinnedLabels],
|
|
662
691
|
);
|
|
663
692
|
|
|
693
|
+
// The top-bar inbox tabs are hidden on mobile, so mirror their non-standard
|
|
694
|
+
// entries in the drawer. Collapsible system views already appear in the
|
|
695
|
+
// fixed drawer list below and must not be duplicated here.
|
|
696
|
+
const mobileInboxTabs = visibleTabs.filter(
|
|
697
|
+
(tab) =>
|
|
698
|
+
tab.id !== "inbox" &&
|
|
699
|
+
!collapsibleViews.some((view) => view.id === tab.id),
|
|
700
|
+
);
|
|
701
|
+
|
|
664
702
|
// Is current view one of the hidden ones? If so force-show it
|
|
665
703
|
const currentInHidden = hiddenViews.some((v) => v.id === view);
|
|
666
704
|
|
|
@@ -1010,6 +1048,15 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1010
1048
|
|
|
1011
1049
|
const useServerLabelCounts = activeAccounts.size === 0;
|
|
1012
1050
|
|
|
1051
|
+
// Gmail totals overlap across labels, while these tabs are an exclusive
|
|
1052
|
+
// partition of the loaded inbox. Keep their badges tied to that partition.
|
|
1053
|
+
const inboxPartitionTabIds = new Set<string>([OTHER_INBOX_TAB_ID]);
|
|
1054
|
+
for (const pinnedId of pinnedTriageLabels(pinnedLabels)) {
|
|
1055
|
+
inboxPartitionTabIds.add(pinnedId);
|
|
1056
|
+
const label = resolveLabelForCount(pinnedId);
|
|
1057
|
+
if (label) inboxPartitionTabIds.add(label.id);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1013
1060
|
type CountKind = "unread" | "total";
|
|
1014
1061
|
const countFieldForKind = (kind: CountKind) =>
|
|
1015
1062
|
kind === "total" ? "totalCount" : "unreadCount";
|
|
@@ -1026,24 +1073,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1026
1073
|
const serverCount = useServerLabelCounts
|
|
1027
1074
|
? (inboxLabel?.[countField] ?? 0)
|
|
1028
1075
|
: 0;
|
|
1029
|
-
const localCount = localCounts["
|
|
1076
|
+
const localCount = localCounts["__inboxTotal"] ?? 0;
|
|
1030
1077
|
return Math.max(serverCount, localCount);
|
|
1031
1078
|
};
|
|
1032
1079
|
|
|
1033
|
-
const isExclusivePinnedTab = (viewId: string) => {
|
|
1034
|
-
if (!hasPinnedFilters) return false;
|
|
1035
|
-
// Pinned label rows (the ones that contribute to the "Other" remainder)
|
|
1036
|
-
// are exclusive: each inbox thread is counted in exactly one tab. Gmail's
|
|
1037
|
-
// server label counts don't have that exclusivity (e.g. server "important"
|
|
1038
|
-
// returns *all* important threads, regardless of whether they're also
|
|
1039
|
-
// categorized elsewhere), so we can't mix the two — use local only.
|
|
1040
|
-
return pinnedLabels.some((id) => {
|
|
1041
|
-
if (collapsibleViews.some((view) => view.id === id)) return false;
|
|
1042
|
-
const label = resolveLabelForCount(id);
|
|
1043
|
-
return (label?.id ?? id) === viewId || id === viewId;
|
|
1044
|
-
});
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1047
1080
|
const getOtherCount = (kind: CountKind) => {
|
|
1048
1081
|
if (!hasPinnedFilters) return getInboxCount(kind);
|
|
1049
1082
|
const localCounts = localCountsForKind(kind);
|
|
@@ -1056,20 +1089,18 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1056
1089
|
};
|
|
1057
1090
|
|
|
1058
1091
|
const getTabCount = (viewId: string, kind: CountKind) => {
|
|
1059
|
-
if (viewId ===
|
|
1092
|
+
if (viewId === OTHER_INBOX_TAB_ID) return getOtherCount(kind);
|
|
1093
|
+
if (viewId === "inbox") return getInboxCount(kind);
|
|
1060
1094
|
const label = resolveLabelForCount(viewId);
|
|
1061
1095
|
const countField = countFieldForKind(kind);
|
|
1062
1096
|
const localCounts = localCountsForKind(kind);
|
|
1063
1097
|
const localCount =
|
|
1064
1098
|
localCounts[viewId] ?? (label ? (localCounts[label.id] ?? 0) : 0);
|
|
1065
|
-
// Exclusive pinned tabs (when hasPinnedFilters is on, a thread belongs to
|
|
1066
|
-
// exactly one tab) can't fall back to Gmail's non-exclusive server count
|
|
1067
|
-
// — that would over-report the badge relative to what the tab renders.
|
|
1068
|
-
if (isExclusivePinnedTab(viewId)) return localCount;
|
|
1069
1099
|
const serverCount =
|
|
1070
1100
|
useServerLabelCounts && viewId !== "note-to-self"
|
|
1071
1101
|
? (label?.[countField] ?? 0)
|
|
1072
1102
|
: 0;
|
|
1103
|
+
if (inboxPartitionTabIds.has(viewId)) return localCount;
|
|
1073
1104
|
return Math.max(serverCount, localCount);
|
|
1074
1105
|
};
|
|
1075
1106
|
const getTopBarCount = (viewId: string) => getTabCount(viewId, "total");
|
|
@@ -1158,7 +1189,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1158
1189
|
{/* Primary tabs stay mounted during search so navigation does not jump. */}
|
|
1159
1190
|
<>
|
|
1160
1191
|
{tabsLoading ? (
|
|
1161
|
-
<nav className="flex items-center gap-2 overflow-x-auto hide-scrollbar">
|
|
1192
|
+
<nav className="hidden sm:flex items-center gap-2 overflow-x-auto hide-scrollbar">
|
|
1162
1193
|
{[1, 2, 3].map((i) => (
|
|
1163
1194
|
<span
|
|
1164
1195
|
key={i}
|
|
@@ -1168,7 +1199,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1168
1199
|
))}
|
|
1169
1200
|
</nav>
|
|
1170
1201
|
) : (
|
|
1171
|
-
<nav className="flex min-w-0 items-center gap-0.5 overflow-x-auto hide-scrollbar">
|
|
1202
|
+
<nav className="hidden sm:flex min-w-0 items-center gap-0.5 overflow-x-auto hide-scrollbar">
|
|
1172
1203
|
{topBarTabs.map((tab, idx) => {
|
|
1173
1204
|
const visibleIndex = visibleTabs.findIndex(
|
|
1174
1205
|
(item) => item.id === tab.id,
|
|
@@ -1252,7 +1283,12 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1252
1283
|
)}
|
|
1253
1284
|
|
|
1254
1285
|
{/* Tab settings cog */}
|
|
1255
|
-
<div
|
|
1286
|
+
<div
|
|
1287
|
+
className={cn(
|
|
1288
|
+
"relative hidden sm:block",
|
|
1289
|
+
tabsLoading && "invisible",
|
|
1290
|
+
)}
|
|
1291
|
+
>
|
|
1256
1292
|
<Popover
|
|
1257
1293
|
open={tabSettingsOpen}
|
|
1258
1294
|
onOpenChange={(open) => {
|
|
@@ -1531,7 +1567,12 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1531
1567
|
<button
|
|
1532
1568
|
type="button"
|
|
1533
1569
|
onClick={() => {
|
|
1534
|
-
|
|
1570
|
+
if (sidebarPinned) {
|
|
1571
|
+
setSidebarPinned(false);
|
|
1572
|
+
setSidebarOpen(!isMobile);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
setSidebarPinned(true);
|
|
1535
1576
|
setSidebarOpen(true);
|
|
1536
1577
|
}}
|
|
1537
1578
|
className={cn(
|
|
@@ -1726,63 +1767,56 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1726
1767
|
))}
|
|
1727
1768
|
</div>
|
|
1728
1769
|
|
|
1729
|
-
{/*
|
|
1730
|
-
{
|
|
1731
|
-
(l) => !collapsibleViews.some((v) => v.id === l),
|
|
1732
|
-
).length > 0 && (
|
|
1770
|
+
{/* Mobile equivalents for the hidden top-bar inbox tabs */}
|
|
1771
|
+
{mobileInboxTabs.length > 0 && (
|
|
1733
1772
|
<>
|
|
1734
1773
|
<h2 className="text-[11px] font-medium text-muted-foreground/50 uppercase tracking-wider mt-5 mb-3">
|
|
1735
1774
|
{t("mail.views.labels")}
|
|
1736
1775
|
</h2>
|
|
1737
1776
|
<div className="space-y-0.5">
|
|
1738
|
-
{
|
|
1739
|
-
.
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
"
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1777
|
+
{mobileInboxTabs.map((tab) => {
|
|
1778
|
+
const count = getUnreadCount(tab.id);
|
|
1779
|
+
const depth =
|
|
1780
|
+
tab.type === "label"
|
|
1781
|
+
? labelDepth(tab.fullLabel ?? tab.label)
|
|
1782
|
+
: 0;
|
|
1783
|
+
return (
|
|
1784
|
+
<Link
|
|
1785
|
+
key={tab.id}
|
|
1786
|
+
to={tab.href}
|
|
1787
|
+
onClick={closeSidebar}
|
|
1788
|
+
className={cn(
|
|
1789
|
+
"flex items-center justify-between rounded-md px-3 py-2.5 text-[14px] transition-colors min-h-[44px]",
|
|
1790
|
+
tab.isActive
|
|
1791
|
+
? "bg-accent/60 text-foreground font-medium"
|
|
1792
|
+
: "text-foreground/70 hover:bg-accent/30",
|
|
1793
|
+
)}
|
|
1794
|
+
>
|
|
1795
|
+
<span
|
|
1796
|
+
className="flex min-w-0 items-center gap-2"
|
|
1797
|
+
style={{ paddingLeft: depth * 12 }}
|
|
1758
1798
|
>
|
|
1799
|
+
{tab.color && (
|
|
1800
|
+
<span
|
|
1801
|
+
className="h-2 w-2 rounded-full shrink-0"
|
|
1802
|
+
style={{ backgroundColor: tab.color }}
|
|
1803
|
+
/>
|
|
1804
|
+
)}
|
|
1759
1805
|
<span
|
|
1760
|
-
className="
|
|
1761
|
-
|
|
1806
|
+
className="truncate"
|
|
1807
|
+
title={tab.fullLabel ?? tab.label}
|
|
1762
1808
|
>
|
|
1763
|
-
{tab.
|
|
1764
|
-
<span
|
|
1765
|
-
className="h-2 w-2 rounded-full shrink-0"
|
|
1766
|
-
style={{ backgroundColor: tab.color }}
|
|
1767
|
-
/>
|
|
1768
|
-
)}
|
|
1769
|
-
<span
|
|
1770
|
-
className="truncate"
|
|
1771
|
-
title={tab.fullLabel}
|
|
1772
|
-
>
|
|
1773
|
-
{shortLabelName(
|
|
1774
|
-
tab.fullLabel ?? tab.label,
|
|
1775
|
-
)}
|
|
1776
|
-
</span>
|
|
1809
|
+
{tab.label}
|
|
1777
1810
|
</span>
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1811
|
+
</span>
|
|
1812
|
+
{count > 0 && (
|
|
1813
|
+
<span className="text-[12px] text-muted-foreground/50 tabular-nums">
|
|
1814
|
+
{count}
|
|
1815
|
+
</span>
|
|
1816
|
+
)}
|
|
1817
|
+
</Link>
|
|
1818
|
+
);
|
|
1819
|
+
})}
|
|
1786
1820
|
</div>
|
|
1787
1821
|
</>
|
|
1788
1822
|
)}
|
|
@@ -1840,6 +1874,31 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|
|
1840
1874
|
>
|
|
1841
1875
|
<InvitationBanner />
|
|
1842
1876
|
|
|
1877
|
+
{labelsError && (
|
|
1878
|
+
<div
|
|
1879
|
+
role="alert"
|
|
1880
|
+
className="flex shrink-0 items-center gap-2 border-b border-destructive/20 bg-destructive/5 px-4 py-2 text-xs text-muted-foreground"
|
|
1881
|
+
>
|
|
1882
|
+
<IconAlertCircle className="h-3.5 w-3.5 shrink-0 text-destructive" />
|
|
1883
|
+
<span className="min-w-0 flex-1 truncate">
|
|
1884
|
+
{labelsQueryError instanceof Error && labelsQueryError.message
|
|
1885
|
+
? labelsQueryError.message
|
|
1886
|
+
: t("mail.error.loadTitle")}
|
|
1887
|
+
</span>
|
|
1888
|
+
<Button
|
|
1889
|
+
type="button"
|
|
1890
|
+
size="sm"
|
|
1891
|
+
variant="ghost"
|
|
1892
|
+
disabled={labelsFetching}
|
|
1893
|
+
onClick={() => void refetchLabels()}
|
|
1894
|
+
>
|
|
1895
|
+
{labelsFetching
|
|
1896
|
+
? t("mail.error.retrying")
|
|
1897
|
+
: t("mail.error.tryAgain")}
|
|
1898
|
+
</Button>
|
|
1899
|
+
</div>
|
|
1900
|
+
)}
|
|
1901
|
+
|
|
1843
1902
|
{/* Show full-page takeover when no accounts connected (except on settings page) */}
|
|
1844
1903
|
{!googleStatus.isLoading &&
|
|
1845
1904
|
!googleStatus.isError &&
|