From a6cb713bd9a3d21b84127362ecfc8a2704e73c13 Mon Sep 17 00:00:00 2001 From: Om Raval <68021378+omraval18@users.noreply.github.com> Date: Fri, 20 Jun 2025 06:35:12 +0530 Subject: [PATCH 01/45] Fix: Arrow-keys focus on background instead of modal - Live Website (Issue #1371) (#1377) --- apps/mail/hooks/use-mail-navigation.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/mail/hooks/use-mail-navigation.ts b/apps/mail/hooks/use-mail-navigation.ts index a803f1eae1..f7c18f3d0b 100644 --- a/apps/mail/hooks/use-mail-navigation.ts +++ b/apps/mail/hooks/use-mail-navigation.ts @@ -1,3 +1,4 @@ +import { useCommandPalette } from '@/components/context/command-palette-context'; import { useCallback, useEffect, useState, useRef } from 'react'; import { useOptimisticActions } from './use-optimistic-actions'; import { useMail } from '@/components/mail/use-mail'; @@ -22,6 +23,7 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa itemsRef.current = items; const onNavigateRef = useRef(onNavigate); onNavigateRef.current = onNavigate; + const { open: isCommandPaletteOpen } = useCommandPalette(); const hoveredMailRef = useRef(null); const keyboardActiveRef = useRef(false); @@ -193,12 +195,12 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa keyboardActiveRef.current = false; }, [setFocusedIndex, onNavigateRef]); - useHotkeys('ArrowUp', handleArrowUp, { preventDefault: true }); - useHotkeys('ArrowDown', handleArrowDown, { preventDefault: true }); - useHotkeys('j', handleArrowDown); - useHotkeys('k', handleArrowUp); - useHotkeys('Enter', handleEnter, { preventDefault: true }); - useHotkeys('Escape', handleEscape, { preventDefault: true }); + useHotkeys('ArrowUp', handleArrowUp, { preventDefault: true, enabled: !isCommandPaletteOpen }); + useHotkeys('ArrowDown', handleArrowDown, { preventDefault: true, enabled: !isCommandPaletteOpen }); + useHotkeys('j', handleArrowDown,{enabled: !isCommandPaletteOpen }); + useHotkeys('k', handleArrowUp, { enabled: !isCommandPaletteOpen }); + useHotkeys('Enter', handleEnter, { preventDefault: true,enabled: !isCommandPaletteOpen }); + useHotkeys('Escape', handleEscape, { preventDefault: true,enabled: !isCommandPaletteOpen }); const handleMouseEnter = useCallback( (threadId: string) => { @@ -239,6 +241,7 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa const MOVE_DELAY = 100; const handleKeyDown = (event: KeyboardEvent) => { + if (isCommandPaletteOpen) return; if (!event.repeat) return; if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return; @@ -266,7 +269,13 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, [fastScroll]); + }, [fastScroll, isCommandPaletteOpen]); + + useEffect(() => { + if (isCommandPaletteOpen) { + keyboardActiveRef.current = false; + } + }, [isCommandPaletteOpen]); return { focusedIndex, From f88f59569230519a87a6b113746fc13e4d5eb6c1 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 20 Jun 2025 06:37:33 +0530 Subject: [PATCH 02/45] fix: use toString() for label color selection comparison (#1378) --- apps/mail/app/(routes)/settings/labels/page.tsx | 2 +- apps/mail/components/labels/label-dialog.tsx | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/mail/app/(routes)/settings/labels/page.tsx b/apps/mail/app/(routes)/settings/labels/page.tsx index ca84b0857f..a91f854f3d 100644 --- a/apps/mail/app/(routes)/settings/labels/page.tsx +++ b/apps/mail/app/(routes)/settings/labels/page.tsx @@ -86,7 +86,7 @@ export default function LabelsPage() { action={ setEditingLabel(null)}> + diff --git a/apps/mail/components/labels/label-dialog.tsx b/apps/mail/components/labels/label-dialog.tsx index 6248665035..cafdecfef3 100644 --- a/apps/mail/components/labels/label-dialog.tsx +++ b/apps/mail/components/labels/label-dialog.tsx @@ -49,7 +49,10 @@ export function LabelDialog({ const form = useForm({ defaultValues: { name: '', - color: { backgroundColor: '#E2E2E2', textColor: '#000000' }, + color: { + backgroundColor: '', + textColor: '', + }, }, }); @@ -127,9 +130,9 @@ export function LabelDialog({ key={index} type="button" className={`h-10 w-10 rounded-[4px] border-[0.5px] border-white/10 transition-all ${ - formColor?.backgroundColor === color.backgroundColor && - formColor?.textColor === color.textColor - ? 'scale-110 ring-2 ring-blue-500' + formColor?.backgroundColor.toString() === color.backgroundColor && + formColor.textColor.toString() === color.textColor + ? 'scale-110 ring-2 ring-blue-500 ring-offset-1' : 'hover:scale-105' }`} style={{ backgroundColor: color.backgroundColor }} From ce4002aa92aff87dff6d1e1b9ddfdb5f4be144ee Mon Sep 17 00:00:00 2001 From: Kartik <103111467+kartik-212004@users.noreply.github.com> Date: Fri, 20 Jun 2025 06:43:16 +0530 Subject: [PATCH 03/45] Add missing home link in mobile view in navigation bar (#1330) --- apps/mail/components/navigation.tsx | 59 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/apps/mail/components/navigation.tsx b/apps/mail/components/navigation.tsx index 0a789aa93f..4576bd1df2 100644 --- a/apps/mail/components/navigation.tsx +++ b/apps/mail/components/navigation.tsx @@ -12,13 +12,13 @@ import { GitHub, Twitter, Discord, LinkedIn, Star } from './icons/icons'; import { AnimatedNumber } from '@/components/ui/animated-number'; import { signIn, useSession } from '@/lib/auth-client'; import { Separator } from '@/components/ui/separator'; +import { useQuery } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router'; import { Button } from '@/components/ui/button'; -import { Menu } from 'lucide-react'; import { useState, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { toast } from 'sonner'; +import { Menu } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { toast } from 'sonner'; const resources = [ { @@ -92,7 +92,7 @@ export function Navigation() { queryFn: async () => { const response = await fetch('https://api.github.com/repos/Mail-0/Zero', { headers: { - 'Accept': 'application/vnd.github.v3+json', + Accept: 'application/vnd.github.v3+json', }, }); if (!response.ok) { @@ -123,7 +123,9 @@ export function Navigation() { - Company + + Company +
    {aboutLinks.map((link) => ( @@ -135,7 +137,9 @@ export function Navigation() { - Resources + + Resources +
      {resources.map((resource) => ( @@ -151,7 +155,7 @@ export function Navigation() {
    - + - + - Zero Email - 0.email Logo + setOpen(false)}> + Zero Email + 0.email Logo +
    -
    +
    + setOpen(false)}> + Home + Pricing From c5d547957baa7808c7e4c8ff5c8e81ffb73d26cf Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Fri, 20 Jun 2025 02:34:25 +0100 Subject: [PATCH 04/45] save default address to db with google code ready (#1355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Add Default Email Alias Selection in Settings This PR adds the ability for users to select a default email alias that will be used as the "From" address when composing new emails. The selected default alias is stored in user settings and automatically applied when creating new emails. ## Type of Change - ✨ New feature (non-breaking change which adds functionality) - 🎨 UI/UX improvement ## Areas Affected - [x] User Interface/Experience - [x] Data Storage/Management ## Features Added 1. Added a new `defaultEmailAlias` field to the user settings schema 2. Added a dropdown selector in the General Settings page to choose a default email alias 3. Modified the email composer to use the selected default alias when creating new emails 4. Added appropriate translations for the new UI elements 5. Added placeholder code (commented out) for future implementation of updating the primary alias in Gmail settings ## Summary by CodeRabbit - **New Features** - Added the ability for users to select and manage a default email alias in general settings. - Introduced a dropdown menu for choosing a default email alias, which will be used as the default 'From' address when composing new emails. - **Improvements** - The email composer now automatically uses the selected default email alias when starting a new message. - **Localization** - Added new English localization strings for the default email alias feature and related notifications. - **Chores** - Removed unnecessary debug logging from email alias retrieval. --- .../app/(routes)/settings/general/page.tsx | 67 ++++++++++++++++--- .../mail/components/create/email-composer.tsx | 40 ++++++----- apps/mail/locales/en.json | 9 ++- apps/server/src/lib/driver/google.ts | 14 ---- apps/server/src/lib/schemas.ts | 2 + 5 files changed, 91 insertions(+), 41 deletions(-) diff --git a/apps/mail/app/(routes)/settings/general/page.tsx b/apps/mail/app/(routes)/settings/general/page.tsx index d0b67fc3a4..dfea0fcf8c 100644 --- a/apps/mail/app/(routes)/settings/general/page.tsx +++ b/apps/mail/app/(routes)/settings/general/page.tsx @@ -17,16 +17,17 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { useForm, type ControllerRenderProps } from 'react-hook-form'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { SettingsCard } from '@/components/settings/settings-card'; +import { useEmailAliases } from '@/hooks/use-email-aliases'; import { useState, useEffect, useMemo, memo } from 'react'; import { userSettingsSchema } from '@zero/server/schemas'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { Globe, Clock, XIcon, Mail } from 'lucide-react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslations, useLocale } from 'use-intl'; import { useTRPC } from '@/providers/query-provider'; import { getBrowserTimezone } from '@/lib/timezones'; import { Textarea } from '@/components/ui/textarea'; import { useSettings } from '@/hooks/use-settings'; -import { Globe, Clock, XIcon } from 'lucide-react'; import { availableLocales } from '@/i18n/config'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; @@ -118,6 +119,7 @@ export default function GeneralPage() { const locale = useLocale(); const t = useTranslations(); const { data } = useSettings(); + const { data: aliases } = useEmailAliases(); const trpc = useTRPC(); const queryClient = useQueryClient(); const { mutateAsync: saveUserSettings } = useMutation(trpc.settings.save.mutationOptions()); @@ -134,6 +136,7 @@ export default function GeneralPage() { dynamicContent: false, customPrompt: '', zeroSignature: true, + defaultEmailAlias: '', }, }); @@ -143,6 +146,15 @@ export default function GeneralPage() { } }, [form, data?.settings]); + useEffect(() => { + if (aliases && !data?.settings?.defaultEmailAlias) { + const primaryAlias = aliases.find((alias) => alias.primary); + if (primaryAlias) { + form.setValue('defaultEmailAlias', primaryAlias.email); + } + } + }, [aliases, data?.settings?.defaultEmailAlias, form]); + async function onSubmit(values: z.infer) { setIsSaving(true); const saved = data?.settings ? { ...data.settings } : undefined; @@ -153,18 +165,19 @@ export default function GeneralPage() { return { settings: { ...updater.settings, ...values } }; }); - await setLocaleCookie({ locale: values.language }); - const localeName = new Intl.DisplayNames([values.language], { type: 'language' }).of( - values.language, - ); - toast.success(t('common.settings.languageChanged', { locale: localeName! })); - await revalidate(); + if (saved?.language !== values.language) { + await setLocaleCookie({ locale: values.language }); + const localeName = new Intl.DisplayNames([values.language], { type: 'language' }).of( + values.language, + ); + toast.success(t('common.settings.languageChanged', { locale: localeName! })); + await revalidate(); + } toast.success(t('common.settings.saved')); } catch (error) { console.error('Failed to save settings:', error); toast.error(t('common.settings.failedToSave')); - // Revert the optimistic update queryClient.setQueryData(trpc.settings.get.queryKey(), (updater) => { if (!updater) return; return saved ? { settings: { ...updater.settings, ...saved } } : updater; @@ -223,6 +236,44 @@ export default function GeneralPage() { )} />
    + {aliases && aliases.length > 0 && ( + ( + + {t('pages.settings.general.defaultEmailAlias')} + + + {t('pages.settings.general.defaultEmailDescription')} + + + )} + /> + )} 0); const [showBcc, setShowBcc] = useState(initialBcc.length > 0); const [isLoading, setIsLoading] = useState(false); @@ -185,7 +175,11 @@ export function EmailComposer({ subject: initialSubject, message: initialMessage, attachments: initialAttachments, - fromEmail: aliases?.find((alias) => alias.primary)?.email || aliases?.[0]?.email || '', + fromEmail: + settings?.settings?.defaultEmailAlias || + aliases?.find((alias) => alias.primary)?.email || + aliases?.[0]?.email || + '', }, }); @@ -263,6 +257,18 @@ export function EmailComposer({ // For forward, we start with empty recipients }, [mode, emailData?.latest, activeConnection?.email]); + // keep fromEmail in sync when settings or aliases load afterwards + useEffect(() => { + const preferred = + settings?.settings?.defaultEmailAlias ?? + aliases?.find((a) => a.primary)?.email ?? + aliases?.[0]?.email; + + if (preferred && form.getValues('fromEmail') !== preferred) { + form.setValue('fromEmail', preferred, { shouldDirty: false }); + } + }, [settings?.settings?.defaultEmailAlias, aliases]); + const { watch, setValue, getValues } = form; const toEmails = watch('to'); const ccEmails = watch('cc'); diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index a5bc5762f8..90275fc1bf 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -269,7 +269,9 @@ "notFound": "Settings not found", "saved": "Settings saved", "failedToSave": "Failed to save settings", - "languageChanged": "Language changed to {locale}" + "languageChanged": "Language changed to {locale}", + "defaultEmailUpdated": "Default email alias updated in Gmail", + "failedToUpdateGmailAlias": "Failed to update default alias in Gmail settings" }, "mail": { "replies": "{count, plural, =0 {replies} one {# reply} other {# replies}}", @@ -396,7 +398,10 @@ "customPromptDescription": "Customize how the AI writes your email replies. This will be added to the base prompt.", "noResultsFound": "No results found", "zeroSignature": "Zero Signature", - "zeroSignatureDescription": "Add a Zero signature to your emails." + "zeroSignatureDescription": "Add a Zero signature to your emails.", + "defaultEmailAlias": "Default Email Alias", + "selectDefaultEmail": "Select default email", + "defaultEmailDescription": "This email will be used as the default 'From' address when composing new emails" }, "connections": { "title": "Email Connections", diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index 124ad7e776..7e4bca189c 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -82,30 +82,22 @@ export class GoogleMailManager implements MailManager { } public getEmailAliases() { return this.withErrorHandler('getEmailAliases', async () => { - console.log('Fetching email aliases...'); - const profile = await this.gmail.users.getProfile({ userId: 'me', }); - console.log('Retrieved user profile:', { email: profile.data.emailAddress }); const primaryEmail = profile.data.emailAddress || ''; const aliases: { email: string; name?: string; primary?: boolean }[] = [ { email: primaryEmail, primary: true }, ]; - console.log('Added primary email to aliases:', { primaryEmail }); const settings = await this.gmail.users.settings.sendAs.list({ userId: 'me', }); - console.log('Retrieved sendAs settings:', { - sendAsCount: settings.data.sendAs?.length || 0, - }); if (settings.data.sendAs) { settings.data.sendAs.forEach((alias) => { if (alias.isPrimary && alias.sendAsEmail === primaryEmail) { - console.log('Skipping duplicate primary email:', { email: alias.sendAsEmail }); return; } @@ -114,15 +106,9 @@ export class GoogleMailManager implements MailManager { name: alias.displayName || undefined, primary: alias.isPrimary || false, }); - console.log('Added alias:', { - email: alias.sendAsEmail, - name: alias.displayName, - primary: alias.isPrimary, - }); }); } - console.log('Returning aliases:', { aliasCount: aliases.length }); return aliases; }); } diff --git a/apps/server/src/lib/schemas.ts b/apps/server/src/lib/schemas.ts index 0a838040f6..15b8fec0d1 100644 --- a/apps/server/src/lib/schemas.ts +++ b/apps/server/src/lib/schemas.ts @@ -119,6 +119,7 @@ export const userSettingsSchema = z.object({ colorTheme: z.enum(['light', 'dark', 'system']).default('system'), zeroSignature: z.boolean().default(true), categories: categoriesSchema.optional(), + defaultEmailAlias: z.string().optional(), }); export type UserSettings = z.infer; @@ -133,5 +134,6 @@ export const defaultUserSettings: UserSettings = { isOnboarded: false, colorTheme: 'system', zeroSignature: true, + defaultEmailAlias: '', categories: defaultMailCategories, }; From 7db4b08c3d865d49b590a4ff4c102f7c4545d011 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 19 Jun 2025 23:57:50 -0700 Subject: [PATCH 05/45] Fixes (#1397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/components/mail/mail-display.tsx | 116 +-- apps/mail/components/mail/mail-iframe.tsx | 18 +- apps/mail/components/mail/mail.tsx | 4 +- apps/mail/lib/email-utils.client.tsx | 4 - apps/mail/locales/en.json | 20 +- .../db/migrations/0030_blue_grandmaster.sql | 1 + .../src/db/migrations/meta/0030_snapshot.json | 878 ++++++++++++++++++ .../src/db/migrations/meta/_journal.json | 7 + 8 files changed, 952 insertions(+), 96 deletions(-) create mode 100644 apps/server/src/db/migrations/0030_blue_grandmaster.sql create mode 100644 apps/server/src/db/migrations/meta/0030_snapshot.json diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index c90e3ff5b8..9f9a03d117 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -1,6 +1,5 @@ import { Bell, - Calendar, Docx, Figma, Forward, @@ -13,7 +12,6 @@ import { Tag, User, ChevronDown, - Check, Printer, } from '../icons/icons'; import { @@ -22,59 +20,41 @@ import { StickyNote, Users, Lock, - Download, - MoreVertical, HardDriveDownload, - Paperclip, Loader2, CopyIcon, SearchIcon, } from 'lucide-react'; -import { - Dialog, - DialogTitle, - DialogHeader, - DialogContent, - DialogTrigger, - DialogDescription, -} from '../ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu'; -import { memo, useEffect, useMemo, useState, useRef, useCallback, useLayoutEffect } from 'react'; +import { Dialog, DialogTitle, DialogHeader, DialogContent } from '../ui/dialog'; +import { memo, useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import type { Sender, ParsedMessage, Attachment } from '@/types'; import { useActiveConnection } from '@/hooks/use-connections'; -import { handleUnsubscribe } from '@/lib/email-utils.client'; -import { getListUnsubscribeAction } from '@/lib/email-utils'; -import AttachmentsAccordion from './attachments-accordion'; import { cn, getEmailLogo, formatDate } from '@/lib/utils'; import { useBrainState } from '../../hooks/use-summary'; import { useTRPC } from '@/providers/query-provider'; import { useThreadLabels } from '@/hooks/use-labels'; import { useMutation } from '@tanstack/react-query'; import { Markdown } from '@react-email/components'; -import AttachmentDialog from './attachment-dialog'; import { useSummary } from '@/hooks/use-summary'; import { TextShimmer } from '../ui/text-shimmer'; -import { useSession } from '@/lib/auth-client'; import { RenderLabels } from './render-labels'; -import ReplyCompose from './reply-composer'; -import { Separator } from '../ui/separator'; import { MailIframe } from './mail-iframe'; import { useTranslations } from 'use-intl'; import { useParams } from 'react-router'; -import { MailLabels } from './mail-list'; import { FileText } from 'lucide-react'; -import { format, set } from 'date-fns'; import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Badge } from '../ui/badge'; +import { format } from 'date-fns'; // HTML escaping function to prevent XSS attacks function escapeHtml(text: string): string { @@ -664,7 +644,7 @@ const MoreAboutPerson = ({ } = useMutation(trpc.ai.webSearch.mutationOptions()); const handleSearch = useCallback(() => { doSearch({ - query: `In 50 words or less: What is the background of ${person.name} & ${person.email}, of ${person.email.split('@')[1]}. + query: `In 50 words or less: What is the background of ${person.name} & ${person.email}, of ${person.email.split('@')[1]}. This could be a phishing email address, indicate if the domain is suspicious, example: x.io is not a valid domain for x.com | example: x.com is a valid domain for x.com | example: paypalcom.com is not a valid domain for paypal.com`, }); }, [person.name]); @@ -934,7 +914,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: padding: 0; box-sizing: border-box; } - + body { font-family: Arial, sans-serif; line-height: 1.5; @@ -943,17 +923,17 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: padding: 20px; font-size: 12px; } - + .email-container { max-width: 100%; margin: 0 auto; background: white; } - + .email-header { margin-bottom: 25px; } - + .email-title { font-size: 18px; font-weight: bold; @@ -961,105 +941,105 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: margin-bottom: 15px; word-wrap: break-word; } - + .email-meta { margin-bottom: 20px; } - + .meta-row { margin-bottom: 5px; display: flex; align-items: flex-start; } - + .meta-label { font-weight: bold; min-width: 60px; color: #333; margin-right: 10px; } - + .meta-value { flex: 1; word-wrap: break-word; color: #333; } - + .separator { width: 100%; height: 1px; background: #ddd; margin: 20px 0; } - + .email-body { margin: 20px 0; background: white; } - + .email-content { word-wrap: break-word; overflow-wrap: break-word; font-size: 12px; line-height: 1.6; } - + .email-content img { max-width: 100% !important; height: auto !important; display: block; margin: 10px 0; } - + .email-content table { width: 100%; border-collapse: collapse; margin: 10px 0; } - + .email-content td, .email-content th { padding: 6px; text-align: left; font-size: 11px; } - + .email-content a { color: #0066cc; text-decoration: underline; } - + .attachments-section { margin-top: 25px; background: white; } - + .attachments-title { font-size: 14px; font-weight: bold; color: #000; margin-bottom: 10px; } - + .attachment-item { margin-bottom: 5px; font-size: 11px; padding: 3px 0; } - + .attachment-name { font-weight: 500; color: #333; } - + .attachment-size { color: #666; font-size: 10px; } - + .labels-section { margin: 10px 0; } - + .label-badge { display: inline-block; padding: 2px 6px; @@ -1069,7 +1049,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: margin-right: 5px; margin-bottom: 3px; } - + @media print { body { margin: 0; @@ -1078,46 +1058,46 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: -webkit-print-color-adjust: exact; print-color-adjust: exact; } - + .email-container { max-width: none; width: 100%; } - - + + .separator { background: #000 !important; } - + .email-content a { color: #000 !important; } - + .label-badge { background: #f0f0f0 !important; border: 1px solid #ccc; } - + .no-print { display: none !important; } - + /* Remove any default borders */ * { border: none !important; box-shadow: none !important; } - + /* Ensure clean page breaks */ .email-header { page-break-after: avoid; } - + .attachments-section { page-break-inside: avoid; } } - + @page { margin: 0.5in; size: A4; @@ -1129,7 +1109,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: - +
    - + - + ${ emailData.attachments && emailData.attachments.length > 0 @@ -1229,7 +1209,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:

    Attachments (${emailData.attachments.length})

    ${emailData.attachments .map( - (attachment, index) => ` + (attachment) => `
    ${attachment.filename} ${formatFileSize(attachment.size) ? ` - ${formatFileSize(attachment.size)}` : ''} diff --git a/apps/mail/components/mail/mail-iframe.tsx b/apps/mail/components/mail/mail-iframe.tsx index 3e7b610714..439c59a924 100644 --- a/apps/mail/components/mail/mail-iframe.tsx +++ b/apps/mail/components/mail/mail-iframe.tsx @@ -58,7 +58,7 @@ export function MailIframe({ html, senderEmail }: { html: string; senderEmail: s }, }); - const { data: processedHtml, isLoading: isProcessingHtml } = useQuery({ + const { data: processedHtml } = useQuery({ queryKey: ['email-template', html, isTrustedSender || temporaryImagesEnabled], queryFn: () => template(html, isTrustedSender || temporaryImagesEnabled), staleTime: 30 * 60 * 1000, // Increase cache time to 30 minutes @@ -76,11 +76,14 @@ export function MailIframe({ html, senderEmail }: { html: string; senderEmail: s const boundingRectHeight = body.getBoundingClientRect().height; const scrollHeight = body.scrollHeight; - // Use the larger of the two values to ensure all content is visible - setHeight(Math.max(boundingRectHeight, scrollHeight)); if (body.innerText.trim() === '') { setHeight(0); + return; } + + // Use the larger of the two values to ensure all content is visible + const newHeight = Math.max(boundingRectHeight, scrollHeight); + setHeight(newHeight); }, [iframeRef, setHeight]); useEffect(() => { @@ -136,15 +139,6 @@ export function MailIframe({ html, senderEmail }: { html: string; senderEmail: s return () => ctrl.abort(); }, []); - // Show loading fallback while processing HTML (similar to HydrateFallback pattern) - if (isProcessingHtml) { - return ( -
    -
    Processing email content...
    -
    - ); - } - return ( <> {cspViolation && !isTrustedSender && !data?.settings?.externalImages && ( diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 28fc682a86..46bbc848ff 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -186,7 +186,7 @@ const AutoLabelingSettings = () => { }; const handleEnableBrain = useCallback(async () => { - toast.promise(EnableBrain(), { + toast.promise(EnableBrain, { loading: 'Enabling autolabeling...', success: 'Autolabeling enabled successfully', error: 'Failed to enable autolabeling', @@ -197,7 +197,7 @@ const AutoLabelingSettings = () => { }, []); const handleDisableBrain = useCallback(async () => { - toast.promise(DisableBrain(), { + toast.promise(DisableBrain, { loading: 'Disabling autolabeling...', success: 'Autolabeling disabled successfully', error: 'Failed to disable autolabeling', diff --git a/apps/mail/lib/email-utils.client.tsx b/apps/mail/lib/email-utils.client.tsx index c33cb71c92..2362fe8ceb 100644 --- a/apps/mail/lib/email-utils.client.tsx +++ b/apps/mail/lib/email-utils.client.tsx @@ -126,10 +126,6 @@ const proxyImageUrls = (html: string): string => { if (proxiedUrl !== src) { img.setAttribute('data-original-src', src); img.setAttribute('src', proxiedUrl); - img.setAttribute( - 'onerror', - `this.onerror=null; this.src=this.getAttribute('data-original-src');`, - ); } }); diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index 90275fc1bf..ec103f7be6 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -135,7 +135,7 @@ "navUser": { "customerSupport": "Community", "documentation": "Documentation", - "appTheme": "App Theme", + "appTheme": "Toggle Theme", "accounts": "Accounts", "signIn": "Sign in", "otherAccounts": "Other Accounts" @@ -330,21 +330,21 @@ "mb": "{amount} MB" }, "labels": { - "color":"Color", - "labelName":"Label Name", - "createLabel":"Create Label", + "color": "Color", + "labelName": "Label Name", + "createLabel": "Create Label", "deleteLabel": "Delete Label", "deleteLabelConfirm": "Are you sure you want to delete this label?", "deleteLabelConfirmDescription": "This action cannot be undone.", "deleteLabelConfirmCancel": "Cancel", "deleteLabelConfirmDelete": "Delete", "deleteLabelSuccess": "Label deleted successfully", - "failedToDeleteLabel":"Failed to delete label", - "deletingLabel":"Deleting label...", - "editLabel":"Edit Label", - "savingLabel":"Saving label...", - "failedToSavingLabel":"Failed to save label", - "saveLabelSuccess":"Label saved successfully" + "failedToDeleteLabel": "Failed to delete label", + "deletingLabel": "Deleting label...", + "editLabel": "Edit Label", + "savingLabel": "Saving label...", + "failedToSavingLabel": "Failed to save label", + "saveLabelSuccess": "Label saved successfully" } }, "navigation": { diff --git a/apps/server/src/db/migrations/0030_blue_grandmaster.sql b/apps/server/src/db/migrations/0030_blue_grandmaster.sql new file mode 100644 index 0000000000..efcaa8ea82 --- /dev/null +++ b/apps/server/src/db/migrations/0030_blue_grandmaster.sql @@ -0,0 +1 @@ +ALTER TABLE "mail0_user_settings" ALTER COLUMN "settings" SET DEFAULT '{"language":"en","timezone":"UTC","dynamicContent":false,"externalImages":true,"customPrompt":"","trustedSenders":[],"isOnboarded":false,"colorTheme":"system","zeroSignature":true,"defaultEmailAlias":"","categories":[{"id":"Important","name":"Important","searchValue":"is:important NOT is:sent NOT is:draft","order":0,"isDefault":false},{"id":"All Mail","name":"All Mail","searchValue":"NOT is:draft (is:inbox OR (is:sent AND to:me))","order":1,"isDefault":true},{"id":"Personal","name":"Personal","searchValue":"is:personal NOT is:sent NOT is:draft","order":2,"isDefault":false},{"id":"Promotions","name":"Promotions","searchValue":"is:promotions NOT is:sent NOT is:draft","order":3,"isDefault":false},{"id":"Updates","name":"Updates","searchValue":"is:updates NOT is:sent NOT is:draft","order":4,"isDefault":false},{"id":"Unread","name":"Unread","searchValue":"is:unread NOT is:sent NOT is:draft","order":5,"isDefault":false}]}'::jsonb; \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/0030_snapshot.json b/apps/server/src/db/migrations/meta/0030_snapshot.json new file mode 100644 index 0000000000..555e64a2e9 --- /dev/null +++ b/apps/server/src/db/migrations/meta/0030_snapshot.json @@ -0,0 +1,878 @@ +{ + "id": "2665d18f-0aa5-48f1-aa41-f4895d385def", + "prevId": "d890f4a8-0a7d-4902-8c1b-f58fb6f10e25", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.mail0_account": { + "name": "mail0_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_account_user_id_mail0_user_id_fk": { + "name": "mail0_account_user_id_mail0_user_id_fk", + "tableFrom": "mail0_account", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_connection": { + "name": "mail0_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_connection_user_id_mail0_user_id_fk": { + "name": "mail0_connection_user_id_mail0_user_id_fk", + "tableFrom": "mail0_connection", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_connection_user_id_email_unique": { + "name": "mail0_connection_user_id_email_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_early_access": { + "name": "mail0_early_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_early_access": { + "name": "is_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "has_used_ticket": { + "name": "has_used_ticket", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_early_access_email_unique": { + "name": "mail0_early_access_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_jwks": { + "name": "mail0_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_note": { + "name": "mail0_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_note_user_id_mail0_user_id_fk": { + "name": "mail0_note_user_id_mail0_user_id_fk", + "tableFrom": "mail0_note", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_session": { + "name": "mail0_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_session_user_id_mail0_user_id_fk": { + "name": "mail0_session_user_id_mail0_user_id_fk", + "tableFrom": "mail0_session", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_session_token_unique": { + "name": "mail0_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_summary": { + "name": "mail0_summary", + "schema": "", + "columns": { + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "saved": { + "name": "saved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_reply": { + "name": "suggested_reply", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user": { + "name": "mail0_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "default_connection_id": { + "name": "default_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_prompt": { + "name": "custom_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number_verified": { + "name": "phone_number_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_email_unique": { + "name": "mail0_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "mail0_user_phone_number_unique": { + "name": "mail0_user_phone_number_unique", + "nullsNotDistinct": false, + "columns": [ + "phone_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_hotkeys": { + "name": "mail0_user_hotkeys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shortcuts": { + "name": "shortcuts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_user_hotkeys_user_id_mail0_user_id_fk": { + "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_hotkeys", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_settings": { + "name": "mail0_user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"isDefault\":false}]}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_user_settings_user_id_mail0_user_id_fk": { + "name": "mail0_user_settings_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_settings", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_settings_user_id_unique": { + "name": "mail0_user_settings_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_verification": { + "name": "mail0_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_writing_style_matrix": { + "name": "mail0_writing_style_matrix", + "schema": "", + "columns": { + "connectionId": { + "name": "connectionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "numMessages": { + "name": "numMessages", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": { + "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk", + "tableFrom": "mail0_writing_style_matrix", + "tableTo": "mail0_connection", + "columnsFrom": [ + "connectionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mail0_writing_style_matrix_connectionId_pk": { + "name": "mail0_writing_style_matrix_connectionId_pk", + "columns": [ + "connectionId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/_journal.json b/apps/server/src/db/migrations/meta/_journal.json index f062fe0f03..358846aaa8 100644 --- a/apps/server/src/db/migrations/meta/_journal.json +++ b/apps/server/src/db/migrations/meta/_journal.json @@ -211,6 +211,13 @@ "when": 1750268247218, "tag": "0029_common_network", "breakpoints": true + }, + { + "idx": 30, + "version": "7", + "when": 1750384854243, + "tag": "0030_blue_grandmaster", + "breakpoints": true } ] } \ No newline at end of file From 41f7df034355bc6652448f99a29e9590a65a108f Mon Sep 17 00:00:00 2001 From: abhix4 Date: Sun, 22 Jun 2025 01:15:09 +0530 Subject: [PATCH 06/45] chore: set default mail category to default Value instead of null (#1402) --- apps/mail/components/mail/mail.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 46bbc848ff..a332a2028b 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -822,8 +822,11 @@ function BulkSelectActions() { export const Categories = () => { const t = useTranslations(); + const defaultCategoryIdInner = useDefaultCategoryId() const categorySettings = useCategorySettings(); - const [activeCategory] = useQueryState('category'); + const [activeCategory] = useQueryState('category',{ + defaultValue: defaultCategoryIdInner, + }); const categories = categorySettings.map((cat) => { const base = { From eed6d02cfd4aaea7b1c361e4f1bc6c83941ada44 Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Sat, 21 Jun 2025 21:51:29 +0200 Subject: [PATCH 07/45] fix: link to correct CONTRIBUTING.md (#1400) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 910ad617e4..2a73b78402 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -61,7 +61,7 @@ For changes involving data or authentication: ## Checklist -- [ ] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document +- [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas From 275443fb12a2f5250cce3f362519fd42bae40070 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 21 Jun 2025 20:54:24 +0100 Subject: [PATCH 08/45] connect the draft to the thread (#1381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Add draft indicator and improve draft handling in mail threads ## Description This PR adds visual indicators for drafts in mail threads and improves draft handling throughout the mail application: 1. Added a pencil icon indicator that appears next to threads containing drafts 2. Improved the display of latest messages by filtering out drafts when showing thread previews 3. Fixed draft handling in reply composer by properly parsing email addresses from drafts 4. Added support for CC and BCC fields when reopening drafts 5. Properly reset draft and reply state when navigating between threads 6. Enhanced the Google Mail Manager to include CC and BCC information from drafts ## Type of Change - [x] 🐛 Bug fix (non-breaking change which fixes an issue) - [x] ✨ New feature (non-breaking change which adds functionality) - [x] 🎨 UI/UX improvement ## Areas Affected - [x] Email Integration (Gmail, IMAP, etc.) - [x] User Interface/Experience ## Summary by CodeRabbit - **New Features** - Draft status is now visually indicated in mail threads with a draft icon. - Draft emails now support CC and BCC recipient fields, which are populated when composing or editing drafts. - The draft data structure now includes an optional sender email address field. - **Bug Fixes** - Email recipient fields (To, CC, BCC) are consistently formatted and cleaned when initializing the email composer. - **Improvements** - Navigating between threads or moving emails clears any draft or reply state to prevent unintended carryover. --- apps/mail/components/mail/mail-list.tsx | 48 ++++++++++++++++++-- apps/mail/components/mail/reply-composer.tsx | 19 +++++++- apps/mail/components/mail/thread-display.tsx | 41 ++++++++++++++--- apps/server/src/lib/driver/google.ts | 7 ++- apps/server/src/lib/driver/types.ts | 2 + apps/server/src/lib/schemas.ts | 1 + 6 files changed, 106 insertions(+), 12 deletions(-) diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index 4fa1b83e20..05bc77b812 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -6,6 +6,14 @@ import { getMainSearchTerm, parseNaturalLanguageSearch, } from '@/lib/utils'; +import { + Archive2, + ExclamationCircle, + GroupPeople, + Star2, + Trash, + PencilCompose, +} from '../icons/icons'; import { memo, useCallback, @@ -15,7 +23,6 @@ import { useState, type ComponentProps, } from 'react'; -import { Archive2, ExclamationCircle, GroupPeople, Star2, Trash } from '../icons/icons'; import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state'; import { focusedIndexAtom, useMailNavigation } from '@/hooks/use-mail-navigation'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -62,11 +69,31 @@ const Thread = memo( const { folder } = useParams<{ folder: string }>(); const [{}, threads] = useThreads(); const [threadId] = useQueryState('threadId'); - const { data: getThreadData, isGroupThread } = useThread(message.id, message.historyId); + const { + data: getThreadData, + isGroupThread, + latestDraft, + } = useThread(message.id, message.historyId); const [id, setThreadId] = useQueryState('threadId'); const [, setActiveReplyId] = useQueryState('activeReplyId'); const [focusedIndex, setFocusedIndex] = useAtom(focusedIndexAtom); - const latestMessage = getThreadData?.latest; + + const latestReceivedMessage = useMemo(() => { + if (!getThreadData?.messages) return getThreadData?.latest; + + const nonDraftMessages = getThreadData.messages.filter((msg) => !msg.isDraft); + if (nonDraftMessages.length === 0) return getThreadData?.latest; + + return ( + nonDraftMessages.sort((a, b) => { + const dateA = new Date(a.receivedOn).getTime(); + const dateB = new Date(b.receivedOn).getTime(); + return dateB - dateA; + })[0] || getThreadData?.latest + ); + }, [getThreadData?.messages, getThreadData?.latest]); + + const latestMessage = latestReceivedMessage; const idToUse = useMemo(() => latestMessage?.threadId ?? latestMessage?.id, [latestMessage]); const { data: settingsData } = useSettings(); const queryClient = useQueryClient(); @@ -234,6 +261,11 @@ const Thread = memo( return latestMessage.sender.name.trim().replace(/^['"]|['"]$/g, ''); }, [latestMessage?.sender?.name]); + // Check if thread has a draft + const hasDraft = useMemo(() => { + return !!latestDraft; + }, [latestDraft]); + const content = latestMessage && getThreadData ? (
    ) : null} + {hasDraft ? ( + + + + + + + Draft + + ) : null}
    {latestMessage.receivedOn ? ( diff --git a/apps/mail/components/mail/reply-composer.tsx b/apps/mail/components/mail/reply-composer.tsx index 5d73476761..d009432864 100644 --- a/apps/mail/components/mail/reply-composer.tsx +++ b/apps/mail/components/mail/reply-composer.tsx @@ -229,6 +229,21 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) { }; }, [mode, enableScope, disableScope]); + const ensureEmailArray = (emails: string | string[] | undefined | null): string[] => { + if (!emails) return []; + if (Array.isArray(emails)) { + return emails.map((email) => email.trim().replace(/[<>]/g, '')); + } + if (typeof emails === 'string') { + return emails + .split(',') + .map((email) => email.trim()) + .filter((email) => email.length > 0) + .map((email) => email.replace(/[<>]/g, '')); + } + return []; + }; + if (!mode || !emailData) return null; return ( @@ -243,7 +258,9 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) { await setActiveReplyId(null); }} initialMessage={draft?.content ?? latestDraft?.decodedBody} - initialTo={draft?.to} + initialTo={ensureEmailArray(draft?.to)} + initialCc={ensureEmailArray(draft?.cc)} + initialBcc={ensureEmailArray(draft?.bcc)} initialSubject={draft?.subject} autofocus={false} settingsLoading={settingsLoading} diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index be33c9319b..97ac3d85bb 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -172,7 +172,7 @@ export function ThreadDisplay() { const [searchParams] = useSearchParams(); const folder = params?.folder ?? 'inbox'; const [id, setThreadId] = useQueryState('threadId'); - const { data: emailData, isLoading, refetch: refetchThread } = useThread(id ?? null); + const { data: emailData, isLoading, refetch: refetchThread, latestDraft } = useThread(id ?? null); const [{ refetch: mutateThreads }, items] = useThreads(); const [isFullscreen, setIsFullscreen] = useState(false); const [isStarred, setIsStarred] = useState(false); @@ -208,11 +208,24 @@ export function ThreadDisplay() { if (focusedIndex > 0) { const prevThread = items[focusedIndex - 1]; if (prevThread) { + // Clear draft and reply state when navigating to previous thread + setMode(null); + setActiveReplyId(null); + setDraftId(null); setThreadId(prevThread.id); setFocusedIndex(focusedIndex - 1); } } - }, [items, id, focusedIndex, setThreadId, setFocusedIndex]); + }, [ + items, + id, + focusedIndex, + setThreadId, + setFocusedIndex, + setMode, + setActiveReplyId, + setDraftId, + ]); const handleNext = useCallback(() => { if (!id || !items.length || focusedIndex === null) return setThreadId(null); @@ -221,14 +234,24 @@ export function ThreadDisplay() { // console.log('nextIndex', nextIndex); const nextThread = items[nextIndex]; - setActiveReplyId(null); if (nextThread) { + setMode(null); + setActiveReplyId(null); + setDraftId(null); setThreadId(nextThread.id); - // Don't clear activeReplyId - let the auto-open effect handle it setFocusedIndex(focusedIndex + 1); } } - }, [items, id, focusedIndex, setThreadId, setFocusedIndex]); + }, [ + items, + id, + focusedIndex, + setThreadId, + setFocusedIndex, + setMode, + setActiveReplyId, + setDraftId, + ]); const handleUnsubscribeProcess = () => { if (!emailData?.latest) return; @@ -246,7 +269,7 @@ export function ThreadDisplay() { setMode(null); setActiveReplyId(null); setDraftId(null); - }, [setThreadId, setMode]); + }, [setThreadId, setMode, setActiveReplyId, setDraftId]); const { optimisticMoveThreadsTo } = useOptimisticActions(); @@ -254,10 +277,14 @@ export function ThreadDisplay() { async (destination: ThreadDestination) => { if (!id) return; + setMode(null); + setActiveReplyId(null); + setDraftId(null); + optimisticMoveThreadsTo([id], folder, destination); handleNext(); }, - [id, folder, optimisticMoveThreadsTo, handleNext], + [id, folder, optimisticMoveThreadsTo, handleNext, setMode, setActiveReplyId, setDraftId], ); const { optimisticToggleStar } = useOptimisticActions(); diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index 7e4bca189c..8184f506e8 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -1104,7 +1104,10 @@ export class GoogleMailManager implements MailManager { } } - // TODO: Hook up CC and BCC from the draft so it can populate the composer on open. + const cc = + draft.message.payload?.headers?.find((h) => h.name === 'Cc')?.value?.split(',') || []; + const bcc = + draft.message.payload?.headers?.find((h) => h.name === 'Bcc')?.value?.split(',') || []; return { id: draft.id || '', @@ -1112,6 +1115,8 @@ export class GoogleMailManager implements MailManager { subject: subject ? he.decode(subject).trim() : '', content, rawMessage: draft.message, + cc, + bcc, }; } private async withErrorHandler( diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index 7cd8650f92..806cc76556 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -16,6 +16,8 @@ export interface ParsedDraft { subject?: string; content?: string; rawMessage?: T; + cc?: string[]; + bcc?: string[]; } export interface IConfig { diff --git a/apps/server/src/lib/schemas.ts b/apps/server/src/lib/schemas.ts index 15b8fec0d1..ce5ad191ac 100644 --- a/apps/server/src/lib/schemas.ts +++ b/apps/server/src/lib/schemas.ts @@ -31,6 +31,7 @@ export const createDraftData = z.object({ attachments: z.array(serializedFileSchema).transform(deserializeFiles).optional(), id: z.string().nullable(), threadId: z.string().nullable(), + fromEmail: z.string().nullable(), }); export type CreateDraftData = z.infer; From 275c3fd51431f31a4df00b319de71bb8b8fcf3d4 Mon Sep 17 00:00:00 2001 From: needle <122770437+dumbneedle@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:03:24 +0300 Subject: [PATCH 09/45] refactor(nav): enhance account switcher (#1367) ## Summary by CodeRabbit - **New Features** - Introduced a global loading indicator with animated overlay and customizable messages for improved user feedback during operations. - **Improvements** - Enhanced account switching with explicit loading states, improved cache management, and clearer success or error notifications. - Added new localized messages for account switching status. - **Chores** - Updated package manager version for improved tooling support. --- .../components/context/loading-context.tsx | 46 +++++++++++++ apps/mail/components/ui/nav-user.tsx | 66 +++++++++++++++++-- apps/mail/locales/en.json | 5 +- apps/mail/providers/client-providers.tsx | 7 +- package.json | 2 +- 5 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 apps/mail/components/context/loading-context.tsx diff --git a/apps/mail/components/context/loading-context.tsx b/apps/mail/components/context/loading-context.tsx new file mode 100644 index 0000000000..9d26669c8d --- /dev/null +++ b/apps/mail/components/context/loading-context.tsx @@ -0,0 +1,46 @@ +import { createContext, useContext, useState, type ReactNode } from 'react'; +import { Spinner } from '@/components/ui/spinner'; + +interface LoadingContextType { + isLoading: boolean; + loadingMessage?: string; + setLoading: (loading: boolean, message?: string) => void; +} + +const LoadingContext = createContext(undefined); + +export function LoadingProvider({ children }: { children: ReactNode }) { + const [isLoading, setIsLoading] = useState(false); + const [loadingMessage, setLoadingMessage] = useState(); + + const setLoading = (loading: boolean, message?: string) => { + setIsLoading(loading); + setLoadingMessage(message); + }; + + return ( + + {children} + {isLoading && ( +
    +
    + +
    +

    + {loadingMessage || 'Loading...'} +

    +
    +
    +
    + )} +
    + ); +} + +export function useLoading() { + const context = useContext(LoadingContext); + if (!context) { + throw new Error('useLoading must be used within a LoadingProvider'); + } + return context; +} \ No newline at end of file diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index 325e5d4c29..87c8694792 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -26,6 +26,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Popover, PopoverContent, PopoverTrigger } from './popover'; import { CallInboxDialog, SetupInboxDialog } from '../setup-phone'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useLoading } from '../context/loading-context'; import { signOut, useSession } from '@/lib/auth-client'; import { AddConnectionDialog } from '../connection/add'; import { useTRPC } from '@/providers/query-provider'; @@ -59,9 +60,9 @@ export function NavUser() { const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const { data: activeConnection, refetch: refetchActiveConnection } = useActiveConnection(); - const { revalidate } = useRevalidator(); const [, setPricingDialog] = useQueryState('pricingDialog'); const [category] = useQueryState('category', { defaultValue: 'All Mail' }); + const { setLoading } = useLoading(); const getSettingsHref = useCallback(() => { const currentPath = category @@ -90,12 +91,63 @@ export function NavUser() { const handleAccountSwitch = (connectionId: string) => async () => { if (connectionId === activeConnection?.id) return; - setThreadId(null); - await setDefaultConnection({ connectionId }); - await refetchActiveConnection(); - await refetchConnections(); - await revalidate(); - refetchSession(); + + try { + setLoading(true, t('common.navUser.switchingAccounts')); + + setThreadId(null); + + await setDefaultConnection({ connectionId }); + + const targetConnection = data?.connections?.find((conn: any) => conn.id === connectionId); + if (targetConnection) { + queryClient.setQueryData(trpc.connections.getDefault.queryKey(), targetConnection); + } + + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: trpc.connections.getDefault.queryKey(), + }), + queryClient.invalidateQueries({ + queryKey: trpc.connections.list.queryKey(), + }), + + queryClient.removeQueries({ + queryKey: [['mail']], + }), + + queryClient.removeQueries({ + queryKey: [['labels']], + }), + + queryClient.removeQueries({ + queryKey: [['stats']], + }), + + queryClient.removeQueries({ + queryKey: [['notes']], + }), + + queryClient.removeQueries({ + queryKey: [['brain']], + }), + + queryClient.removeQueries({ + queryKey: [['settings']], + }), + + queryClient.removeQueries({ + queryKey: [['drafts']], + }), + ]); + } catch (error) { + console.error('Error switching accounts:', error); + toast.error(t('common.navUser.failedToSwitchAccount')); + + await refetchActiveConnection(); + } finally { + setLoading(false); + } }; const handleLogout = async () => { diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index ec103f7be6..7a183ffe9b 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -138,7 +138,10 @@ "appTheme": "Toggle Theme", "accounts": "Accounts", "signIn": "Sign in", - "otherAccounts": "Other Accounts" + "otherAccounts": "Other Accounts", + "switchingAccounts": "Switching accounts...", + "accountSwitched": "Account switched successfully", + "failedToSwitchAccount": "Failed to switch accounts. Please try again." }, "mailCategories": { "primary": "Primary", diff --git a/apps/mail/providers/client-providers.tsx b/apps/mail/providers/client-providers.tsx index 78368fb314..c2692908bf 100644 --- a/apps/mail/providers/client-providers.tsx +++ b/apps/mail/providers/client-providers.tsx @@ -1,6 +1,7 @@ import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'; import { SidebarProvider } from '@/components/ui/sidebar'; import { PostHogProvider } from '@/lib/posthog-provider'; +import { LoadingProvider } from '@/components/context/loading-context'; import { useSettings } from '@/hooks/use-settings'; import { Provider as JotaiProvider } from 'jotai'; import type { PropsWithChildren } from 'react'; @@ -23,8 +24,10 @@ export function ClientProviders({ children }: PropsWithChildren) { > - {children} - + + {children} + + diff --git a/package.json b/package.json index 4bf328a2ef..784bc4a16f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "zero", "version": "0.1.0", "private": true, - "packageManager": "pnpm@10.11.0", + "packageManager": "pnpm@10.12.1", "scripts": { "go": "pnpm docker:db:up && pnpm run dev", "prepare": "husky", From 276796673d49106d0c667c56b3f56328a9caa2dc Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 21 Jun 2025 20:25:24 -0700 Subject: [PATCH 10/45] ZeroMCP (#1412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- .../db/migrations/0031_legal_colleen_wing.sql | 40 + .../src/db/migrations/meta/0031_snapshot.json | 1114 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 7 + apps/server/src/db/schema.ts | 38 + apps/server/src/lib/auth.ts | 9 +- apps/server/src/main.ts | 21 +- apps/server/src/routes/agent/tools.ts | 4 +- apps/server/src/routes/chat.ts | 121 +- apps/server/src/services/mcp-service/mcp.ts | 4 +- 9 files changed, 1330 insertions(+), 28 deletions(-) create mode 100644 apps/server/src/db/migrations/0031_legal_colleen_wing.sql create mode 100644 apps/server/src/db/migrations/meta/0031_snapshot.json diff --git a/apps/server/src/db/migrations/0031_legal_colleen_wing.sql b/apps/server/src/db/migrations/0031_legal_colleen_wing.sql new file mode 100644 index 0000000000..a25b7f7ce1 --- /dev/null +++ b/apps/server/src/db/migrations/0031_legal_colleen_wing.sql @@ -0,0 +1,40 @@ +CREATE TABLE "mail0_oauth_access_token" ( + "id" text PRIMARY KEY NOT NULL, + "access_token" text, + "refresh_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "client_id" text, + "user_id" text, + "scopes" text, + "created_at" timestamp, + "updated_at" timestamp, + CONSTRAINT "mail0_oauth_access_token_access_token_unique" UNIQUE("access_token"), + CONSTRAINT "mail0_oauth_access_token_refresh_token_unique" UNIQUE("refresh_token") +); +--> statement-breakpoint +CREATE TABLE "mail0_oauth_application" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "icon" text, + "metadata" text, + "client_id" text, + "client_secret" text, + "redirect_u_r_ls" text, + "type" text, + "disabled" boolean, + "user_id" text, + "created_at" timestamp, + "updated_at" timestamp, + CONSTRAINT "mail0_oauth_application_client_id_unique" UNIQUE("client_id") +); +--> statement-breakpoint +CREATE TABLE "mail0_oauth_consent" ( + "id" text PRIMARY KEY NOT NULL, + "client_id" text, + "user_id" text, + "scopes" text, + "created_at" timestamp, + "updated_at" timestamp, + "consent_given" boolean +); diff --git a/apps/server/src/db/migrations/meta/0031_snapshot.json b/apps/server/src/db/migrations/meta/0031_snapshot.json new file mode 100644 index 0000000000..d7b9320125 --- /dev/null +++ b/apps/server/src/db/migrations/meta/0031_snapshot.json @@ -0,0 +1,1114 @@ +{ + "id": "b111bbbe-f6fe-45b5-8265-c535fa34a077", + "prevId": "2665d18f-0aa5-48f1-aa41-f4895d385def", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.mail0_account": { + "name": "mail0_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_account_user_id_mail0_user_id_fk": { + "name": "mail0_account_user_id_mail0_user_id_fk", + "tableFrom": "mail0_account", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_connection": { + "name": "mail0_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_connection_user_id_mail0_user_id_fk": { + "name": "mail0_connection_user_id_mail0_user_id_fk", + "tableFrom": "mail0_connection", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_connection_user_id_email_unique": { + "name": "mail0_connection_user_id_email_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_early_access": { + "name": "mail0_early_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_early_access": { + "name": "is_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "has_used_ticket": { + "name": "has_used_ticket", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_early_access_email_unique": { + "name": "mail0_early_access_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_jwks": { + "name": "mail0_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_note": { + "name": "mail0_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_note_user_id_mail0_user_id_fk": { + "name": "mail0_note_user_id_mail0_user_id_fk", + "tableFrom": "mail0_note", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_access_token": { + "name": "mail0_oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_oauth_access_token_access_token_unique": { + "name": "mail0_oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": [ + "access_token" + ] + }, + "mail0_oauth_access_token_refresh_token_unique": { + "name": "mail0_oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": [ + "refresh_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_application": { + "name": "mail0_oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_u_r_ls": { + "name": "redirect_u_r_ls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_oauth_application_client_id_unique": { + "name": "mail0_oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_consent": { + "name": "mail0_oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_session": { + "name": "mail0_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_session_user_id_mail0_user_id_fk": { + "name": "mail0_session_user_id_mail0_user_id_fk", + "tableFrom": "mail0_session", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_session_token_unique": { + "name": "mail0_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_summary": { + "name": "mail0_summary", + "schema": "", + "columns": { + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "saved": { + "name": "saved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_reply": { + "name": "suggested_reply", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user": { + "name": "mail0_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "default_connection_id": { + "name": "default_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_prompt": { + "name": "custom_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number_verified": { + "name": "phone_number_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_email_unique": { + "name": "mail0_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "mail0_user_phone_number_unique": { + "name": "mail0_user_phone_number_unique", + "nullsNotDistinct": false, + "columns": [ + "phone_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_hotkeys": { + "name": "mail0_user_hotkeys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shortcuts": { + "name": "shortcuts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_user_hotkeys_user_id_mail0_user_id_fk": { + "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_hotkeys", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_settings": { + "name": "mail0_user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"isDefault\":false}]}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_user_settings_user_id_mail0_user_id_fk": { + "name": "mail0_user_settings_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_settings", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_settings_user_id_unique": { + "name": "mail0_user_settings_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_verification": { + "name": "mail0_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_writing_style_matrix": { + "name": "mail0_writing_style_matrix", + "schema": "", + "columns": { + "connectionId": { + "name": "connectionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "numMessages": { + "name": "numMessages", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": { + "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk", + "tableFrom": "mail0_writing_style_matrix", + "tableTo": "mail0_connection", + "columnsFrom": [ + "connectionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mail0_writing_style_matrix_connectionId_pk": { + "name": "mail0_writing_style_matrix_connectionId_pk", + "columns": [ + "connectionId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/_journal.json b/apps/server/src/db/migrations/meta/_journal.json index 358846aaa8..242dbe6803 100644 --- a/apps/server/src/db/migrations/meta/_journal.json +++ b/apps/server/src/db/migrations/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1750384854243, "tag": "0030_blue_grandmaster", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1750551357064, + "tag": "0031_legal_colleen_wing", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index 8751a48f4f..ffdd57a967 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -172,3 +172,41 @@ export const jwks = createTable('jwks', { privateKey: text('private_key').notNull(), createdAt: timestamp('created_at').notNull(), }); + +export const oauthApplication = createTable('oauth_application', { + id: text('id').primaryKey(), + name: text('name'), + icon: text('icon'), + metadata: text('metadata'), + clientId: text('client_id').unique(), + clientSecret: text('client_secret'), + redirectURLs: text('redirect_u_r_ls'), + type: text('type'), + disabled: boolean('disabled'), + userId: text('user_id'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), +}); + +export const oauthAccessToken = createTable('oauth_access_token', { + id: text('id').primaryKey(), + accessToken: text('access_token').unique(), + refreshToken: text('refresh_token').unique(), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + clientId: text('client_id'), + userId: text('user_id'), + scopes: text('scopes'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), +}); + +export const oauthConsent = createTable('oauth_consent', { + id: text('id').primaryKey(), + clientId: text('client_id'), + userId: text('user_id'), + scopes: text('scopes'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + consentGiven: boolean('consent_given'), +}); diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts index 68b5004665..e162615b62 100644 --- a/apps/server/src/lib/auth.ts +++ b/apps/server/src/lib/auth.ts @@ -6,7 +6,7 @@ import { session, userHotkeys, } from '../db/schema'; -import { createAuthMiddleware, phoneNumber, jwt, bearer } from 'better-auth/plugins'; +import { createAuthMiddleware, phoneNumber, jwt, bearer, mcp } from 'better-auth/plugins'; import { type Account, betterAuth, type BetterAuthOptions } from 'better-auth'; import { getBrowserTimezone, isValidTimezone } from './timezones'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; @@ -14,6 +14,7 @@ import { getSocialProviders } from './auth-providers'; import { redis, resend, twilio } from './services'; import { getContext } from 'hono/context-storage'; import { defaultUserSettings } from './schemas'; +import { getMigrations } from 'better-auth/db'; import { disableBrainFunction } from './brain'; import { APIError } from 'better-auth/api'; import { getZeroDB } from './server-utils'; @@ -78,6 +79,9 @@ export const createAuth = () => { return betterAuth({ plugins: [ + mcp({ + loginPage: env.VITE_PUBLIC_APP_URL + '/login', + }), jwt(), bearer(), phoneNumber({ @@ -235,7 +239,8 @@ const createAuthConfig = () => { database: drizzleAdapter(db, { provider: 'pg' }), secondaryStorage: { get: async (key: string) => { - return ((await cache.get(key)) as string) ?? null; + const value = await cache.get(key); + return typeof value === 'string' ? value : value ? JSON.stringify(value) : null; }, set: async (key: string, value: string, ttl?: number) => { if (ttl) await cache.set(key, value, { ex: ttl }); diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index d31698ca54..edbd237df0 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -16,6 +16,7 @@ import { } from './db/schema'; import { env, WorkerEntrypoint, DurableObject } from 'cloudflare:workers'; import { MainWorkflow, ThreadWorkflow, ZeroWorkflow } from './pipelines'; +import { oAuthDiscoveryMetadata } from 'better-auth/plugins'; import { getZeroDB, verifyToken } from './lib/server-utils'; import { EProviders, type ISubscribeBatch } from './types'; import { eq, and, desc, asc, inArray } from 'drizzle-orm'; @@ -23,6 +24,7 @@ import { contextStorage } from 'hono/context-storage'; import { defaultUserSettings } from './lib/schemas'; import { createLocalJWKSet, jwtVerify } from 'jose'; import { routePartykitRequest } from 'partyserver'; +import { withMcpAuth } from 'better-auth/plugins'; import { enableBrainFunction } from './lib/brain'; import { trpcServer } from '@hono/trpc-server'; import { agentsMiddleware } from 'hono-agents'; @@ -379,7 +381,9 @@ export default class extends WorkerEntrypoint { .route('/ai', aiRouter) .route('/autumn', autumnApi) .route('/public', publicRouter) - .on(['GET', 'POST'], '/auth/*', (c) => c.var.auth.handler(c.req.raw)) + .on(['GET', 'POST', 'OPTIONS'], '/auth/*', (c) => { + return c.var.auth.handler(c.req.raw); + }) .use( trpcServer({ endpoint: '/api/trpc', @@ -421,6 +425,10 @@ export default class extends WorkerEntrypoint { exposeHeaders: ['X-Zero-Redirect'], }), ) + .get('.well-known/oauth-authorization-server', async (c) => { + const auth = createAuth(); + return oAuthDiscoveryMetadata(auth)(c.req.raw); + }) .mount( '/sse', async (request, env, ctx) => { @@ -428,8 +436,13 @@ export default class extends WorkerEntrypoint { if (!authBearer) { return new Response('Unauthorized', { status: 401 }); } + const auth = createAuth(); + const session = await auth.api.getMcpSession({ headers: request.headers }); + if (!session) { + return new Response('Unauthorized', { status: 401 }); + } ctx.props = { - cookie: authBearer, + userId: session?.userId, }; return ZeroMCP.serveSSE('/sse', { binding: 'ZERO_MCP' }).fetch(request, env, ctx); }, @@ -442,8 +455,10 @@ export default class extends WorkerEntrypoint { if (!authBearer) { return new Response('Unauthorized', { status: 401 }); } + const auth = createAuth(); + const session = await auth.api.getMcpSession({ headers: request.headers }); ctx.props = { - cookie: authBearer, + userId: session?.userId, }; return ZeroMCP.serve('/mcp', { binding: 'ZERO_MCP' }).fetch(request, env, ctx); }, diff --git a/apps/server/src/routes/agent/tools.ts b/apps/server/src/routes/agent/tools.ts index 1cc5b6abba..fd07d48d71 100644 --- a/apps/server/src/routes/agent/tools.ts +++ b/apps/server/src/routes/agent/tools.ts @@ -65,7 +65,7 @@ const askZeroMailbox = (connectionId: string) => }; } return { - response: threadResults.matches.map((e) => e.metadata?.['content'] ?? 'no content'), + response: threadResults.matches.map((e) => e.metadata?.['summary'] ?? 'no content'), success: true, }; }, @@ -96,7 +96,7 @@ const askZeroThread = (connectionId: string) => const topThread = threadResults.matches[0]; if (!topThread) return { response: "I don't know, no threads found", success: false }; return { - response: topThread.metadata?.['content'] ?? 'no content', + response: topThread.metadata?.['summary'] ?? 'no content', success: true, }; }, diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 75e07755a7..cfd4fec003 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -25,9 +25,9 @@ import type { Message as ChatMessage } from 'ai'; import { connection } from '../db/schema'; import { env } from 'cloudflare:workers'; import { openai } from '@ai-sdk/openai'; +import { and, eq } from 'drizzle-orm'; import { McpAgent } from 'agents/mcp'; import { groq } from '@ai-sdk/groq'; -import { eq } from 'drizzle-orm'; import { createDb } from '../db'; import { z } from 'zod'; @@ -351,33 +351,86 @@ export class ZeroAgent extends AIChatAgent { } } -export class ZeroMCP extends McpAgent { - auth: SimpleAuth; +export class ZeroMCP extends McpAgent { server = new McpServer({ name: 'zero-mcp', version: '1.0.0', description: 'Zero MCP', }); + activeConnectionId: string | undefined; + constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); - this.auth = createSimpleAuth(); } async init(): Promise { - const session = await this.auth.api.getSession({ headers: parseHeaders(this.props.cookie) }); - if (!session) { - throw new Error('Unauthorized'); - } const db = createDb(env.HYPERDRIVE.connectionString); const _connection = await db.query.connection.findFirst({ - where: eq(connection.email, session.user.email), + where: eq(connection.userId, this.props.userId), }); if (!_connection) { throw new Error('Unauthorized'); } + this.activeConnectionId = _connection.id; const driver = connectionToDriver(_connection); + this.server.tool('getConnections', async () => { + const connections = await db.query.connection.findMany({ + where: eq(connection.userId, this.props.userId), + }); + return { + content: connections.map((c) => ({ + type: 'text', + text: `Email: ${c.email} | Provider: ${c.providerId}`, + })), + }; + }); + + this.server.tool('getActiveConnection', async () => { + if (!this.activeConnectionId) { + throw new Error('No active connection'); + } + const _connection = await db.query.connection.findFirst({ + where: eq(connection.id, this.activeConnectionId), + }); + if (!_connection) { + throw new Error('Connection not found'); + } + return { + content: [ + { + type: 'text' as const, + text: `Email: ${_connection.email} | Provider: ${_connection.providerId}`, + }, + ], + }; + }); + + this.server.tool( + 'setActiveConnection', + { + email: z.string(), + }, + async (s) => { + const _connection = await db.query.connection.findFirst({ + where: and(eq(connection.userId, this.props.userId), eq(connection.email, s.email)), + }); + if (!_connection) { + throw new Error('Connection not found'); + } + this.activeConnectionId = _connection.id; + return { + content: [ + { + type: 'text' as const, + text: `Active connection set to ${_connection.email}`, + }, + ], + }; + }, + ); + this.server.tool( 'buildGmailSearchQuery', { @@ -423,7 +476,11 @@ export class ZeroMCP extends McpAgent { return [ { type: 'text' as const, - text: `Subject: ${loadedThread.latest?.subject} | ID: ${thread.id} | Received: ${loadedThread.latest?.receivedOn}`, + text: `Subject: ${loadedThread.latest?.subject} | ID: ${thread.id} | Latest Message Received: ${loadedThread.latest?.receivedOn}`, + }, + { + type: 'text' as const, + text: `Latest Message Sender: ${loadedThread.latest?.sender}`, }, ]; }), @@ -448,28 +505,54 @@ export class ZeroMCP extends McpAgent { }, async (s) => { const thread = await driver.get(s.threadId); + const initialResponse = [ + { + type: 'text' as const, + text: `Subject: ${thread.latest?.subject}`, + }, + { + type: 'text' as const, + text: `Latest Message Received: ${thread.latest?.receivedOn}`, + }, + { + type: 'text' as const, + text: `Latest Message Sender: ${thread.latest?.sender}`, + }, + { + type: 'text' as const, + text: `Latest Message Raw Content: ${thread.latest?.decodedBody}`, + }, + { + type: 'text' as const, + text: `Thread ID: ${s.threadId}`, + }, + ]; const response = await env.VECTORIZE.getByIds([s.threadId]); - if (response.length && response?.[0]?.metadata?.['content']) { - const content = response[0].metadata['content'] as string; + if (response.length && response?.[0]?.metadata?.['summary']) { + const content = response[0].metadata['summary'] as string; const shortResponse = await env.AI.run('@cf/facebook/bart-large-cnn', { input_text: content, }); return { content: [ + ...initialResponse, + { + type: 'text', + text: `Subject: ${thread.latest?.subject}`, + }, + { + type: 'text', + text: `Long Summary: ${content}`, + }, { type: 'text', - text: shortResponse.summary, + text: `Short Summary: ${shortResponse.summary}`, }, ], }; } return { - content: [ - { - type: 'text', - text: `Subject: ${thread.latest?.subject}`, - }, - ], + content: initialResponse, }; }, ); diff --git a/apps/server/src/services/mcp-service/mcp.ts b/apps/server/src/services/mcp-service/mcp.ts index 1ecd2bfb1b..7b3b715930 100644 --- a/apps/server/src/services/mcp-service/mcp.ts +++ b/apps/server/src/services/mcp-service/mcp.ts @@ -138,8 +138,8 @@ export class ZeroMCP extends McpAgent }; // const response = await env.VECTORIZE.getByIds([s.threadId]); - // if (response.length && response?.[0]?.metadata?.['content']) { - // const content = response[0].metadata['content'] as string; + // if (response.length && response?.[0]?.metadata?.['summary']) { + // const content = response[0].metadata['summary'] as string; // const shortResponse = await env.AI.run('@cf/facebook/bart-large-cnn', { // input_text: content, // }); From 0980b82916e03bc17613b969e7eeedfcebce6940 Mon Sep 17 00:00:00 2001 From: Max Comperatore <131000419+pyoneerC@users.noreply.github.com> Date: Sun, 22 Jun 2025 00:25:34 -0300 Subject: [PATCH 11/45] Add dependabot configuration for automated dependency updates (#1405) --- .github/dependabot.yml | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..de941520d6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,64 @@ +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + groups: + # Group all minor and patch updates for the main repo together + dependencies: + patterns: + - "*" + + # Enable version updates for the mail app + - package-ecosystem: "npm" + directory: "/apps/mail" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + groups: + # Group React ecosystem updates together + react: + patterns: + - "react*" + - "@types/react*" + # Group UI-related packages + ui-dependencies: + patterns: + - "@tiptap*" + - "@dnd-kit*" + - "@hookform*" + # Group all other dependencies + other-dependencies: + patterns: + - "*" + + # Enable version updates for the server app + - package-ecosystem: "npm" + directory: "/apps/server" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + # Enable version updates for Docker + - package-ecosystem: "docker" + directory: "/docker/app" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + + # Enable version updates for Docker DB + - package-ecosystem: "docker" + directory: "/docker/db" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 From b56b6f839f94e2ea5cf3c2b9cc28059ceb4468f8 Mon Sep 17 00:00:00 2001 From: amrit Date: Sun, 22 Jun 2025 08:56:50 +0530 Subject: [PATCH 12/45] Add prompt template selection to general settings page (#1354) --- .../app/(routes)/settings/general/page.tsx | 1 + apps/mail/components/ui/prompts-dialog.tsx | 163 ++++++++++++++---- apps/server/src/lib/brain.ts | 9 +- apps/server/src/routes/chat.ts | 6 +- apps/server/src/trpc/routes/ai/compose.ts | 7 +- apps/server/src/trpc/routes/brain.ts | 18 +- apps/server/src/types.ts | 5 +- 7 files changed, 167 insertions(+), 42 deletions(-) diff --git a/apps/mail/app/(routes)/settings/general/page.tsx b/apps/mail/app/(routes)/settings/general/page.tsx index dfea0fcf8c..4266f9b3cd 100644 --- a/apps/mail/app/(routes)/settings/general/page.tsx +++ b/apps/mail/app/(routes)/settings/general/page.tsx @@ -311,6 +311,7 @@ export default function GeneralPage() { )} /> + diff --git a/apps/mail/components/ui/prompts-dialog.tsx b/apps/mail/components/ui/prompts-dialog.tsx index d10bce2505..319b1794f5 100644 --- a/apps/mail/components/ui/prompts-dialog.tsx +++ b/apps/mail/components/ui/prompts-dialog.tsx @@ -1,3 +1,16 @@ +import { + BookDashedIcon, + GitBranchPlus, + MessageSquareIcon, + RefreshCcwDotIcon, + SendIcon, + RotateCcwIcon, +} from 'lucide-react'; +import { + SummarizeMessage, + SummarizeThread, + ReSummarizeThread, +} from '../../../server/src/lib/brain.fallback.prompts'; import { Dialog, DialogContent, @@ -6,26 +19,106 @@ import { DialogTitle, DialogTrigger, } from './dialog'; -import { - BookDashedIcon, - GitBranchPlus, - MessageSquareIcon, - RefreshCcwDotIcon, - SendIcon, -} from 'lucide-react'; import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'; import { AiChatPrompt, StyledEmailAssistantSystemPrompt } from '@/lib/prompts'; import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTRPC } from '@/providers/query-provider'; +import { EPrompts } from '../../../server/src/types'; +import { useMutation } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; +import { useForm } from 'react-hook-form'; import { Paper } from '../icons/icons'; import { Textarea } from './textarea'; import { Link } from 'react-router'; +import { useMemo } from 'react'; +import { toast } from 'sonner'; + +const isPromptValid = (prompt: string): boolean => { + const trimmed = prompt.trim(); + return trimmed !== '' && trimmed.toLowerCase() !== 'undefined'; +}; + +const initialValues: Record = { + [EPrompts.Chat]: '', + [EPrompts.Compose]: '', + [EPrompts.SummarizeThread]: '', + [EPrompts.ReSummarizeThread]: '', + [EPrompts.SummarizeMessage]: '', +}; + +const fallbackPrompts = { + [EPrompts.Chat]: AiChatPrompt('', '', ''), + [EPrompts.Compose]: StyledEmailAssistantSystemPrompt(), + [EPrompts.SummarizeThread]: SummarizeThread, + [EPrompts.ReSummarizeThread]: ReSummarizeThread, + [EPrompts.SummarizeMessage]: SummarizeMessage, +}; export function PromptsDialog() { const trpc = useTRPC(); + const queryClient = useQueryClient(); const { data: prompts } = useQuery(trpc.brain.getPrompts.queryOptions()); + + const { mutateAsync: updatePrompt, isPending: isSavingPrompt } = useMutation( + trpc.brain.updatePrompt.mutationOptions({ + onSuccess: () => { + toast.success('Prompt updated'); + queryClient.invalidateQueries({ queryKey: trpc.brain.getPrompts.queryKey() }); + }, + onError: (error) => { + toast.error(error.message ?? 'Failed to update prompt'); + }, + }), + ); + + const mappedValues = useMemo(() => { + if (!prompts) return initialValues; + return Object.fromEntries( + Object.entries(initialValues).map(([key]) => [ + key, + isPromptValid(prompts[key as EPrompts] ?? '') + ? prompts[key as EPrompts] + : fallbackPrompts[key as EPrompts], + ]), + ) as Record; + }, [prompts]); + + const { register, getValues, setValue } = useForm>({ + defaultValues: initialValues, + values: mappedValues, + }); + + const resetToDefault = (promptType: EPrompts) => { + setValue(promptType, fallbackPrompts[promptType]); + }; + + const renderPromptButtons = (promptType: EPrompts, enumType: EPrompts) => ( +
    + + +
    + ); + return ( @@ -53,8 +146,7 @@ export function PromptsDialog() { - We believe in Open Source, so we're open sourcing our AI system prompts. Soon you will - be able to customize them to your liking. + We believe in Open Source, so we're open sourcing our AI system prompts. @@ -75,48 +167,53 @@ export function PromptsDialog() { Summarize Message - - - This system prompt is used in the chat sidebar agent. The agent has multiple tools - available. - -