11"use client"
22
3- import { useEffect } from "react"
3+ import { Suspense , useEffect } from "react"
44import dynamic from "next/dynamic"
5- import { useRouter } from "next/navigation"
5+ import { usePathname , useRouter , useSearchParams } from "next/navigation"
66import { PanelLeft } from "lucide-react"
77import { Sheet , SheetContent , SheetDescription , SheetTitle } from "@/components/ui/sheet"
88import { Button } from "@/components/ui/button"
@@ -11,6 +11,8 @@ import { inboxUI, useInboxUI } from "@/hooks/inbox/store"
1111import { useHotkeys } from "@/hooks/inbox/use-hotkeys"
1212import { usePersistentState } from "@/hooks/inbox/use-persistent-state"
1313import { useRealtime } from "@/hooks/inbox/use-realtime"
14+ import { parseAddresses } from "@/lib/inbox/format"
15+ import { keysFor , type ShortcutAction } from "@/lib/inbox/shortcuts"
1416import type { Bootstrap } from "@/lib/inbox/types"
1517import { InboxProvider , useInbox } from "./inbox-provider"
1618import { ResizeHandle } from "./resize-handle"
@@ -37,7 +39,7 @@ export function MailShell({ slug, bootstrap, children }: { slug: string; bootstr
3739}
3840
3941function ShellInner ( { children } : { children : React . ReactNode } ) {
40- const { slug, meId, bootstrap, shortcutsEnabled } = useInbox ( )
42+ const { slug, meId, bootstrap, shortcutsEnabled, shortcutScheme } = useInbox ( )
4143 const router = useRouter ( )
4244 const [ width , setWidth ] = usePersistentState ( "dispatch:sidebar:width" , SIDEBAR . default )
4345 const [ collapsed , setCollapsed ] = usePersistentState ( "dispatch:sidebar:collapsed" , false )
@@ -72,28 +74,29 @@ function ShellInner({ children }: { children: React.ReactNode }) {
7274
7375 const go = ( box : string ) => router . push ( `/w/${ slug } /${ box } ` )
7476 const firstTeam = bootstrap . teams . find ( ( t ) => t . isMember ) ?? bootstrap . teams [ 0 ]
77+ const k = ( action : ShortcutAction ) => keysFor ( shortcutScheme , action )
78+ const togglePalette = ( ) => inboxUI . set ( ( s ) => ( { paletteOpen : ! s . paletteOpen , paletteQuery : "" } ) )
7579 useHotkeys (
7680 [
77- { keys : [ "mod+k" ] , allowInInput : true , handler : ( ) => inboxUI . set ( ( s ) => ( { paletteOpen : ! s . paletteOpen , paletteQuery : "" } ) ) } ,
78- { keys : [ "/" ] , handler : ( ) => inboxUI . set ( { paletteOpen : true , paletteQuery : "" } ) } ,
79- { keys : [ "n" ] , handler : ( ) => inboxUI . openCompose ( ) } ,
80- { keys : [ "?" , "shift+?" ] , handler : ( ) => inboxUI . set ( { shortcutsOpen : true } ) } ,
81- { keys : [ "g i" ] , handler : ( ) => go ( "inbox" ) } ,
82- { keys : [ "g a" ] , handler : ( ) => go ( "assigned" ) } ,
83- { keys : [ "g s" ] , handler : ( ) => go ( "starred" ) } ,
84- { keys : [ "g d" ] , handler : ( ) => go ( "drafts" ) } ,
85- { keys : [ "g m" ] , handler : ( ) => go ( "mentions" ) } ,
86- { keys : [ "g c" ] , handler : ( ) => go ( "chats" ) } ,
87- { keys : [ "g t" ] , handler : ( ) => ( firstTeam ? go ( `team.${ firstTeam . id } ` ) : go ( "unassigned" ) ) } ,
88- { keys : [ "mod+shift+n" ] , allowInInput : true , handler : ( ) => inboxUI . set ( { newChatOpen : true } ) } ,
81+ { keys : [ "mod+k" ] , allowInInput : true , handler : togglePalette } ,
82+ { keys : k ( "search" ) . filter ( ( key ) => key !== "mod+k" ) , handler : ( ) => inboxUI . set ( { paletteOpen : true , paletteQuery : "" } ) } ,
83+ { keys : k ( "compose" ) , handler : ( ) => inboxUI . openCompose ( ) } ,
84+ { keys : [ ...k ( "help" ) , "shift+?" ] , handler : ( ) => inboxUI . set ( { shortcutsOpen : true } ) } ,
85+ { keys : k ( "goInbox" ) , handler : ( ) => go ( "inbox" ) } ,
86+ { keys : k ( "goAssigned" ) , handler : ( ) => go ( "assigned" ) } ,
87+ { keys : k ( "goStarred" ) , handler : ( ) => go ( "starred" ) } ,
88+ { keys : k ( "goDrafts" ) , handler : ( ) => go ( "drafts" ) } ,
89+ { keys : k ( "goMentions" ) , handler : ( ) => go ( "mentions" ) } ,
90+ { keys : k ( "goChats" ) , handler : ( ) => go ( "chats" ) } ,
91+ { keys : k ( "goSent" ) , handler : ( ) => go ( "sent" ) } ,
92+ { keys : k ( "goAll" ) , handler : ( ) => go ( "all" ) } ,
93+ { keys : k ( "goTeam" ) , handler : ( ) => ( firstTeam ? go ( `team.${ firstTeam . id } ` ) : go ( "unassigned" ) ) } ,
94+ { keys : k ( "newChat" ) , allowInInput : true , handler : ( ) => inboxUI . set ( { newChatOpen : true } ) } ,
8995 ] ,
9096 shortcutsEnabled
9197 )
9298 // ⌘K must work even with shortcuts turned off.
93- useHotkeys (
94- [ { keys : [ "mod+k" ] , allowInInput : true , handler : ( ) => inboxUI . set ( ( s ) => ( { paletteOpen : ! s . paletteOpen , paletteQuery : "" } ) ) } ] ,
95- ! shortcutsEnabled
96- )
99+ useHotkeys ( [ { keys : [ "mod+k" ] , allowInInput : true , handler : togglePalette } ] , ! shortcutsEnabled )
97100
98101 return (
99102 < div className = "flex h-full min-h-0 bg-background" >
@@ -135,6 +138,9 @@ function ShellInner({ children }: { children: React.ReactNode }) {
135138 </ Sheet >
136139
137140 < main className = "flex h-full min-w-0 flex-1 flex-col" > { children } </ main >
141+ < Suspense >
142+ < ComposeFromUrl />
143+ </ Suspense >
138144
139145 { paletteOpen && < CommandPalette /> }
140146 { composeOpen && < ComposeDialog /> }
@@ -144,6 +150,24 @@ function ShellInner({ children }: { children: React.ReactNode }) {
144150 )
145151}
146152
153+ /** `?compose=<email>` (links from contacts etc.) opens a new message to that address. */
154+ function ComposeFromUrl ( ) {
155+ const params = useSearchParams ( )
156+ const router = useRouter ( )
157+ const pathname = usePathname ( )
158+ const to = params . get ( "compose" )
159+ useEffect ( ( ) => {
160+ if ( ! to ) return
161+ const emails = parseAddresses ( to )
162+ inboxUI . openCompose ( emails . length ? { to : emails } : undefined )
163+ const next = new URLSearchParams ( params . toString ( ) )
164+ next . delete ( "compose" )
165+ const qs = next . toString ( )
166+ router . replace ( `${ pathname } ${ qs ? `?${ qs } ` : "" } ` , { scroll : false } )
167+ } , [ to , params , pathname , router ] )
168+ return null
169+ }
170+
147171/** Hamburger that opens the sidebar sheet below `lg` (also when the desktop sidebar is collapsed). */
148172export function MobileNavButton ( { className } : { className ?: string } ) {
149173 return (
0 commit comments