-
ULTIMAIL
-
-
- Synchronisation de votre boite de reception...
-
+
{config.pill}
+ {activeApp === "mail" ? (
+
+ ) : config.markDark ? (
+ <>
+
})
+
})
+ >
+ ) : (
+
})
+ )}
+
{config.subtitle}
diff --git a/components/gmail/calendar-invitation-preview.tsx b/components/gmail/calendar-invitation-preview.tsx
index 692dbf9..52deddd 100644
--- a/components/gmail/calendar-invitation-preview.tsx
+++ b/components/gmail/calendar-invitation-preview.tsx
@@ -3,6 +3,7 @@
import { useMemo, useState } from "react"
import { format } from "date-fns"
import { InvitationTimeChipText } from "@/components/gmail/invitation-time-chip-text"
+import { AgendaMark } from "@/components/suite/agenda-mark"
import { Icon } from "@iconify/react"
import { ThumbsDown, ThumbsUp, Users, MoreVertical } from "lucide-react"
import {
@@ -94,12 +95,7 @@ export function CalendarInvitationPreview({
-

+
Dans votre agenda
diff --git a/components/gmail/compose-identities-sync.tsx b/components/gmail/compose-identities-sync.tsx
index 9594f51..c1b48fb 100644
--- a/components/gmail/compose-identities-sync.tsx
+++ b/components/gmail/compose-identities-sync.tsx
@@ -3,11 +3,12 @@
import { useEffect, useMemo } from "react"
import { useQueries } from "@tanstack/react-query"
import { useAuthReady } from "@/lib/api/use-auth-ready"
+import { useIsDemoMail } from "@/lib/demo/demo-mail-context"
import { useMailAccounts } from "@/lib/api/hooks/use-mail-queries"
import { apiClient } from "@/lib/api/client"
import type { ApiIdentity } from "@/lib/api/types"
import { useMailSignatures } from "@/lib/api/hooks/use-mail-signatures"
-import { apiIdentityToCompose } from "@/lib/compose/identity-map"
+import { apiIdentityToCompose, dedupeComposeIdentities } from "@/lib/compose/identity-map"
import type { Identity } from "@/lib/compose-context"
import { useComposeIdentitiesStore } from "@/lib/stores/compose-identities-store"
@@ -20,6 +21,7 @@ async function fetchIdentities(accountId: string) {
/** Hydrate compose From identities from server for all mail accounts. */
export function ComposeIdentitiesSync() {
+ const isDemoMail = useIsDemoMail()
const { ready, authenticated } = useAuthReady()
const { data: accounts = [], isSuccess: accountsReady } = useMailAccounts()
const { data: signatures = [], isSuccess: signaturesReady } = useMailSignatures()
@@ -45,8 +47,10 @@ export function ComposeIdentitiesSync() {
if (identityQueries.some((q) => q.isPending && q.fetchStatus !== "idle")) {
return null
}
- return identityQueries.flatMap((q) =>
- (q.data ?? []).map((id) => apiIdentityToCompose(id, signaturesById))
+ return dedupeComposeIdentities(
+ identityQueries.flatMap((q) =>
+ (q.data ?? []).map((id) => apiIdentityToCompose(id, signaturesById)),
+ ),
)
}, [
ready,
@@ -60,13 +64,14 @@ export function ComposeIdentitiesSync() {
])
useEffect(() => {
+ if (isDemoMail) return
if (!ready || !authenticated) {
useComposeIdentitiesStore.getState().clear()
return
}
if (merged === null) return
useComposeIdentitiesStore.getState().hydrateFromApi(merged)
- }, [ready, authenticated, merged])
+ }, [isDemoMail, ready, authenticated, merged])
return null
}
diff --git a/components/gmail/contacts-page/contacts-sidebar.tsx b/components/gmail/contacts-page/contacts-sidebar.tsx
index 8659e8b..c6e9804 100644
--- a/components/gmail/contacts-page/contacts-sidebar.tsx
+++ b/components/gmail/contacts-page/contacts-sidebar.tsx
@@ -27,7 +27,6 @@ import { cn } from "@/lib/utils"
import {
CONTACTS_CREATE_BTN_CLASS,
CONTACTS_FIELD_CLASS,
- CONTACTS_HEADING_TEXT,
CONTACTS_MUTED_TEXT,
CONTACTS_NAV_ACTIVE_CLASS,
CONTACTS_NAV_ICON_MUTED,
@@ -150,7 +149,6 @@ export function ContactsSidebar({
onNavigate("contacts"))}
- titleClassName={cn("text-[22px] font-normal", CONTACTS_HEADING_TEXT)}
/>
diff --git a/components/gmail/contacts/contacts-panel-logo.tsx b/components/gmail/contacts/contacts-panel-logo.tsx
index a2dc5b6..4d150cf 100644
--- a/components/gmail/contacts/contacts-panel-logo.tsx
+++ b/components/gmail/contacts/contacts-panel-logo.tsx
@@ -2,9 +2,8 @@
import { cn } from "@/lib/utils"
import { suitePublicAsset } from "@/lib/suite/suite-public-asset"
-import {
- CONTACTS_PANEL_TITLE_CLASS,
-} from "@/lib/contacts-chrome-classes"
+import { CONTACTS_PANEL_TITLE_CLASS } from "@/lib/contacts-chrome-classes"
+import { SUITE_APP_LOGO_LOCKUP_CLASS, SUITE_APP_LOGO_MARK_CLASS } from "@/lib/suite/suite-chrome-classes"
const CONTACTS_MARK_SRC = suitePublicAsset("/contacts-mark.svg")
@@ -19,14 +18,15 @@ export function ContactsPanelLogo({
onClick,
className,
titleClassName = CONTACTS_PANEL_TITLE_CLASS,
- markClassName = "h-8 w-8",
+ markClassName = SUITE_APP_LOGO_MARK_CLASS,
}: ContactsPanelLogoProps) {
return (
-
Contacts
+
Contacts
)
}
diff --git a/components/gmail/email-list/email-list-layout.tsx b/components/gmail/email-list/email-list-layout.tsx
index c59e665..a9910e2 100644
--- a/components/gmail/email-list/email-list-layout.tsx
+++ b/components/gmail/email-list/email-list-layout.tsx
@@ -2,7 +2,7 @@
import { Pencil } from "lucide-react"
import { cn } from "@/lib/utils"
-import { buildSearchUrl } from "@/lib/mail-search/search-params"
+import { useBuildSearchUrl } from "@/hooks/use-build-search-url"
import { MobileXsBulkSheets } from "@/components/gmail/mobile-xs-bulk-sheets"
import { EmailListToolbar } from "@/components/gmail/email-list/email-list-toolbar"
import { EmailListBody } from "@/components/gmail/email-list/email-list-body"
@@ -30,6 +30,7 @@ export function EmailListLayout({
selection,
reading,
}: EmailListLayoutProps) {
+ const buildSearchUrl = useBuildSearchUrl()
const { onToggleSidebar } = props
const {
splitView,
diff --git a/components/gmail/email-list/email-list-toolbar.tsx b/components/gmail/email-list/email-list-toolbar.tsx
index 92746a3..3065c52 100644
--- a/components/gmail/email-list/email-list-toolbar.tsx
+++ b/components/gmail/email-list/email-list-toolbar.tsx
@@ -603,7 +603,7 @@ const mailPaginationControls = (mode: "list" | "view") => {
{openMailToolbar(false)}
- {mailPaginationControls("view")}
+ {countsMounted ? mailPaginationControls("view") : null}
)
}
@@ -1098,8 +1098,12 @@ const mailPaginationControls = (mode: "list" | "view") => {
- {listToolbarMode && !infiniteScroll ? mailPaginationControls("list") : null}
- {!splitView && !listToolbarMode ? mailPaginationControls("view") : null}
+ {listToolbarMode && !infiniteScroll && countsMounted
+ ? mailPaginationControls("list")
+ : null}
+ {!splitView && !listToolbarMode && countsMounted
+ ? mailPaginationControls("view")
+ : null}
{selectedFolder === "inbox" && (
diff --git a/components/gmail/email-list/hooks/use-email-list-data.ts b/components/gmail/email-list/hooks/use-email-list-data.ts
index 14fc8d4..28ba27c 100644
--- a/components/gmail/email-list/hooks/use-email-list-data.ts
+++ b/components/gmail/email-list/hooks/use-email-list-data.ts
@@ -10,7 +10,14 @@ import {
import { useSearchParams, useRouter } from "next/navigation"
import { useQueryClient } from "@tanstack/react-query"
import { buildLabelTextToNavColorClass } from "@/components/gmail/mail-label-pills"
-import { useMessages, useMailSearch, fetchMessagesPage, messagesQueryKey } from "@/lib/api/hooks/use-mail-queries"
+import { useIsDemoMail } from "@/lib/demo/demo-mail-context"
+import { useDemoMailStore } from "@/lib/demo/demo-mail-store"
+import {
+ useMessages,
+ useMailSearch,
+ fetchMessagesListPage,
+ messagesListQueryKey,
+} from "@/lib/api/hooks/use-mail-queries"
import {
useUpdateFlags,
useUpdateLabels,
@@ -49,9 +56,9 @@ import {
} from "@/lib/mail-url"
import {
parseSearchParams,
- buildSearchUrl,
type SearchParams,
} from "@/lib/mail-search/search-params"
+import { useBuildSearchUrl } from "@/hooks/use-build-search-url"
import { searchParamsToMessageSearchFilter } from "@/lib/mail-search/search-filter"
import { useSidebarNav, registerNavEmailSync } from "@/lib/sidebar-nav-context"
import { useMoveTargets } from "@/components/gmail/move-to-menu-items"
@@ -121,6 +128,7 @@ export function useEmailListData({
const showSplitReadingPane = splitView && openMailId !== null
const isSearchMode = selectedFolder === SEARCH_FOLDER_ID
const searchRouter = useRouter()
+ const buildSearchUrl = useBuildSearchUrl()
const searchAccount = useActiveAccount()
const setAdvancedOpen = useMailSearchStore((s) => s.setAdvancedOpen)
const urlSearchParams = useSearchParams()
@@ -134,7 +142,7 @@ export function useEmailListData({
if (!searchParams) return
searchRouter.push(buildSearchUrl({ ...searchParams, ...patch }))
},
- [searchParams, searchRouter]
+ [searchParams, searchRouter, buildSearchUrl]
)
const toggleSearchFilter = useCallback(
@@ -153,7 +161,7 @@ export function useEmailListData({
}
searchRouter.push(buildSearchUrl(next))
},
- [searchParams, searchRouter]
+ [searchParams, searchRouter, buildSearchUrl]
)
const { savedThreadReplyDrafts } = useComposeDrafts()
@@ -179,6 +187,8 @@ export function useEmailListData({
} = useScheduledMail()
const scheduledPersistHydrated = usePersistHydrated(useScheduledStore)
+ const isDemoMail = useIsDemoMail()
+ const demoVersion = useDemoMailStore((s) => s.version)
const accountId = searchAccount?.id
const queryClient = useQueryClient()
@@ -209,6 +219,11 @@ export function useEmailListData({
effectiveApiFolder === "__search__" || effectiveApiFolder === "__local__"
? "inbox"
: effectiveApiFolder
+ const messagesQueryContextKey = useMemo(
+ () =>
+ `${messagesApiFolder}:${accountId ?? ""}:${listPageSize}:${isDemoMail ? `demo-${demoVersion}` : "live"}`,
+ [messagesApiFolder, accountId, listPageSize, isDemoMail, demoVersion]
+ )
const messagesQueryPage = usesApiInfiniteScroll ? 1 : listPage
const messagesQuery = useMessages(
@@ -686,18 +701,21 @@ export function useEmailListData({
setIsFetchingNextInfinitePage(true)
try {
const result = await queryClient.fetchQuery({
- queryKey: messagesQueryKey(
+ queryKey: messagesListQueryKey(
messagesApiFolder,
accountId,
nextPage,
- listPageSize
+ listPageSize,
+ isDemoMail,
+ demoVersion
),
queryFn: () =>
- fetchMessagesPage(
+ fetchMessagesListPage(
messagesApiFolder,
accountId,
nextPage,
- listPageSize
+ listPageSize,
+ isDemoMail
),
staleTime: 60_000,
})
@@ -714,18 +732,21 @@ export function useEmailListData({
if (nextPage < totalPages) {
void queryClient.prefetchQuery({
- queryKey: messagesQueryKey(
+ queryKey: messagesListQueryKey(
messagesApiFolder,
accountId,
nextPage + 1,
- listPageSize
+ listPageSize,
+ isDemoMail,
+ demoVersion
),
queryFn: () =>
- fetchMessagesPage(
+ fetchMessagesListPage(
messagesApiFolder,
accountId,
nextPage + 1,
- listPageSize
+ listPageSize,
+ isDemoMail
),
staleTime: 60_000,
})
@@ -739,18 +760,29 @@ export function useEmailListData({
loadedApiPage,
totalPages,
queryClient,
- messagesApiFolder,
- accountId,
- listPageSize,
+ messagesQueryContextKey,
processEmailsForDisplay,
])
useEffect(() => {
if (!usesApiInfiniteScroll || loadedApiPage !== 1 || totalPages <= 1) return
void queryClient.prefetchQuery({
- queryKey: messagesQueryKey(messagesApiFolder, accountId, 2, listPageSize),
+ queryKey: messagesListQueryKey(
+ messagesApiFolder,
+ accountId,
+ 2,
+ listPageSize,
+ isDemoMail,
+ demoVersion
+ ),
queryFn: () =>
- fetchMessagesPage(messagesApiFolder, accountId, 2, listPageSize),
+ fetchMessagesListPage(
+ messagesApiFolder,
+ accountId,
+ 2,
+ listPageSize,
+ isDemoMail
+ ),
staleTime: 60_000,
})
}, [
@@ -758,9 +790,7 @@ export function useEmailListData({
loadedApiPage,
totalPages,
queryClient,
- messagesApiFolder,
- accountId,
- listPageSize,
+ messagesQueryContextKey,
])
const infiniteScrollSourceEmails = usesApiInfiniteScroll
@@ -1109,10 +1139,6 @@ export function useEmailListData({
listEmails,
listMailIndex,
listRowExtras,
- scrollInfiniteList,
- hasMoreInfinite,
- loadMoreSentinelRef,
- isFetchingNextInfinitePage,
moveTargets,
folderUnreadCounts,
unseenInTabById,
diff --git a/components/gmail/email-view/sandboxed-content.tsx b/components/gmail/email-view/sandboxed-content.tsx
index f229408..f3d5074 100644
--- a/components/gmail/email-view/sandboxed-content.tsx
+++ b/components/gmail/email-view/sandboxed-content.tsx
@@ -2,7 +2,6 @@
import {
useCallback,
- useEffect,
useLayoutEffect,
useMemo,
useRef,
@@ -21,6 +20,7 @@ import {
import {
EMAIL_PREVIEW_MIN_IFRAME_HEIGHT,
measureEmailPreviewIframeHeight,
+ resolveEmailPreviewMeasureRoot,
} from "@/lib/email-preview-iframe-height"
import {
buildEmailPreviewSrcdoc,
@@ -36,12 +36,43 @@ import {
const EMAIL_PREVIEW_IFRAME_STYLE: CSSProperties = {
display: "block",
background: "transparent",
+ overflow: "hidden",
}
function documentIsDark(): boolean {
return document.documentElement.classList.contains("dark")
}
+function bindEmailPreviewImageLoads(
+ root: HTMLElement,
+ onChange: () => void
+): () => void {
+ const images = root.querySelectorAll("img")
+ if (images.length === 0) return () => {}
+
+ let pending = 0
+ const onDone = () => {
+ pending -= 1
+ if (pending <= 0) onChange()
+ }
+
+ for (const img of images) {
+ if (img.complete) continue
+ pending += 1
+ img.addEventListener("load", onDone, { once: true })
+ img.addEventListener("error", onDone, { once: true })
+ }
+
+ if (pending === 0) return () => {}
+
+ return () => {
+ for (const img of images) {
+ img.removeEventListener("load", onDone)
+ img.removeEventListener("error", onDone)
+ }
+ }
+}
+
export function SandboxedContent({
html,
blockRemoteContent,
@@ -67,7 +98,10 @@ export function SandboxedContent({
const contrastLoggedKeyRef = useRef
(null)
const contrastDelayTimerRef = useRef(null)
const resizeObserverRef = useRef(null)
+ const unbindImageLoadsRef = useRef<(() => void) | null>(null)
const contentGenerationRef = useRef(0)
+ const appliedHeightRef = useRef(EMAIL_PREVIEW_MIN_IFRAME_HEIGHT)
+ const syncRafRef = useRef(null)
const [height, setHeight] = useState(EMAIL_PREVIEW_MIN_IFRAME_HEIGHT)
const sandboxValue = restrictPopups
@@ -133,23 +167,30 @@ export function SandboxedContent({
const iframeKey = `${messageId ?? "no-id"}:${previewPart}:${blockRemoteContent ? "remote-blocked" : "remote-allowed"}:${isDark ? "dark" : "light"}`
- const syncHeight = useCallback((generation: number) => {
+ const applyMeasuredHeight = useCallback((generation: number) => {
if (generation !== contentGenerationRef.current) return
const doc = iframeRef.current?.contentDocument
- if (!doc?.body) return
+ if (!doc) return
+
const next = measureEmailPreviewIframeHeight(doc)
- setHeight((prev) => (prev === next ? prev : next))
+ if (appliedHeightRef.current === next) return
+ appliedHeightRef.current = next
+ setHeight(next)
}, [])
const scheduleHeightSync = useCallback(
(generation: number) => {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- syncHeight(generation)
+ if (syncRafRef.current !== null) {
+ cancelAnimationFrame(syncRafRef.current)
+ }
+ syncRafRef.current = requestAnimationFrame(() => {
+ syncRafRef.current = requestAnimationFrame(() => {
+ syncRafRef.current = null
+ applyMeasuredHeight(generation)
})
})
},
- [syncHeight]
+ [applyMeasuredHeight]
)
const runContrastPipeline = useCallback(
@@ -207,13 +248,21 @@ export function SandboxedContent({
const handleIframeLoad = useCallback(() => {
const generation = contentGenerationRef.current
const doc = iframeRef.current?.contentDocument
+ const measureRoot = doc ? resolveEmailPreviewMeasureRoot(doc) : null
resizeObserverRef.current?.disconnect()
- if (doc?.body) {
+ unbindImageLoadsRef.current?.()
+ unbindImageLoadsRef.current = null
+
+ if (measureRoot) {
resizeObserverRef.current = new ResizeObserver(() => {
- syncHeight(generation)
+ scheduleHeightSync(generation)
+ })
+ resizeObserverRef.current.observe(measureRoot)
+
+ unbindImageLoadsRef.current = bindEmailPreviewImageLoads(measureRoot, () => {
+ scheduleHeightSync(generation)
})
- resizeObserverRef.current.observe(doc.body)
}
scheduleHeightSync(generation)
@@ -231,14 +280,21 @@ export function SandboxedContent({
runContrastPipeline(lateDoc, "delayed", generation)
}
}, 1000)
- }, [scheduleHeightSync, runContrastPipeline, syncHeight])
+ }, [scheduleHeightSync, runContrastPipeline])
useLayoutEffect(() => {
contentGenerationRef.current += 1
+ appliedHeightRef.current = EMAIL_PREVIEW_MIN_IFRAME_HEIGHT
setHeight(EMAIL_PREVIEW_MIN_IFRAME_HEIGHT)
contrastLoggedKeyRef.current = null
resizeObserverRef.current?.disconnect()
resizeObserverRef.current = null
+ unbindImageLoadsRef.current?.()
+ unbindImageLoadsRef.current = null
+ if (syncRafRef.current !== null) {
+ cancelAnimationFrame(syncRafRef.current)
+ syncRafRef.current = null
+ }
if (contrastDelayTimerRef.current !== null) {
window.clearTimeout(contrastDelayTimerRef.current)
contrastDelayTimerRef.current = null
@@ -250,6 +306,7 @@ export function SandboxedContent({
key={iframeKey}
ref={iframeRef}
sandbox={sandboxValue}
+ scrolling="no"
title="Contenu du message"
className="w-full border-0 bg-transparent"
style={{ ...EMAIL_PREVIEW_IFRAME_STYLE, height: `${height}px` }}
diff --git a/components/gmail/mail-search-bar.tsx b/components/gmail/mail-search-bar.tsx
index 22cc637..311278a 100644
--- a/components/gmail/mail-search-bar.tsx
+++ b/components/gmail/mail-search-bar.tsx
@@ -22,6 +22,8 @@ import { cn } from "@/lib/utils"
import { useSearchContacts } from "@/lib/api/hooks/use-contact-queries"
import { scoreApiContact } from "@/lib/contacts/contact-match-score"
import { useActiveAccount } from "@/lib/stores/account-store"
+import { useMailRouteRoot } from "@/lib/demo/demo-mail-context"
+import { buildMailPath, DEFAULT_INBOX_TAB } from "@/lib/mail-url"
import { useMailSearchStore } from "@/lib/stores/mail-search-store"
import {
bestCompletion,
@@ -54,7 +56,8 @@ export function MailSearchBar({
}: MailSearchBarProps) {
const router = useRouter()
const pathname = usePathname()
- const isOnSearchPage = pathname?.includes("/mail/search") ?? false
+ const routeRoot = useMailRouteRoot()
+ const isOnSearchPage = pathname?.includes(`/${routeRoot}/search`) ?? false
const urlSearchParams = useSearchParams()
const currentSearchParams = useMemo(
() => parseSearchParams(urlSearchParams),
@@ -136,6 +139,7 @@ export function MailSearchBar({
})
if (!Object.keys(params).length) return
submitMailSearch(router, params, {
+ routeRoot,
onAfter: () => {
setInputValue(q.trim())
setDropdownOpen(false)
@@ -143,7 +147,7 @@ export function MailSearchBar({
},
})
},
- [inputValue, chipAttachment, chipLast7Days, chipFromMe, account?.email, router]
+ [inputValue, chipAttachment, chipLast7Days, chipFromMe, account?.email, router, routeRoot]
)
const selectSuggestion = useCallback(
@@ -155,6 +159,7 @@ export function MailSearchBar({
fromEmail: account?.email ?? "",
})
submitMailSearch(router, params, {
+ routeRoot,
onAfter: () => {
setInputValue(s.email)
setDropdownOpen(false)
@@ -296,7 +301,17 @@ export function MailSearchBar({
onClick={() => {
reset()
if (isOnSearchPage) {
- router.push("/mail/inbox")
+ router.push(
+ buildMailPath(
+ {
+ folderId: "inbox",
+ inboxTab: DEFAULT_INBOX_TAB,
+ page: 1,
+ mailId: null,
+ },
+ routeRoot
+ )
+ )
} else {
inputRef.current?.focus()
}
diff --git a/components/gmail/mail-search/advanced-search-panel.tsx b/components/gmail/mail-search/advanced-search-panel.tsx
index 351a9cc..9133fe9 100644
--- a/components/gmail/mail-search/advanced-search-panel.tsx
+++ b/components/gmail/mail-search/advanced-search-panel.tsx
@@ -1,7 +1,8 @@
"use client"
import { useRouter } from "next/navigation"
-import { buildSearchUrl, type SearchParams } from "@/lib/mail-search/search-params"
+import { type SearchParams } from "@/lib/mail-search/search-params"
+import { useBuildSearchUrl } from "@/hooks/use-build-search-url"
import { useAdvancedSearchForm } from "@/lib/mail-search/use-advanced-search-form"
import { AdvancedSearchPanelDesktop } from "@/components/gmail/mail-search/advanced-search-fields"
@@ -15,6 +16,7 @@ export function AdvancedSearchPanel({
currentParams: SearchParams | null
}) {
const router = useRouter()
+ const buildSearchUrl = useBuildSearchUrl()
const form = useAdvancedSearchForm(initialQuery, currentParams)
const handleSubmit = () => {
diff --git a/components/gmail/mail-settings-fields.tsx b/components/gmail/mail-settings-fields.tsx
index 4c6c1ee..603f2c3 100644
--- a/components/gmail/mail-settings-fields.tsx
+++ b/components/gmail/mail-settings-fields.tsx
@@ -5,6 +5,7 @@ import {
MAIL_SETTINGS_PAGE_MASONRY_CLASS,
MAIL_SETTINGS_PAGE_MASONRY_SECTION_CLASS,
} from "@/lib/mail-chrome-classes"
+import { useThemeModeControls } from "@/lib/demo/use-theme-mode-controls"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
import type {
InboxSortMode,
@@ -187,8 +188,7 @@ export function MailSettingsFields({
}) {
const density = useMailSettingsStore((s) => s.density)
const setDensity = useMailSettingsStore((s) => s.setDensity)
- const themeMode = useMailSettingsStore((s) => s.themeMode)
- const setThemeMode = useMailSettingsStore((s) => s.setThemeMode)
+ const { themeMode, setThemeMode } = useThemeModeControls()
const backgroundId = useMailSettingsStore((s) => s.backgroundId)
const setBackgroundId = useMailSettingsStore((s) => s.setBackgroundId)
const inboxSort = useMailSettingsStore((s) => s.inboxSort)
diff --git a/components/gmail/mail-theme-applier.tsx b/components/gmail/mail-theme-applier.tsx
index 857fdae..84f0eb1 100644
--- a/components/gmail/mail-theme-applier.tsx
+++ b/components/gmail/mail-theme-applier.tsx
@@ -3,24 +3,50 @@
import { useEffect } from "react"
import { usePathname } from "next/navigation"
import { useTheme } from "next-themes"
-import { applyMailBackgroundDom } from "@/lib/mail-settings/mail-background-dom"
+import {
+ applyMailBackgroundDom,
+ clearMailBackgroundDom,
+} from "@/lib/mail-settings/mail-background-dom"
+import { isDemoPath } from "@/lib/demo/demo-route"
+import { useDemoThemeStore } from "@/lib/demo/demo-theme-store"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
-import { isDriveAppPath } from "@/lib/suite/drive-route"
+import { isMailAppPath } from "@/lib/suite/mail-route"
+import type { MailThemeMode } from "@/lib/mail-settings/types"
+
+function resolveAppliedThemeMode(
+ pathname: string | null,
+ mailThemeMode: MailThemeMode,
+ demoThemeMode: MailThemeMode,
+): MailThemeMode {
+ if (isDemoPath(pathname)) return demoThemeMode
+ return mailThemeMode
+}
/** Applique thème clair/sombre/système et fond décoratif sur le document. */
export function MailThemeApplier() {
const pathname = usePathname()
- const themeMode = useMailSettingsStore((s) => s.themeMode)
+ const mailThemeMode = useMailSettingsStore((s) => s.themeMode)
+ const demoThemeMode = useDemoThemeStore((s) => s.themeMode)
+ const appliedThemeMode = resolveAppliedThemeMode(
+ pathname,
+ mailThemeMode,
+ demoThemeMode,
+ )
const backgroundId = useMailSettingsStore((s) => s.backgroundId)
- const { setTheme } = useTheme()
+ const { theme, setTheme } = useTheme()
useEffect(() => {
- setTheme(themeMode)
- }, [themeMode, setTheme])
+ if (!theme || theme === appliedThemeMode) return
+ setTheme(appliedThemeMode)
+ }, [appliedThemeMode, theme, setTheme])
useEffect(() => {
- if (isDriveAppPath(pathname)) return
+ if (!isMailAppPath(pathname)) {
+ clearMailBackgroundDom()
+ return
+ }
applyMailBackgroundDom(backgroundId)
+ return () => clearMailBackgroundDom()
}, [backgroundId, pathname])
return null
diff --git a/components/gmail/mobile-search-overlay.tsx b/components/gmail/mobile-search-overlay.tsx
index 0a9d97d..55608ae 100644
--- a/components/gmail/mobile-search-overlay.tsx
+++ b/components/gmail/mobile-search-overlay.tsx
@@ -9,6 +9,7 @@ import {
type KeyboardEvent,
} from "react"
import { useRouter } from "next/navigation"
+import { useMailRouteRoot } from "@/lib/demo/demo-mail-context"
import {
ArrowLeft,
Search,
@@ -51,6 +52,7 @@ interface MobileSearchOverlayProps {
export function MobileSearchOverlay({ open, onClose, initialQuery = "" }: MobileSearchOverlayProps) {
const router = useRouter()
+ const routeRoot = useMailRouteRoot()
const account = useActiveAccount()
const inputValue = useMailSearchStore((s) => s.inputValue)
@@ -119,9 +121,9 @@ export function MobileSearchOverlay({ open, onClose, initialQuery = "" }: Mobile
fromEmail: account?.email ?? "",
})
if (!Object.keys(params).length) return
- submitMailSearch(router, params, { onAfter: onClose })
+ submitMailSearch(router, params, { routeRoot, onAfter: onClose })
},
- [inputValue, chipAttachment, chipLast7Days, chipFromMe, account?.email, router, onClose]
+ [inputValue, chipAttachment, chipLast7Days, chipFromMe, account?.email, router, routeRoot, onClose]
)
const selectSuggestion = useCallback(
@@ -132,9 +134,9 @@ export function MobileSearchOverlay({ open, onClose, initialQuery = "" }: Mobile
chipFromMe,
fromEmail: account?.email ?? "",
})
- submitMailSearch(router, params, { onAfter: onClose })
+ submitMailSearch(router, params, { routeRoot, onAfter: onClose })
},
- [chipAttachment, chipLast7Days, chipFromMe, account?.email, router, onClose]
+ [chipAttachment, chipLast7Days, chipFromMe, account?.email, router, routeRoot, onClose]
)
const handleKeyDown = useCallback(
diff --git a/components/gmail/quick-settings/quick-settings-panel.tsx b/components/gmail/quick-settings/quick-settings-panel.tsx
index 4b21bbe..aa59e26 100644
--- a/components/gmail/quick-settings/quick-settings-panel.tsx
+++ b/components/gmail/quick-settings/quick-settings-panel.tsx
@@ -3,8 +3,10 @@
import Link from "next/link"
import { X } from "lucide-react"
import { Button } from "@/components/ui/button"
+import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"
import { MailSettingsFields } from "@/components/gmail/mail-settings-fields"
import { useMailSettingsStore } from "@/lib/stores/mail-settings-store"
+import { cn } from "@/lib/utils"
export function QuickSettingsPanel() {
const open = useMailSettingsStore((s) => s.quickSettingsOpen)
@@ -12,27 +14,26 @@ export function QuickSettingsPanel() {
const setOpen = useMailSettingsStore((s) => s.setQuickSettingsOpen)
const setThemeDialogOpen = useMailSettingsStore((s) => s.setThemeDialogOpen)
- if (!open) return null
-
return (
- <>
- {!themeDialogOpen && (
-
-
- >
+
+
)
}
diff --git a/components/gmail/settings/automation/api-token-agenda-scope-editor.tsx b/components/gmail/settings/automation/api-token-agenda-scope-editor.tsx
new file mode 100644
index 0000000..3dfa475
--- /dev/null
+++ b/components/gmail/settings/automation/api-token-agenda-scope-editor.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import type { ApiTokenAgendaScope } from "@/lib/api/types"
+import type { WebhookAgendaScope } from "@/lib/mail-automation/webhook-config"
+import { WebhookAgendaScopeEditor } from "@/components/gmail/settings/automation/webhook-agenda-scope-editor"
+
+export function ApiTokenAgendaScopeEditor({
+ scope,
+ onChange,
+ enabled,
+ className,
+}: {
+ scope: ApiTokenAgendaScope
+ onChange: (scope: ApiTokenAgendaScope) => void
+ enabled: boolean
+ className?: string
+}) {
+ return (
+
+ )
+}
diff --git a/components/gmail/settings/automation/api-tokens-panel.tsx b/components/gmail/settings/automation/api-tokens-panel.tsx
index b3c4756..1eb54c6 100644
--- a/components/gmail/settings/automation/api-tokens-panel.tsx
+++ b/components/gmail/settings/automation/api-tokens-panel.tsx
@@ -14,6 +14,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ApiTokenCreatedDialog } from "@/components/gmail/settings/automation/api-token-created-dialog"
import { ApiTokenDriveScopeEditor } from "@/components/gmail/settings/automation/api-token-drive-scope-editor"
+import { ApiTokenAgendaScopeEditor } from "@/components/gmail/settings/automation/api-token-agenda-scope-editor"
import { ApiTokenMailScopeEditor } from "@/components/gmail/settings/automation/api-token-mail-scope-editor"
import { ApiTokenPermissionEditor } from "@/components/gmail/settings/automation/api-token-permission-editor"
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
@@ -27,8 +28,10 @@ import { useAuthReady } from "@/lib/api/use-auth-ready"
import type { ApiTokenCreated } from "@/lib/api/types"
import {
defaultDriveScope,
+ defaultAgendaScope,
defaultMailScope,
emptyPermissionGrants,
+ hasAgendaPermissions,
hasAnyPermission,
hasDrivePermissions,
hasMailPermissions,
@@ -58,18 +61,21 @@ export function ApiTokensPanel() {
const [permissions, setPermissions] = useState