From 010b0ec51f0b44fe69109412ebe7eb2890cdf7b2 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Tue, 15 Apr 2025 22:34:23 +0100 Subject: [PATCH 01/21] added contacts dropdown for create email --- apps/mail/components/create/create-email.tsx | 111 +++++++++++++------ apps/mail/hooks/use-contacts.ts | 58 ++++++++++ 2 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 apps/mail/hooks/use-contacts.ts diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index 202506dbe1..b7c3712795 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -9,6 +9,7 @@ import { Separator } from '@/components/ui/separator'; import { SidebarToggle } from '../ui/sidebar-toggle'; import Paragraph from '@tiptap/extension-paragraph'; import { useSettings } from '@/hooks/use-settings'; +import { useContacts } from '@/hooks/use-contacts'; import Document from '@tiptap/extension-document'; import { Button } from '@/components/ui/button'; import { useSession } from '@/lib/auth-client'; @@ -58,9 +59,23 @@ export function CreateEmail({ initialSubject?: string; initialBody?: string; }) { - const [toInput, setToInput] = React.useState(''); const [toEmails, setToEmails] = React.useState(initialTo ? [initialTo] : []); + const [toInput, setToInput] = React.useState(''); + const [selectedContactIndex, setSelectedContactIndex] = React.useState(0); + const { contacts } = useContacts(); const [ccInput, setCcInput] = React.useState(''); + + const filteredContacts = React.useMemo(() => { + if (!toInput) return []; + const searchTerm = toInput.toLowerCase(); + return contacts.filter( + (contact) => + (contact.email?.toLowerCase().includes(searchTerm) || + contact.name?.toLowerCase().includes(searchTerm)) && + !toEmails.includes(contact.email), + ); + }, [contacts, toInput, toEmails]); + const [ccEmails, setCcEmails] = React.useState([]); const [bccInput, setBccInput] = React.useState(''); const [bccEmails, setBccEmails] = React.useState([]); @@ -437,21 +452,56 @@ export function CreateEmail({ ))} - handleEmailInputChange('to', e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleAddEmail('to', toInput); - } - }} - /> +
+ handleEmailInputChange('to', e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (filteredContacts.length > 0) { + const selectedEmail = filteredContacts[selectedContactIndex]?.email; + if (selectedEmail) handleAddEmail('to', selectedEmail); + setSelectedContactIndex(0); + } else { + handleAddEmail('to', toInput); + } + } else if (e.key === 'ArrowDown' && filteredContacts.length > 0) { + e.preventDefault(); + setSelectedContactIndex((prev) => + Math.min(prev + 1, filteredContacts.length - 1), + ); + } else if (e.key === 'ArrowUp' && filteredContacts.length > 0) { + e.preventDefault(); + setSelectedContactIndex((prev) => Math.max(prev - 1, 0)); + } + }} + /> + {toInput && filteredContacts.length > 0 && ( +
+ {filteredContacts.map((contact, index) => ( + + ))} +
+ )} +
+ onChange={handleAttachment} + multiple + accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt" + /> +
diff --git a/apps/mail/hooks/use-contacts.ts b/apps/mail/hooks/use-contacts.ts new file mode 100644 index 0000000000..72784c766e --- /dev/null +++ b/apps/mail/hooks/use-contacts.ts @@ -0,0 +1,58 @@ +'use client'; + +import type { InitialThread, Sender } from '@/types'; +import { useSession } from '@/lib/auth-client'; +import { useMemo } from 'react'; +import axios from 'axios'; +import useSWR from 'swr'; + +interface Contact extends Sender {} + +const fetchContacts = async (connectionId: string): Promise => { + try { + const response = await axios.get(`/api/driver?folder=inbox&max=100`); + const data = response.data; + + const uniqueContacts = new Map(); + + if (data && data.threads && Array.isArray(data.threads)) { + data.threads.forEach((thread: InitialThread) => { + const { sender } = thread; + + if (sender && sender.email && !uniqueContacts.has(sender.email)) { + uniqueContacts.set(sender.email, { + name: sender.name, + email: sender.email, + }); + } + }); + } + + return Array.from(uniqueContacts.values()); + } catch (error) { + console.error('Error fetching contacts:', error); + return []; + } +}; + +export const useContacts = () => { + const { data: session } = useSession(); + + const { data, error, isLoading } = useSWR( + session?.connectionId ? ['contacts', session.connectionId] : null, + () => (session?.connectionId ? fetchContacts(session.connectionId) : []), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 60000, + }, + ); + + const contacts = useMemo(() => data || [], [data]); + + return { + contacts, + isLoading, + error, + }; +}; From f75473e59cd9370f4bd21a1fb12d5f4ea0eb5539 Mon Sep 17 00:00:00 2001 From: Aj Wazzan Date: Tue, 15 Apr 2025 15:42:45 -0700 Subject: [PATCH 02/21] Add useContacts hook for managing contacts in mail app --- apps/mail/hooks/use-contacts.ts | 32 ++++++++++++++++++++++++++++++++ apps/mail/lib/idb.ts | 7 ++++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 apps/mail/hooks/use-contacts.ts diff --git a/apps/mail/hooks/use-contacts.ts b/apps/mail/hooks/use-contacts.ts new file mode 100644 index 0000000000..eef043f632 --- /dev/null +++ b/apps/mail/hooks/use-contacts.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react" +import { dexieStorageProvider } from "@/lib/idb" +import { Sender } from "@/types" +import useSWRImmutable from "swr/immutable" +import { useSession } from "@/lib/auth-client" + +export const useContacts = () => { + const { data: session } = useSession() + const { mutate, data } = useSWRImmutable(['contacts', session?.connectionId]) + + useEffect(() => { + const provider = dexieStorageProvider() + provider.list('$').then((cachedThreadsResponses) => { + const seen = new Set(); + const contacts: Sender[] = cachedThreadsResponses.reduce((acc: Sender[], { state }) => { + if (state.data) { + for (const thread of state.data[0].threads) { + const email = thread.sender.email; + if (!seen.has(email)) { + seen.add(email); + acc.push(thread.sender); + } + } + } + return acc; + }, []); + mutate(contacts) + }) + }, []) + + return data +} \ No newline at end of file diff --git a/apps/mail/lib/idb.ts b/apps/mail/lib/idb.ts index 4336c39c14..5106307901 100644 --- a/apps/mail/lib/idb.ts +++ b/apps/mail/lib/idb.ts @@ -1,5 +1,6 @@ import type { Cache, State } from "swr"; import Dexie from "dexie"; +import { InitialThread } from "@/types"; interface CacheEntry { key: string; @@ -21,7 +22,7 @@ class SWRDatabase extends Dexie { const db = new SWRDatabase(); const ONE_DAY = 1000 * 60 * 60 * 24; -export function dexieStorageProvider(): Cache & { clear: () => void } { +export function dexieStorageProvider(): Cache & { clear: () => void; list: (prefix: string) => Promise<{ key: string, state: { data?: [{ threads: InitialThread[] }] } }[]> } { const memoryCache = new Map>(); db.cache @@ -37,6 +38,10 @@ export function dexieStorageProvider(): Cache & { clear: () => void } { return memoryCache.keys(); }, + list(prefix: string) { + return db.cache.where("key").startsWith(prefix).toArray(); + }, + get(key: string) { return memoryCache.get(key); }, From 8c5413df3af2879c456b9f98d382bf9ae945631c Mon Sep 17 00:00:00 2001 From: Aj Wazzan Date: Tue, 15 Apr 2025 15:46:59 -0700 Subject: [PATCH 03/21] Refactor useContacts to handle missing connectionId --- apps/mail/hooks/use-contacts.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/mail/hooks/use-contacts.ts b/apps/mail/hooks/use-contacts.ts index eef043f632..2ffb41b9f7 100644 --- a/apps/mail/hooks/use-contacts.ts +++ b/apps/mail/hooks/use-contacts.ts @@ -9,8 +9,9 @@ export const useContacts = () => { const { mutate, data } = useSWRImmutable(['contacts', session?.connectionId]) useEffect(() => { + if (!session?.connectionId) return const provider = dexieStorageProvider() - provider.list('$').then((cachedThreadsResponses) => { + provider.list(`$inf$@"${session?.connectionId}"`).then((cachedThreadsResponses) => { const seen = new Set(); const contacts: Sender[] = cachedThreadsResponses.reduce((acc: Sender[], { state }) => { if (state.data) { @@ -26,7 +27,7 @@ export const useContacts = () => { }, []); mutate(contacts) }) - }, []) + }, [session?.connectionId]) return data } \ No newline at end of file From a7763232ec338993c0a644b921ac4f4f21f20438 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Tue, 15 Apr 2025 16:25:27 -0700 Subject: [PATCH 04/21] review time plz --- apps/mail/components/create/create-email.tsx | 686 +++++++++---------- apps/mail/components/ui/app-sidebar.tsx | 2 +- 2 files changed, 340 insertions(+), 348 deletions(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index 3f26e29294..f8fb9a8f3d 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -91,6 +91,8 @@ export function CreateEmail({ const [draftId, setDraftId] = useQueryState('draftId'); const [includeSignature, setIncludeSignature] = React.useState(true); const { settings } = useSettings(); + const [isCardHovered, setIsCardHovered] = React.useState(false); + const dragCounter = React.useRef(0); const [defaultValue, setDefaultValue] = React.useState(() => { if (initialBody) { @@ -174,6 +176,7 @@ export function CreateEmail({ const ccInputRef = React.useRef(null); const bccInputRef = React.useRef(null); const subjectInputRef = React.useRef(null); + const fileInputRef = React.useRef(null); // Remove auto-focus logic React.useEffect(() => { @@ -359,7 +362,7 @@ export function CreateEmail({ // Add a mount ref to ensure we only auto-focus once const isFirstMount = React.useRef(true); - const handleAttachment = async (e: React.ChangeEvent) => { + const handleAttachment = (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { setAttachments((prev) => [...prev, ...Array.from(files)]); @@ -372,6 +375,21 @@ export function CreateEmail({ setHasUnsavedChanges(true); }; + const handleDragEnterCard = (e: React.DragEvent) => { + e.preventDefault(); + dragCounter.current += 1; + setIsCardHovered(true); + }; + + const handleDragLeaveCard = (e: React.DragEvent) => { + e.preventDefault(); + dragCounter.current -= 1; + if (dragCounter.current <= 0) { + setIsCardHovered(false); + dragCounter.current = 0; + } + }; + React.useEffect(() => { if (initialTo) { const emails = initialTo.split(',').map((email) => email.trim()); @@ -413,140 +431,29 @@ export function CreateEmail({ onDragLeave={handleDragLeave} onDrop={handleDrop} > - {isDragging && ( -
-
- -

