From 22e7b8e1d2c2a133de8d5eb41978417e347a2ecc Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Fri, 15 May 2026 23:22:24 +0200 Subject: [PATCH] Major improvements on mobile --- app/mail/mail-app-shell.tsx | 53 ++--- components/gmail/compose-modal.tsx | 294 ++++++++++++++++------- components/gmail/email-list.tsx | 368 +++++++++++++++++++++++------ components/gmail/header.tsx | 6 +- components/gmail/sidebar.tsx | 81 +++---- components/ui/sheet.tsx | 16 +- hooks/use-xs.ts | 16 +- lib/label-edits.ts | 14 ++ lib/mail-nav-metrics.ts | 8 +- lib/scheduled-mail-context.tsx | 8 + lib/stores/mail-store.ts | 21 +- lib/stores/scheduled-store.ts | 57 +++++ tsconfig.tsbuildinfo | 2 +- 13 files changed, 689 insertions(+), 255 deletions(-) diff --git a/app/mail/mail-app-shell.tsx b/app/mail/mail-app-shell.tsx index 90abba5..1f5b952 100644 --- a/app/mail/mail-app-shell.tsx +++ b/app/mail/mail-app-shell.tsx @@ -4,12 +4,13 @@ import { Suspense, useCallback, useEffect, + useLayoutEffect, useMemo, useState, type CSSProperties, } from "react" -import dynamic from "next/dynamic" -import { useIsXs } from "@/hooks/use-xs" +import { readXsMatches, useIsXs } from "@/hooks/use-xs" +import { MobileBottomBar } from "@/components/gmail/mobile-bottom-bar" import { Toaster } from "sonner" import { useRouter, usePathname } from "next/navigation" import { Sidebar } from "@/components/gmail/sidebar" @@ -24,14 +25,6 @@ import { ComposeModalManager } from "@/components/gmail/compose-modal" import { SidebarNavProvider } from "@/lib/sidebar-nav-context" import { mailNavVisitKey } from "@/lib/mail-folder-display" import { useMailStore } from "@/lib/stores/mail-store" - -const MobileBottomBar = dynamic( - () => - import("@/components/gmail/mobile-bottom-bar").then( - (m) => m.MobileBottomBar - ), - { ssr: false } -) import { parseMailSegments, buildMailPath, @@ -54,7 +47,12 @@ function MailAppInner() { const isXs = useIsXs() const pushRecentFolderVisit = useMailStore((s) => s.pushRecentFolderVisit) - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + /** Start closed so narrow viewports match SSR/CSS before JS runs; desktop opens in layout. */ + const [sidebarCollapsed, setSidebarCollapsed] = useState(true) + + useLayoutEffect(() => { + if (!readXsMatches()) setSidebarCollapsed(false) + }, []) useEffect(() => { if (isXs) setSidebarCollapsed(true) @@ -91,9 +89,9 @@ function MailAppInner() { page: 1, mailId: null, }) - if (isXs) setSidebarCollapsed(true) + if (readXsMatches()) setSidebarCollapsed(true) }, - [navigateRoute, isXs] + [navigateRoute] ) return ( @@ -109,36 +107,33 @@ function MailAppInner() { } >
- {!isXs && ( +
setSidebarCollapsed(!sidebarCollapsed)} /> - )} +
- {isXs && !sidebarCollapsed && ( + {!sidebarCollapsed && (
) @@ -179,7 +172,7 @@ export function MailAppShell({ -
+
} diff --git a/components/gmail/compose-modal.tsx b/components/gmail/compose-modal.tsx index 92cba0a..f806d31 100644 --- a/components/gmail/compose-modal.tsx +++ b/components/gmail/compose-modal.tsx @@ -10,6 +10,8 @@ import { lazy, Suspense, } from "react" +import { useIsXs } from "@/hooks/use-xs" +import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet" import { useEditor, EditorContent } from "@tiptap/react" import { Editor, Node as TipTapNode, mergeAttributes, type Extensions } from "@tiptap/core" import StarterKit from "@tiptap/starter-kit" @@ -141,6 +143,9 @@ function insertSignatureHtml(html: string, sigId: string | null) { return clean + `

--

${sig.html}
` } +/** Menus/popovers Radix default z-50 ; compose sheet content uses z-61+. */ +const COMPOSE_PORTAL_Z = "z-[100]" + const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ function RecipientField({ @@ -408,7 +413,10 @@ function AlignmentDropdown({ - + editor.chain().focus().setTextAlign("left").run()} className={cn(editor.isActive({ textAlign: "left" }) && "bg-[#e8eaed]")} @@ -483,7 +491,10 @@ function FontDropdown({ - + {FONT_FAMILIES.map((f) => ( - + {FONT_SIZES.map((s) => ( - e.preventDefault()}> + e.preventDefault()} + >
- + { e.preventDefault() @@ -1076,7 +1098,7 @@ function ComposeRecipientFields({ {showFromField && (
De - + - + {DEFAULT_IDENTITIES.map((id) => ( void) | null) => void }) { const { closeCompose, @@ -1262,7 +1289,11 @@ export function ComposeWindow({ attributes: { class: cn( "prose prose-sm max-w-none px-3 py-2 text-sm text-[#202124] outline-none focus:outline-none", - isInline ? "min-h-[200px]" : "min-h-[150px]" + isInline + ? "min-h-[200px]" + : isXsSheet + ? "min-h-[min(36vh,280px)]" + : "min-h-[150px]" ), }, }, @@ -1307,6 +1338,17 @@ export function ComposeWindow({ } } + const handleCloseRef = useRef(handleClose) + handleCloseRef.current = handleClose + + useLayoutEffect(() => { + if (!isXsSheet || !bindXsSheetClose) return + bindXsSheetClose(() => { + handleCloseRef.current() + }) + return () => bindXsSheetClose(null) + }, [isXsSheet, bindXsSheetClose, compose.id]) + const htmlToPreviewText = useCallback((html: string) => { return html .replace(/]*>[\s\S]*?<\/style>/gi, " ") @@ -1500,7 +1542,7 @@ export function ComposeWindow({ } }, []) - const showFromField = recipientsFocused + const showFromField = recipientsFocused || isXsSheet useLayoutEffect(() => { if (!isInline || !compose.focusToOnMount) return @@ -1509,10 +1551,14 @@ export function ComposeWindow({ useEffect(() => { if (!recipientsFocused) return - const handleClickOutside = (e: MouseEvent) => { + const handleClickOutside = (e: Event) => { + const target = e.target as Node const root = isInline ? inlineRecipientShellRef.current : fieldsRef.current - if (root && !root.contains(e.target as Node)) { - const portal = (e.target as HTMLElement)?.closest?.("[data-radix-popper-content-wrapper]") + if (root && !root.contains(target)) { + const el = e.target as HTMLElement | null + const portal = el?.closest?.( + "[data-radix-popper-content-wrapper], [data-radix-dropdown-menu-content], [data-slot='dropdown-menu-content'], [data-slot='popover-content']" + ) if (portal) return setRecipientsFocused(false) if (compose.showCc && compose.cc.length === 0) { @@ -1523,8 +1569,8 @@ export function ComposeWindow({ } } } - document.addEventListener("mousedown", handleClickOutside) - return () => document.removeEventListener("mousedown", handleClickOutside) + document.addEventListener("pointerdown", handleClickOutside) + return () => document.removeEventListener("pointerdown", handleClickOutside) }, [ recipientsFocused, isInline, @@ -1645,12 +1691,14 @@ export function ComposeWindow({ "relative flex flex-col overflow-hidden bg-white", isInline ? "min-h-[360px] w-full rounded-xl border border-[#dadce0] shadow-none transition-shadow focus-within:shadow-[0_1px_4px_rgba(60,64,67,0.12)]" - : cn( - "rounded-t-lg shadow-[0_-2px_8px_rgba(0,0,0,0.08),_-4px_0_12px_rgba(0,0,0,0.12),_4px_0_12px_rgba(0,0,0,0.12)]", - compose.maximized - ? "fixed inset-12 z-60 rounded-lg" - : "h-[480px] w-[500px]" - ) + : isXsSheet + ? "h-full min-h-0 w-full max-w-none flex-1 rounded-none shadow-none" + : cn( + "rounded-t-lg shadow-[0_-2px_8px_rgba(0,0,0,0.08),_-4px_0_12px_rgba(0,0,0,0.12),_4px_0_12px_rgba(0,0,0,0.12)]", + compose.maximized + ? "fixed inset-12 z-60 rounded-lg" + : "h-[480px] w-[500px]" + ) )} onDrop={handleDrop} onDragOver={handleDragOver} @@ -1702,7 +1750,7 @@ export function ComposeWindow({ : undefined } > - +
- ) : ( - <> - {/* Title bar */} -
toggleMinimize(compose.id)} - > - - {titleText} - -
+ ) : isXsSheet ? ( +
+ + {titleText} + - -
-
+ ) : ( + <> + {/* Title bar */} +
toggleMinimize(compose.id)} + > + + {titleText} + +
+ + + +
+
)} @@ -1941,7 +2008,7 @@ export function ComposeWindow({ > Enregistrer - + - + { void sendScheduledFromEditNow() @@ -1964,7 +2031,7 @@ export function ComposeWindow({ Planifier - + { void applyScheduledPlanAt( @@ -1999,7 +2066,7 @@ export function ComposeWindow({ > Envoyer - + - + { void submitScheduledSendAt( @@ -2108,7 +2175,7 @@ export function ComposeWindow({
) - if (compose.minimized && !isInline) { + if (compose.minimized && !isInline && !isXsSheet) { return (
!w.maximized && w.placement !== "inline" ) const maximized = composeWindows.filter((w) => w.maximized && w.placement !== "inline") + const xsSheetCloseRef = useRef<(() => void) | null>(null) + const bindXsSheetClose = useCallback((fn: (() => void) | null) => { + xsSheetCloseRef.current = fn + }, []) + + /** Une seule fenêtre dock visible en xs : la plus récente (comportement type pile). */ + const xsActiveDock = + isXs && nonMaximized.length > 0 ? nonMaximized[nonMaximized.length - 1] : null + + const handleXsSheetOpenChange = useCallback((open: boolean) => { + if (!open) { + xsSheetCloseRef.current?.() + } + }, []) + const MODAL_WIDTH = 500 const MINIMIZED_WIDTH = 280 const GAP = 12 @@ -2188,6 +2271,39 @@ export function ComposeModalManager() { return result }, [nonMaximized]) + if (isXs) { + return ( + <> + + + + {(xsActiveDock?.subject ?? "").trim() || "Nouveau message"} + + {xsActiveDock ? ( + + ) : null} + + + + {maximized.map((compose) => ( +
+ +
+ ))} + + ) + } + return ( <> {nonMaximized.map((compose) => { diff --git a/components/gmail/email-list.tsx b/components/gmail/email-list.tsx index c7d6fa9..c5992d0 100644 --- a/components/gmail/email-list.tsx +++ b/components/gmail/email-list.tsx @@ -56,6 +56,7 @@ import { CalendarClock, X, CheckSquare, + Inbox as InboxIcon, } from "lucide-react" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" @@ -125,10 +126,11 @@ import { computeFolderUnreadCounts } from "@/lib/mail-nav-metrics" import { effectiveLabels, mergeEmailLabelEdits, + mergeEmailNotSpam, } from "@/lib/label-edits" import type { LabelEditState } from "@/lib/stores/mail-store" import type { MailRouteState } from "@/lib/mail-url" -import { useIsXs } from "@/hooks/use-xs" +import { readXsMatches, useIsXs } from "@/hooks/use-xs" addCollection(mdiIcons) @@ -574,6 +576,14 @@ function listRowCheckboxClass(circular: boolean) { ) } +function listRowQuickHoverTrayToneClass(isSelected: boolean, isRead: boolean) { + return isSelected + ? "bg-[#e8f0fe]" + : isRead + ? "bg-[#f5f5f5]" + : "bg-white" +} + export function EmailList({ selectedFolder, inboxTab, @@ -603,6 +613,8 @@ export function EmailList({ requestRescheduleScheduled, requestGetScheduledEditPayload, requestSendScheduledNow, + requestSnoozeMailboxEmail, + requestRestoreSnoozedToInbox, } = useScheduledMail() const allEmails = useMemo( @@ -723,6 +735,7 @@ export function EmailList({ }, [allEmails]) const [labelPickerQuery, setLabelPickerQuery] = useState("") const hiddenEmailIds = useMailStore((s) => s.hiddenEmailIds) + const notSpamEmailIds = useMailStore((s) => s.notSpamEmailIds) const recentMoveTargets = useMailStore((s) => s.recentMoveTargets) const rowContextMenuOpenedAtRef = useRef(0) const contextMenuTargetIdsRef = useRef([]) @@ -873,7 +886,9 @@ export function EmailList({ const filteredEmails = useMemo(() => { const visible = allEmails .filter((email) => !hiddenEmailIds.includes(email.id)) - .map((e) => mergeEmailLabelEdits(e, labelEdits)) + .map((e) => + mergeEmailNotSpam(mergeEmailLabelEdits(e, labelEdits), notSpamEmailIds) + ) let rows = visible.filter((email) => emailMatchesFolder(email, selectedFolder, folderFilterCtx, navMaps) ) @@ -887,6 +902,7 @@ export function EmailList({ hiddenEmailIds, folderFilterCtx, labelEdits, + notSpamEmailIds, allEmails, navMaps, ]) @@ -1116,13 +1132,16 @@ export function EmailList({ for (const l of collectTreeLabels(sidebarNav.folderTree)) s.add(l) for (const row of sidebarNav.labelRows) s.add(row.label) for (const e of allEmails) { - const eff = mergeEmailLabelEdits(e, labelEdits) + const eff = mergeEmailNotSpam( + mergeEmailLabelEdits(e, labelEdits), + notSpamEmailIds + ) for (const lab of eff.labels ?? []) { if (!LABEL_PICKER_EXCLUDE.has(lab)) s.add(lab) } } return [...s].sort((a, b) => a.localeCompare(b, "fr")) - }, [sidebarNav.folderTree, sidebarNav.labelRows, allEmails, labelEdits]) + }, [sidebarNav.folderTree, sidebarNav.labelRows, allEmails, labelEdits, notSpamEmailIds]) const resolveLabelCasing = useCallback( (raw: string) => { @@ -1264,9 +1283,18 @@ export function EmailList({ hiddenEmailIds, readOverrides, navMaps, - labelEdits + labelEdits, + notSpamEmailIds ), - [folderFilterCtx, hiddenEmailIds, readOverrides, allEmails, navMaps, labelEdits] + [ + folderFilterCtx, + hiddenEmailIds, + readOverrides, + allEmails, + navMaps, + labelEdits, + notSpamEmailIds, + ] ) const pageIds = useMemo(() => listEmails.map((e) => e.id), [listEmails]) @@ -1338,7 +1366,9 @@ export function EmailList({ const hidden = new Set(hiddenEmailIds) const visible = allEmails .filter((email) => !hidden.has(email.id)) - .map((e) => mergeEmailLabelEdits(e, labelEdits)) + .map((e) => + mergeEmailNotSpam(mergeEmailLabelEdits(e, labelEdits), notSpamEmailIds) + ) const inboxPool = visible.filter((e) => emailMatchesFolder(e, "inbox", folderFilterCtx, navMaps) ) @@ -1361,7 +1391,7 @@ export function EmailList({ preview[tab.id] = chain.join(", ") } return { unseenInTabById: counts, tabUnseenSenderLineById: preview } - }, [folderFilterCtx, hiddenEmailIds, labelEdits, seenSerialized, allEmails, navMaps]) + }, [folderFilterCtx, hiddenEmailIds, labelEdits, seenSerialized, allEmails, navMaps, notSpamEmailIds]) const effectiveStarred = (email: Email) => starredEmails.includes(email.id) || email.starred @@ -1512,8 +1542,8 @@ export function EmailList({ const raw = allEmails.find((e) => e.id === openMailId) ?? null if (!raw) return null if (raw.labels?.includes("scheduled")) return null - return mergeEmailLabelEdits(raw, labelEdits) - }, [openMailId, labelEdits, allEmails]) + return mergeEmailNotSpam(mergeEmailLabelEdits(raw, labelEdits), notSpamEmailIds) + }, [openMailId, labelEdits, allEmails, notSpamEmailIds]) const openMailIndex = useMemo( () => (openMailId ? filteredEmails.findIndex((e) => e.id === openMailId) : -1), [openMailId, filteredEmails] @@ -1544,6 +1574,33 @@ export function EmailList({ const goBack = useCallback(() => navigateToMail(null), [navigateToMail]) + const closeViewIfShowingEmail = useCallback( + (emailId: string) => { + if (openMailId === emailId) goBack() + }, + [openMailId, goBack] + ) + + const restoreSnoozedRowToMailbox = useCallback( + (emailRow: Email) => { + void requestRestoreSnoozedToInbox(emailRow) + if (emailRow.id.startsWith("snz-")) { + const baseId = emailRow.id.slice(4) + if (baseId.length > 0) mailActions.unhideEmail(baseId) + onSelectFolder?.("inbox") + } else { + onSelectFolder?.("scheduled") + } + closeViewIfShowingEmail(emailRow.id) + }, + [ + requestRestoreSnoozedToInbox, + mailActions, + closeViewIfShowingEmail, + onSelectFolder, + ] + ) + const handleCategoryInboxTabClick = useCallback( (tabId: string) => { onMailRouteNavigate({ @@ -1656,7 +1713,7 @@ export function EmailList({ const singleNotSpam = useCallback(() => { if (!openMailId) return - mailActions.hideEmail(openMailId) + mailActions.markNotSpam(openMailId) onSelectFolder?.("inbox") goBack() }, [openMailId, goBack, onSelectFolder, mailActions]) @@ -1738,8 +1795,8 @@ export function EmailList({ return (
{/* Mobile xs top bar */} - {isXs && !isViewMode && ( -
+ {!isViewMode && ( +

{mobileFolderLabel} @@ -1891,7 +1948,7 @@ export function EmailList({ className={cn( "relative z-20 flex shrink-0 min-h-12 gap-2 rounded-t-2xl border-b border-gray-200 bg-white py-1.5 pl-2 pr-4", isViewMode ? "items-start" : "items-center", - isXs && !isViewMode && "hidden" + !isViewMode && "max-sm:hidden" )} > @@ -2429,7 +2486,7 @@ export function EmailList({
{filteredEmails.length === 0 ? ( @@ -2601,11 +2658,11 @@ export function EmailList({
- {isXs && !isViewMode ? ( + {!isViewMode && (
@@ -2618,12 +2675,11 @@ export function EmailList({ style={{ opacity: 0 }} />
- ) : null} + )}
{isViewMode && openEmail ? ( @@ -2739,6 +2795,8 @@ export function EmailList({ }) const isRescheduleOpenThisRow = rescheduleTarget?.id === email.id + const spamRowHoverNoArchive = selectedFolder === "spam" + const snoozedFolderRow = selectedFolder === "snoozed" return ( startRowDrag(email.id, e)} onClick={() => { - if (isXs && mobileSelectionMode) { + if (readXsMatches() && mobileSelectionMode) { toggleSelect(email.id) lastSelectionAnchorIdRef.current = email.id return @@ -2776,7 +2834,7 @@ export function EmailList({ handleRowActivate(email) }} className={cn( - "group relative z-0 w-full cursor-pointer pl-3 pr-2 py-2 transition-[background-color,box-shadow] duration-150 md:flex md:items-start md:gap-2 md:px-2 md:py-1.5", + "group relative z-0 w-full cursor-pointer pl-3 pr-2 py-2 transition-[background-color,box-shadow] duration-[50ms] ease-out md:flex md:items-start md:gap-2 md:px-2 md:py-1.5", isSelected ? "bg-[#e8f0fe]" : isRead @@ -2788,13 +2846,14 @@ export function EmailList({ {/* Compact < md */}
- {isXs && mobileSelectionMode && ( + {mobileSelectionMode && (
e.stopPropagation()} onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)} > @@ -2811,18 +2870,17 @@ export function EmailList({
- {!isXs && (
e.stopPropagation()} onClickCapture={(e) => handleRowCheckboxClickCapture(email.id, e)} > { toggleSelect(email.id) @@ -2830,7 +2888,6 @@ export function EmailList({ }} />
- )}
-
+
{email.tag && ( {email.tag} @@ -2967,7 +3024,7 @@ export function EmailList({
-
+

{email.preview}

@@ -3077,7 +3134,7 @@ export function EmailList({
@@ -3147,8 +3204,7 @@ export function EmailList({
@@ -3156,8 +3212,8 @@ export function EmailList({
+ {!spamRowHoverNoArchive && (
) : ( -
- {(parsedInvitation || hasInvitation) && ( - - )} - - {email.date} - +
+
+ {(parsedInvitation || hasInvitation) && ( + + )} + + {email.date} + +
+
+ {!spamRowHoverNoArchive && ( + + + + + + Archiver + + + )} + + + + + + Supprimer + + + + + + + + {isRead ? "Marquer comme non lu" : "Marquer comme lu"} + + + {spamRowHoverNoArchive && ( + + + + + + Boîte de réception + + + )} + {!spamRowHoverNoArchive && + (snoozedFolderRow ? ( + + + + + + {email.id.startsWith("snz-") + ? "Boîte de réception" + : "Planifiés"} + + + ) : ( + + + + + + Mettre en attente + + + ))} +
)}
@@ -3817,8 +4039,8 @@ export function EmailList({ )} - {!isXs && !isViewMode ? ( -
+ {!isViewMode ? ( +
)} {/* Google Apps Menu */} @@ -121,7 +121,7 @@ export function Header({ aria-label="Applications" onClick={() => setAppsMenuOpen(!appsMenuOpen)} > - + {appsMenuOpen && ( diff --git a/components/gmail/sidebar.tsx b/components/gmail/sidebar.tsx index f257279..33eb909 100644 --- a/components/gmail/sidebar.tsx +++ b/components/gmail/sidebar.tsx @@ -31,6 +31,7 @@ import { Check, } from "lucide-react" import { cn, formatCount } from "@/lib/utils" +import { readXsMatches } from "@/hooks/use-xs" import { useState, useRef, useEffect, useMemo, type ReactNode, type CSSProperties } from "react" import { useEmailDropTarget } from "@/lib/drag-context" import { useCompose } from "@/lib/compose-context" @@ -107,8 +108,6 @@ interface SidebarProps { selectedFolder: string onSelectFolder: (folder: string) => void collapsed: boolean - /** Viewport below `sm` — drawer overlay, no rail. */ - isXs?: boolean /** Nombre de messages non lus par id de ligne (boîte, catégorie, dossier, libellé). */ folderUnreadCounts?: Record } @@ -503,7 +502,6 @@ export function Sidebar({ selectedFolder, onSelectFolder, collapsed, - isXs = false, folderUnreadCounts = {}, }: SidebarProps) { const { openCompose } = useCompose() @@ -514,8 +512,7 @@ export function Sidebar({ const hoverTimeoutRef = useRef(null) const sidebarRef = useRef(null) - const isOverlayOpen = isXs && !collapsed - const isExpanded = isOverlayOpen || !collapsed || hoverExpanded + const isExpanded = !collapsed || hoverExpanded const { folderTree, @@ -666,7 +663,7 @@ export function Sidebar({ }, [selectedFolder]) const handleMouseEnter = () => { - if (isXs) return + if (readXsMatches()) return if (collapsed) { hoverTimeoutRef.current = setTimeout(() => { setHoverExpanded(true) @@ -675,11 +672,11 @@ export function Sidebar({ } const handleMouseLeave = () => { - if (isXs) return if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current) hoverTimeoutRef.current = null } + if (readXsMatches()) return setHoverExpanded(false) } @@ -1731,48 +1728,44 @@ export function Sidebar({ className={cn( "absolute left-0 top-0 bottom-0 flex flex-col overflow-hidden bg-app-canvas transition-[width,transform] duration-200 z-40", isExpanded ? "w-60" : "w-[68px]", - (hoverExpanded || isOverlayOpen) && "shadow-xl border-r border-gray-200", - isOverlayOpen && "z-50", - isXs && collapsed && "-translate-x-full pointer-events-none" + hoverExpanded && "shadow-xl border-r border-gray-200", + !collapsed && "max-sm:z-50 max-sm:shadow-xl max-sm:border-r max-sm:border-gray-200", + collapsed && "max-sm:-translate-x-full max-sm:pointer-events-none" )} > - {isXs && ( -
- - -
- )} +
+ + +
- {!isXs && ( -
+ -
- )} + + {isExpanded && ( + + Nouveau message + + )} + +