diff --git a/src/components/FlowConsole.module.css b/src/components/FlowConsole.module.css index ababde3..09f71fe 100644 --- a/src/components/FlowConsole.module.css +++ b/src/components/FlowConsole.module.css @@ -2015,3 +2015,79 @@ .readerMessage { border-bottom-color: rgba(15, 23, 42, 0.06); } + +.headerPanel { + position: absolute; + top: 58px; + right: 72px; + z-index: 55; + display: grid; + min-width: 230px; + gap: 6px; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 18px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 24px 70px rgba(15, 23, 42, 0.16); + padding: 10px; + backdrop-filter: blur(18px); +} + +.headerPanel strong { + color: #0f172a; + font-size: 13px; + padding: 6px 8px; +} + +.headerPanel button, +.inlineActionMenu button { + display: flex; + width: 100%; + align-items: center; + border: 0; + border-radius: 10px; + background: transparent; + color: #334155; + font: inherit; + font-size: 13px; + justify-content: flex-start; + padding: 8px 10px; + text-align: left; +} + +.headerPanel button:hover, +.inlineActionMenu button:hover { + background: rgba(15, 118, 110, 0.1); + color: #0f766e; +} + +.inlineActionMenu { + display: flex; + width: max-content; + max-width: 100%; + flex-wrap: wrap; + gap: 4px; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 14px; + background: #ffffff; + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.12); + padding: 6px; +} + +.inlineActionMenu button { + width: auto; + min-width: 38px; + justify-content: center; +} + +.compactDensity .messageRow { + min-height: 38px; +} + +.compactDensity .listPane, +.compactDensity .reader { + margin: 8px; +} + +.compactDensity .insightBar { + padding-block: 7px; +} diff --git a/src/components/FlowConsole.tsx b/src/components/FlowConsole.tsx index 8e5b092..04c44da 100644 --- a/src/components/FlowConsole.tsx +++ b/src/components/FlowConsole.tsx @@ -142,6 +142,9 @@ export default function FlowConsole({ const sendLockRef = useRef(false); const [accountOpen, setAccountOpen] = useState(false); const [activeFolder, setActiveFolder] = useState('inbox'); + const [activeHeaderPanel, setActiveHeaderPanel] = useState< + 'apps' | 'help' | 'settings' | null + >(null); const [composeAttachments, setComposeAttachments] = useState< ComposeAttachment[] >([]); @@ -162,6 +165,7 @@ export default function FlowConsole({ const [formatToolbarOpen, setFormatToolbarOpen] = useState(true); const [moreToolsOpen, setMoreToolsOpen] = useState(false); const [messages, setMessages] = useState([]); + const [messageMenuId, setMessageMenuId] = useState(null); const [query, setQuery] = useState(''); const [readerMoreOpen, setReaderMoreOpen] = useState(false); const [readerMoveOpen, setReaderMoveOpen] = useState(false); @@ -174,6 +178,11 @@ export default function FlowConsole({ ); const [sidebarOpen, setSidebarOpen] = useState(true); const [status, setStatus] = useState(null); + const [themeDensity, setThemeDensity] = useState<'comfortable' | 'compact'>( + 'comfortable', + ); + + const debouncedQuery = useDebounce(query, 140); const debouncedQuery = useDebounce(query, 140); @@ -412,6 +421,7 @@ export default function FlowConsole({ setActiveFolder(folder); setReaderMoreOpen(false); setReaderMoveOpen(false); + setMessageMenuId(null); setSelectedIds([]); setSelectedMessageId(null); setStatus(null); @@ -432,6 +442,7 @@ export default function FlowConsole({ setSelectedMessageId(threadId); setReaderMoreOpen(false); setReaderMoveOpen(false); + setMessageMenuId(null); setSelectedIds([]); if (!unreadMessageIds.length) return; @@ -648,9 +659,72 @@ export default function FlowConsole({ const showReaderToolStatus = (text: string) => { setReaderMoreOpen(false); setReaderMoveOpen(false); + setMessageMenuId(null); setStatus({ kind: 'info', text }); }; + const downloadTextFile = ( + filename: string, + content: string, + type = 'text/plain', + ) => { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = filename.replace(/[\\/:*?"<>|]+/g, '-'); + document.body.append(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); + }; + + const threadToPlainText = () => + selectedThread?.allMessages + .map(message => { + const recipients = message.to.join(', '); + return [ + `From: ${message.from}`, + `To: ${recipients}`, + `Date: ${formatMessageDate(message.date)}`, + `Subject: ${message.subject}`, + '', + message.body || message.preview, + ].join('\n'); + }) + .join('\n\n---\n\n') || ''; + + const showOriginalSource = () => { + if (!selectedThread) return; + + const popup = window.open('', '_blank', 'noopener,noreferrer'); + if (!popup) { + showReaderToolStatus('Allow pop-ups to view the original source.'); + return; + } + + const pre = popup.document.createElement('pre'); + pre.style.whiteSpace = 'pre-wrap'; + pre.style.wordBreak = 'break-word'; + pre.style.font = '13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace'; + pre.textContent = threadToPlainText(); + popup.document.title = `Original source - ${selectedThread.subject}`; + popup.document.body.style.margin = '24px'; + popup.document.body.append(pre); + setReaderMoreOpen(false); + }; + + const downloadThread = () => { + if (!selectedThread) return; + + downloadTextFile( + `${selectedThread.subject || 'conversation'}.txt`, + threadToPlainText(), + ); + setReaderMoreOpen(false); + setStatus({ kind: 'success', text: 'Conversation downloaded.' }); + }; + const openThreadDocument = (print = false) => { if (!selectedThread) return; @@ -1046,11 +1120,121 @@ export default function FlowConsole({ setStatus(null); }; + const savedContactsFromStorage = () => { + const rawContacts = window.localStorage.getItem('flowSavedContacts'); + if (!rawContacts) return [] as ContactPreview[]; + + try { + const parsed = JSON.parse(rawContacts) as ContactPreview[]; + return Array.isArray(parsed) ? parsed : []; + } catch { + return [] as ContactPreview[]; + } + }; + const showContactToolStatus = (tool: string, contact: ContactPreview) => { - setStatus({ - kind: 'info', - text: `${tool} for ${contact.name} is ready for integration.`, - }); + const safeEmail = encodeURIComponent(contact.email); + const safeName = encodeURIComponent(contact.name || contact.email); + + if (tool === 'Contact card') { + const savedContacts = savedContactsFromStorage(); + const nextContacts = [ + ...savedContacts.filter(item => item.email !== contact.email), + contact, + ]; + window.localStorage.setItem( + 'flowSavedContacts', + JSON.stringify(nextContacts), + ); + setStatus({ kind: 'success', text: `${contact.name} saved to contacts.` }); + return; + } + + if (tool === 'Chat') { + window.location.href = `mailto:${safeEmail}?subject=${encodeURIComponent( + 'Quick chat', + )}`; + return; + } + + if (tool === 'Video call') { + window.open( + `https://meet.google.com/new?hs=180&authuser=0&pli=1&email=${safeEmail}`, + '_blank', + 'noopener,noreferrer', + ); + return; + } + + if (tool === 'Calendar') { + const details = encodeURIComponent( + `Meeting with ${contact.name} <${contact.email}>`, + ); + window.open( + `https://calendar.google.com/calendar/render?action=TEMPLATE&text=Meeting%20with%20${safeName}&details=${details}`, + '_blank', + 'noopener,noreferrer', + ); + } + }; + + const addReactionToMessage = (messageId: string, emoji: string) => { + setMessages(current => + current.map(message => + message.id === messageId + ? { + ...message, + reactionCount: (message.reactionCount || 0) + 1, + reactionEmoji: emoji, + reactionFrom: accessSession.keyLabel, + } + : message, + ), + ); + setMessageMenuId(null); + setStatus({ kind: 'success', text: `Reaction ${emoji} added.` }); + }; + + const copyMessageLink = async (message: MailMessage) => { + const url = `${window.location.origin}${ + window.location.pathname + }#${encodeURIComponent(message.id)}`; + try { + await navigator.clipboard.writeText(url); + setStatus({ kind: 'success', text: 'Message link copied.' }); + } catch { + window.location.hash = message.id; + setStatus({ kind: 'info', text: 'Message link added to the address bar.' }); + } + setMessageMenuId(null); + }; + + const downloadMessage = (message: MailMessage) => { + downloadTextFile( + `${message.subject || message.id}.txt`, + [ + `From: ${message.from}`, + `To: ${message.to.join(', ')}`, + `Date: ${formatMessageDate(message.date)}`, + `Subject: ${message.subject}`, + '', + message.body || message.preview, + ].join('\n'), + ); + setMessageMenuId(null); + setStatus({ kind: 'success', text: 'Message downloaded.' }); + }; + + const openHelpPanel = () => { + setActiveHeaderPanel(panel => (panel === 'help' ? null : 'help')); + }; + + const openSettingsPanel = () => { + setActiveHeaderPanel(panel => (panel === 'settings' ? null : 'settings')); + }; + + const openAppsPanel = () => { + setActiveHeaderPanel(panel => (panel === 'apps' ? null : 'apps')); }; const submitCompose = async (event: FormEvent) => { @@ -1120,7 +1304,11 @@ export default function FlowConsole({ }; return ( -
+
+ {activeHeaderPanel ? ( +
+ {activeHeaderPanel === 'help' ? ( + <> + Flow shortcuts + + + + + ) : null} + {activeHeaderPanel === 'settings' ? ( + <> + Display settings + + + + + ) : null} + {activeHeaderPanel === 'apps' ? ( + <> + Flow apps + + + + + ) : null} +
+ ) : null}
+ {messageMenuId === `reaction-${message.id}` ? ( +
+ {composeEmojis.slice(0, 8).map(emoji => ( + + ))} +
+ ) : null} + {messageMenuId === message.id ? ( +
+ + + +
+ ) : null} {message.direction === 'outbound' ? (
( mountedRef.current = true; if (immediate) { + void Promise.resolve().then(() => { + if (!cancelled) void execute(); + }); void Promise.resolve().then(execute); }