{t('pages.createEmail.dropFilesToAttach')}

-
-
- )}
-
-
-
-
-
- {t('common.mailDisplay.to')} -
-
- {toEmails.map((email, index) => ( -
- - {email} - - -
- ))} -
- handleEmailInputChange('to', e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - if (filteredContacts.length > 0) { - const selectedEmail = filteredContacts[selectedContactIndex]?.email; - if (selectedEmail) handleAddEmail('to', selectedEmail); - setSelectedContactIndex(0); - } else { - handleAddEmail('to', toInput); - } - } else if (e.key === 'ArrowDown' && filteredContacts.length > 0) { - e.preventDefault(); - setSelectedContactIndex((prev) => - Math.min(prev + 1, filteredContacts.length - 1), - ); - } else if (e.key === 'ArrowUp' && filteredContacts.length > 0) { - e.preventDefault(); - setSelectedContactIndex((prev) => Math.max(prev - 1, 0)); - } - }} - /> - {toInput && filteredContacts.length > 0 && ( -
- {filteredContacts.map((contact, index) => ( - - ))} -
- )} -
-
- - -
+
+
+ {isDragging && isCardHovered && ( +
+
+ +

{t('pages.createEmail.dropFilesToAttach')}

- - {showCc && ( + )} +
+
- Cc + {t('common.mailDisplay.to')}
- {ccEmails.map((email, index) => ( + {toEmails.map((email, index) => (
{ - setCcEmails((emails) => emails.filter((_, i) => i !== index)); + setToEmails((emails) => emails.filter((_, i) => i !== index)); setHasUnsavedChanges(true); }} > @@ -568,263 +475,348 @@ export function CreateEmail({
))} handleEmailInputChange('cc', e.target.value)} + placeholder={toEmails.length ? '' : t('pages.createEmail.example')} + value={toInput} + onChange={(e) => handleEmailInputChange('to', e.target.value)} + onBlur={() => handleAddEmail('to', toInput)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - handleAddEmail('cc', ccInput); + handleAddEmail('to', toInput); } }} /> +
+ + +
- )} - {showBcc && ( -
-
- Bcc + {showCc && ( +
+
+ Cc +
+
+ {ccEmails.map((email, index) => ( +
+ + {email} + + +
+ ))} + handleEmailInputChange('cc', e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleAddEmail('cc', ccInput); + } + }} + /> +
-
- {bccEmails.map((email, index) => ( -
- - {email} - - -
- ))} - handleEmailInputChange('bcc', e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleAddEmail('bcc', bccInput); - } - }} - /> + + {email} + + +
+ ))} + handleEmailInputChange('bcc', e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleAddEmail('bcc', bccInput); + } + }} + /> +
-
- )} + )} -
-
- {t('common.searchBar.subject')} +
+
+ {t('common.searchBar.subject')} +
+ { + setSubjectInput(e.target.value); + setHasUnsavedChanges(true); + }} + />
- { - setSubjectInput(e.target.value); - setHasUnsavedChanges(true); - }} - /> -
-
-
- {t('pages.createEmail.body')} +
+
+ {t('pages.createEmail.body')} +
+
+ {defaultValue && ( + { + setMessageContent(newContent); + if (newContent.trim() !== '') { + setHasUnsavedChanges(true); + } + }} + key={resetEditorKey} + placeholder={t('pages.createEmail.writeYourMessageHere')} + onAttachmentsChange={setAttachments} + onCommandEnter={handleSendEmail} + /> + )} +
-
- {defaultValue && ( - { - setMessageContent(newContent); - if (newContent.trim() !== '') { - setHasUnsavedChanges(true); - } - }} - key={resetEditorKey} - placeholder={t('pages.createEmail.writeYourMessageHere')} - onAttachmentsChange={setAttachments} - onCommandEnter={handleSendEmail} - /> - )} -
-
-
- { - console.log('CreateEmail: Received AI-generated content', { - jsonContentType: jsonContent.type, - hasContent: Boolean(jsonContent.content), - contentLength: jsonContent.content?.length || 0, - newSubject: newSubject, - }); - - try { - // Update the editor content with the AI-generated content - setDefaultValue(jsonContent); - - // Extract and set the text content for validation purposes - // This ensures the submit button is enabled immediately - if (jsonContent.content && jsonContent.content.length > 0) { - // Extract text content from JSON structure recursively - const extractTextContent = (node: any): string => { - if (!node) return ''; - - if (node.text) return node.text; - - if (node.content && Array.isArray(node.content)) { - return node.content.map(extractTextContent).join(' '); - } - - return ''; - }; - - // Process all content nodes - const textContent = jsonContent.content - .map(extractTextContent) - .join('\n') - .trim(); - setMessageContent(textContent); +
+
+
+
+
+
+ { + console.log('CreateEmail: Received AI-generated content', { + jsonContentType: jsonContent.type, + hasContent: Boolean(jsonContent.content), + contentLength: jsonContent.content?.length || 0, + newSubject: newSubject, + }); + + try { + // Update the editor content with the AI-generated content + setDefaultValue(jsonContent); + + // Extract and set the text content for validation purposes + // This ensures the submit button is enabled immediately + if (jsonContent.content && jsonContent.content.length > 0) { + // Extract text content from JSON structure recursively + const extractTextContent = (node: any): string => { + if (!node) return ''; + + if (node.text) return node.text; + + if (node.content && Array.isArray(node.content)) { + return node.content.map(extractTextContent).join(' '); } - // Update the subject if provided - if (newSubject && (!subjectInput || subjectInput.trim() === '')) { - console.log('CreateEmail: Setting new subject from AI', newSubject); - setSubjectInput(newSubject); - } + return ''; + }; - // Mark as having unsaved changes - setHasUnsavedChanges(true); + // Process all content nodes + const textContent = jsonContent.content + .map(extractTextContent) + .join('\n') + .trim(); + setMessageContent(textContent); + } - // Reset the editor to ensure it picks up the new content - setResetEditorKey((prev) => prev + 1); + // Update the subject if provided + if (newSubject && (!subjectInput || subjectInput.trim() === '')) { + console.log('CreateEmail: Setting new subject from AI', newSubject); + setSubjectInput(newSubject); + } - console.log('CreateEmail: Successfully applied AI content'); - } catch (error) { - console.error('CreateEmail: Error applying AI content', error); - toast.error( + // Mark as having unsaved changes + setHasUnsavedChanges(true); + + // Reset the editor to ensure it picks up the new content + setResetEditorKey((prev) => prev + 1); + + console.log('CreateEmail: Successfully applied AI content'); + } catch (error) { + console.error('CreateEmail: Error applying AI content', error); + toast.error( 'Error applying AI content to your email. Please try again.', ); - } - }} - /> -
-
-
- {attachments.length > 0 && ( - - -
+
+
+ {attachments.length > 0 && ( + + + - - -
-
-

- {t('common.replyCompose.attachments')} -

-

- {attachments.length}{' '} - {t('common.replyCompose.fileCount', { + + + + +

+
+

+ {t('common.replyCompose.attachments')} +

+

+ {attachments.length}{' '} + {t('common.replyCompose.fileCount', { count: attachments.length, })} -

-
- -
-
- {attachments.map((file, index) => ( -
- -
-

- {truncateFileName(file.name, 20)} -

-

- {(file.size / (1024 * 1024)).toFixed(2)} MB -

-
-
- ))} +

+
+ +
+
+ {attachments.map((file, index) => ( +
+ +
+

+ {truncateFileName(file.name, 20)} +

+

+ {(file.size / (1024 * 1024)).toFixed(2)} MB +

+
-
+ ))}
- - - )} -
- - -
-
+ + + )} +
+ + +
+ -
-
+ } + > + +
diff --git a/apps/mail/components/ui/app-sidebar.tsx b/apps/mail/components/ui/app-sidebar.tsx index 4e374c8c2b..8be4d42481 100644 --- a/apps/mail/components/ui/app-sidebar.tsx +++ b/apps/mail/components/ui/app-sidebar.tsx @@ -124,7 +124,7 @@ function ComposeButton() { return (
- router.push('/settings/general')}> + router.push(getSettingsHref())}>

{t('common.actions.settings')}

@@ -246,7 +263,7 @@ export function NavUser() {

{t('common.actions.logout')}

- + ) : ( <> From a6ef4aca10be3f261cc3c5c271dff34b4257fd55 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Tue, 15 Apr 2025 22:14:50 -0700 Subject: [PATCH 06/21] dropdown reccomendation email --- apps/mail/components/create/create-email.tsx | 263 +++++++++++++++---- 1 file changed, 214 insertions(+), 49 deletions(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index f8fb9a8f3d..ae7963b63c 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -27,6 +27,12 @@ import { toast } from 'sonner'; import * as React from 'react'; import Editor from './editor'; import './prosemirror.css'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; const MAX_VISIBLE_ATTACHMENTS = 12; @@ -50,6 +56,17 @@ const createEmptyDocContent = (): JSONContent => ({ ], }); +const filterContacts = (contacts: any[], searchTerm: string, excludeEmails: string[]) => { + if (!searchTerm) return []; + const term = searchTerm.toLowerCase(); + return contacts.filter( + (contact) => + (contact.email?.toLowerCase().includes(term) || + contact.name?.toLowerCase().includes(term)) && + !excludeEmails.includes(contact.email) + ); +}; + export function CreateEmail({ initialTo = '', initialSubject = '', @@ -62,20 +79,10 @@ export function CreateEmail({ const [toEmails, setToEmails] = React.useState(initialTo ? [initialTo] : []); const [toInput, setToInput] = React.useState(''); const [selectedContactIndex, setSelectedContactIndex] = React.useState(0); + const [selectedCcContactIndex, setSelectedCcContactIndex] = React.useState(0); + const [selectedBccContactIndex, setSelectedBccContactIndex] = React.useState(0); const contacts = useContacts(); const [ccInput, setCcInput] = React.useState(''); - - const filteredContacts = React.useMemo(() => { - if (!toInput) return []; - const searchTerm = toInput.toLowerCase(); - return contacts.filter( - (contact) => - (contact.email?.toLowerCase().includes(searchTerm) || - contact.name?.toLowerCase().includes(searchTerm)) && - !toEmails.includes(contact.email), - ); - }, [contacts, toInput, toEmails]); - const [ccEmails, setCcEmails] = React.useState([]); const [bccInput, setBccInput] = React.useState(''); const [bccEmails, setBccEmails] = React.useState([]); @@ -119,6 +126,21 @@ export function CreateEmail({ const userEmail = activeAccount?.email || session?.activeConnection?.email || session?.user?.email || ''; + const filteredContacts = React.useMemo( + () => filterContacts(contacts, toInput, toEmails), + [contacts, toInput, toEmails] + ); + + const filteredCcContacts = React.useMemo( + () => filterContacts(contacts, ccInput, [...toEmails, ...ccEmails]), + [contacts, ccInput, toEmails, ccEmails] + ); + + const filteredBccContacts = React.useMemo( + () => filterContacts(contacts, bccInput, [...toEmails, ...ccEmails, ...bccEmails]), + [contacts, bccInput, toEmails, ccEmails, bccEmails] + ); + React.useEffect(() => { if (!draftId && !defaultValue) { setDefaultValue(createEmptyDocContent()); @@ -198,7 +220,7 @@ export function CreateEmail({ }, []); const handleEmailInputChange = (type: 'to' | 'cc' | 'bcc', value: string) => { - // Just update the input value, no validation or checks + // Update the input value immediately without any validation switch (type) { case 'to': setToInput(value); @@ -225,7 +247,8 @@ export function CreateEmail({ setEmailState([...emailState, trimmedEmail]); setInputState(''); setHasUnsavedChanges(true); - } else { + } else if (emailState.length === 0) { + // Only show error if there are no emails yet toast.error(t('pages.createEmail.invalidEmail')); } }; @@ -424,6 +447,41 @@ export function CreateEmail({ } }, [initialTo, initialSubject, initialBody, defaultValue]); + const toDropdownRef = React.useRef(null); + const ccDropdownRef = React.useRef(null); + const bccDropdownRef = React.useRef(null); + + // Add this effect to handle scrolling + React.useEffect(() => { + const dropdownRef = toDropdownRef.current; + if (dropdownRef && selectedContactIndex >= 0) { + const selectedItem = dropdownRef.children[selectedContactIndex] as HTMLElement; + if (selectedItem) { + selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + }, [selectedContactIndex]); + + React.useEffect(() => { + const dropdownRef = ccDropdownRef.current; + if (dropdownRef && selectedCcContactIndex >= 0) { + const selectedItem = dropdownRef.children[selectedCcContactIndex] as HTMLElement; + if (selectedItem) { + selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + }, [selectedCcContactIndex]); + + React.useEffect(() => { + const dropdownRef = bccDropdownRef.current; + if (dropdownRef && selectedBccContactIndex >= 0) { + const selectedItem = dropdownRef.children[selectedBccContactIndex] as HTMLElement; + if (selectedItem) { + selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + }, [selectedBccContactIndex]); + return (
))} +
handleEmailInputChange('to', e.target.value)} - onBlur={() => handleAddEmail('to', toInput)} onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === 'Enter') { + e.preventDefault(); + if (filteredContacts.length > 0) { + const selectedEmail = filteredContacts[selectedContactIndex]?.email; + if (selectedEmail) handleAddEmail('to', selectedEmail); + setSelectedContactIndex(0); + } else { + handleAddEmail('to', toInput); + } + } else if (e.key === 'ArrowDown' && filteredContacts.length > 0) { + e.preventDefault(); + setSelectedContactIndex((prev) => + Math.min(prev + 1, filteredContacts.length - 1), + ); + } else if (e.key === 'ArrowUp' && filteredContacts.length > 0) { e.preventDefault(); - handleAddEmail('to', toInput); + setSelectedContactIndex((prev) => Math.max(prev - 1, 0)); } }} /> + {toInput && filteredContacts.length > 0 && ( +
+ {filteredContacts.map((contact, index) => ( + + ))} +
+ )} +
))} - handleEmailInputChange('cc', e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleAddEmail('cc', ccInput); - } - }} - /> +
+ handleEmailInputChange('cc', e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (filteredCcContacts.length > 0) { + const selectedEmail = filteredCcContacts[selectedCcContactIndex]?.email; + if (selectedEmail) { + handleAddEmail('cc', selectedEmail); + setSelectedCcContactIndex(0); + } + } else { + handleAddEmail('cc', ccInput); + } + } else if (e.key === 'ArrowDown' && filteredCcContacts.length > 0) { + e.preventDefault(); + setSelectedCcContactIndex((prev) => + Math.min(prev + 1, filteredCcContacts.length - 1), + ); + } else if (e.key === 'ArrowUp' && filteredCcContacts.length > 0) { + e.preventDefault(); + setSelectedCcContactIndex((prev) => Math.max(prev - 1, 0)); + } + }} + /> + {ccInput && filteredCcContacts.length > 0 && ( +
+ {filteredCcContacts.map((contact, index) => ( + + ))} +
+ )} +
)} @@ -599,21 +727,58 @@ export function CreateEmail({
))} - handleEmailInputChange('bcc', e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleAddEmail('bcc', bccInput); - } - }} - /> +
+ handleEmailInputChange('bcc', e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (filteredBccContacts.length > 0) { + const selectedEmail = filteredBccContacts[selectedBccContactIndex]?.email; + if (selectedEmail) { + handleAddEmail('bcc', selectedEmail); + setSelectedBccContactIndex(0); + } + } else { + handleAddEmail('bcc', bccInput); + } + } else if (e.key === 'ArrowDown' && filteredBccContacts.length > 0) { + e.preventDefault(); + setSelectedBccContactIndex((prev) => + Math.min(prev + 1, filteredBccContacts.length - 1), + ); + } else if (e.key === 'ArrowUp' && filteredBccContacts.length > 0) { + e.preventDefault(); + setSelectedBccContactIndex((prev) => Math.max(prev - 1, 0)); + } + }} + /> + {bccInput && filteredBccContacts.length > 0 && ( +
+ {filteredBccContacts.map((contact, index) => ( + + ))} +
+ )} +
)} From b6dc7b2a2c277c11c815166cd5a9ee713c12279f Mon Sep 17 00:00:00 2001 From: kingstondoesit Date: Wed, 16 Apr 2025 06:41:24 +0100 Subject: [PATCH 07/21] use next/Link --- apps/mail/components/ui/nav-user.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index fee7e3ffea..99ada537b7 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -31,6 +31,7 @@ import { useTheme } from 'next-themes'; import { toast } from 'sonner'; import { dexieStorageProvider } from '@/lib/idb'; import { usePathname, useSearchParams } from 'next/navigation'; +import Link from 'next/link'; export function NavUser() { const { data: session, refetch } = useSession(); @@ -243,11 +244,13 @@ export function NavUser() {

{t('common.navUser.appTheme')}

- router.push(getSettingsHref())}> -
- -

{t('common.actions.settings')}

-
+ + +
+ +

{t('common.actions.settings')}

+
+
From 81c6aa0b3c0a05bb7437ceb843bd8288e3759997 Mon Sep 17 00:00:00 2001 From: needle <122770437+needleXO@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:19:30 +0200 Subject: [PATCH 08/21] chore: bring back login button --- apps/mail/components/home/navbar.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/mail/components/home/navbar.tsx b/apps/mail/components/home/navbar.tsx index 5a0c396586..28e38a6b25 100644 --- a/apps/mail/components/home/navbar.tsx +++ b/apps/mail/components/home/navbar.tsx @@ -73,7 +73,7 @@ export default function Navbar() { /> {/* Mobile Navigation */} - {/*
+
@@ -106,13 +106,11 @@ export default function Navbar() {
-
*/} +
- {/* <> -
- {desktopNavContent()} -
- */} +
+ {desktopNavContent()} +
); } From b5e6b16fd60d2d45bc893444b7801cfe81ec6c45 Mon Sep 17 00:00:00 2001 From: Aj Wazzan Date: Wed, 16 Apr 2025 14:46:01 -0700 Subject: [PATCH 09/21] Refactor auth.ts for early access validation --- apps/mail/lib/auth.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/mail/lib/auth.ts b/apps/mail/lib/auth.ts index 85c9c523ec..93d5353091 100644 --- a/apps/mail/lib/auth.ts +++ b/apps/mail/lib/auth.ts @@ -72,7 +72,7 @@ const options = { .select({ activeConnectionId: _user.defaultConnectionId, hasEarlyAccess: earlyAccess.isEarlyAccess, - hasUsedTicket: earlyAccess.hasUsedTicket + hasUsedTicket: earlyAccess.hasUsedTicket, }) .from(_user) .leftJoin(earlyAccess, eq(_user.email, earlyAccess.email)) @@ -80,7 +80,11 @@ const options = { .limit(1); // Check early access and proceed - if (!foundUser?.hasEarlyAccess && process.env.NODE_ENV === 'production') { + if ( + !foundUser?.hasEarlyAccess && + process.env.NODE_ENV === 'production' && + process.env.EARLY_ACCESS_ENABLED + ) { await db .insert(earlyAccess) .values({ @@ -92,8 +96,7 @@ const options = { .catch((err) => console.log('Tried to add user to earlyAccess after error, failed', foundUser), ); - redirect('/login?error=early_access_required'); - + redirect('/login?error=early_access_required'); } let activeConnection = null; @@ -114,9 +117,12 @@ const options = { picture: connectionDetails.picture, }; } else { - await db.update(_user).set({ - defaultConnectionId: null, - }).where(eq(_user.id, user.id)); + await db + .update(_user) + .set({ + defaultConnectionId: null, + }) + .where(eq(_user.id, user.id)); } } @@ -174,7 +180,7 @@ const options = { activeConnection, user, session, - hasUsedTicket: foundUser?.hasUsedTicket ?? false + hasUsedTicket: foundUser?.hasUsedTicket ?? false, }; }), ], From 21b00f2ce7876d226b10b5b0a37a0fb7ce791188 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Wed, 16 Apr 2025 14:58:05 -0700 Subject: [PATCH 10/21] posthog --- apps/mail/components/create/ai-assistant.tsx | 7 +++++++ apps/mail/components/create/create-email.tsx | 14 ++++++++++++++ apps/mail/components/mail/reply-composer.tsx | 12 ++++++++++++ 3 files changed, 33 insertions(+) diff --git a/apps/mail/components/create/ai-assistant.tsx b/apps/mail/components/create/ai-assistant.tsx index a89b44f9d6..27ebf65db1 100644 --- a/apps/mail/components/create/ai-assistant.tsx +++ b/apps/mail/components/create/ai-assistant.tsx @@ -9,6 +9,7 @@ import { useSession } from '@/lib/auth-client'; import { Input } from '@/components/ui/input'; import { type JSONContent } from 'novel'; import { toast } from 'sonner'; +import posthog from 'posthog-js'; // Types interface AIAssistantProps { @@ -285,6 +286,9 @@ export const AIAssistant = ({ try { setIsLoading(true); + // Track AI assistant usage + posthog.capture('Create Email AI Assistant Submit'); + // Add user message addMessage('user', prompt, 'question'); @@ -352,6 +356,9 @@ export const AIAssistant = ({ onContentGenerated(generatedContent.jsonContent); } + // Track AI assistant usage + posthog.capture('Create Email AI Assistant Submit'); + // Add confirmation message addMessage('system', 'Email content applied successfully.', 'system'); resetStates(); diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index ae7963b63c..a91396644b 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -33,6 +33,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import posthog from 'posthog-js'; const MAX_VISIBLE_ATTACHMENTS = 12; @@ -327,6 +328,17 @@ export function CreateEmail({ attachments: attachments, }); + // Track different email sending scenarios + if (showCc && showBcc) { + console.log(posthog.capture('Create Email Sent with CC and BCC')); + } else if (showCc) { + console.log(posthog.capture('Create Email Sent with CC')); + } else if (showBcc) { + console.log(posthog.capture('Create Email Sent with BCC')); + } else { + console.log(posthog.capture('Create Email Sent')); + } + setIsLoading(false); toast.success(t('pages.createEmail.emailSentSuccessfully')); @@ -970,6 +982,8 @@ export function CreateEmail({