From 36d9b476f126cd5098516e26c9a5850bfa0d58b3 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 22:14:34 -0700 Subject: [PATCH 01/38] separate vectorized dbs (#1637) 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._ --- apps/server/wrangler.jsonc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 5071e05f64..5a08cf3f5f 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -12,11 +12,11 @@ "vectorize": [ { "binding": "VECTORIZE", - "index_name": "threads-vector", + "index_name": "threads-vector-staging", }, { "binding": "VECTORIZE_MESSAGE", - "index_name": "messages-vector", + "index_name": "messages-vector-staging", }, ], "r2_buckets": [ @@ -157,11 +157,11 @@ "vectorize": [ { "binding": "VECTORIZE", - "index_name": "threads-vector", + "index_name": "threads-vector-staging", }, { "binding": "VECTORIZE_MESSAGE", - "index_name": "messages-vector", + "index_name": "messages-vector-staging", }, ], "limits": { From 38bcc6e96bb1e612ce17a5ce426d2e97a334d80e Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 23:04:51 -0700 Subject: [PATCH 02/38] Enable voice (#1639) 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._ --- apps/mail/app/(routes)/layout.tsx | 2 -- apps/mail/app/(routes)/mail/layout.tsx | 2 -- apps/mail/components/create/ai-chat.tsx | 6 +++++- apps/mail/lib/hotkeys/global-hotkeys.tsx | 5 ++--- apps/mail/providers/voice-provider.tsx | 24 +++++++++--------------- 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/apps/mail/app/(routes)/layout.tsx b/apps/mail/app/(routes)/layout.tsx index 8e3f11f897..1bc11bcf90 100644 --- a/apps/mail/app/(routes)/layout.tsx +++ b/apps/mail/app/(routes)/layout.tsx @@ -6,13 +6,11 @@ import { Outlet } from 'react-router'; export default function Layout() { return ( - {/* */}
- {/*
*/}
); } diff --git a/apps/mail/app/(routes)/mail/layout.tsx b/apps/mail/app/(routes)/mail/layout.tsx index 50c81c51e5..d1ea00286b 100644 --- a/apps/mail/app/(routes)/mail/layout.tsx +++ b/apps/mail/app/(routes)/mail/layout.tsx @@ -8,7 +8,6 @@ import type { Route } from './+types/layout'; export default function MailLayout() { return ( - //
@@ -17,6 +16,5 @@ export default function MailLayout() { - // ); } diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index ce0b000300..50faed6b77 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -2,6 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useAIFullScreen, useAISidebar } from '../ui/ai-sidebar'; +import { VoiceProvider } from '@/providers/voice-provider'; import useComposeEditor from '@/hooks/use-compose-editor'; import { useRef, useCallback, useEffect } from 'react'; import { Markdown } from '@react-email/components'; @@ -10,6 +11,7 @@ import { TextShimmer } from '../ui/text-shimmer'; import { useThread } from '@/hooks/use-threads'; import { MailLabels } from '../mail/mail-list'; import { cn, getEmailLogo } from '@/lib/utils'; +import { VoiceButton } from '../voice-button'; import { EditorContent } from '@tiptap/react'; import { CurvedArrow } from '../icons/icons'; import { Tools } from '../../types/tools'; @@ -426,7 +428,9 @@ export function AIChat({
- {/* */} + + +
)} -
+
); } diff --git a/apps/server/src/lib/email-processor.ts b/apps/server/src/lib/email-processor.ts index 2ebd3fa1ca..a436614639 100644 --- a/apps/server/src/lib/email-processor.ts +++ b/apps/server/src/lib/email-processor.ts @@ -14,8 +14,12 @@ export function processEmailHtml({ html, shouldLoadImages, theme }: ProcessEmail let hasBlockedImages = false; const sanitizeConfig: sanitizeHtml.IOptions = { - allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), - allowedAttributes: false, + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'title']), + allowedAttributes: { + img: ['src', 'alt'], + a: ['href', 'target', 'rel'], + '*': ['style', 'class', 'width', 'height', 'colspan', 'rowspan'], + }, allowedSchemes: shouldLoadImages ? ['http', 'https', 'mailto', 'tel', 'data', 'cid', 'blob'] : ['http', 'https', 'mailto', 'tel', 'cid'], @@ -47,6 +51,8 @@ export function processEmailHtml({ html, shouldLoadImages, theme }: ProcessEmail const $ = cheerio.load(sanitized); + $('title').remove(); + $('img[width="1"][height="1"]').remove(); $('img[width="0"][height="0"]').remove(); @@ -112,12 +118,6 @@ export function processEmailHtml({ html, shouldLoadImages, theme }: ProcessEmail border-collapse: collapse; } - .gmail_quote { - margin: 1em 0; - padding-left: 1em; - border-left: 1px solid ${theme === 'dark' ? '#666' : '#ccc'}; - } - ::selection { background: #b3d4fc; text-shadow: none; From 307f1de14d98b0324238e30e78fdfd32c636e28d Mon Sep 17 00:00:00 2001 From: amrit Date: Mon, 7 Jul 2025 22:28:36 +0530 Subject: [PATCH 04/38] fix: allow `; From 9b6b3c6abf46fa724cd66ff0142cde8baf83617a Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 8 Jul 2025 09:58:59 -0700 Subject: [PATCH 14/38] Drop not (#1676) 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._ ## Summary by CodeRabbit * **Chores** * Updated environment configuration to disable the automatic dropping of agent-related tables in both staging and production environments. --- apps/server/wrangler.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index be7a84a592..d6c97fe925 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -260,7 +260,7 @@ "VITE_PUBLIC_BACKEND_URL": "https://sapi.0.email", "VITE_PUBLIC_APP_URL": "https://staging.0.email", "DISABLE_CALLS": "", - "DROP_AGENT_TABLES": "true", + "DROP_AGENT_TABLES": "false", "THREAD_SYNC_MAX_COUNT": "40", "THREAD_SYNC_LOOP": "true", }, @@ -401,7 +401,7 @@ "VITE_PUBLIC_BACKEND_URL": "https://api.0.email", "VITE_PUBLIC_APP_URL": "https://0.email", "DISABLE_CALLS": "true", - "DROP_AGENT_TABLES": "true", + "DROP_AGENT_TABLES": "false", "THREAD_SYNC_MAX_COUNT": "40", "THREAD_SYNC_LOOP": "true", }, From e8b61adf1e87f5d25c8c874b463a3dc4fa1d3616 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Tue, 8 Jul 2025 18:15:43 +0100 Subject: [PATCH 15/38] better checbox in mail list (#1677) # Enhanced Select All Checkbox UI in Mail Component ## Description This PR enhances the "Select All" checkbox in the mail component with a more user-friendly design. The changes include: ## Summary by CodeRabbit * **Style** * Improved layout and spacing for sidebar toggle and select-all checkbox elements. * Updated the visual appearance of the select-all checkbox with a custom-styled label and icon, enhancing clarity and accessibility. * Adjusted icon rendering to ensure consistent coloring with the rest of the interface. --- apps/mail/components/icons/icons.tsx | 1 + apps/mail/components/mail/mail.tsx | 4 +- .../components/mail/select-all-checkbox.tsx | 53 ++++++++++++++----- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx index ac459a50d5..9774098ba0 100644 --- a/apps/mail/components/icons/icons.tsx +++ b/apps/mail/components/icons/icons.tsx @@ -1641,6 +1641,7 @@ export const Check = ({ className }: { className?: string }) => ( ); diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 9c041a901a..6a434f64eb 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -486,9 +486,9 @@ export function MailLayout() { )} >
-
+
- +
diff --git a/apps/mail/components/mail/select-all-checkbox.tsx b/apps/mail/components/mail/select-all-checkbox.tsx index d347844fda..33990f9a62 100644 --- a/apps/mail/components/mail/select-all-checkbox.tsx +++ b/apps/mail/components/mail/select-all-checkbox.tsx @@ -1,12 +1,13 @@ -import { Checkbox } from '@/components/ui/checkbox'; -import { useMail } from '@/components/mail/use-mail'; -import { useThreads } from '@/hooks/use-threads'; +import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; import { useSearchValue } from '@/hooks/use-search-value'; import { trpcClient } from '@/providers/query-provider'; -import { cn } from '@/lib/utils'; +import { useMail } from '@/components/mail/use-mail'; +import { Checkbox } from '@/components/ui/checkbox'; +import { useThreads } from '@/hooks/use-threads'; import { useParams } from 'react-router'; +import { Check } from '../icons/icons'; +import { cn } from '@/lib/utils'; import { toast } from 'sonner'; -import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; export default function SelectAllCheckbox({ className }: { className?: string }) { const [mail, setMail] = useMail(); @@ -97,12 +98,38 @@ export default function SelectAllCheckbox({ className }: { className?: string }) }, [folder, query]); return ( - +
+ + +
); -} \ No newline at end of file +} From 8abb73996046a7094c1a990b0699f331022c83aa Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 8 Jul 2025 11:19:02 -0700 Subject: [PATCH 16/38] fix reply close button (#1678) 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._ --- apps/mail/components/create/email-composer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index e46a379fc8..4a86b2f4b7 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -924,7 +924,7 @@ export function EmailComposer({ > Bcc - {onClose && isMobile && ( + {onClose && ( - - ))} -
- ); -}; diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index d60d03f1d0..9eaeb834f3 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -755,7 +755,7 @@ export function ThreadDisplay() {
-
+

It's empty here

Choose an email to view details diff --git a/apps/mail/lib/constants.tsx b/apps/mail/lib/constants.tsx index 8031338b34..981cc76603 100644 --- a/apps/mail/lib/constants.tsx +++ b/apps/mail/lib/constants.tsx @@ -10,7 +10,7 @@ export const SIDEBAR_WIDTH_ICON = '3rem'; export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; export const BASE_URL = import.meta.env.VITE_PUBLIC_APP_URL; export const MAX_URL_LENGTH = 2000; -export const CACHE_BURST_KEY = 'cache-burst:v0.0.4'; +export const CACHE_BURST_KEY = 'cache-burst:v0.0.5'; export const emailProviders = [ { diff --git a/apps/mail/providers/query-provider.tsx b/apps/mail/providers/query-provider.tsx index 2c0f429955..a1d5282f81 100644 --- a/apps/mail/providers/query-provider.tsx +++ b/apps/mail/providers/query-provider.tsx @@ -120,7 +120,7 @@ export function QueryProvider({ persistOptions={{ persister, buster: CACHE_BURST_KEY, - maxAge: 1000 * 60 * 60 * 24 * 3, // 3 days + maxAge: 1000 * 60 * 1, // 1 minute, we're storing in DOs }} onSuccess={() => { const threadQueryKey = [['mail', 'listThreads'], { type: 'infinite' }]; From 2b1e4002ea56439612f2482d2c719d75c6fa7cf0 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 8 Jul 2025 14:15:07 -0700 Subject: [PATCH 22/38] even less cache (#1686) 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._ --- apps/mail/hooks/use-threads.ts | 2 +- apps/mail/providers/query-provider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mail/hooks/use-threads.ts b/apps/mail/hooks/use-threads.ts index 232911c9fd..cd7ef36131 100644 --- a/apps/mail/hooks/use-threads.ts +++ b/apps/mail/hooks/use-threads.ts @@ -73,7 +73,7 @@ export const useThread = (threadId: string | null, historyId?: string | null) => }, { enabled: !!id && !!session?.user.id, - staleTime: 1000 * 60 * 60, // 60 minutes + staleTime: 1000 * 60 * 1, // 1 minute }, ), ); diff --git a/apps/mail/providers/query-provider.tsx b/apps/mail/providers/query-provider.tsx index a1d5282f81..3d661e7da5 100644 --- a/apps/mail/providers/query-provider.tsx +++ b/apps/mail/providers/query-provider.tsx @@ -51,7 +51,7 @@ export const makeQueryClient = (connectionId: string | null) => retry: false, refetchOnWindowFocus: false, queryKeyHashFn: (queryKey) => hashKey([{ connectionId }, ...queryKey]), - gcTime: 1000 * 60 * 60 * 24, + gcTime: 1000 * 60 * 1, }, mutations: { onError: (err) => console.error(err.message), From 7fec116c24393c9a6a982e00aeb029d3859c9bd3 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 8 Jul 2025 16:16:53 -0700 Subject: [PATCH 23/38] categories (#1689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Refactor Labels API Usage and Optimize Component Performance ## Description This PR refactors how labels are accessed throughout the application by restructuring the `useLabels` hook to return `userLabels` and `systemLabels` separately. It also adds performance optimizations by memoizing component calculations and action handlers in the thread context menu. Additionally, it introduces a new category dropdown component for better label filtering and temporarily disables the categories settings page. ## Type of Change - [x] 🐛 Bug fix (non-breaking change which fixes an issue) - [x] ⚡ Performance improvement ## Areas Affected - [x] User Interface/Experience - [x] Data Storage/Management ## Testing Done - [x] Manual testing performed ## Checklist - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] All tests pass locally ## Additional Notes The main changes include: 1. Restructured `useLabels` hook to separate user and system labels 2. Updated all components to use the new label structure 3. Added memoization to thread context menu actions and calculations 4. Implemented a new CategoryDropdown component for label filtering 5. Commented out the categories settings page route 6. Fixed spacing and alignment issues in the mail layout --- apps/mail/app/(routes)/mail/[folder]/page.tsx | 2 +- .../app/(routes)/settings/labels/page.tsx | 16 +- apps/mail/app/routes.ts | 2 +- .../context/command-palette-context.tsx | 2 +- .../components/context/thread-context.tsx | 179 ++++++------ apps/mail/components/mail/mail.tsx | 260 ++++++++++++------ apps/mail/components/ui/nav-main.tsx | 6 +- apps/mail/components/ui/sidebar-labels.tsx | 13 +- apps/mail/config/navigation.ts | 10 +- apps/mail/hooks/use-labels.ts | 13 +- apps/mail/hooks/use-stats.ts | 1 - apps/mail/hooks/use-threads.ts | 4 +- apps/server/src/routes/chat.ts | 2 +- 13 files changed, 302 insertions(+), 208 deletions(-) diff --git a/apps/mail/app/(routes)/mail/[folder]/page.tsx b/apps/mail/app/(routes)/mail/[folder]/page.tsx index fcbcbf6373..8558675f75 100644 --- a/apps/mail/app/(routes)/mail/[folder]/page.tsx +++ b/apps/mail/app/(routes)/mail/[folder]/page.tsx @@ -26,7 +26,7 @@ export default function MailPage() { const isStandardFolder = ALLOWED_FOLDERS.includes(folder); - const { data: userLabels, isLoading: isLoadingLabels } = useLabels(); + const { userLabels, isLoading: isLoadingLabels } = useLabels(); useEffect(() => { if (isStandardFolder) { diff --git a/apps/mail/app/(routes)/settings/labels/page.tsx b/apps/mail/app/(routes)/settings/labels/page.tsx index 787fc0502a..059356a0c8 100644 --- a/apps/mail/app/(routes)/settings/labels/page.tsx +++ b/apps/mail/app/(routes)/settings/labels/page.tsx @@ -28,18 +28,18 @@ import { HexColorPicker } from 'react-colorful'; import { Bin } from '@/components/icons/icons'; import { useLabels } from '@/hooks/use-labels'; import { GMAIL_COLORS } from '@/lib/constants'; -import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { m } from '@/paraglide/messages'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; import { useForm } from 'react-hook-form'; +import { m } from '@/paraglide/messages'; import { Command } from 'lucide-react'; import { COLORS } from './colors'; import { useState } from 'react'; import { toast } from 'sonner'; export default function LabelsPage() { - const { data: labels, isLoading, error, refetch } = useLabels(); + const { userLabels: labels, isLoading, error, refetch } = useLabels(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingLabel, setEditingLabel] = useState(null); @@ -63,7 +63,7 @@ export default function LabelsPage() { const handleDelete = async (id: string) => { toast.promise(deleteLabel({ id }), { - loading: m['common.labels.deletingLabel'](), + loading: m['common.labels.deletingLabel'](), success: m['common.labels.deleteLabelSuccess'](), error: m['common.labels.failedToDeleteLabel'](), finally: async () => { @@ -113,7 +113,7 @@ export default function LabelsPage() {

{error.message}

) : labels?.length === 0 ? (

- {m['common.mail.noLabelsAvailable']()} + {m['common.mail.noLabelsAvailable']()}

) : (
@@ -147,7 +147,7 @@ export default function LabelsPage() { - {m['common.labels.editLabel']()} + {m['common.labels.editLabel']()} @@ -162,7 +162,7 @@ export default function LabelsPage() { - {m['common.labels.deleteLabel']()} + {m['common.labels.deleteLabel']()}
diff --git a/apps/mail/app/routes.ts b/apps/mail/app/routes.ts index 57d9fbe23f..29b9244baf 100644 --- a/apps/mail/app/routes.ts +++ b/apps/mail/app/routes.ts @@ -42,7 +42,7 @@ export default [ route('/danger-zone', '(routes)/settings/danger-zone/page.tsx'), route('/general', '(routes)/settings/general/page.tsx'), route('/labels', '(routes)/settings/labels/page.tsx'), - route('/categories', '(routes)/settings/categories/page.tsx'), + // route('/categories', '(routes)/settings/categories/page.tsx'), route('/notifications', '(routes)/settings/notifications/page.tsx'), route('/privacy', '(routes)/settings/privacy/page.tsx'), route('/security', '(routes)/settings/security/page.tsx'), diff --git a/apps/mail/components/context/command-palette-context.tsx b/apps/mail/components/context/command-palette-context.tsx index cb1059b094..3118809694 100644 --- a/apps/mail/components/context/command-palette-context.tsx +++ b/apps/mail/components/context/command-palette-context.tsx @@ -197,7 +197,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const { pathname } = useLocation(); - const { data: userLabels = [] } = useLabels(); + const { userLabels = [] } = useLabels(); const trpc = useTRPC(); const { mutateAsync: generateSearchQuery, isPending } = useMutation( trpc.ai.generateSearchQuery.mutationOptions(), diff --git a/apps/mail/components/context/thread-context.tsx b/apps/mail/components/context/thread-context.tsx index e68effcf20..53e5fe2e14 100644 --- a/apps/mail/components/context/thread-context.tsx +++ b/apps/mail/components/context/thread-context.tsx @@ -58,7 +58,7 @@ interface EmailContextMenuProps { } const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected: string[] }) => { - const { data: labels } = useLabels(); + const { userLabels: labels } = useLabels(); const { optimisticToggleLabel } = useOptimisticActions(); const targetThreadIds = bulkSelected.length > 0 ? bulkSelected : [threadId]; @@ -141,35 +141,40 @@ export function ThreadContextMenu({ const [, setMode] = useQueryState('mode'); const [, setThreadId] = useQueryState('threadId'); const { data: threadData } = useThread(threadId); + const [, setActiveReplyId] = useQueryState('activeReplyId'); const optimisticState = useOptimisticThreadState(threadId); - - const isUnread = useMemo(() => { - return threadData?.hasUnread ?? false; - }, [threadData]); - - const isStarred = useMemo(() => { + const { + optimisticMoveThreadsTo, + optimisticToggleStar, + optimisticToggleImportant, + optimisticMarkAsRead, + optimisticMarkAsUnread, + optimisticDeleteThreads, + } = useOptimisticActions(); + + const { isUnread, isStarred, isImportant } = useMemo(() => { + const unread = threadData?.hasUnread ?? false; + + let starred; if (optimisticState.optimisticStarred !== null) { - return optimisticState.optimisticStarred; + starred = optimisticState.optimisticStarred; + } else { + starred = threadData?.messages.some((message) => + message.tags?.some((tag) => tag.name.toLowerCase() === 'starred'), + ); } - return threadData?.messages.some((message) => - message.tags?.some((tag) => tag.name.toLowerCase() === 'starred'), - ); - }, [threadData, optimisticState.optimisticStarred]); - const isImportant = useMemo(() => { + let important; if (optimisticState.optimisticImportant !== null) { - return optimisticState.optimisticImportant; + important = optimisticState.optimisticImportant; + } else { + important = threadData?.messages.some((message) => + message.tags?.some((tag) => tag.name.toLowerCase() === 'important'), + ); } - return threadData?.messages.some((message) => - message.tags?.some((tag) => tag.name.toLowerCase() === 'important'), - ); - }, [threadData]); - - const noopAction = () => async () => { - toast.info(m['common.actions.featureNotImplemented']()); - }; - const { optimisticMoveThreadsTo } = useOptimisticActions(); + return { isUnread: unread, isStarred: starred, isImportant: important }; + }, [threadData, optimisticState.optimisticStarred, optimisticState.optimisticImportant]); const handleMove = (from: string, to: string) => () => { try { @@ -197,8 +202,6 @@ export function ThreadContextMenu({ } }; - const { optimisticToggleStar } = useOptimisticActions(); - const handleFavorites = () => { const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId]; @@ -211,8 +214,6 @@ export function ThreadContextMenu({ } }; - const { optimisticToggleImportant } = useOptimisticActions(); - const handleToggleImportant = () => { const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId]; const newImportantState = !isImportant; @@ -226,8 +227,6 @@ export function ThreadContextMenu({ } }; - const { optimisticMarkAsRead, optimisticMarkAsUnread } = useOptimisticActions(); - const handleReadUnread = () => { const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId]; const newReadState = isUnread; // If currently unread, mark as read (true) @@ -246,7 +245,6 @@ export function ThreadContextMenu({ setMail((prev) => ({ ...prev, bulkSelected: [] })); } }; - const [, setActiveReplyId] = useQueryState('activeReplyId'); const handleThreadReply = () => { setMode('reply'); @@ -266,30 +264,32 @@ export function ThreadContextMenu({ if (threadData?.latest) setActiveReplyId(threadData?.latest?.id); }; - const primaryActions: EmailAction[] = [ - { - id: 'reply', - label: m['common.mail.reply'](), - icon: , - action: handleThreadReply, - disabled: false, - }, - { - id: 'reply-all', - label: m['common.mail.replyAll'](), - icon: , - action: handleThreadReplyAll, - disabled: false, - }, - { - id: 'forward', - label: m['common.mail.forward'](), - icon: , - action: handleThreadForward, - disabled: false, - }, - ]; - const { optimisticDeleteThreads } = useOptimisticActions(); + const primaryActions: EmailAction[] = useMemo( + () => [ + { + id: 'reply', + label: m['common.mail.reply'](), + icon: , + action: handleThreadReply, + disabled: false, + }, + { + id: 'reply-all', + label: m['common.mail.replyAll'](), + icon: , + action: handleThreadReplyAll, + disabled: false, + }, + { + id: 'forward', + label: m['common.mail.forward'](), + icon: , + action: handleThreadForward, + disabled: false, + }, + ], + [m, handleThreadReply, handleThreadReplyAll, handleThreadForward], + ); const handleDelete = () => () => { const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId]; @@ -308,7 +308,7 @@ export function ThreadContextMenu({ // } }; - const getActions = () => { + const getActions = useMemo(() => { if (isSpam) { return [ { @@ -408,39 +408,42 @@ export function ThreadContextMenu({ disabled: false, }, ]; - }; + }, [isSpam, isBin, isArchiveFolder, isInbox, isSent, handleMove, handleDelete]); - const otherActions: EmailAction[] = [ - { - id: 'toggle-read', - label: isUnread ? m['common.mail.markAsRead']() : m['common.mail.markAsUnread'](), - icon: !isUnread ? ( - - ) : ( - - ), - action: handleReadUnread, - disabled: false, - }, - { - id: 'toggle-important', - label: isImportant - ? m['common.mail.removeFromImportant']() - : m['common.mail.markAsImportant'](), - icon: , - action: handleToggleImportant, - }, - { - id: 'favorite', - label: isStarred ? m['common.mail.removeFavorite']() : m['common.mail.addFavorite'](), - icon: isStarred ? ( - - ) : ( - - ), - action: handleFavorites, - }, - ]; + const otherActions: EmailAction[] = useMemo( + () => [ + { + id: 'toggle-read', + label: isUnread ? m['common.mail.markAsRead']() : m['common.mail.markAsUnread'](), + icon: !isUnread ? ( + + ) : ( + + ), + action: handleReadUnread, + disabled: false, + }, + { + id: 'toggle-important', + label: isImportant + ? m['common.mail.removeFromImportant']() + : m['common.mail.markAsImportant'](), + icon: , + action: handleToggleImportant, + }, + { + id: 'favorite', + label: isStarred ? m['common.mail.removeFavorite']() : m['common.mail.addFavorite'](), + icon: isStarred ? ( + + ) : ( + + ), + action: handleFavorites, + }, + ], + [isUnread, isImportant, isStarred, m, handleReadUnread, handleToggleImportant, handleFavorites], + ); const renderAction = (action: EmailAction) => { return ( @@ -482,7 +485,7 @@ export function ThreadContextMenu({ - {getActions().map(renderAction as any)} + {getActions.map(renderAction)} diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 6a434f64eb..8f5673b9d8 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -22,6 +22,12 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; import { useCategorySettings, useDefaultCategoryId } from '@/hooks/use-categories'; @@ -29,6 +35,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useActiveConnection, useConnections } from '@/hooks/use-connections'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCommandPalette } from '../context/command-palette-context'; +import { Check, ChevronDown, Command, RefreshCcw } from 'lucide-react'; import { useOptimisticActions } from '@/hooks/use-optimistic-actions'; import { ThreadDisplay } from '@/components/mail/thread-display'; import { trpcClient, useTRPC } from '@/providers/query-provider'; @@ -36,6 +43,7 @@ import { backgroundQueueAtom } from '@/store/backgroundQueue'; import { handleUnsubscribe } from '@/lib/email-utils.client'; import { useMediaQuery } from '../../hooks/use-media-query'; import { useSearchValue } from '@/hooks/use-search-value'; +import useSearchLabels from '@/hooks/use-labels-search'; import * as CustomIcons from '@/components/icons/icons'; import { isMac } from '@/lib/hotkeys/use-hotkey-utils'; import { MailList } from '@/components/mail/mail-list'; @@ -49,7 +57,6 @@ import { Textarea } from '@/components/ui/textarea'; import { useBrainState } from '@/hooks/use-summary'; import { clearBulkSelectionAtom } from './use-mail'; import AISidebar from '@/components/ui/ai-sidebar'; -import { Command, RefreshCcw } from 'lucide-react'; import { cleanSearchValue, cn } from '@/lib/utils'; import { useThreads } from '@/hooks/use-threads'; import { useBilling } from '@/hooks/use-billing'; @@ -57,6 +64,7 @@ import AIToggleButton from '../ai-toggle-button'; import { useIsMobile } from '@/hooks/use-mobile'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; +import { useLabels } from '@/hooks/use-labels'; import { useSession } from '@/lib/auth-client'; import { ScrollArea } from '../ui/scroll-area'; import { Label } from '@/components/ui/label'; @@ -111,9 +119,17 @@ export const defaultLabels = [ const AutoLabelingSettings = () => { const trpc = useTRPC(); const [open, setOpen] = useState(false); - const { data: storedLabels } = useQuery(trpc.brain.getLabels.queryOptions()); + const { data: storedLabels, refetch: refetchStoredLabels } = useQuery( + trpc.brain.getLabels.queryOptions(void 0, { + staleTime: 1000 * 60 * 60, // 1 hour + }), + ); const { mutateAsync: updateLabels, isPending } = useMutation( - trpc.brain.updateLabels.mutationOptions(), + trpc.brain.updateLabels.mutationOptions({ + onSuccess: () => { + refetchStoredLabels(); + }, + }), ); const [, setPricingDialog] = useQueryState('pricingDialog'); const [labels, setLabels] = useState([]); @@ -482,7 +498,7 @@ export function MailLayout() {
@@ -528,11 +544,11 @@ export function MailLayout() {
-
+
)} - + - {/*
- {activeAccount?.providerId === 'google' && folder === 'inbox' && ( - 0} /> - )} -
*/} + {activeConnection?.providerId === 'google' && folder === 'inbox' && ( + 0} /> + )}
(); + const folder = params?.folder ?? 'inbox'; + const [isOpen, setIsOpen] = useState(false); + + if (folder !== 'inbox' || isMultiSelectMode) return null; + + const handleLabelChange = (labelId: string) => { + const index = labels.indexOf(labelId); + if (index !== -1) { + const newLabels = [...labels]; + newLabels.splice(index, 1); + setLabels(newLabels); + } else { + setLabels([...labels, labelId]); + } + }; + + return ( + + + + + + {systemLabels.map((label) => ( + { + e.preventDefault(); + e.stopPropagation(); + handleLabelChange(label.id); + }} + role="menuitemcheckbox" + aria-checked={labels.includes(label.id)} + > + {label.name.toLowerCase()} + {labels.includes(label.id) && } + + ))} + + + ); +} + function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) { const [mail, setMail] = useMail(); - const [searchValue, setSearchValue] = useSearchValue(); + const { setLabels } = useSearchLabels(); const categories = Categories(); const params = useParams<{ folder: string }>(); const folder = params?.folder ?? 'inbox'; @@ -962,59 +1048,61 @@ function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) { const [textSize, setTextSize] = useState<'normal' | 'small' | 'xs' | 'hidden'>('normal'); const isDesktop = useMediaQuery('(min-width: 1024px)'); - if (folder !== 'inbox') return
; - - useEffect(() => { - const checkTextSize = () => { - const container = containerRef.current; - if (!container) return; - - const containerWidth = container.offsetWidth; - const selectedCategory = categories.find((cat) => cat.id === category); - - // Calculate approximate widths needed for different text sizes - const baseIconWidth = (categories.length - 1) * 40; // unselected icons + gaps - const selectedTextLength = selectedCategory ? selectedCategory.name.length : 10; - - // Estimate width needed for different text sizes - const normalTextWidth = selectedTextLength * 8 + 60; // normal text - const smallTextWidth = selectedTextLength * 7 + 50; // smaller text - const xsTextWidth = selectedTextLength * 6 + 40; // extra small text - const minIconWidth = 40; // minimum width for icon-only selected button - - const totalNormal = baseIconWidth + normalTextWidth; - const totalSmall = baseIconWidth + smallTextWidth; - const totalXs = baseIconWidth + xsTextWidth; - const totalIconOnly = baseIconWidth + minIconWidth; - - if (containerWidth >= totalNormal) { - setTextSize('normal'); - } else if (containerWidth >= totalSmall) { - setTextSize('small'); - } else if (containerWidth >= totalXs) { - setTextSize('xs'); - } else if (containerWidth >= totalIconOnly) { - setTextSize('hidden'); // Hide text but keep button wide - } else { - setTextSize('hidden'); // Hide text in very tight spaces - } - }; - - checkTextSize(); + // const categories - // Use ResizeObserver to handle container size changes - const resizeObserver = new ResizeObserver(() => { - checkTextSize(); - }); + if (folder !== 'inbox') return
; - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [category, categories]); + // useEffect(() => { + // const checkTextSize = () => { + // const container = containerRef.current; + // if (!container) return; + + // const containerWidth = container.offsetWidth; + // const selectedCategory = categories.find((cat) => cat.id === category); + + // // Calculate approximate widths needed for different text sizes + // const baseIconWidth = (categories.length - 1) * 40; // unselected icons + gaps + // const selectedTextLength = selectedCategory ? selectedCategory.name.length : 10; + + // // Estimate width needed for different text sizes + // const normalTextWidth = selectedTextLength * 8 + 60; // normal text + // const smallTextWidth = selectedTextLength * 7 + 50; // smaller text + // const xsTextWidth = selectedTextLength * 6 + 40; // extra small text + // const minIconWidth = 40; // minimum width for icon-only selected button + + // const totalNormal = baseIconWidth + normalTextWidth; + // const totalSmall = baseIconWidth + smallTextWidth; + // const totalXs = baseIconWidth + xsTextWidth; + // const totalIconOnly = baseIconWidth + minIconWidth; + + // if (containerWidth >= totalNormal) { + // setTextSize('normal'); + // } else if (containerWidth >= totalSmall) { + // setTextSize('small'); + // } else if (containerWidth >= totalXs) { + // setTextSize('xs'); + // } else if (containerWidth >= totalIconOnly) { + // setTextSize('hidden'); // Hide text but keep button wide + // } else { + // setTextSize('hidden'); // Hide text in very tight spaces + // } + // }; + + // checkTextSize(); + + // // Use ResizeObserver to handle container size changes + // const resizeObserver = new ResizeObserver(() => { + // checkTextSize(); + // }); + + // if (containerRef.current) { + // resizeObserver.observe(containerRef.current); + // } + + // return () => { + // resizeObserver.disconnect(); + // }; + // }, [category, categories]); const renderCategoryButton = (cat: CategoryType, isOverlay = false, idx: number) => { const isSelected = cat.id === (category || 'Primary'); @@ -1059,11 +1147,11 @@ function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) { ref={!isOverlay ? activeTabElementRef : null} onClick={() => { setCategory(cat.id); - setSearchValue({ - value: `${cat.searchValue} ${cleanSearchValue(searchValue.value).trim().length ? `AND ${cleanSearchValue(searchValue.value)}` : ''}`, - highlight: searchValue.highlight, - folder: '', - }); + // setSearchValue({ + // value: `${cat.searchValue} ${cleanSearchValue(searchValue.value).trim().length ? `AND ${cleanSearchValue(searchValue.value)}` : ''}`, + // highlight: searchValue.highlight, + // folder: '', + // }); }} className={cn( 'flex h-8 items-center justify-center gap-1 overflow-hidden rounded-lg border transition-all duration-300 ease-out dark:border-none', @@ -1110,22 +1198,22 @@ function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) { }; // Update clip path when category changes - useEffect(() => { - const container = overlayContainerRef.current; - const activeTabElement = activeTabElementRef.current; - - if (category && container && activeTabElement) { - setMail({ ...mail, bulkSelected: [] }); - const { offsetLeft, offsetWidth } = activeTabElement; - const clipLeft = Math.max(0, offsetLeft - 2); - const clipRight = Math.min(container.offsetWidth, offsetLeft + offsetWidth + 2); - const containerWidth = container.offsetWidth; - - if (containerWidth) { - container.style.clipPath = `inset(0 ${Number(100 - (clipRight / containerWidth) * 100).toFixed(2)}% 0 ${Number((clipLeft / containerWidth) * 100).toFixed(2)}%)`; - } - } - }, [category, textSize]); // Changed from showText to textSize + // useEffect(() => { + // const container = overlayContainerRef.current; + // const activeTabElement = activeTabElementRef.current; + + // if (category && container && activeTabElement) { + // setMail({ ...mail, bulkSelected: [] }); + // const { offsetLeft, offsetWidth } = activeTabElement; + // const clipLeft = Math.max(0, offsetLeft - 2); + // const clipRight = Math.min(container.offsetWidth, offsetLeft + offsetWidth + 2); + // const containerWidth = container.offsetWidth; + + // if (containerWidth) { + // container.style.clipPath = `inset(0 ${Number(100 - (clipRight / containerWidth) * 100).toFixed(2)}% 0 ${Number((clipLeft / containerWidth) * 100).toFixed(2)}%)`; + // } + // } + // }, [category, textSize]); // Changed from showText to textSize if (isMultiSelectMode) { return ; diff --git a/apps/mail/components/ui/nav-main.tsx b/apps/mail/components/ui/nav-main.tsx index 397963c188..f12ce2b650 100644 --- a/apps/mail/components/ui/nav-main.tsx +++ b/apps/mail/components/ui/nav-main.tsx @@ -72,7 +72,7 @@ export function NavMain({ items }: NavMainProps) { const { mutateAsync: createLabel } = useMutation(trpc.labels.create.mutationOptions()); - const { data, refetch } = useLabels(); + const { userLabels, refetch } = useLabels(); const { state } = useSidebar(); @@ -259,9 +259,7 @@ export function NavMain({ items }: NavMainProps) { ) : activeAccount?.providerId === 'microsoft' ? null : null}
- {activeAccount ? ( - - ) : null} + {activeAccount ? : null} )} diff --git a/apps/mail/components/ui/sidebar-labels.tsx b/apps/mail/components/ui/sidebar-labels.tsx index 688c6df31b..cd29b87fed 100644 --- a/apps/mail/components/ui/sidebar-labels.tsx +++ b/apps/mail/components/ui/sidebar-labels.tsx @@ -1,20 +1,17 @@ import type { IConnection, Label as LabelType } from '@/types'; +import { useActiveConnection } from '@/hooks/use-connections'; import { RecursiveFolder } from './recursive-folder'; +import { useStats } from '@/hooks/use-stats'; import { Tree } from '../magicui/file-tree'; import { useCallback } from 'react'; type Props = { data: LabelType[]; - activeAccount: IConnection | null | undefined; - stats: - | { - count?: number; - label?: string; - }[] - | undefined; }; -const SidebarLabels = ({ data, activeAccount, stats }: Props) => { +const SidebarLabels = ({ data }: Props) => { + const { data: stats } = useStats(); + const { data: activeAccount } = useActiveConnection(); const getLabelCount = useCallback( (labelName: string | undefined): number => { if (!stats || !labelName) return 0; diff --git a/apps/mail/config/navigation.ts b/apps/mail/config/navigation.ts index e5d840f6df..23ed5562c0 100644 --- a/apps/mail/config/navigation.ts +++ b/apps/mail/config/navigation.ts @@ -169,11 +169,11 @@ export const navigationConfig: Record = { url: '/settings/labels', icon: Sheet, }, - { - title: m['navigation.settings.categories'](), - url: '/settings/categories', - icon: Tabs, - }, + // { + // title: m['navigation.settings.categories'](), + // url: '/settings/categories', + // icon: Tabs, + // }, { title: m['navigation.settings.signatures'](), url: '/settings/signatures', diff --git a/apps/mail/hooks/use-labels.ts b/apps/mail/hooks/use-labels.ts index 3826885a06..3969b7601b 100644 --- a/apps/mail/hooks/use-labels.ts +++ b/apps/mail/hooks/use-labels.ts @@ -9,11 +9,20 @@ export function useLabels() { staleTime: 1000 * 60 * 60, // 1 hour }), ); - return labelQuery; + + const { userLabels, systemLabels } = useMemo(() => { + if (!labelQuery.data) return { userLabels: [], systemLabels: [] }; + return { + userLabels: labelQuery.data.filter((label) => label.type === 'user'), + systemLabels: labelQuery.data.filter((label) => label.type === 'system'), + }; + }, [labelQuery.data]); + + return { userLabels, systemLabels, ...labelQuery }; } export function useThreadLabels(ids: string[]) { - const { data: labels = [] } = useLabels(); + const { userLabels: labels = [] } = useLabels(); const threadLabels = useMemo(() => { if (!labels) return []; diff --git a/apps/mail/hooks/use-stats.ts b/apps/mail/hooks/use-stats.ts index 4b65df7fe4..7a9243f2cc 100644 --- a/apps/mail/hooks/use-stats.ts +++ b/apps/mail/hooks/use-stats.ts @@ -9,7 +9,6 @@ export const useStats = () => { const statsQuery = useQuery( trpc.mail.count.queryOptions(void 0, { enabled: !!session?.user.id, - staleTime: 1000 * 60 * 60, // 1 hour }), ); diff --git a/apps/mail/hooks/use-threads.ts b/apps/mail/hooks/use-threads.ts index cd7ef36131..159c1c8de7 100644 --- a/apps/mail/hooks/use-threads.ts +++ b/apps/mail/hooks/use-threads.ts @@ -28,7 +28,7 @@ export const useThreads = () => { { initialCursor: '', getNextPageParam: (lastPage) => lastPage?.nextPageToken ?? null, - staleTime: 60 * 1000 * 60, // 1 minute + staleTime: 60 * 1000 * 1, // 1 minute refetchOnMount: true, refetchIntervalInBackground: true, }, @@ -73,7 +73,7 @@ export const useThread = (threadId: string | null, historyId?: string | null) => }, { enabled: !!id && !!session?.user.id, - staleTime: 1000 * 60 * 1, // 1 minute + staleTime: 1000 * 60 * 60, // 1 minute }, ), ); diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index f8c1f65dc3..441eebeb51 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -672,7 +672,7 @@ export class ZeroAgent extends AIChatAgent { if (!this.driver) { throw new Error('No driver available'); } - return (await this.driver.getUserLabels()).filter((label) => label.type === 'user'); + return await this.driver.getUserLabels(); } async getLabel(id: string) { From 16c3f95d282971161357b66a5a3de8ee0d4679a3 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:09:15 -0700 Subject: [PATCH 24/38] disable workflows (#1690) 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._ --- apps/server/src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 4d49a83ecb..9e6110b186 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -635,6 +635,7 @@ export default class extends WorkerEntrypoint { .get('/', (c) => c.redirect(`${env.VITE_PUBLIC_APP_URL}`)) .post('/a8n/notify/:providerId', async (c) => { if (!c.req.header('Authorization')) return c.json({ error: 'Unauthorized' }, { status: 401 }); + return c.json({ message: 'OK' }, { status: 200 }); const providerId = c.req.param('providerId'); if (providerId === EProviders.google) { const body = await c.req.json<{ historyId: string }>(); From 7cc9838ab7b988d0bd8e568cf19a04a1446a459a Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:49:14 +0300 Subject: [PATCH 25/38] =?UTF-8?q?refactor:=20perf=20improvements=20to=20op?= =?UTF-8?q?ening=20emails,=20tanstack=20query=20caching=E2=80=A6=20(#1694)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/mail/components/mail/mail-list.tsx | 8 +- apps/mail/components/mail/thread-display.tsx | 95 ++++++----- apps/mail/hooks/use-notes.tsx | 3 + apps/mail/hooks/use-optimistic-actions.ts | 157 ++++++++++--------- apps/mail/hooks/use-summary.ts | 9 +- 5 files changed, 143 insertions(+), 129 deletions(-) diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index d706cfc1ee..4e52ff9e9c 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -70,7 +70,7 @@ const Thread = memo( }: ThreadProps & { index?: number }) { const [searchValue, setSearchValue] = useSearchValue(); const { folder } = useParams<{ folder: string }>(); - const [{}, threads] = useThreads(); + const [, threads] = useThreads(); const [threadId] = useQueryState('threadId'); const { data: getThreadData, @@ -250,7 +250,7 @@ const Thread = memo( className={cn( 'hover:bg-offsetLight hover:bg-primary/5 group relative mx-1 flex cursor-pointer flex-col items-start rounded-lg py-2 text-left text-sm transition-all hover:opacity-100', (isMailSelected || isMailBulkSelected || isKeyboardFocused) && - 'border-border bg-primary/5 opacity-100', + 'border-border bg-primary/5 opacity-100', isKeyboardFocused && 'ring-primary/50', 'relative', 'group', @@ -849,8 +849,8 @@ export const MailList = memo( const clickedIndex = itemsRef.current.findIndex((item) => item.id === messageThreadId); setFocusedIndex(clickedIndex); if (message.unread && autoRead) optimisticMarkAsRead([messageThreadId], true); - await setThreadId(messageThreadId); - await setDraftId(null); + setThreadId(messageThreadId); + setDraftId(null); // Don't clear activeReplyId - let ThreadDisplay handle Reply All auto-opening }, [ diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index 9eaeb834f3..638d8ad462 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -514,24 +514,23 @@ export function ThreadDisplay() { ${emailData?.messages - ?.map( - (message, index) => ` + ?.map( + (message, index) => `
- +
); }; diff --git a/apps/mail/components/mail/reply-composer.tsx b/apps/mail/components/mail/reply-composer.tsx index 27a0b40082..70f183d55e 100644 --- a/apps/mail/components/mail/reply-composer.tsx +++ b/apps/mail/components/mail/reply-composer.tsx @@ -191,6 +191,7 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) { message: emailBody, attachments: await serializeFiles(data.attachments), fromEmail: fromEmail, + draftId: draftId ?? undefined, headers: { 'In-Reply-To': replyToMessage?.messageId ?? '', References: [ @@ -253,9 +254,9 @@ export default function ReplyCompose({ messageId }: ReplyComposeProps) { className="w-full !max-w-none border pb-1" onSendEmail={handleSendEmail} onClose={async () => { - await setMode(null); - await setDraftId(null); - await setActiveReplyId(null); + setMode(null); + setDraftId(null); + setActiveReplyId(null); }} initialMessage={draft?.content ?? latestDraft?.decodedBody} initialTo={ensureEmailArray(draft?.to)} diff --git a/apps/mail/components/ui/ai-sidebar.tsx b/apps/mail/components/ui/ai-sidebar.tsx index b14b3c3628..454e6d50b7 100644 --- a/apps/mail/components/ui/ai-sidebar.tsx +++ b/apps/mail/components/ui/ai-sidebar.tsx @@ -279,9 +279,9 @@ export function useAISidebar() { // Update query parameter and localStorage when viewMode changes const setViewMode = useCallback( - async (mode: ViewMode) => { + (mode: ViewMode) => { setViewModeState(mode); - await setViewModeQuery(mode === 'popup' ? null : mode); + setViewModeQuery(mode === 'popup' ? null : mode); // Save to localStorage for persistence across sessions if (typeof window !== 'undefined') { diff --git a/apps/mail/components/ui/app-sidebar.tsx b/apps/mail/components/ui/app-sidebar.tsx index 11548f688d..56ccac72b3 100644 --- a/apps/mail/components/ui/app-sidebar.tsx +++ b/apps/mail/components/ui/app-sidebar.tsx @@ -157,7 +157,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { className="mt-3 inline-flex h-7 w-full items-center justify-center gap-0.5 overflow-hidden rounded-lg bg-[#8B5CF6] px-2" >
-
+
Start 7 day free trial
@@ -186,11 +186,14 @@ function ComposeButton() { const handleOpenChange = async (open: boolean) => { if (!open) { - await setDialogOpen(null); + setDialogOpen(null); } else { - await setDialogOpen('true'); + setDialogOpen('true'); } - await Promise.all([setDraftId(null), setTo(null), setActiveReplyId(null), setMode(null)]); + setDraftId(null); + setTo(null); + setActiveReplyId(null); + setMode(null); }; return ( diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index 4b35d91960..cce4e8a97c 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -95,47 +95,7 @@ export function NavUser() { 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']], - }), - ]); + queryClient.clear(); } catch (error) { console.error('Error switching accounts:', error); toast.error(m['common.navUser.failedToSwitchAccount']()); diff --git a/apps/mail/messages/en.json b/apps/mail/messages/en.json index 98c8a2fb9e..57068b5dfe 100644 --- a/apps/mail/messages/en.json +++ b/apps/mail/messages/en.json @@ -370,7 +370,8 @@ "failedToMute": "Failed to mute", "failedToUnmute": "Failed to unmute", "archived": "Archived", - "failedToArchive": "Failed to archive" + "failedToArchive": "Failed to archive", + "openInNewTab": "Open in new tab" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/providers/query-provider.tsx b/apps/mail/providers/query-provider.tsx index 7146811d87..d1986deef9 100644 --- a/apps/mail/providers/query-provider.tsx +++ b/apps/mail/providers/query-provider.tsx @@ -34,7 +34,10 @@ export const makeQueryClient = (connectionId: string | null) => onError: (err, { meta }) => { if (meta && meta.noGlobalError === true) return; if (meta && typeof meta.customError === 'string') console.error(meta.customError); - else if (err.message === 'Required scopes missing') { + else if ( + err.message === 'Required scopes missing' || + err.message.includes('Invalid connection') + ) { signOut({ fetchOptions: { onSuccess: () => { @@ -121,29 +124,6 @@ export function QueryProvider({ persister, buster: CACHE_BURST_KEY, maxAge: 1000 * 60 * 1, // 1 minute, we're storing in DOs, - dehydrateOptions: { - shouldDehydrateQuery(query) { - return (query.queryKey[0] as string[]).some((e) => e === 'listThreads'); - }, - }, - }} - onSuccess={() => { - const threadQueryKey = [['mail', 'listThreads'], { type: 'infinite' }]; - queryClient.setQueriesData( - { queryKey: threadQueryKey }, - (data: InfiniteData) => { - if (!data) return data; - // We only keep few pages of threads in the cache before we invalidate them - // invalidating will attempt to refetch every page that was in cache, if someone have too many pages in cache, it will refetch every page every time - // We don't want that, just keep like 3 pages (20 * 3 = 60 threads) in cache - return { - pages: data.pages.slice(0, 3), - pageParams: data.pageParams.slice(0, 3), - }; - }, - ); - // invalidate the query, it will refetch when the data is it is being accessed - queryClient.invalidateQueries({ queryKey: threadQueryKey }); }} > diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index b42c5d10ee..e122137540 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -389,7 +389,7 @@ export class GoogleMailManager implements MailManager { return { labels: Array.from(labels).map((id) => ({ id, name: id })), messages, - latest: messages.findLast((e) => !e.isDraft), + latest: messages.findLast((e) => e.isDraft !== true), hasUnread, totalReplies: messages.filter((e) => !e.isDraft).length, }; @@ -1124,12 +1124,12 @@ export class GoogleMailManager implements MailManager { .filter(Boolean) || []; const subject = headers.find((h) => h.name === 'Subject')?.value; - + 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(',') || []; - + const payload = draft.message.payload; let content = ''; let attachments: { @@ -1150,13 +1150,16 @@ export class GoogleMailManager implements MailManager { // Get attachments const attachmentParts = payload.parts.filter( - (part) => !!part.filename && !!part.body?.attachmentId + (part) => !!part.filename && !!part.body?.attachmentId, ); attachments = await Promise.all( attachmentParts.map(async (part) => { try { - const attachmentData = await this.getAttachment(draft.message!.id!, part.body!.attachmentId!); + const attachmentData = await this.getAttachment( + draft.message!.id!, + part.body!.attachmentId!, + ); return { filename: part.filename || '', mimeType: part.mimeType || '', @@ -1172,7 +1175,7 @@ export class GoogleMailManager implements MailManager { } catch (e) { return null; } - }) + }), ).then((a) => a.filter((a): a is NonNullable => a !== null)); } else if (payload?.body?.data) { content = fromBinary(payload.body.data); diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts index b6c3b857e6..45fe3c6308 100644 --- a/apps/server/src/lib/driver/microsoft.ts +++ b/apps/server/src/lib/driver/microsoft.ts @@ -1297,4 +1297,7 @@ export class OutlookMailManager implements MailManager { throw new StandardizedError(error, operation, context); } } + listHistory(historyId: string): Promise<{ history: T[]; historyId: string }> { + return Promise.resolve({ history: [], historyId }); + } } diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index 28d9b784e8..fc5b062ffd 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -25,7 +25,7 @@ export interface ParsedDraft { subject?: string; content?: string; rawMessage?: { - internalDate?: string; + internalDate?: string | null; }; cc?: string[]; bcc?: string[]; diff --git a/apps/server/src/lib/server-utils.ts b/apps/server/src/lib/server-utils.ts index 97662a697f..24b0e5769e 100644 --- a/apps/server/src/lib/server-utils.ts +++ b/apps/server/src/lib/server-utils.ts @@ -57,36 +57,6 @@ export const connectionToDriver = (activeConnection: typeof connection.$inferSel }); }; -export const notifyUser = async ({ - connectionId, - result, - threadId, -}: { - connectionId: string; - result: IGetThreadResponse; - threadId: string; -}) => { - const mailbox = await getZeroAgent(connectionId); - - try { - await mailbox.broadcast( - JSON.stringify({ - type: OutgoingMessageType.Mail_Get, - threadId, - result, - } as OutgoingMessage), - ); - } catch (error) { - console.error(`[notifyUser] Failed to broadcast message`, { - connectionId, - threadId, - result, - error, - }); - throw error; - } -}; - export const verifyToken = async (token: string) => { const response = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${token}`, { method: 'GET', diff --git a/apps/server/src/pipelines.effect.ts b/apps/server/src/pipelines.effect.ts index 6da0367462..1d2e1334f4 100644 --- a/apps/server/src/pipelines.effect.ts +++ b/apps/server/src/pipelines.effect.ts @@ -18,8 +18,8 @@ import { ThreadLabels, } from './lib/brain.fallback.prompts'; import { defaultLabels, EPrompts, EProviders, type ParsedMessage, type Sender } from './types'; -import { connectionToDriver, getZeroAgent, notifyUser } from './lib/server-utils'; import { Effect, Console, pipe, Match, Option } from 'effect'; +import { getZeroAgent } from './lib/server-utils'; import { type gmail_v1 } from '@googleapis/gmail'; import { getPromptName } from './pipelines'; import { env } from 'cloudflare:workers'; diff --git a/apps/server/src/routes/agent/tools.ts b/apps/server/src/routes/agent/tools.ts index fe94a204e1..0b571eb918 100644 --- a/apps/server/src/routes/agent/tools.ts +++ b/apps/server/src/routes/agent/tools.ts @@ -330,7 +330,7 @@ const deleteLabel = (driver: MailManager) => }, }); -export const webSearch = (dataStream: DataStreamWriter) => +export const webSearch = (dataStream?: DataStreamWriter) => tool({ description: 'Search the web for information using Perplexity AI', parameters: z.object({ @@ -338,7 +338,23 @@ export const webSearch = (dataStream: DataStreamWriter) => }), execute: async ({ query }) => { try { - const response = streamText({ + if (dataStream) { + const response = streamText({ + model: perplexity('sonar'), + messages: [ + { role: 'system', content: 'Be precise and concise.' }, + { role: 'system', content: 'Do not include sources in your response.' }, + { role: 'system', content: 'Do not use markdown formatting in your response.' }, + { role: 'user', content: query }, + ], + maxTokens: 1024, + }); + response.mergeIntoDataStream(dataStream); + + return { type: 'streaming_response', query }; + } + + const response = await generateText({ model: perplexity('sonar'), messages: [ { role: 'system', content: 'Be precise and concise.' }, @@ -349,9 +365,7 @@ export const webSearch = (dataStream: DataStreamWriter) => maxTokens: 1024, }); - response.mergeIntoDataStream(dataStream); - - return { type: 'streaming_response', query }; + return response.text; } catch (error) { console.error('Error searching the web:', error); throw new Error('Failed to search the web'); diff --git a/apps/server/src/routes/ai.ts b/apps/server/src/routes/ai.ts index a1d3c21eeb..1928c41e92 100644 --- a/apps/server/src/routes/ai.ts +++ b/apps/server/src/routes/ai.ts @@ -1,8 +1,7 @@ import { getCurrentDateContext, GmailSearchAssistantSystemPrompt } from '../lib/prompts'; -import { getDriverFromConnectionId } from '../services/mcp-service/mcp'; import { systemPrompt } from '../services/call-service/system-prompt'; import { composeEmail } from '../trpc/routes/ai/compose'; -import { connectionToDriver } from '../lib/server-utils'; +import { getZeroAgent } from '../lib/server-utils'; import { env } from 'cloudflare:workers'; import { openai } from '@ai-sdk/openai'; import { FOLDERS } from '../lib/utils'; @@ -40,14 +39,17 @@ aiRouter.post('/do/:action', async (c) => { const action = c.req.param('action') as Tools; const body = await c.req.json(); console.log('[DEBUG] action', action, body); - const driver = await getDriverFromConnectionId(connection.id); + const agent = await getZeroAgent(connection.id); switch (action) { case Tools.ListThreads: const threads = await Promise.all( ( - await driver.list({ folder: body.folder ?? 'inbox', maxResults: body.maxResults ?? 5 }) - ).threads.map((thread) => - driver.get(thread.id).then((thread) => ({ + await agent.listThreads({ + folder: body.folder ?? 'inbox', + maxResults: body.maxResults ?? 5, + }) + ).threads.map((thread: any) => + agent.getThread(thread.id).then((thread) => ({ id: thread.latest?.id, subject: thread.latest?.subject, sender: thread.latest?.sender, @@ -65,7 +67,7 @@ aiRouter.post('/do/:action', async (c) => { }); return c.json({ success: true, result: newBody }); case Tools.SendEmail: - const result = await driver.create({ + const result = await agent.create({ to: body.to.map((to: any) => ({ name: to.name ?? to.email, email: to.email ?? 'founders@0.email', @@ -142,7 +144,7 @@ aiRouter.post('/call', async (c) => { } console.log('[DEBUG] Creating driver for connection:', connection.id); - const driver = connectionToDriver(connection); + const agent = await getZeroAgent(connection.id); const { text } = await generateText({ model: openai(env.OPENAI_MODEL || 'gpt-4o'), @@ -188,7 +190,7 @@ aiRouter.post('/call', async (c) => { execute: async (params) => { console.log('[DEBUG] listThreads', params); - const result = await driver.list({ + const result = await agent.listThreads({ folder: params.folder, query: params.query, maxResults: params.maxResults, @@ -196,12 +198,12 @@ aiRouter.post('/call', async (c) => { pageToken: params.pageToken, }); const content = await Promise.all( - result.threads.map(async (thread) => { - const loadedThread = await driver.get(thread.id); + result.threads.map(async (thread: any) => { + const loadedThread = await agent.getThread(thread.id); return [ { type: 'text' as const, - text: `Subject: ${loadedThread.latest?.subject} | ID: ${thread.id} | Received: ${loadedThread.latest?.receivedOn}`, + text: `Subject: ${loadedThread.latest?.subject} | Received: ${loadedThread.latest?.receivedOn}`, }, ]; }), @@ -227,7 +229,7 @@ aiRouter.post('/call', async (c) => { console.log('[DEBUG] getThread', params); try { - const thread = await driver.get(params.threadId); + const thread = await agent.getThread(params.threadId); const content = thread.messages.at(-1)?.body; @@ -260,10 +262,7 @@ aiRouter.post('/call', async (c) => { execute: async (params) => { console.log('[DEBUG] markThreadsRead', params); - await driver.modifyLabels(params.threadIds, { - addLabels: [], - removeLabels: ['UNREAD'], - }); + await agent.modifyLabels(params.threadIds, [], ['UNREAD']); return { content: [ { @@ -282,10 +281,7 @@ aiRouter.post('/call', async (c) => { execute: async (params) => { console.log('[DEBUG] markThreadsUnread', params); - await driver.modifyLabels(params.threadIds, { - addLabels: ['UNREAD'], - removeLabels: [], - }); + await agent.modifyLabels(params.threadIds, ['UNREAD'], []); return { content: [ { @@ -306,10 +302,7 @@ aiRouter.post('/call', async (c) => { execute: async (params) => { console.log('[DEBUG] modifyLabels', params); - await driver.modifyLabels(params.threadIds, { - addLabels: params.addLabelIds, - removeLabels: params.removeLabelIds, - }); + await agent.modifyLabels(params.threadIds, params.addLabelIds, params.removeLabelIds); return { content: [ { @@ -342,7 +335,7 @@ aiRouter.post('/call', async (c) => { execute: async () => { console.log('[DEBUG] getUserLabels'); - const labels = await driver.getUserLabels(); + const labels = await agent.getUserLabels(); return { content: [ { @@ -363,7 +356,7 @@ aiRouter.post('/call', async (c) => { execute: async (s) => { console.log('[DEBUG] getLabel', s); - const label = await driver.getLabel(s.id); + const label = await agent.getLabel(s.id); return { content: [ { @@ -389,7 +382,7 @@ aiRouter.post('/call', async (c) => { console.log('[DEBUG] createLabel', params); try { - await driver.createLabel({ + await agent.createLabel({ name: params.name, color: params.backgroundColor && params.textColor @@ -430,10 +423,7 @@ aiRouter.post('/call', async (c) => { console.log('[DEBUG] bulkDelete', params); try { - await driver.modifyLabels(params.threadIds, { - addLabels: ['TRASH'], - removeLabels: ['INBOX'], - }); + await agent.modifyLabels(params.threadIds, ['TRASH'], ['INBOX']); return { content: [ { @@ -465,10 +455,7 @@ aiRouter.post('/call', async (c) => { console.log('[DEBUG] bulkArchive', params); try { - await driver.modifyLabels(params.threadIds, { - addLabels: [], - removeLabels: ['INBOX'], - }); + await agent.modifyLabels(params.threadIds, [], ['INBOX']); return { content: [ { diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index d36c0dc741..85663a306c 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -28,9 +28,9 @@ import { import { type Connection, type ConnectionContext, type WSMessage } from 'agents'; import { EPrompts, type IOutgoingMessage, type ParsedMessage } from '../types'; import type { IGetThreadResponse, MailManager } from '../lib/driver/types'; +import { connectionToDriver, getZeroAgent } from '../lib/server-utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createSimpleAuth, type SimpleAuth } from '../lib/auth'; -import { connectionToDriver } from '../lib/server-utils'; import { ToolOrchestrator } from './agent/orchestrator'; import type { CreateDraftData } from '../lib/schemas'; import { FOLDERS, parseHeaders } from '../lib/utils'; @@ -1209,9 +1209,9 @@ export class ZeroAgent extends AIChatAgent { return { messages, - latest: messages.length > 0 ? messages[messages.length - 1] : undefined, + latest: messages.findLast((e) => e.isDraft !== true), hasUnread: latestLabelIds.includes('UNREAD'), - totalReplies: messages.length, + totalReplies: messages.filter((e) => e.isDraft !== true).length, labels: latestLabelIds.map((id: string) => ({ id, name: id })), } satisfies IGetThreadResponse; } catch (error) { @@ -1243,7 +1243,7 @@ export class ZeroMCP extends McpAgent { throw new Error('Unauthorized'); } this.activeConnectionId = _connection.id; - const driver = connectionToDriver(_connection); + const agent = await getZeroAgent(_connection.id); this.server.tool('getConnections', async () => { const connections = await db.query.connection.findMany({ @@ -1333,7 +1333,7 @@ export class ZeroMCP extends McpAgent { pageToken: z.string().optional(), }, async (s) => { - const result = await driver.list({ + const result = await agent.listThreads({ folder: s.folder, query: s.query, maxResults: s.maxResults, @@ -1341,8 +1341,8 @@ export class ZeroMCP extends McpAgent { pageToken: s.pageToken, }); const content = await Promise.all( - result.threads.map(async (thread) => { - const loadedThread = await driver.get(thread.id); + result.threads.map(async (thread: any) => { + const loadedThread = await agent.getThread(thread.id); return [ { type: 'text' as const, @@ -1374,7 +1374,7 @@ export class ZeroMCP extends McpAgent { threadId: z.string(), }, async (s) => { - const thread = await driver.get(s.threadId); + const thread = await agent.getThread(s.threadId); const initialResponse = [ { type: 'text' as const, @@ -1433,10 +1433,7 @@ export class ZeroMCP extends McpAgent { threadIds: z.array(z.string()), }, async (s) => { - await driver.modifyLabels(s.threadIds, { - addLabels: [], - removeLabels: ['UNREAD'], - }); + await agent.modifyLabels(s.threadIds, [], ['UNREAD']); return { content: [ { @@ -1454,10 +1451,7 @@ export class ZeroMCP extends McpAgent { threadIds: z.array(z.string()), }, async (s) => { - await driver.modifyLabels(s.threadIds, { - addLabels: ['UNREAD'], - removeLabels: [], - }); + await agent.modifyLabels(s.threadIds, ['UNREAD'], []); return { content: [ { @@ -1477,10 +1471,7 @@ export class ZeroMCP extends McpAgent { removeLabelIds: z.array(z.string()), }, async (s) => { - await driver.modifyLabels(s.threadIds, { - addLabels: s.addLabelIds, - removeLabels: s.removeLabelIds, - }); + await agent.modifyLabels(s.threadIds, s.addLabelIds, s.removeLabelIds); return { content: [ { @@ -1504,7 +1495,7 @@ export class ZeroMCP extends McpAgent { }); this.server.tool('getUserLabels', async () => { - const labels = await driver.getUserLabels(); + const labels = await agent.getUserLabels(); return { content: [ { @@ -1523,7 +1514,7 @@ export class ZeroMCP extends McpAgent { id: z.string(), }, async (s) => { - const label = await driver.getLabel(s.id); + const label = await agent.getLabel(s.id); return { content: [ { @@ -1548,7 +1539,7 @@ export class ZeroMCP extends McpAgent { }, async (s) => { try { - await driver.createLabel({ + await agent.createLabel({ name: s.name, color: s.backgroundColor && s.textColor @@ -1586,10 +1577,7 @@ export class ZeroMCP extends McpAgent { }, async (s) => { try { - await driver.modifyLabels(s.threadIds, { - addLabels: ['TRASH'], - removeLabels: ['INBOX'], - }); + await agent.modifyLabels(s.threadIds, ['TRASH'], ['INBOX']); return { content: [ { @@ -1618,10 +1606,7 @@ export class ZeroMCP extends McpAgent { }, async (s) => { try { - await driver.modifyLabels(s.threadIds, { - addLabels: [], - removeLabels: ['INBOX'], - }); + await agent.modifyLabels(s.threadIds, [], ['INBOX']); return { content: [ { diff --git a/apps/server/src/services/mcp-service/mcp.ts b/apps/server/src/services/mcp-service/mcp.ts deleted file mode 100644 index 822d29dc32..0000000000 --- a/apps/server/src/services/mcp-service/mcp.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { getCurrentDateContext, GmailSearchAssistantSystemPrompt } from '../../lib/prompts'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { createDriver } from '../../lib/driver'; -import { FOLDERS } from '../../lib/utils'; -import { env } from 'cloudflare:workers'; -import { openai } from '@ai-sdk/openai'; -import { McpAgent } from 'agents/mcp'; -import { createDb } from '../../db'; -import { generateText } from 'ai'; -import { z } from 'zod'; - -export const getDriverFromConnectionId = async (connectionId: string) => { - const { db, conn } = createDb(env.HYPERDRIVE.connectionString); - const activeConnection = await db.query.connection.findFirst({ - where: (connection, ops) => ops.eq(connection.id, connectionId), - columns: { - providerId: true, - userId: true, - accessToken: true, - refreshToken: true, - email: true, - }, - }); - - await conn.end(); - - if (!activeConnection || !activeConnection.accessToken || !activeConnection.refreshToken) { - throw new Error('No connection found'); - } - - return createDriver(activeConnection.providerId, { - auth: { - userId: activeConnection.userId, - accessToken: activeConnection.accessToken, - refreshToken: activeConnection.refreshToken, - email: activeConnection.email, - }, - }); -}; - -export class ZeroMCP extends McpAgent { - public server = new McpServer({ - name: 'zero-mcp', - version: '1.0.0', - description: 'Zero MCP', - }); - - async init(): Promise { - const driver = await getDriverFromConnectionId(this.props.connectionId); - - this.server.tool( - 'buildGmailSearchQuery', - { - query: z.string(), - }, - async (s) => { - const result = await generateText({ - model: openai(env.OPENAI_MODEL || 'gpt-4o'), - system: GmailSearchAssistantSystemPrompt(), - prompt: s.query, - }); - return { - content: [ - { - type: 'text', - text: result.text, - }, - ], - }; - }, - ); - - this.server.tool( - 'listThreads', - { - folder: z.string().default(FOLDERS.INBOX).describe('The folder to list threads from'), - query: z.string().optional().describe('The query to filter threads by'), - maxResults: z - .number() - .optional() - .default(5) - .describe('The maximum number of threads to return'), - labelIds: z.array(z.string()).optional().describe('The label IDs to filter threads by'), - pageToken: z.string().optional().describe('The page token to use for pagination'), - }, - async (s) => { - console.log('[DEBUG] listThreads', s); - - const result = await driver.list({ - folder: s.folder, - query: s.query, - maxResults: s.maxResults, - labelIds: s.labelIds, - pageToken: s.pageToken, - }); - const content = await Promise.all( - result.threads.map(async (thread) => { - const loadedThread = await driver.get(thread.id); - return [ - { - type: 'text' as const, - text: `Subject: ${loadedThread.latest?.subject} | ID: ${thread.id} | Received: ${loadedThread.latest?.receivedOn}`, - }, - ]; - }), - ); - return { - content: content.length - ? content.flat() - : [ - { - type: 'text' as const, - text: 'No threads found', - }, - ], - }; - }, - ); - - this.server.tool( - 'getThread', - { - threadId: z.string().describe('The ID of the thread to get'), - }, - async (s) => { - console.log('[DEBUG] getThread', s); - - try { - const thread = await driver.get(s.threadId); - - const content = thread.messages.at(-1)?.body; - - return { - content: [ - { - type: 'text', - text: `Subject:\n\n${thread.latest?.subject}\n\nBody:\n\n${content}`, - }, - ], - }; - - // const response = await env.VECTORIZE.getByIds([s.threadId]); - // 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: [ - // { - // type: 'text', - // text: shortResponse.summary, - // }, - // ], - // }; - // } - // return { - // content: [ - // { - // type: 'text', - // text: `Subject: ${thread.latest?.subject}`, - // }, - // ], - // }; - } catch (error) { - console.error('[DEBUG] getThread error', error); - return { - content: [ - { - type: 'text', - text: 'Failed to get thread', - }, - ], - }; - } - }, - ); - - this.server.tool( - 'markThreadsRead', - { - threadIds: z.array(z.string()).describe('The IDs of the threads to mark as read'), - }, - async (s) => { - console.log('[DEBUG] markThreadsRead', s); - - await driver.modifyLabels(s.threadIds, { - addLabels: [], - removeLabels: ['UNREAD'], - }); - return { - content: [ - { - type: 'text', - text: 'Threads marked as read', - }, - ], - }; - }, - ); - - this.server.tool( - 'markThreadsUnread', - { - threadIds: z.array(z.string()).describe('The IDs of the threads to mark as unread'), - }, - async (s) => { - console.log('[DEBUG] markThreadsUnread', s); - - await driver.modifyLabels(s.threadIds, { - addLabels: ['UNREAD'], - removeLabels: [], - }); - return { - content: [ - { - type: 'text', - text: 'Threads marked as unread', - }, - ], - }; - }, - ); - - this.server.tool( - 'modifyLabels', - { - threadIds: z.array(z.string()).describe('The IDs of the threads to modify'), - addLabelIds: z.array(z.string()).describe('The IDs of the labels to add'), - removeLabelIds: z.array(z.string()).describe('The IDs of the labels to remove'), - }, - async (s) => { - console.log('[DEBUG] modifyLabels', s); - - await driver.modifyLabels(s.threadIds, { - addLabels: s.addLabelIds, - removeLabels: s.removeLabelIds, - }); - return { - content: [ - { - type: 'text', - text: `Successfully modified ${s.threadIds.length} thread(s)`, - }, - ], - }; - }, - ); - - this.server.tool('getCurrentDate', async () => { - console.log('[DEBUG] getCurrentDate'); - - return { - content: [ - { - type: 'text', - text: getCurrentDateContext(), - }, - ], - }; - }); - - this.server.tool('getUserLabels', async () => { - console.log('[DEBUG] getUserLabels'); - - const labels = await driver.getUserLabels(); - return { - content: [ - { - type: 'text', - text: labels - .map((label) => `Name: ${label.name} ID: ${label.id} Color: ${label.color}`) - .join('\n'), - }, - ], - }; - }); - - this.server.tool( - 'getLabel', - { - id: z.string().describe('The ID of the label to get'), - }, - async (s) => { - console.log('[DEBUG] getLabel', s); - - const label = await driver.getLabel(s.id); - return { - content: [ - { - type: 'text', - text: `Name: ${label.name}`, - }, - { - type: 'text', - text: `ID: ${label.id}`, - }, - ], - }; - }, - ); - - this.server.tool( - 'createLabel', - { - name: z.string().describe('The name of the label to create'), - backgroundColor: z.string().optional().describe('The background color of the label'), - textColor: z.string().optional().describe('The text color of the label'), - }, - async (s) => { - console.log('[DEBUG] createLabel', s); - - try { - await driver.createLabel({ - name: s.name, - color: - s.backgroundColor && s.textColor - ? { - backgroundColor: s.backgroundColor, - textColor: s.textColor, - } - : undefined, - }); - return { - content: [ - { - type: 'text', - text: 'Label has been created', - }, - ], - }; - } catch (e) { - return { - content: [ - { - type: 'text', - text: 'Failed to create label', - }, - ], - }; - } - }, - ); - - this.server.tool( - 'bulkDelete', - { - threadIds: z.array(z.string()).describe('The IDs of the threads to delete'), - }, - async (s) => { - console.log('[DEBUG] bulkDelete', s); - - try { - await driver.modifyLabels(s.threadIds, { - addLabels: ['TRASH'], - removeLabels: ['INBOX'], - }); - return { - content: [ - { - type: 'text', - text: 'Threads moved to trash', - }, - ], - }; - } catch (e) { - return { - content: [ - { - type: 'text', - text: 'Failed to move threads to trash', - }, - ], - }; - } - }, - ); - - this.server.tool( - 'bulkArchive', - { - threadIds: z.array(z.string()).describe('The IDs of the threads to archive'), - }, - async (s) => { - console.log('[DEBUG] bulkArchive', s); - - try { - await driver.modifyLabels(s.threadIds, { - addLabels: [], - removeLabels: ['INBOX'], - }); - return { - content: [ - { - type: 'text', - text: 'Threads archived', - }, - ], - }; - } catch (e) { - return { - content: [ - { - type: 'text', - text: 'Failed to archive threads', - }, - ], - }; - } - }, - ); - } -} diff --git a/apps/server/src/trpc/routes/ai/compose.ts b/apps/server/src/trpc/routes/ai/compose.ts index f0fbc5b890..03cd0bd1f2 100644 --- a/apps/server/src/trpc/routes/ai/compose.ts +++ b/apps/server/src/trpc/routes/ai/compose.ts @@ -3,15 +3,15 @@ import { type WritingStyleMatrix, } from '../../../services/writing-style-service'; import { StyledEmailAssistantSystemPrompt } from '../../../lib/prompts'; -import { getPrompt } from '../../../lib/brain'; -import { EPrompts } from '../../../types'; import { webSearch } from '../../../routes/agent/tools'; import { activeConnectionProcedure } from '../../trpc'; +import { getPrompt } from '../../../lib/brain'; import { stripHtml } from 'string-strip-html'; +import { EPrompts } from '../../../types'; +import { env } from 'cloudflare:workers'; import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { z } from 'zod'; -import { env } from 'cloudflare:workers'; type ComposeEmailInput = { prompt: string; @@ -37,8 +37,8 @@ export async function composeEmail(input: ComposeEmailInput) { }); const systemPrompt = await getPrompt( - `${connectionId}-${EPrompts.Compose}`, - StyledEmailAssistantSystemPrompt() + `${connectionId}-${EPrompts.Compose}`, + StyledEmailAssistantSystemPrompt(), ); const userPrompt = EmailAssistantPrompt({ currentSubject: emailSubject, @@ -104,7 +104,7 @@ export async function composeEmail(input: ComposeEmailInput) { presencePenalty: 0.1, maxRetries: 1, tools: { - webSearch, + webSearch: webSearch(), }, }); diff --git a/apps/server/src/trpc/trpc.ts b/apps/server/src/trpc/trpc.ts index 959d1d6b21..e2947c02af 100644 --- a/apps/server/src/trpc/trpc.ts +++ b/apps/server/src/trpc/trpc.ts @@ -1,4 +1,4 @@ -import { connectionToDriver, getActiveConnection, getZeroDB } from '../lib/server-utils'; +import { getActiveConnection, getZeroDB } from '../lib/server-utils'; import { Ratelimit, type RatelimitConfig } from '@upstash/ratelimit'; import type { HonoContext, HonoVariables } from '../ctx'; import { getConnInfo } from 'hono/cloudflare-workers'; From 3532012ada37a6306bd2136bfc8826e7801b5d9b Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Thu, 10 Jul 2025 00:04:37 +0100 Subject: [PATCH 28/38] use bimi records to get logos (#1681) # Implement BIMI Avatar Support for Email Logos This PR adds support for Brand Indicators for Message Identification (BIMI) avatars in the mail interface. BIMI is an email specification that allows organizations to display their logos in supporting email clients by adding specific DNS records. The implementation includes: - New `BimiAvatar` component that fetches and displays BIMI logos from DNS records - Server-side TRPC routes to fetch and validate BIMI records - Integration of the component throughout the mail interface - Fallback mechanisms when BIMI logos aren't available ## Summary by CodeRabbit * **New Features** * Introduced BIMI (Brand Indicators for Message Identification) avatar support, displaying brand logos or fallback initials for email senders and recipients. * Added a new avatar component that automatically fetches and displays BIMI logos when available. * Enhanced avatar rendering in mail lists and mail display views with improved fallback and styling. * Added backend support for querying BIMI DNS records and fetching SVG logos via a new API route. * **Bug Fixes** * Improved reliability and consistency of avatar display, ensuring appropriate fallbacks if brand logos are unavailable. * **Chores** * Updated backend to support BIMI logo lookups and retrieval for enhanced email branding. --- apps/mail/components/mail/mail-display.tsx | 42 ++-- apps/mail/components/mail/mail-list.tsx | 79 ++++---- apps/mail/components/ui/bimi-avatar.tsx | 90 +++++++++ apps/server/src/trpc/index.ts | 4 +- apps/server/src/trpc/routes/bimi.ts | 221 +++++++++++++++++++++ 5 files changed, 363 insertions(+), 73 deletions(-) create mode 100644 apps/mail/components/ui/bimi-avatar.tsx create mode 100644 apps/server/src/trpc/routes/bimi.ts diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index d284741633..85c5a71516 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -34,15 +34,16 @@ import { import { cn, getEmailLogo, formatDate, formatTime, shouldShowSeparateTime } from '@/lib/utils'; import { Dialog, DialogTitle, DialogHeader, DialogContent } from '../ui/dialog'; import { memo, useEffect, useMemo, useState, useRef, useCallback } from 'react'; +import { BimiAvatar, getFirstLetterCharacter } from '../ui/bimi-avatar'; 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 { useMutation, useQuery } from '@tanstack/react-query'; 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 { useSummary } from '@/hooks/use-summary'; import { TextShimmer } from '../ui/text-shimmer'; @@ -383,13 +384,6 @@ const MailDisplayLabels = ({ labels }: { labels: string[] }) => { ); }; -// Helper function to get first letter character -const getFirstLetterCharacter = (name?: string) => { - if (!name) return ''; - const match = name.match(/[a-zA-Z]/); - return match ? match[0].toUpperCase() : ''; -}; - // Helper function to clean email display const cleanEmailDisplay = (email?: string) => { if (!email) return ''; @@ -1294,12 +1288,11 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: key={person.email} className="dark:bg-panelDark inline-flex items-center justify-start gap-1.5 overflow-hidden rounded-full border bg-white p-1 pr-2" > - - - - {getFirstLetterCharacter(person.name || person.email)} - - +
{person.name || person.email}
@@ -1307,12 +1300,11 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
- - - - {getFirstLetterCharacter(person.name || person.email)} - - +

{person.name || 'Unknown'}

@@ -1448,15 +1440,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: >
- - - - {getFirstLetterCharacter(emailData?.sender?.name)} - - +
diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index 4e52ff9e9c..132051d724 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -47,6 +47,7 @@ import { useSettings } from '@/hooks/use-settings'; import { useThreadNotes } from '@/hooks/use-notes'; import { useKeyState } from '@/hooks/use-hot-key'; import { VList, type VListHandle } from 'virtua'; +import { BimiAvatar } from '../ui/bimi-avatar'; import { RenderLabels } from './render-labels'; import { Badge } from '@/components/ui/badge'; import { useDraft } from '@/hooks/use-drafts'; @@ -362,55 +363,47 @@ const Thread = memo( className={`relative flex w-full items-center justify-between gap-4 px-4 ${displayUnread ? '' : 'opacity-60'}`} >
- -
+
{ + e.stopPropagation(); + setMail((prev: Config) => ({ + ...prev, + bulkSelected: prev.bulkSelected.filter((id: string) => id !== idToUse), + })); + }} + > + +
+ + ) : isGroupThread ? ( + { - e.stopPropagation(); - setMail((prev: Config) => ({ - ...prev, - bulkSelected: prev.bulkSelected.filter((id: string) => id !== idToUse), - })); - }} > - -
- {isGroupThread ? (
- ) : ( - <> - { - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - }} - /> - - {cleanName - ? cleanName[0]?.toUpperCase() - : latestMessage.sender.email[0]?.toUpperCase()} - - - )} -
+ + ) : ( + + )} {/* {displayUnread && !isMailSelected && !isFolderSent ? ( <> diff --git a/apps/mail/components/ui/bimi-avatar.tsx b/apps/mail/components/ui/bimi-avatar.tsx new file mode 100644 index 0000000000..ed9a2e1daa --- /dev/null +++ b/apps/mail/components/ui/bimi-avatar.tsx @@ -0,0 +1,90 @@ +import { Avatar, AvatarFallback, AvatarImage } from './avatar'; +import { useState, useCallback, useMemo } from 'react'; +import { useTRPC } from '@/providers/query-provider'; +import { useQuery } from '@tanstack/react-query'; +import { getEmailLogo } from '@/lib/utils'; +import DOMPurify from 'dompurify'; + +export const getFirstLetterCharacter = (name?: string) => { + if (!name) return ''; + const match = name.match(/[a-zA-Z]/); + return match ? match[0].toUpperCase() : ''; +}; + +interface BimiAvatarProps { + email?: string; + name?: string; + className?: string; + fallbackClassName?: string; + onImageError?: (e: React.SyntheticEvent) => void; +} + +export const BimiAvatar = ({ + email, + name, + className = 'h-8 w-8 rounded-full border dark:border-none', + fallbackClassName = 'rounded-full bg-[#FFFFFF] font-bold text-[#9F9F9F] dark:bg-[#373737]', + onImageError, +}: BimiAvatarProps) => { + const trpc = useTRPC(); + const [useDefaultFallback, setUseDefaultFallback] = useState(false); + + const { data: bimiData, isLoading } = useQuery({ + ...trpc.bimi.getByEmail.queryOptions({ email: email || '' }), + enabled: !!email && !useDefaultFallback, + staleTime: 1000 * 60 * 60 * 24, // Cache for 24 hours + gcTime: 1000 * 60 * 60 * 24 * 7, // Keep in cache for 7 days + }); + + const fallbackImageSrc = useMemo(() => { + if (useDefaultFallback || !email) return ''; + return getEmailLogo(email); + }, [email, useDefaultFallback]); + + const handleFallbackImageError = useCallback( + (e: React.SyntheticEvent) => { + setUseDefaultFallback(true); + if (onImageError) { + onImageError(e); + } + }, + [onImageError], + ); + + const firstLetter = getFirstLetterCharacter(name || email); + + if (!email) { + return ( + + {firstLetter} + + ); + } + + return ( + + {bimiData?.logo?.svgContent && !isLoading ? ( +
+ ) : fallbackImageSrc && !useDefaultFallback ? ( + + ) : getEmailLogo(email) ? ( + + ) : ( + {firstLetter} + )} + + ); +}; diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts index aa68800b2b..d629790257 100644 --- a/apps/server/src/trpc/index.ts +++ b/apps/server/src/trpc/index.ts @@ -1,6 +1,7 @@ import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; import { cookiePreferencesRouter } from './routes/cookies'; import { connectionsRouter } from './routes/connections'; +import { categoriesRouter } from './routes/categories'; import { shortcutRouter } from './routes/shortcut'; import { settingsRouter } from './routes/settings'; import { getContext } from 'hono/context-storage'; @@ -10,13 +11,14 @@ import { notesRouter } from './routes/notes'; import { brainRouter } from './routes/brain'; import { userRouter } from './routes/user'; import { mailRouter } from './routes/mail'; +import { bimiRouter } from './routes/bimi'; import type { HonoContext } from '../ctx'; import { aiRouter } from './routes/ai'; import { router } from './trpc'; -import { categoriesRouter } from './routes/categories'; export const appRouter = router({ ai: aiRouter, + bimi: bimiRouter, brain: brainRouter, categories: categoriesRouter, connections: connectionsRouter, diff --git a/apps/server/src/trpc/routes/bimi.ts b/apps/server/src/trpc/routes/bimi.ts new file mode 100644 index 0000000000..4276c542ce --- /dev/null +++ b/apps/server/src/trpc/routes/bimi.ts @@ -0,0 +1,221 @@ +import { router, privateProcedure, createRateLimiterMiddleware } from '../trpc'; +import { Ratelimit } from '@upstash/ratelimit'; +import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; + +const parseBimiRecord = (record: string) => { + const parts = record.split(';').map((part) => part.trim()); + const result: { version?: string; logoUrl?: string; authorityUrl?: string } = {}; + + for (const part of parts) { + if (part.startsWith('v=')) { + result.version = part.substring(2); + } else if (part.startsWith('l=')) { + result.logoUrl = part.substring(2); + } else if (part.startsWith('a=')) { + result.authorityUrl = part.substring(2); + } + } + + return result; +}; + +const fetchDnsRecord = async (domain: string): Promise => { + try { + const response = await fetch( + `https://dns.google/resolve?name=default._bimi.${domain}&type=TXT`, + ); + + if (!response.ok) { + return null; + } + + const data = (await response.json()) as { + Status: number; + Answer?: Array<{ data: string }>; + }; + + if (data.Status !== 0 || !data.Answer || data.Answer.length === 0) { + return null; + } + + const bimiRecord = data.Answer.find((answer) => answer.data.includes('v=BIMI1')); + + if (!bimiRecord) { + return null; + } + + return bimiRecord.data.replace(/"/g, ''); + } catch (error) { + console.error(`Error fetching BIMI record for ${domain}:`, error); + return null; + } +}; + +const fetchLogoContent = async (logoUrl: string): Promise => { + try { + const url = new URL(logoUrl); + if (url.protocol !== 'https:') { + return null; + } + + const response = await fetch(logoUrl, { + headers: { + Accept: 'image/svg+xml', + }, + }); + + if (!response.ok) { + return null; + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('svg')) { + return null; + } + + const svgContent = await response.text(); + + if (!svgContent.includes('')) { + return null; + } + + return svgContent; + } catch (error) { + console.error(`Error fetching logo from ${logoUrl}:`, error); + return null; + } +}; + +export const bimiRouter = router({ + getByEmail: privateProcedure + .use( + createRateLimiterMiddleware({ + generatePrefix: ({ sessionUser }) => `ratelimit:bimi-${sessionUser?.id}`, + limiter: Ratelimit.slidingWindow(30, '1m'), + }), + ) + .input( + z.object({ + email: z.string().email(), + }), + ) + .output( + z.object({ + domain: z.string(), + bimiRecord: z + .object({ + version: z.string().optional(), + logoUrl: z.string().optional(), + authorityUrl: z.string().optional(), + }) + .nullable(), + logo: z + .object({ + url: z.string(), + svgContent: z.string(), + }) + .nullable(), + }), + ) + .query(async ({ input }) => { + const domain = input.email.split('@')[1]; + + if (!domain) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Unable to extract domain from email address', + }); + } + + const bimiRecordText = await fetchDnsRecord(domain); + + if (!bimiRecordText) { + return { + domain, + bimiRecord: null, + logo: null, + }; + } + + const bimiRecord = parseBimiRecord(bimiRecordText); + + let logo = null; + if (bimiRecord.logoUrl) { + const svgContent = await fetchLogoContent(bimiRecord.logoUrl); + if (svgContent) { + logo = { + url: bimiRecord.logoUrl, + svgContent, + }; + } + } + + return { + domain, + bimiRecord, + logo, + }; + }), + + getByDomain: privateProcedure + .use( + createRateLimiterMiddleware({ + generatePrefix: ({ sessionUser }) => `ratelimit:bimi-domain-${sessionUser?.id}`, + limiter: Ratelimit.slidingWindow(30, '1m'), + }), + ) + .input( + z.object({ + domain: z.string().min(1), + }), + ) + .output( + z.object({ + domain: z.string(), + bimiRecord: z + .object({ + version: z.string().optional(), + logoUrl: z.string().optional(), + authorityUrl: z.string().optional(), + }) + .nullable(), + logo: z + .object({ + url: z.string(), + svgContent: z.string(), + }) + .nullable(), + }), + ) + .query(async ({ input }) => { + const bimiRecordText = await fetchDnsRecord(input.domain); + + if (!bimiRecordText) { + return { + domain: input.domain, + bimiRecord: null, + logo: null, + }; + } + + const bimiRecord = parseBimiRecord(bimiRecordText); + + let logo = null; + if (bimiRecord.logoUrl) { + const svgContent = await fetchLogoContent(bimiRecord.logoUrl); + if (svgContent) { + logo = { + url: bimiRecord.logoUrl, + svgContent, + }; + } + } + + return { + domain: input.domain, + bimiRecord, + logo, + }; + }), +}); From 277f476575e6b6f3e07cf4abc90d18001698b21b Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:59:40 -0700 Subject: [PATCH 29/38] cleanup on isle zero (#1699) Ran oxc (https://oxc.rs/docs/guide/usage/linter.html#vscode-extension) and fixed all the issues that came up, set it up to run as a PR check and added steps to the README.md asking users to use it. ## Summary by CodeRabbit * **New Features** * Introduced JavaScript linting using oxlint in development guidelines and CI workflow for improved code quality. * Added oxlint configuration and dependencies to the project. * **Bug Fixes** * Improved error logging in various components and utilities for better debugging. * Enhanced React list rendering by updating keys to use unique values instead of array indices, reducing rendering issues. * Replaced browser alerts with toast notifications for a smoother user experience. * **Refactor** * Simplified component logic and state management by removing unused code, imports, props, and components across multiple files. * Updated function and component signatures for clarity and maintainability. * Improved efficiency of certain operations by switching from arrays to sets for membership checks. * **Chores** * Cleaned up and reorganized import statements throughout the codebase. * Removed deprecated files, components, and middleware to streamline the codebase. * **Documentation** * Updated contribution guidelines to include linting requirements for code submissions. * **Style** * Minor formatting and readability improvements in JSX and code structure. --- .github/CONTRIBUTING.md | 2 + .github/workflows/ci.yml | 3 + .oxlintrc.json | 18 + apps/mail/app/(full-width)/contributors.tsx | 13 +- apps/mail/app/(full-width)/hr.tsx | 4 +- apps/mail/app/(full-width)/pricing.tsx | 69 +- apps/mail/app/(full-width)/privacy.tsx | 6 +- apps/mail/app/(full-width)/terms.tsx | 6 +- apps/mail/app/(routes)/layout.tsx | 2 +- apps/mail/app/(routes)/mail/[folder]/page.tsx | 6 +- apps/mail/app/(routes)/mail/layout.tsx | 6 +- .../app/(routes)/settings/appearance/page.tsx | 2 +- .../app/(routes)/settings/categories/page.tsx | 9 +- .../(routes)/settings/connections/page.tsx | 6 +- .../(routes)/settings/danger-zone/page.tsx | 4 +- .../app/(routes)/settings/general/page.tsx | 5 +- .../app/(routes)/settings/labels/page.tsx | 33 +- .../app/(routes)/settings/security/page.tsx | 2 +- .../app/(routes)/settings/shortcuts/page.tsx | 26 +- .../context/command-palette-context.tsx | 28 +- .../context/label-sidebar-context.tsx | 9 - .../components/context/sidebar-context.tsx | 1 - apps/mail/components/create/ai-chat.tsx | 21 +- apps/mail/components/create/create-email.tsx | 19 +- .../mail/components/create/editor-buttons.tsx | 87 --- apps/mail/components/create/editor.colors.tsx | 8 +- .../create/editor.link-selector.tsx | 94 --- .../create/editor.node-selector.tsx | 132 ---- .../components/create/editor.text-buttons.tsx | 4 +- apps/mail/components/create/editor.tsx | 265 +------- .../mail/components/create/email-composer.tsx | 16 +- .../create/image-compression-settings.tsx | 2 +- apps/mail/components/create/image-upload.ts | 0 .../create/selectors/link-selector.tsx | 101 --- .../create/selectors/math-selector.tsx | 35 - .../create/selectors/node-selector.tsx | 132 ---- apps/mail/components/create/slash-command.tsx | 6 +- apps/mail/components/create/toolbar.tsx | 2 - .../components/create/uploaded-file-icon.tsx | 2 +- apps/mail/components/home/HomeContent.tsx | 37 +- apps/mail/components/home/footer.tsx | 29 +- apps/mail/components/icons/icons.tsx | 2 +- apps/mail/components/labels/label-dialog.tsx | 16 +- apps/mail/components/magicui/file-tree.tsx | 263 ++++---- apps/mail/components/mail/mail-display.tsx | 165 +---- apps/mail/components/mail/mail-list.tsx | 57 +- apps/mail/components/mail/mail.tsx | 611 +----------------- apps/mail/components/mail/navbar.tsx | 6 +- apps/mail/components/mail/note-panel.tsx | 10 - apps/mail/components/mail/render-labels.tsx | 1 - apps/mail/components/mail/reply-composer.tsx | 16 +- .../components/mail/select-all-checkbox.tsx | 1 + apps/mail/components/mail/thread-display.tsx | 133 ++-- apps/mail/components/mail/thread-subject.tsx | 1 - .../motion-primitives/text-effect.tsx | 4 +- apps/mail/components/navigation.tsx | 8 +- apps/mail/components/onboarding.tsx | 6 +- apps/mail/components/party.tsx | 14 +- apps/mail/components/pricing/comparision.tsx | 2 +- apps/mail/components/setup-phone.tsx | 1 + apps/mail/components/theme/mode-toggle.tsx | 0 apps/mail/components/ui/ai-sidebar.tsx | 6 +- apps/mail/components/ui/app-sidebar.tsx | 37 +- apps/mail/components/ui/command-menu.tsx | 0 apps/mail/components/ui/dialog.tsx | 2 +- apps/mail/components/ui/gauge.tsx | 4 +- apps/mail/components/ui/nav-main.tsx | 10 +- apps/mail/components/ui/nav-user.tsx | 47 +- apps/mail/components/ui/pricing-dialog.tsx | 9 +- apps/mail/components/ui/recursive-folder.tsx | 5 +- apps/mail/components/ui/sheet.tsx | 2 +- apps/mail/components/ui/sidebar-labels.tsx | 2 +- apps/mail/components/user/user-button.tsx | 0 apps/mail/components/voice-button.tsx | 8 +- apps/mail/config/navigation.ts | 4 - apps/mail/hooks/driver/use-delete.ts | 2 +- apps/mail/hooks/use-compose-editor.ts | 41 -- apps/mail/hooks/use-mail-navigation.ts | 4 +- apps/mail/hooks/use-notes.tsx | 4 +- apps/mail/hooks/use-optimistic-actions.ts | 10 +- apps/mail/hooks/use-threads.ts | 6 +- apps/mail/lib/constants.tsx | 2 +- apps/mail/lib/elevenlabs-tools.ts | 8 +- apps/mail/lib/email-utils.client.tsx | 2 +- apps/mail/lib/hotkeys/mail-list-hotkeys.tsx | 4 +- .../lib/hotkeys/thread-display-hotkeys.tsx | 2 +- apps/mail/lib/hotkeys/use-hotkey-utils.ts | 9 +- apps/mail/lib/redis.ts | 0 apps/mail/lib/timezones.ts | 1 + apps/mail/lib/utils.ts | 5 +- apps/mail/middleware.ts | 36 -- apps/mail/package.json | 2 + apps/mail/providers/query-provider.tsx | 5 +- apps/mail/providers/voice-provider.tsx | 46 +- apps/mail/vite.config.ts | 2 + apps/server/src/lib/auth.ts | 19 +- apps/server/src/lib/driver/google.ts | 6 +- apps/server/src/lib/driver/microsoft.ts | 18 - apps/server/src/lib/driver/types.ts | 2 +- apps/server/src/lib/driver/utils.ts | 4 +- .../factories/base-subscription.factory.ts | 8 +- .../factories/google-subscription.factory.ts | 8 +- .../factories/outlook-subscription.factory.ts | 6 +- apps/server/src/lib/server-utils.ts | 2 - apps/server/src/lib/services.ts | 2 +- apps/server/src/lib/timezones.ts | 1 + apps/server/src/main.ts | 4 +- apps/server/src/pipelines.effect.ts | 21 +- apps/server/src/routes/agent/orchestrator.ts | 2 +- apps/server/src/routes/agent/tools.ts | 125 ++-- apps/server/src/routes/chat.ts | 30 +- .../src/services/writing-style-service.ts | 6 +- apps/server/src/trpc/routes/bimi.ts | 15 +- apps/server/src/trpc/routes/connections.ts | 6 +- apps/server/src/trpc/routes/label.ts | 2 +- apps/server/src/trpc/routes/mail.ts | 7 +- apps/server/src/trpc/routes/settings.ts | 3 +- apps/server/src/trpc/trpc.ts | 2 +- eslint.config.mjs | 2 +- pnpm-lock.yaml | 102 +++ scripts/seed-style/seeder.ts | 200 ------ 121 files changed, 757 insertions(+), 2742 deletions(-) create mode 100644 .oxlintrc.json delete mode 100644 apps/mail/components/create/editor.link-selector.tsx delete mode 100644 apps/mail/components/create/editor.node-selector.tsx delete mode 100644 apps/mail/components/create/image-upload.ts delete mode 100644 apps/mail/components/create/selectors/link-selector.tsx delete mode 100644 apps/mail/components/create/selectors/math-selector.tsx delete mode 100644 apps/mail/components/create/selectors/node-selector.tsx delete mode 100644 apps/mail/components/theme/mode-toggle.tsx delete mode 100644 apps/mail/components/ui/command-menu.tsx delete mode 100644 apps/mail/components/user/user-button.tsx delete mode 100644 apps/mail/lib/redis.ts delete mode 100644 apps/mail/middleware.ts delete mode 100644 scripts/seed-style/seeder.ts diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ad4f16ab2e..ebc0a5587f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -79,6 +79,8 @@ Thank you for your interest in contributing to 0.email! We're excited to have yo - Make sure the app runs without errors - Test your feature thoroughly + - Please lint using `pnpm dlx oxlint@latest` or by downloading an IDE extension here: https://oxc.rs/docs/guide/usage/linter.html#vscode-extension + 5. **Commit Your Changes** - Use clear, descriptive commit messages diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a11c0453b7..952f3a4603 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,6 @@ jobs: - name: Install dependencies 📦 run: pnpm install + + - name: Lint JS + run: pnpm dlx oxlint@latest --deny-warnings diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000000..2af0b8ba09 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,18 @@ +{ + "plugins": ["react", "unicorn", "typescript", "oxc"], + "rules": { + "no-alert": "error", // Emit an error message when a call to `alert()` is found + "oxc/approx-constant": "warn", // Show a warning when you write a number close to a known constant + "no-plusplus": "off", // Allow using the `++` and `--` operators + "no-useless-call": "error", + "no-accumulating-spread": "error", + "no-array-index-key": "error", + "jsx-no-jsx-as-prop": "error", + "jsx-no-new-array-as-prop": "error", + "jsx-no-new-function-as-prop": "error", + "jsx-no-new-object-as-prop": "error", + "prefer-array-find": "error", + "prefer-set-has": "error", + "exhaustive-deps": "off" + } +} diff --git a/apps/mail/app/(full-width)/contributors.tsx b/apps/mail/app/(full-width)/contributors.tsx index b97c35e3ea..7df1f63727 100644 --- a/apps/mail/app/(full-width)/contributors.tsx +++ b/apps/mail/app/(full-width)/contributors.tsx @@ -7,7 +7,6 @@ import { ChartAreaIcon, GitPullRequest, LayoutGrid, - FileCode, } from 'lucide-react'; import { Area, @@ -52,13 +51,13 @@ interface ActivityData { pullRequests: number; } -const excludedUsernames = [ +const excludedUsernames = new Set([ 'bot1', 'dependabot', 'github-actions', 'zerodotemail', 'autofix-ci[bot]', -]; +]); const coreTeamMembers = [ 'nizzyabi', 'ahmetskilinc', @@ -142,7 +141,7 @@ export default function OpenPage() { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [allContributors, setAllContributors] = useState([]); - const [isRendered, setIsRendered] = useState(false); + const [, setIsRendered] = useState(false); useEffect(() => setIsRendered(true), []); @@ -199,7 +198,7 @@ export default function OpenPage() { return allContributors ?.filter( (contributor) => - !excludedUsernames.includes(contributor.login) && + !excludedUsernames.has(contributor.login) && coreTeamMembers.some( (member) => member.toLowerCase() === contributor.login.toLowerCase(), ), @@ -216,7 +215,7 @@ export default function OpenPage() { allContributors ?.filter( (contributor) => - !excludedUsernames.includes(contributor.login) && + !excludedUsernames.has(contributor.login) && !coreTeamMembers.some( (member) => member.toLowerCase() === contributor.login.toLowerCase(), ), @@ -1011,6 +1010,7 @@ export default function OpenPage() { @@ -1019,6 +1019,7 @@ export default function OpenPage() { diff --git a/apps/mail/app/(full-width)/hr.tsx b/apps/mail/app/(full-width)/hr.tsx index 5bef425a92..d7248fc5a1 100644 --- a/apps/mail/app/(full-width)/hr.tsx +++ b/apps/mail/app/(full-width)/hr.tsx @@ -7,7 +7,7 @@ import { } from '@/components/ui/select'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { formatInTimeZone, fromZonedTime, toZonedTime } from 'date-fns-tz'; -import { getBrowserTimezone } from '@/lib/timezones'; + import { Plus, Trash2, Clock } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -145,7 +145,7 @@ export default function HRPage() { }, ]); // Company timezone - const [userTimezone, setUserTimezone] = useState('America/Los_Angeles'); + const [userTimezone] = useState('America/Los_Angeles'); const [userWorkingHours, setUserWorkingHours] = useState({ startTime: '09:00', endTime: '17:00', diff --git a/apps/mail/app/(full-width)/pricing.tsx b/apps/mail/app/(full-width)/pricing.tsx index 8cd0f2253f..693c04d6d9 100644 --- a/apps/mail/app/(full-width)/pricing.tsx +++ b/apps/mail/app/(full-width)/pricing.tsx @@ -1,77 +1,12 @@ -import { - NavigationMenu, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, - NavigationMenuContent, - ListItem, -} from '@/components/ui/navigation-menu'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { PixelatedBackground } from '@/components/home/pixelated-bg'; import PricingCard from '@/components/pricing/pricing-card'; import Comparision from '@/components/pricing/comparision'; -import { signIn, useSession } from '@/lib/auth-client'; -import { Separator } from '@/components/ui/separator'; -import { Navigation } from '@/components/navigation'; -import { useBilling } from '@/hooks/use-billing'; -import { Link, useNavigate } from 'react-router'; -import { Button } from '@/components/ui/button'; -import Footer from '@/components/home/footer'; -import { useState, useMemo } from 'react'; -import { Menu } from 'lucide-react'; -import { toast } from 'sonner'; -const resources = [ - { - title: 'GitHub', - href: 'https://github.com/Mail-0/Zero', - description: 'Check out our open-source projects and contributions.', - platform: 'github' as const, - }, - { - title: 'Twitter', - href: 'https://x.com/mail0dotcom', - description: 'Follow us for the latest updates and announcements.', - platform: 'twitter' as const, - }, - { - title: 'LinkedIn', - href: 'https://www.linkedin.com/company/mail0/', - description: 'Connect with us professionally and stay updated.', - platform: 'linkedin' as const, - }, - { - title: 'Discord', - href: 'https://discord.gg/mail0', - description: 'Join our community and chat with the team.', - platform: 'discord' as const, - }, -]; +import { Navigation } from '@/components/navigation'; -const aboutLinks = [ - { - title: 'About', - href: '/about', - description: 'Learn more about Zero and our mission.', - }, - { - title: 'Privacy', - href: '/privacy', - description: 'Read our privacy policy and data handling practices.', - }, - { - title: 'Terms of Service', - href: '/terms', - description: 'Review our terms of service and usage guidelines.', - }, -]; +import Footer from '@/components/home/footer'; export default function PricingPage() { - const navigate = useNavigate(); - const [open, setOpen] = useState(false); - const { data: session } = useSession(); - return (
{ diff --git a/apps/mail/app/(full-width)/terms.tsx b/apps/mail/app/(full-width)/terms.tsx index 63d5785d02..99f31b0058 100644 --- a/apps/mail/app/(full-width)/terms.tsx +++ b/apps/mail/app/(full-width)/terms.tsx @@ -5,15 +5,15 @@ import { Navigation } from '@/components/navigation'; import { Button } from '@/components/ui/button'; import Footer from '@/components/home/footer'; import { createSectionId } from '@/lib/utils'; -import { useNavigate } from 'react-router'; -import { toast } from 'sonner'; + + import React from 'react'; const LAST_UPDATED = 'February 13, 2025'; export default function TermsOfService() { const { copiedValue: copiedSection, copyToClipboard } = useCopyToClipboard(); - const navigate = useNavigate(); + const handleCopyLink = (sectionId: string) => { const url = `${window.location.origin}${window.location.pathname}#${sectionId}`; diff --git a/apps/mail/app/(routes)/layout.tsx b/apps/mail/app/(routes)/layout.tsx index 1bc11bcf90..c570e2e198 100644 --- a/apps/mail/app/(routes)/layout.tsx +++ b/apps/mail/app/(routes)/layout.tsx @@ -1,6 +1,6 @@ import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper'; import { CommandPaletteProvider } from '@/components/context/command-palette-context'; -import { VoiceProvider } from '@/providers/voice-provider'; + import { Outlet } from 'react-router'; export default function Layout() { diff --git a/apps/mail/app/(routes)/mail/[folder]/page.tsx b/apps/mail/app/(routes)/mail/[folder]/page.tsx index 8558675f75..95780ce9e4 100644 --- a/apps/mail/app/(routes)/mail/[folder]/page.tsx +++ b/apps/mail/app/(routes)/mail/[folder]/page.tsx @@ -1,12 +1,12 @@ import { useLoaderData, useNavigate } from 'react-router'; -import { useTRPC } from '@/providers/query-provider'; + import { MailLayout } from '@/components/mail/mail'; import { useLabels } from '@/hooks/use-labels'; import { authProxy } from '@/lib/auth-proxy'; import { useEffect, useState } from 'react'; import type { Route } from './+types/page'; -const ALLOWED_FOLDERS = ['inbox', 'draft', 'sent', 'spam', 'bin', 'archive']; +const ALLOWED_FOLDERS = new Set(['inbox', 'draft', 'sent', 'spam', 'bin', 'archive']); export async function clientLoader({ params, request }: Route.ClientLoaderArgs) { if (!params.folder) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/inbox`); @@ -24,7 +24,7 @@ export default function MailPage() { const navigate = useNavigate(); const [isLabelValid, setIsLabelValid] = useState(true); - const isStandardFolder = ALLOWED_FOLDERS.includes(folder); + const isStandardFolder = ALLOWED_FOLDERS.has(folder); const { userLabels, isLoading: isLoadingLabels } = useLabels(); diff --git a/apps/mail/app/(routes)/mail/layout.tsx b/apps/mail/app/(routes)/mail/layout.tsx index d1ea00286b..d6805729f4 100644 --- a/apps/mail/app/(routes)/mail/layout.tsx +++ b/apps/mail/app/(routes)/mail/layout.tsx @@ -1,10 +1,10 @@ import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper'; import { OnboardingWrapper } from '@/components/onboarding'; -import { VoiceProvider } from '@/providers/voice-provider'; + import { NotificationProvider } from '@/components/party'; import { AppSidebar } from '@/components/ui/app-sidebar'; -import { Outlet, useLoaderData } from 'react-router'; -import type { Route } from './+types/layout'; +import { Outlet, } from 'react-router'; + export default function MailLayout() { return ( diff --git a/apps/mail/app/(routes)/settings/appearance/page.tsx b/apps/mail/app/(routes)/settings/appearance/page.tsx index 5264254878..94a444538a 100644 --- a/apps/mail/app/(routes)/settings/appearance/page.tsx +++ b/apps/mail/app/(routes)/settings/appearance/page.tsx @@ -110,7 +110,7 @@ export default function AppearancePage() { ( + render={() => ( {m['pages.settings.appearance.theme']()} diff --git a/apps/mail/app/(routes)/settings/categories/page.tsx b/apps/mail/app/(routes)/settings/categories/page.tsx index c983847470..c58e905fc7 100644 --- a/apps/mail/app/(routes)/settings/categories/page.tsx +++ b/apps/mail/app/(routes)/settings/categories/page.tsx @@ -10,16 +10,11 @@ import { useTRPC } from '@/providers/query-provider'; import { toast } from 'sonner'; import type { CategorySetting } from '@/hooks/use-categories'; import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'; -import * as Icons from '@/components/icons/icons'; + import { Sparkles } from '@/components/icons/icons'; import { Loader, GripVertical } from 'lucide-react'; import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from '@/components/ui/select'; + } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { DndContext, diff --git a/apps/mail/app/(routes)/settings/connections/page.tsx b/apps/mail/app/(routes)/settings/connections/page.tsx index db63098096..189f749f27 100644 --- a/apps/mail/app/(routes)/settings/connections/page.tsx +++ b/apps/mail/app/(routes)/settings/connections/page.tsx @@ -10,7 +10,7 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { SettingsCard } from '@/components/settings/settings-card'; import { AddConnectionDialog } from '@/components/connection/add'; -import { PricingDialog } from '@/components/ui/pricing-dialog'; + import { useSession, authClient } from '@/lib/auth-client'; import { useConnections } from '@/hooks/use-connections'; import { useTRPC } from '@/providers/query-provider'; @@ -62,9 +62,9 @@ export default function ConnectionsPage() {
{isLoading ? (
- {[...Array(3)].map((_, i) => ( + {[...Array(3)].map((n) => (
diff --git a/apps/mail/app/(routes)/settings/danger-zone/page.tsx b/apps/mail/app/(routes)/settings/danger-zone/page.tsx index 89e8dadd8a..75a787f26e 100644 --- a/apps/mail/app/(routes)/settings/danger-zone/page.tsx +++ b/apps/mail/app/(routes)/settings/danger-zone/page.tsx @@ -15,7 +15,7 @@ import { useMutation } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { AlertTriangle } from 'lucide-react'; -import { useNavigate } from 'react-router'; + import { useForm } from 'react-hook-form'; import { m } from '@/paraglide/messages'; import { clear } from 'idb-keyval'; @@ -33,7 +33,7 @@ const formSchema = z.object({ function DeleteAccountDialog() { const [isOpen, setIsOpen] = useState(false); - const navigate = useNavigate(); + const trpc = useTRPC(); const { refetch } = useSession(); const { mutateAsync: deleteAccount, isPending } = useMutation(trpc.user.delete.mutationOptions()); diff --git a/apps/mail/app/(routes)/settings/general/page.tsx b/apps/mail/app/(routes)/settings/general/page.tsx index 76429a7934..b9bf5a2962 100644 --- a/apps/mail/app/(routes)/settings/general/page.tsx +++ b/apps/mail/app/(routes)/settings/general/page.tsx @@ -18,8 +18,8 @@ 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 { Globe, Clock, XIcon, Mail, InfoIcon } from 'lucide-react'; import { useEmailAliases } from '@/hooks/use-email-aliases'; +import { Globe, Clock, Mail, InfoIcon } from 'lucide-react'; import { getLocale, setLocale } from '@/paraglide/runtime'; import { useState, useEffect, useMemo, memo } from 'react'; import { userSettingsSchema } from '@zero/server/schemas'; @@ -28,7 +28,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTRPC } from '@/providers/query-provider'; import { getBrowserTimezone } from '@/lib/timezones'; -import { Textarea } from '@/components/ui/textarea'; + import { useSettings } from '@/hooks/use-settings'; import { locales as localesData } from '@/locales'; import { Switch } from '@/components/ui/switch'; @@ -167,6 +167,7 @@ export default function GeneralPage() { toast.success(m['common.settings.saved']()); } catch (error) { + console.error(error); toast.error(m['common.settings.failedToSave']()); queryClient.setQueryData(trpc.settings.get.queryKey(), (updater) => { if (!updater) return; diff --git a/apps/mail/app/(routes)/settings/labels/page.tsx b/apps/mail/app/(routes)/settings/labels/page.tsx index 059356a0c8..e0e4a90676 100644 --- a/apps/mail/app/(routes)/settings/labels/page.tsx +++ b/apps/mail/app/(routes)/settings/labels/page.tsx @@ -1,40 +1,29 @@ import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog'; + } from '@/components/ui/dialog'; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; + } from '@/components/ui/form'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { SettingsCard } from '@/components/settings/settings-card'; import { LabelDialog } from '@/components/labels/label-dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { CurvedArrow } from '@/components/icons/icons'; + import { Separator } from '@/components/ui/separator'; import { useTRPC } from '@/providers/query-provider'; import { useMutation } from '@tanstack/react-query'; -import { Check, Plus, Pencil } from 'lucide-react'; +import { Plus, Pencil } from 'lucide-react'; import { type Label as LabelType } from '@/types'; import { Button } from '@/components/ui/button'; -import { HexColorPicker } from 'react-colorful'; + import { Bin } from '@/components/icons/icons'; import { useLabels } from '@/hooks/use-labels'; -import { GMAIL_COLORS } from '@/lib/constants'; -import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; + + + import { Badge } from '@/components/ui/badge'; -import { useForm } from 'react-hook-form'; + import { m } from '@/paraglide/messages'; -import { Command } from 'lucide-react'; -import { COLORS } from './colors'; + + import { useState } from 'react'; import { toast } from 'sonner'; diff --git a/apps/mail/app/(routes)/settings/security/page.tsx b/apps/mail/app/(routes)/settings/security/page.tsx index e3eee76d4e..8ddaefe33f 100644 --- a/apps/mail/app/(routes)/settings/security/page.tsx +++ b/apps/mail/app/(routes)/settings/security/page.tsx @@ -12,7 +12,7 @@ import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { m } from '@/paraglide/messages'; import { useForm } from 'react-hook-form'; -import { KeyRound } from 'lucide-react'; + import { useState } from 'react'; import * as z from 'zod'; diff --git a/apps/mail/app/(routes)/settings/shortcuts/page.tsx b/apps/mail/app/(routes)/settings/shortcuts/page.tsx index 377967fc8a..c4266667e7 100644 --- a/apps/mail/app/(routes)/settings/shortcuts/page.tsx +++ b/apps/mail/app/(routes)/settings/shortcuts/page.tsx @@ -1,19 +1,17 @@ -import { keyboardShortcuts, type Shortcut } from '@/config/shortcuts'; import { SettingsCard } from '@/components/settings/settings-card'; import { formatDisplayKeys } from '@/lib/hotkeys/use-hotkey-utils'; import { useShortcutCache } from '@/lib/hotkeys/use-hotkey-utils'; import { useCategorySettings } from '@/hooks/use-categories'; -import { useState, type ReactNode, useEffect } from 'react'; -import { useSession } from '@/lib/auth-client'; +import { type Shortcut } from '@/config/shortcuts'; import { m } from '@/paraglide/messages'; +import { type ReactNode } from 'react'; export default function ShortcutsPage() { - const { data: session } = useSession(); const { shortcuts, // TODO: Implement shortcuts syncing and caching // updateShortcut, - } = useShortcutCache(session?.user?.id); + } = useShortcutCache(); const categorySettings = useCategorySettings(); return ( @@ -77,13 +75,13 @@ export default function ShortcutsPage() { } return ( - {label} - + ); })}
@@ -95,18 +93,10 @@ export default function ShortcutsPage() { ); } -function Shortcut({ - children, - keys, - action, -}: { - children: ReactNode; - keys: string[]; - action: string; -}) { +function ShortcutItem({ children, keys }: { children: ReactNode; keys: string[] }) { // const [isRecording, setIsRecording] = useState(false); const displayKeys = formatDisplayKeys(keys); - const { data: session } = useSession(); + // const { updateShortcut } = useShortcutCache(session?.user?.id); // const handleHotkeyRecorded = async (newKeys: string[]) => { diff --git a/apps/mail/components/context/command-palette-context.tsx b/apps/mail/components/context/command-palette-context.tsx index 3118809694..b9da0c4cf5 100644 --- a/apps/mail/components/context/command-palette-context.tsx +++ b/apps/mail/components/context/command-palette-context.tsx @@ -18,16 +18,6 @@ import { Users, X as XIcon, } from 'lucide-react'; -import { - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, - CommandShortcut, -} from '@/components/ui/command'; import { createContext, Fragment, @@ -39,6 +29,14 @@ import { useState, type ComponentType, } from 'react'; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; import { getMainSearchTerm, parseNaturalLanguageSearch } from '@/lib/utils'; import { DialogDescription, DialogTitle } from '@/components/ui/dialog'; import { useSearchValue } from '@/hooks/use-search-value'; @@ -188,7 +186,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { const [activeFilters, setActiveFilters] = useState([]); const [recentSearches, setRecentSearches] = useState([]); const [savedSearches, setSavedSearches] = useState([]); - const [selectedLabels, setSelectedLabels] = useState([]); + // const [selectedLabels] = useState([]); const [filterBuilderState, setFilterBuilderState] = useState>({}); const [saveSearchName, setSaveSearchName] = useState(''); const [emailSuggestions, setEmailSuggestions] = useState([]); @@ -199,7 +197,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { const { userLabels = [] } = useLabels(); const trpc = useTRPC(); - const { mutateAsync: generateSearchQuery, isPending } = useMutation( + const { mutateAsync: generateSearchQuery } = useMutation( trpc.ai.generateSearchQuery.mutationOptions(), ); @@ -845,7 +843,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { )} {allCommands.map((group, groupIndex) => ( - + {group.items.length > 0 && ( {group.items.map((item) => ( @@ -1460,9 +1458,9 @@ export function CommandPalette({ children }: { children: React.ReactNode }) { /> )} {label.name || 'Unnamed Label'} - {selectedLabels.includes(label.id || '') && ( + {/* {selectedLabels.includes(label.id || '') && ( - )} + )} */}
))}
diff --git a/apps/mail/components/context/label-sidebar-context.tsx b/apps/mail/components/context/label-sidebar-context.tsx index 580af18d6a..d98ca528e2 100644 --- a/apps/mail/components/context/label-sidebar-context.tsx +++ b/apps/mail/components/context/label-sidebar-context.tsx @@ -22,15 +22,6 @@ import { Trash } from '../icons/icons'; import { Button } from '../ui/button'; import { toast } from 'sonner'; -interface LabelAction { - id: string; - label: string | ReactNode; - icon?: ReactNode; - shortcut?: string; - action: () => void; - disabled?: boolean; -} - interface LabelSidebarContextMenuProps { children: ReactNode; labelId: string; diff --git a/apps/mail/components/context/sidebar-context.tsx b/apps/mail/components/context/sidebar-context.tsx index ca61dedfa3..967505ac08 100644 --- a/apps/mail/components/context/sidebar-context.tsx +++ b/apps/mail/components/context/sidebar-context.tsx @@ -1,7 +1,6 @@ import { SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, - SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON, } from '@/lib/constants'; diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index 50faed6b77..e9c4051040 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -1,5 +1,4 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; -import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useAIFullScreen, useAISidebar } from '../ui/ai-sidebar'; import { VoiceProvider } from '@/providers/voice-provider'; @@ -91,9 +90,9 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi {/* First row */}
- {firstRowQueries.map((query, index) => ( + {firstRowQueries.map((query) => ( - - -
-
-

Attachments

-

- {attachments.length} {attachments.length === 1 ? 'file' : 'files'} -

-
- -
-
- {attachments.map((file, index) => ( -
- - onAttachmentRemove && onAttachmentRemove(index) - } - index={index} - file={file} - /> -
-

{truncateFileName(file.name, 20)}

-

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

-
-
- ))} -
-
-
-
- - )} - - {/* Add Attachment Button */} - -
- ); -}; - export default () => { return }>; }; diff --git a/apps/mail/components/create/editor.colors.tsx b/apps/mail/components/create/editor.colors.tsx index 1f55686ba1..a3422452d4 100644 --- a/apps/mail/components/create/editor.colors.tsx +++ b/apps/mail/components/create/editor.colors.tsx @@ -127,9 +127,9 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => { >
Color
- {TEXT_COLORS.map(({ name, color }, index) => ( + {TEXT_COLORS.map(({ name, color }) => ( { // editor.commands.unsetColor(); name !== 'Default' && @@ -152,9 +152,9 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
Background
- {HIGHLIGHT_COLORS.map(({ name, color }, index) => ( + {HIGHLIGHT_COLORS.map(({ name, color }) => ( { editor.commands.unsetHighlight(); name !== 'Default' && editor.commands.setHighlight({ color }); diff --git a/apps/mail/components/create/editor.link-selector.tsx b/apps/mail/components/create/editor.link-selector.tsx deleted file mode 100644 index 44b67079dc..0000000000 --- a/apps/mail/components/create/editor.link-selector.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { PopoverContent, Popover, PopoverTrigger } from '@/components/ui/popover'; -import { Button } from '@/components/ui/button'; -import { Check, Trash } from 'lucide-react'; -import { useEffect, useRef } from 'react'; -import { useEditor } from 'novel'; -import { cn } from '@/lib/utils'; - -export function isValidUrl(url: string) { - try { - new URL(url); - return true; - } catch (e) { - return false; - } -} -export function getUrlFromString(str: string) { - if (isValidUrl(str)) return str; - try { - if (str.includes('.') && !str.includes(' ')) { - return new URL(`https://${str}`).toString(); - } - } catch (e) { - return null; - } -} -interface LinkSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => { - const inputRef = useRef(null); - const { editor } = useEditor(); - - // Autofocus on input by default - useEffect(() => { - inputRef.current && inputRef.current?.focus(); - }); - if (!editor) return null; - - return ( - - - - - -
{ - const target = e.currentTarget as HTMLFormElement; - e.preventDefault(); - const input = target[0] as HTMLInputElement; - const url = getUrlFromString(input.value); - url && editor.chain().focus().setLink({ href: url }).run(); - }} - className="flex p-1" - > - - {editor.getAttributes('link').href ? ( - - ) : ( - - )} -
-
-
- ); -}; diff --git a/apps/mail/components/create/editor.node-selector.tsx b/apps/mail/components/create/editor.node-selector.tsx deleted file mode 100644 index 6bca3870f3..0000000000 --- a/apps/mail/components/create/editor.node-selector.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { - Check, - ChevronDown, - Heading1, - Heading2, - Heading3, - TextQuote, - ListOrdered, - TextIcon, - Code, - CheckSquare, - type LucideIcon, -} from 'lucide-react'; -import { PopoverContent, PopoverTrigger, Popover } from '@/components/ui/popover'; -import { EditorBubbleItem, useEditor } from 'novel'; -import { Button } from '@/components/ui/button'; -import { type Editor } from '@tiptap/react'; - -export type SelectorItem = { - name: string; - icon: LucideIcon; - command: (editor: Editor) => void; - isActive: (editor: Editor) => boolean; -}; - -const items: SelectorItem[] = [ - { - name: 'Text', - icon: TextIcon, - command: (editor) => editor.chain().focus().toggleNode('paragraph', 'paragraph').run(), - // I feel like there has to be a more efficient way to do this – feel free to PR if you know how! - isActive: (editor) => - editor.isActive('paragraph') && - !editor.isActive('bulletList') && - !editor.isActive('orderedList'), - }, - { - name: 'Heading 1', - icon: Heading1, - command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(), - isActive: (editor) => editor.isActive('heading', { level: 1 }), - }, - { - name: 'Heading 2', - icon: Heading2, - command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(), - isActive: (editor) => editor.isActive('heading', { level: 2 }), - }, - { - name: 'Heading 3', - icon: Heading3, - command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(), - isActive: (editor) => editor.isActive('heading', { level: 3 }), - }, - { - name: 'To-do List', - icon: CheckSquare, - command: (editor) => editor.chain().focus().toggleTaskList().run(), - isActive: (editor) => editor.isActive('taskItem'), - }, - { - name: 'Bullet List', - icon: ListOrdered, - command: (editor) => editor.chain().focus().toggleBulletList().run(), - isActive: (editor) => editor.isActive('bulletList'), - }, - { - name: 'Numbered List', - icon: ListOrdered, - command: (editor) => editor.chain().focus().toggleOrderedList().run(), - isActive: (editor) => editor.isActive('orderedList'), - }, - { - name: 'Quote', - icon: TextQuote, - command: (editor) => - editor.chain().focus().toggleNode('paragraph', 'paragraph').toggleBlockquote().run(), - isActive: (editor) => editor.isActive('blockquote'), - }, - { - name: 'Code', - icon: Code, - command: (editor) => editor.chain().focus().toggleCodeBlock().run(), - isActive: (editor) => editor.isActive('codeBlock'), - }, -]; -interface NodeSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const NodeSelector = ({ open, onOpenChange }: NodeSelectorProps) => { - const { editor } = useEditor(); - if (!editor) return null; - const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? { - name: 'Multiple', - }; - - return ( - - - - - - {items.map((item, index) => ( - { - item.command(editor); - onOpenChange(false); - }} - className="hover:bg-accent flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm" - > -
-
- -
- {item.name} -
- {activeItem.name === item.name && } -
- ))} -
-
- ); -}; diff --git a/apps/mail/components/create/editor.text-buttons.tsx b/apps/mail/components/create/editor.text-buttons.tsx index adb241dbce..ee903f81ef 100644 --- a/apps/mail/components/create/editor.text-buttons.tsx +++ b/apps/mail/components/create/editor.text-buttons.tsx @@ -41,9 +41,9 @@ export const TextButtons = () => { ]; return (
- {items.map((item, index) => ( + {items.map((item) => ( { item.command(editor); }} diff --git a/apps/mail/components/create/editor.tsx b/apps/mail/components/create/editor.tsx index fbf8b925cf..dcd64c5ddb 100644 --- a/apps/mail/components/create/editor.tsx +++ b/apps/mail/components/create/editor.tsx @@ -1,18 +1,3 @@ -import { - Bold, - Italic, - Strikethrough, - Underline, - Code, - Link as LinkIcon, - List, - ListOrdered, - Heading1, - Heading2, - Heading3, - Paperclip, - Plus, -} from 'lucide-react'; import { EditorCommand, EditorCommandEmpty, @@ -20,39 +5,25 @@ import { EditorCommandList, EditorContent, EditorRoot, - useEditor, type JSONContent, } from 'novel'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { useEditor as useEditorContext } from '@/components/providers/editor-provider'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { Editor as TiptapEditor, useCurrentEditor } from '@tiptap/react'; + import { suggestionItems } from '@/components/create/slash-command'; import { defaultExtensions } from '@/components/create/extensions'; -import { ImageResizer, handleCommandNavigation } from 'novel'; -import { handleImageDrop, handleImagePaste } from 'novel'; import EditorMenu from '@/components/create/editor-menu'; -import { UploadedFileIcon } from './uploaded-file-icon'; -import { Separator } from '@/components/ui/separator'; -import { useReducer, useRef, useEffect } from 'react'; +import { Editor as TiptapEditor } from '@tiptap/react'; +import { handleCommandNavigation } from 'novel'; +import { handleImageDrop } from 'novel'; + import { AutoComplete } from './editor-autocomplete'; -import { Editor as CoreEditor } from '@tiptap/core'; -import { cn, truncateFileName } from '@/lib/utils'; +import { useReducer, useRef } from 'react'; + import { TextSelection } from 'prosemirror-state'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { EditorView } from 'prosemirror-view'; + +import { cn } from '@/lib/utils'; + import { Markdown } from 'tiptap-markdown'; -import { Slice } from 'prosemirror-model'; -import { m } from '@/paraglide/messages'; + import { useState } from 'react'; import React from 'react'; @@ -121,217 +92,6 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState { } } -// Update the MenuBar component with icons -interface MenuBarProps { - onAttachmentsChange?: (attachments: File[]) => void; - includeSignature?: boolean; - onSignatureToggle?: (include: boolean) => void; - hasSignature?: boolean; -} - -const MenuBar = () => { - const { editor } = useCurrentEditor(); - - const [linkDialogOpen, setLinkDialogOpen] = useState(false); - const [linkUrl, setLinkUrl] = useState(''); - - if (!editor) { - return null; - } - - // Replace the old setLink function with this new implementation - const handleLinkDialogOpen = () => { - // If a link is already active, pre-fill the input with the current URL - if (editor.isActive('link')) { - const attrs = editor.getAttributes('link'); - setLinkUrl(attrs.href || ''); - } else { - setLinkUrl(''); - } - setLinkDialogOpen(true); - }; - - const handleSaveLink = () => { - // empty - if (linkUrl === '') { - editor.chain().focus().unsetLink().run(); - } else { - // Format the URL with proper protocol if missing - let formattedUrl = linkUrl; - if (formattedUrl && !/^https?:\/\//i.test(formattedUrl)) { - formattedUrl = `https://${formattedUrl}`; - } - // set link - editor.chain().focus().setLink({ href: formattedUrl }).run(); - } - setLinkDialogOpen(false); - }; - - const handleRemoveLink = () => { - editor.chain().focus().unsetLink().run(); - setLinkDialogOpen(false); - }; - - return ( - <> - -
-
-
- - - - - {m.pages.createEmail.editor.menuBar.bold()} - - - - - - {m.pages.createEmail.editor.menuBar.italic()} - - - - - - - {m.pages.createEmail.editor.menuBar.strikethrough()} - - - - - - - {m.pages.createEmail.editor.menuBar.underline()} - - - - - - {m.pages.createEmail.editor.menuBar.link()} - -
- - - -
- - - - - {m.pages.createEmail.editor.menuBar.bulletList()} - - - - - - {m.pages.createEmail.editor.menuBar.orderedList()} - -
-
-
-
- - - - - {m.pages.createEmail.addLink()} - {m.pages.createEmail.addUrlToCreateALink()} - -
-
- - setLinkUrl(e.target.value)} - placeholder="https://example.com" - /> -
-
- - - - -
-
- - ); -}; - export default function Editor({ initialValue, onChange, @@ -345,7 +105,6 @@ export default function Editor({ senderInfo, myInfo, readOnly, - hideToolbar, }: EditorProps) { const [state, dispatch] = useReducer(editorReducer, { openNode: false, @@ -358,7 +117,7 @@ export default function Editor({ const [editor, setEditor] = useState(null); const containerRef = useRef(null); - const { openNode, openColor, openLink, openAI } = state; + const { openAI } = state; // Function to focus the editor const focusEditor = () => { diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index aa3ac4a6d6..5a1f676476 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -33,7 +33,7 @@ import { Avatar, AvatarFallback } from '../ui/avatar'; import { useTRPC } from '@/providers/query-provider'; import { useMutation } from '@tanstack/react-query'; import { useSettings } from '@/hooks/use-settings'; -import { useIsMobile } from '@/hooks/use-mobile'; + import { cn, formatFileSize } from '@/lib/utils'; import { useThread } from '@/hooks/use-threads'; import { serializeFiles } from '@/lib/schemas'; @@ -114,10 +114,8 @@ export function EmailComposer({ className, autofocus = false, settingsLoading = false, - replyingTo, editorClassName, }: EmailComposerProps) { - const isMobile = useIsMobile(); const { data: aliases } = useEmailAliases(); const { data: settings } = useSettings(); const [showCc, setShowCc] = useState(initialCc.length > 0); @@ -730,7 +728,7 @@ export function EmailComposer({
{toEmails.map((email, index) => (
@@ -864,7 +862,7 @@ export function EmailComposer({
{toEmails.slice(0, 3).map((email, index) => (
@@ -949,7 +947,7 @@ export function EmailComposer({
{ccEmails?.map((email, index) => (
@@ -1040,7 +1038,7 @@ export function EmailComposer({
{ccEmails.slice(0, 3).map((email, index) => (
@@ -1095,7 +1093,7 @@ export function EmailComposer({
{bccEmails?.map((email, index) => (
@@ -1186,7 +1184,7 @@ export function EmailComposer({
{bccEmails.slice(0, 3).map((email, index) => (
diff --git a/apps/mail/components/create/image-compression-settings.tsx b/apps/mail/components/create/image-compression-settings.tsx index 681c749c54..bf0405963f 100644 --- a/apps/mail/components/create/image-compression-settings.tsx +++ b/apps/mail/components/create/image-compression-settings.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Settings, Image, FileImage, Zap } from 'lucide-react'; import type { ImageQuality } from '@/lib/image-compression'; -import { Button } from '@/components/ui/button'; + import { Label } from '@/components/ui/label'; import { m } from '@/paraglide/messages'; diff --git a/apps/mail/components/create/image-upload.ts b/apps/mail/components/create/image-upload.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/mail/components/create/selectors/link-selector.tsx b/apps/mail/components/create/selectors/link-selector.tsx deleted file mode 100644 index f3b21c8eb7..0000000000 --- a/apps/mail/components/create/selectors/link-selector.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { PopoverContent, Popover, PopoverTrigger } from '@/components/ui/popover'; -import { Button } from '@/components/ui/button'; -import { Check, Trash } from 'lucide-react'; -import { useEffect, useRef } from 'react'; -import { useEditor } from 'novel'; -import { cn } from '@/lib/utils'; - -export function isValidUrl(url: string) { - try { - new URL(url); - return true; - } catch (_e) { - return false; - } -} -export function getUrlFromString(str: string) { - if (isValidUrl(str)) return str; - try { - if (str.includes('.') && !str.includes(' ')) { - return new URL(`https://${str}`).toString(); - } - } catch (_e) { - return null; - } -} -interface LinkSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const LinkSelector = ({ open, onOpenChange }: LinkSelectorProps) => { - const inputRef = useRef(null); - const { editor } = useEditor(); - - // Autofocus on input by default - useEffect(() => { - inputRef.current?.focus(); - }); - if (!editor) return null; - - return ( - - - - - -
{ - const target = e.currentTarget as HTMLFormElement; - e.preventDefault(); - const input = target[0] as HTMLInputElement; - const url = getUrlFromString(input.value); - if (url) { - editor.chain().focus().setLink({ href: url }).run(); - onOpenChange(false); - } - }} - className="flex p-1" - > - - {editor.getAttributes('link').href ? ( - - ) : ( - - )} -
-
-
- ); -}; diff --git a/apps/mail/components/create/selectors/math-selector.tsx b/apps/mail/components/create/selectors/math-selector.tsx deleted file mode 100644 index 0b9d4e081c..0000000000 --- a/apps/mail/components/create/selectors/math-selector.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { SigmaIcon } from 'lucide-react'; -import { useEditor } from 'novel'; -import { cn } from '@/lib/utils'; - -export const MathSelector = () => { - const { editor } = useEditor(); - - if (!editor) return null; - - return ( - - ); -}; diff --git a/apps/mail/components/create/selectors/node-selector.tsx b/apps/mail/components/create/selectors/node-selector.tsx deleted file mode 100644 index d44bb17f37..0000000000 --- a/apps/mail/components/create/selectors/node-selector.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { - Check, - CheckSquare, - ChevronDown, - Code, - Heading1, - Heading2, - Heading3, - ListOrdered, - type LucideIcon, - TextIcon, - TextQuote, -} from 'lucide-react'; -import { PopoverContent, PopoverTrigger, Popover } from '@/components/ui/popover'; -import { EditorBubbleItem, useEditor } from 'novel'; -import { Button } from '@/components/ui/button'; - -export type SelectorItem = { - name: string; - icon: LucideIcon; - command: (editor: ReturnType['editor']) => void; - isActive: (editor: ReturnType['editor']) => boolean; -}; - -const items: SelectorItem[] = [ - { - name: 'Text', - icon: TextIcon, - command: (editor) => editor?.chain().focus().clearNodes().run(), - // I feel like there has to be a more efficient way to do this – feel free to PR if you know how! - isActive: (editor) => - editor - ? editor.isActive('paragraph') && - !editor.isActive('bulletList') && - !editor.isActive('orderedList') - : false, - }, - { - name: 'Heading 1', - icon: Heading1, - command: (editor) => editor?.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(), - isActive: (editor) => (editor ? editor.isActive('heading', { level: 1 }) : false), - }, - { - name: 'Heading 2', - icon: Heading2, - command: (editor) => editor?.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(), - isActive: (editor) => (editor ? editor.isActive('heading', { level: 2 }) : false), - }, - { - name: 'Heading 3', - icon: Heading3, - command: (editor) => editor?.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(), - isActive: (editor) => (editor ? editor.isActive('heading', { level: 3 }) : false), - }, - { - name: 'To-do List', - icon: CheckSquare, - command: (editor) => editor?.chain().focus().clearNodes().toggleTaskList().run(), - isActive: (editor) => (editor ? editor.isActive('taskItem') : false), - }, - { - name: 'Bullet List', - icon: ListOrdered, - command: (editor) => editor?.chain().focus().clearNodes().toggleBulletList().run(), - isActive: (editor) => (editor ? editor.isActive('bulletList') : false), - }, - { - name: 'Numbered List', - icon: ListOrdered, - command: (editor) => editor?.chain().focus().clearNodes().toggleOrderedList().run(), - isActive: (editor) => (editor ? editor.isActive('orderedList') : false), - }, - { - name: 'Quote', - icon: TextQuote, - command: (editor) => editor?.chain().focus().clearNodes().toggleBlockquote().run(), - isActive: (editor) => (editor ? editor.isActive('blockquote') : false), - }, - { - name: 'Code', - icon: Code, - command: (editor) => editor?.chain().focus().clearNodes().toggleCodeBlock().run(), - isActive: (editor) => (editor ? editor.isActive('codeBlock') : false), - }, -]; -interface NodeSelectorProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const NodeSelector = ({ open, onOpenChange }: NodeSelectorProps) => { - const { editor } = useEditor(); - if (!editor) return null; - const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? { - name: 'Multiple', - }; - - return ( - - - - - - {items.map((item) => ( - { - item.command(editor); - onOpenChange(false); - }} - className="hover:bg-accent flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm" - > -
-
- -
- {item.name} -
- {activeItem.name === item.name && } -
- ))} -
-
- ); -}; diff --git a/apps/mail/components/create/slash-command.tsx b/apps/mail/components/create/slash-command.tsx index fb51b52803..713449d2c2 100644 --- a/apps/mail/components/create/slash-command.tsx +++ b/apps/mail/components/create/slash-command.tsx @@ -1,15 +1,11 @@ import { - CheckSquare, - Code, Heading1, Heading2, Heading3, - ImageIcon, List, ListOrdered, Text, - TextQuote, -} from 'lucide-react'; + } from 'lucide-react'; import { createSuggestionItems } from 'novel'; export const suggestionItems = createSuggestionItems([ diff --git a/apps/mail/components/create/toolbar.tsx b/apps/mail/components/create/toolbar.tsx index 1c090f6661..1730bfce36 100644 --- a/apps/mail/components/create/toolbar.tsx +++ b/apps/mail/components/create/toolbar.tsx @@ -3,8 +3,6 @@ import { Italic, Strikethrough, Underline, - Code, - Link as LinkIcon, List, ListOrdered, Heading1, diff --git a/apps/mail/components/create/uploaded-file-icon.tsx b/apps/mail/components/create/uploaded-file-icon.tsx index db54360f4d..5e79f871f1 100644 --- a/apps/mail/components/create/uploaded-file-icon.tsx +++ b/apps/mail/components/create/uploaded-file-icon.tsx @@ -1,5 +1,5 @@ import { Button } from '@/components/ui/button'; -import { FileIcon, X } from 'lucide-react'; +import { X } from 'lucide-react'; import React from 'react'; const getLogo = (mimetype: string): string => { diff --git a/apps/mail/components/home/HomeContent.tsx b/apps/mail/components/home/HomeContent.tsx index 2f05e1a30c..49741635b3 100644 --- a/apps/mail/components/home/HomeContent.tsx +++ b/apps/mail/components/home/HomeContent.tsx @@ -1,11 +1,7 @@ import { - ArrowRight, ChevronDown, CurvedArrow, - Discord, GitHub, - LinkedIn, - Twitter, Plus, Cube, MediumStack, @@ -30,15 +26,15 @@ import { Expand, } from '../icons/icons'; import { PixelatedBackground, PixelatedLeft, PixelatedRight } from '@/components/home/pixelated-bg'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Tabs, TabsContent } from '@/components/ui/tabs'; import { signIn, useSession } from '@/lib/auth-client'; import { Link, useNavigate } from 'react-router'; import { Button } from '@/components/ui/button'; import { Balancer } from 'react-wrap-balancer'; import { Navigation } from '../navigation'; import { useTheme } from 'next-themes'; -import { use, useEffect } from 'react'; import { motion } from 'motion/react'; +import { useEffect } from 'react'; import { toast } from 'sonner'; import Footer from './footer'; import React from 'react'; @@ -1221,9 +1217,9 @@ export default function HomeContent() { {/* First row */}
- {firstRowQueries.map((query, i) => ( + {firstRowQueries.map((query) => (
@@ -1241,9 +1237,9 @@ export default function HomeContent() { {/* Second row */}
- {secondRowQueries.map((query, i) => ( + {secondRowQueries.map((query) => (
@@ -1334,24 +1330,3 @@ export default function HomeContent() {
); } -const CustomTabGlow = ({ glowStyle }: { glowStyle: { left: number; width: number } }) => { - return ( -
-
-
-
- ); -}; diff --git a/apps/mail/components/home/footer.tsx b/apps/mail/components/home/footer.tsx index 5efc310ec7..1b056250b4 100644 --- a/apps/mail/components/home/footer.tsx +++ b/apps/mail/components/home/footer.tsx @@ -1,5 +1,5 @@ import { LinkedIn, Twitter, Discord } from '../icons/icons'; -import { motion, useInView } from 'motion/react'; +import { motion } from 'motion/react'; import { Button } from '../ui/button'; import { Link } from 'react-router'; import { useRef } from 'react'; @@ -121,26 +121,26 @@ export default function Footer() {
-
+
@@ -152,6 +152,7 @@ export default function Footer() { href="https://x.com/nizzyabi/status/1918064165530550286" className="w-full" target="_blank" + rel="noreferrer" >
Chat with Zero @@ -161,6 +162,7 @@ export default function Footer() { href="https://x.com/nizzyabi/status/1918051282881069229" className="w-full" target="_blank" + rel="noreferrer" >
Zero AI @@ -170,6 +172,7 @@ export default function Footer() { href="https://x.com/nizzyabi/status/1919292505260249486" className="w-full" target="_blank" + rel="noreferrer" >
Shortcuts @@ -192,14 +195,18 @@ export default function Footer() { About
- +
Github
-
diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx index 9774098ba0..725e8423d9 100644 --- a/apps/mail/components/icons/icons.tsx +++ b/apps/mail/components/icons/icons.tsx @@ -308,7 +308,7 @@ export const Inbox = ({ className }: { className?: string }) => ( ); -export const PaperPlane = ({ className }: { className?: string }) => ( +export const PaperPlane = () => ( {trigger}} - {editingLabel ? m['common.labels.editLabel']() : m['common.mail.createNewLabel']()} + + {editingLabel ? m['common.labels.editLabel']() : m['common.mail.createNewLabel']()} +
{m['common.labels.color']()}
- {LABEL_COLORS.map((color, index) => ( + {LABEL_COLORS.map((color) => ( - ); - }, -); +>(({ value, className, isSelectable = true, isSelect, fileIcon, children, ...props }, ref) => { + const { direction, selectedId, selectItem } = useTree(); + const isSelected = isSelect ?? selectedId === value; + return ( + + ); +}); File.displayName = 'File'; @@ -368,7 +357,7 @@ const CollapseButton = forwardRef< elements: TreeViewElement[]; expandAll?: boolean; } & React.HTMLAttributes ->(({ className, elements, expandAll = false, children, ...props }, ref) => { +>(({ elements, expandAll = false, children, ...props }, ref) => { const { expandedItems, setExpandedItems } = useTree(); const expendAllTree = useCallback((elements: TreeViewElement[]) => { diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index 85c5a71516..25fd702890 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -23,7 +23,6 @@ import { HardDriveDownload, Loader2, CopyIcon, - SearchIcon, } from 'lucide-react'; import { DropdownMenu, @@ -31,30 +30,27 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu'; -import { cn, getEmailLogo, formatDate, formatTime, shouldShowSeparateTime } from '@/lib/utils'; +import { cn, formatDate, formatTime, shouldShowSeparateTime } from '@/lib/utils'; import { Dialog, DialogTitle, DialogHeader, DialogContent } from '../ui/dialog'; import { memo, useEffect, useMemo, useState, useRef, useCallback } from 'react'; -import { BimiAvatar, getFirstLetterCharacter } from '../ui/bimi-avatar'; 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 { useMutation, useQuery } from '@tanstack/react-query'; 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 { useSummary } from '@/hooks/use-summary'; import { TextShimmer } from '../ui/text-shimmer'; import { useThread } from '@/hooks/use-threads'; +import { BimiAvatar } from '../ui/bimi-avatar'; import { RenderLabels } from './render-labels'; -import { cleanHtml } from '@/lib/email-utils'; import { MailContent } from './mail-content'; import { m } from '@/paraglide/messages'; import { useParams } from 'react-router'; import { FileText } from 'lucide-react'; -import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Badge } from '../ui/badge'; import { format } from 'date-fns'; @@ -68,140 +64,6 @@ function escapeHtml(text: string): string { return div.innerHTML; } -function TextSelectionPopover({ - children, - onSearch, -}: { - children: React.ReactNode; - onSearch: (query: string) => void; -}) { - const [selectionCoords, setSelectionCoords] = useState<{ x: number; y: number } | null>(null); - const [selectedText, setSelectedText] = useState(''); - const popoverTriggerRef = useRef(null); - const popoverRef = useRef(null); - - const handleSelectionChange = useCallback((e: MouseEvent) => { - if (window.getSelection()?.toString().trim()) { - e.preventDefault(); - e.stopPropagation(); - } - - const selection = window.getSelection(); - if (!selection || selection.isCollapsed) { - setSelectionCoords(null); - setSelectedText(''); - return; - } - - try { - const range = selection.getRangeAt(0); - const rect = range.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2 + window.scrollX - window.innerWidth / 2; - const y = rect.top + window.scrollY; - - setSelectionCoords({ x: centerX, y }); - setSelectedText(selection.toString().trim()); - } catch (error) { - console.error('Error handling text selection:', error); - setSelectionCoords(null); - setSelectedText(''); - } - }, []); - - // const handleClickOutside = useCallback((event: MouseEvent) => { - // if ( - // popoverRef.current && - // !popoverRef.current.contains(event.target as Node) && - // !popoverTriggerRef.current?.contains(event.target as Node) - // ) { - // setSelectionCoords(null); - // setSelectedText(''); - // } - // }, []); - - useEffect(() => { - document.addEventListener('mouseup', handleSelectionChange); - // document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - setSelectionCoords(null); - setSelectedText(''); - } - }); - - return () => { - document.removeEventListener('mouseup', handleSelectionChange); - // document.removeEventListener('mousedown', handleClickOutside); - }; - }, [handleSelectionChange]); - - return ( -
- {children} - {selectionCoords && ( -
- (open ? undefined : setSelectedText(''))} - > - - - -
-
- - -
- )} -
- ); -} - // Add formatFileSize utility function const formatFileSize = (size: number) => { const sizeInMB = (size / (1024 * 1024)).toFixed(2); @@ -404,7 +266,7 @@ const ThreadAttachments = ({ attachments }: { attachments: Attachment[] }) => { try { // Convert base64 to blob const byteCharacters = atob(attachment.body); - const byteNumbers = new Array(byteCharacters.length); + const byteNumbers: number[] = Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } @@ -519,7 +381,7 @@ const ActionButton = ({ onClick, icon, text, shortcut }: ActionButtonProps) => { const downloadAttachment = (attachment: { body: string; mimeType: string; filename: string }) => { try { const byteCharacters = atob(attachment.body); - const byteNumbers = new Array(byteCharacters.length); + const byteNumbers: number[] = Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } @@ -551,7 +413,7 @@ const handleDownloadAllAttachments = attachments.forEach((attachment) => { try { const byteCharacters = atob(attachment.body); - const byteNumbers = new Array(byteCharacters.length); + const byteNumbers: number[] = Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } @@ -596,7 +458,7 @@ const handleDownloadAllAttachments = const openAttachment = (attachment: { body: string; mimeType: string; filename: string }) => { try { const byteCharacters = atob(attachment.body); - const byteNumbers = new Array(byteCharacters.length); + const byteNumbers: number[] = Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } @@ -627,7 +489,6 @@ const openAttachment = (attachment: { body: string; mimeType: string; filename: const MoreAboutPerson = ({ person, - extra, open, onOpenChange, }: { @@ -776,7 +637,7 @@ const MoreAboutQuery = ({ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: Props) => { const [isCollapsed, setIsCollapsed] = useState(false); - const { data: threadData } = useThread(emailData.threadId); + const { data: threadData } = useThread(emailData.threadId ?? null); // const [unsubscribed, setUnsubscribed] = useState(false); // const [isUnsubscribing, setIsUnsubscribing] = useState(false); const [preventCollapse, setPreventCollapse] = useState(false); @@ -1276,7 +1137,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: } } catch (error) { console.error('Error printing email:', error); - alert('Failed to print email. Please try again.'); + toast.error('Failed to print email. Please try again.'); } }; @@ -1414,8 +1275,8 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: - {people.slice(2).map((person, index) => ( -
{renderPerson(person)}
+ {people.slice(2).map((person) => ( +
{renderPerson(person)}
))}
@@ -1779,8 +1640,8 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: {/* mail attachments */} {emailData?.attachments && emailData?.attachments.length > 0 ? (
- {emailData?.attachments.map((attachment, index) => ( -
+ {emailData?.attachments.map((attachment) => ( +
)} @@ -1038,14 +1019,6 @@ export const MailLabels = memo( }, ); -function getNormalizedLabelKey(label: string) { - return label.toLowerCase().replace(/^category_/i, ''); -} - -function capitalize(str: string) { - return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase(); -} - function getLabelIcon(label: string) { const normalizedLabel = label.toLowerCase().replace(/^category_/i, ''); diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 3b867dc2e0..0f275cedb9 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -1,18 +1,3 @@ -import { - Archive2, - Bell, - CurvedArrow, - Eye, - Lightning, - Mail, - ScanEye, - Star2, - Tag, - Trash, - User, - X, - Search, -} from '../icons/icons'; import { Dialog, DialogContent, @@ -28,21 +13,21 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from '../ui/dropdown-menu'; +import { Bell, Lightning, Mail, ScanEye, Tag, Trash, User, X, Search } from '../icons/icons'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; import { useCategorySettings, useDefaultCategoryId } from '@/hooks/use-categories'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useActiveConnection, useConnections } from '@/hooks/use-connections'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useCommandPalette } from '../context/command-palette-context'; -import { Check, ChevronDown, Command, RefreshCcw } from 'lucide-react'; -import { useOptimisticActions } from '@/hooks/use-optimistic-actions'; +import { Check, ChevronDown, RefreshCcw } from 'lucide-react'; + import { ThreadDisplay } from '@/components/mail/thread-display'; -import { trpcClient, useTRPC } from '@/providers/query-provider'; -import { backgroundQueueAtom } from '@/store/backgroundQueue'; -import { handleUnsubscribe } from '@/lib/email-utils.client'; +import { useActiveConnection } from '@/hooks/use-connections'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useTRPC } from '@/providers/query-provider'; + import { useMediaQuery } from '../../hooks/use-media-query'; -import { useSearchValue } from '@/hooks/use-search-value'; + import useSearchLabels from '@/hooks/use-labels-search'; import * as CustomIcons from '@/components/icons/icons'; import { isMac } from '@/lib/hotkeys/use-hotkey-utils'; @@ -57,7 +42,6 @@ import { Textarea } from '@/components/ui/textarea'; import { useBrainState } from '@/hooks/use-summary'; import { clearBulkSelectionAtom } from './use-mail'; import AISidebar from '@/components/ui/ai-sidebar'; -import { cleanSearchValue, cn } from '@/lib/utils'; import { useThreads } from '@/hooks/use-threads'; import { useBilling } from '@/hooks/use-billing'; import AIToggleButton from '../ai-toggle-button'; @@ -69,8 +53,9 @@ import { useSession } from '@/lib/auth-client'; import { ScrollArea } from '../ui/scroll-area'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; -import { useStats } from '@/hooks/use-stats'; -import type { IConnection } from '@/types'; + +import { cn } from '@/lib/utils'; + import { m } from '@/paraglide/messages'; import { useQueryState } from 'nuqs'; import { useAtom } from 'jotai'; @@ -423,7 +408,7 @@ export function MailLayout() { const [{ isFetching, refetch: refetchThreads }] = useThreads(); const isDesktop = useMediaQuery('(min-width: 768px)'); - const [threadId, setThreadId] = useQueryState('threadId'); + const [threadId] = useQueryState('threadId'); useEffect(() => { if (threadId) { @@ -451,8 +436,6 @@ export function MailLayout() { disableScope('mail-list'); }, [disableScope]); - const [, setActiveReplyId] = useQueryState('activeReplyId'); - // Add mailto protocol handler registration useEffect(() => { // Register as a mailto protocol handler if browser supports it @@ -473,7 +456,7 @@ export function MailLayout() { }, []); const defaultCategoryId = useDefaultCategoryId(); - const [category, setCategory] = useQueryState('category', { defaultValue: defaultCategoryId }); + const [category] = useQueryState('category', { defaultValue: defaultCategoryId }); return ( @@ -652,186 +635,6 @@ export function MailLayout() { ); } -function BulkSelectActions() { - const [isLoading, setIsLoading] = useState(false); - const [isUnsub, setIsUnsub] = useState(false); - const [mail, setMail] = useMail(); - const params = useParams<{ folder: string }>(); - const folder = params?.folder ?? 'inbox'; - const [{ refetch: refetchThreads }] = useThreads(); - const { refetch: refetchStats } = useStats(); - const { - optimisticMarkAsRead, - optimisticToggleStar, - optimisticMoveThreadsTo, - optimisticDeleteThreads, - } = useOptimisticActions(); - - const handleMassUnsubscribe = async () => { - setIsLoading(true); - toast.promise( - Promise.all( - mail.bulkSelected.filter(Boolean).map(async (bulkSelected) => { - await new Promise((resolve) => setTimeout(resolve, 499)); - const emailData = await trpcClient.mail.get.query({ id: bulkSelected }); - if (emailData) { - const firstEmail = emailData.latest; - if (firstEmail) - return handleUnsubscribe({ emailData: firstEmail }).catch((e) => { - toast.error(e.message ?? 'Unknown error while unsubscribing'); - }); - } - }), - ).then(async () => { - setIsUnsub(false); - setIsLoading(false); - await refetchThreads(); - await refetchStats(); - setMail({ ...mail, bulkSelected: [] }); - }), - { - loading: 'Unsubscribing...', - success: 'All done! you will no longer receive emails from these mailing lists.', - error: 'Something went wrong!', - }, - ); - }; - - return ( -
- - - - - - - {m['common.mail.starAll']()} - - - - - - - {m['common.mail.archive']()} - - - - - - - - - - {m['common.mail.unSubscribeFromAll']()} - - - { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - handleMassUnsubscribe(); - } - }} - > - - Mass Unsubscribe - - We will remove you from all of the mailing lists in the selected threads. If your - action is required to unsubscribe from certain threads, you will be notified. - - - - - - - - - - - - - - - {m['common.mail.moveToBin']()} - -
- ); -} - export const Categories = () => { const defaultCategoryIdInner = useDefaultCategoryId(); const categorySettings = useCategorySettings(); @@ -936,30 +739,6 @@ export const Categories = () => { return categories; }; - -type CategoryType = ReturnType[0]; - -function getCategoryColor(categoryId: string): string { - switch (categoryId.toLowerCase()) { - case 'primary': - return 'bg-[#006FFE]'; - case 'all mail': - return 'bg-[#006FFE]'; - case 'important': - return 'bg-[#F59E0D]'; - case 'promotions': - return 'bg-[#F43F5E]'; - case 'personal': - return 'bg-[#39ae4a]'; - case 'updates': - return 'bg-[#8B5CF6]'; - case 'unread': - return 'bg-[#FF4800]'; - default: - return 'bg-base-primary-500'; - } -} - interface CategoryDropdownProps { isMultiSelectMode?: boolean; } @@ -1031,363 +810,3 @@ function CategoryDropdown({ isMultiSelectMode }: CategoryDropdownProps) { ); } - -function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) { - const [mail, setMail] = useMail(); - const { setLabels } = useSearchLabels(); - const categories = Categories(); - const params = useParams<{ folder: string }>(); - const folder = params?.folder ?? 'inbox'; - const defaultCategoryIdInner = useDefaultCategoryId(); - const [category, setCategory] = useQueryState('category', { - defaultValue: defaultCategoryIdInner, - }); - const containerRef = useRef(null); - const activeTabElementRef = useRef(null); - const overlayContainerRef = useRef(null); - const [textSize, setTextSize] = useState<'normal' | 'small' | 'xs' | 'hidden'>('normal'); - const isDesktop = useMediaQuery('(min-width: 1024px)'); - - // const categories - - if (folder !== 'inbox') return
; - - // useEffect(() => { - // const checkTextSize = () => { - // const container = containerRef.current; - // if (!container) return; - - // const containerWidth = container.offsetWidth; - // const selectedCategory = categories.find((cat) => cat.id === category); - - // // Calculate approximate widths needed for different text sizes - // const baseIconWidth = (categories.length - 1) * 40; // unselected icons + gaps - // const selectedTextLength = selectedCategory ? selectedCategory.name.length : 10; - - // // Estimate width needed for different text sizes - // const normalTextWidth = selectedTextLength * 8 + 60; // normal text - // const smallTextWidth = selectedTextLength * 7 + 50; // smaller text - // const xsTextWidth = selectedTextLength * 6 + 40; // extra small text - // const minIconWidth = 40; // minimum width for icon-only selected button - - // const totalNormal = baseIconWidth + normalTextWidth; - // const totalSmall = baseIconWidth + smallTextWidth; - // const totalXs = baseIconWidth + xsTextWidth; - // const totalIconOnly = baseIconWidth + minIconWidth; - - // if (containerWidth >= totalNormal) { - // setTextSize('normal'); - // } else if (containerWidth >= totalSmall) { - // setTextSize('small'); - // } else if (containerWidth >= totalXs) { - // setTextSize('xs'); - // } else if (containerWidth >= totalIconOnly) { - // setTextSize('hidden'); // Hide text but keep button wide - // } else { - // setTextSize('hidden'); // Hide text in very tight spaces - // } - // }; - - // checkTextSize(); - - // // Use ResizeObserver to handle container size changes - // const resizeObserver = new ResizeObserver(() => { - // checkTextSize(); - // }); - - // if (containerRef.current) { - // resizeObserver.observe(containerRef.current); - // } - - // return () => { - // resizeObserver.disconnect(); - // }; - // }, [category, categories]); - - const renderCategoryButton = (cat: CategoryType, isOverlay = false, idx: number) => { - const isSelected = cat.id === (category || 'Primary'); - const bgColor = getCategoryColor(cat.id); - - // Determine text classes based on current text size - const getTextClasses = () => { - switch (textSize) { - case 'normal': - return 'text-sm'; - case 'small': - return 'text-xs'; - case 'xs': - return 'text-[10px]'; - case 'hidden': - return 'text-sm'; // Doesn't matter since text is hidden - default: - return 'text-sm'; - } - }; - - // Determine padding based on text size - const getPaddingClasses = () => { - switch (textSize) { - case 'normal': - return 'px-3'; - case 'small': - return 'px-2.5'; - case 'xs': - return 'px-2'; - case 'hidden': - return 'px-2'; // Just enough padding for the icon - default: - return 'px-3'; - } - }; - - const showText = textSize !== 'hidden'; - - const button = ( - - ); - - if (!isDesktop) { - return React.cloneElement(button, { key: cat.id }); - } - - return ( - - {button} - - {cat.name} - - {idx + 1} - - - - ); - }; - - // Update clip path when category changes - // useEffect(() => { - // const container = overlayContainerRef.current; - // const activeTabElement = activeTabElementRef.current; - - // if (category && container && activeTabElement) { - // setMail({ ...mail, bulkSelected: [] }); - // const { offsetLeft, offsetWidth } = activeTabElement; - // const clipLeft = Math.max(0, offsetLeft - 2); - // const clipRight = Math.min(container.offsetWidth, offsetLeft + offsetWidth + 2); - // const containerWidth = container.offsetWidth; - - // if (containerWidth) { - // container.style.clipPath = `inset(0 ${Number(100 - (clipRight / containerWidth) * 100).toFixed(2)}% 0 ${Number((clipLeft / containerWidth) * 100).toFixed(2)}%)`; - // } - // } - // }, [category, textSize]); // Changed from showText to textSize - - if (isMultiSelectMode) { - return ; - } - - return ( -
-
- {categories.map((cat, idx) => renderCategoryButton(cat, false, idx))} -
- -
-
- {categories.map((cat, idx) => renderCategoryButton(cat, true, idx))} -
-
-
- ); -} - -function MailCategoryTabs({ - iconsOnly = false, - onCategoryChange, - initialCategory, -}: { - iconsOnly?: boolean; - onCategoryChange?: (category: string) => void; - initialCategory?: string; -}) { - const [, setSearchValue] = useSearchValue(); - const categories = Categories(); - - // Initialize with just the initialCategory or "Primary" - const [activeCategory, setActiveCategory] = useState(initialCategory || 'Primary'); - - const containerRef = useRef(null); - const activeTabElementRef = useRef(null); - - const activeTab = useMemo( - () => categories.find((cat) => cat.id === activeCategory), - [activeCategory], - ); - - // Save to localStorage when activeCategory changes - useEffect(() => { - if (onCategoryChange) { - onCategoryChange(activeCategory); - } - }, [activeCategory, onCategoryChange]); - - useEffect(() => { - if (activeTab) { - setSearchValue({ - value: activeTab.searchValue, - highlight: '', - folder: '', - }); - } - }, [activeCategory, setSearchValue]); - - // Cleanup on unmount - useEffect(() => { - return () => { - setSearchValue({ - value: '', - highlight: '', - folder: '', - }); - }; - }, [setSearchValue]); - - // Function to update clip path - const updateClipPath = useCallback(() => { - const container = containerRef.current; - const activeTabElement = activeTabElementRef.current; - - if (activeCategory && container && activeTabElement) { - const { offsetLeft, offsetWidth } = activeTabElement; - const clipLeft = Math.max(0, offsetLeft - 2); - const clipRight = Math.min(container.offsetWidth, offsetLeft + offsetWidth + 2); - const containerWidth = container.offsetWidth; - - if (containerWidth) { - container.style.clipPath = `inset(0 ${Number(100 - (clipRight / containerWidth) * 100).toFixed(2)}% 0 ${Number((clipLeft / containerWidth) * 100).toFixed(2)}%)`; - } - } - }, [activeCategory]); - - // Update clip path when active category changes - useEffect(() => { - updateClipPath(); - }, [activeCategory, updateClipPath]); - - // Update clip path when iconsOnly changes - useEffect(() => { - // Small delay to ensure DOM has updated with new sizes - const timer = setTimeout(() => { - updateClipPath(); - }, 10); - - return () => clearTimeout(timer); - }, [iconsOnly, updateClipPath]); - - // Update clip path on window resize - useEffect(() => { - const handleResize = () => { - updateClipPath(); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [updateClipPath]); - - return ( -
-
    - {categories.map((category) => ( -
  • - - - - - {iconsOnly && ( - - {category.name} - - )} - -
  • - ))} -
- -
-
    - {categories.map((category) => ( -
  • - -
  • - ))} -
-
-
- ); -} diff --git a/apps/mail/components/mail/navbar.tsx b/apps/mail/components/mail/navbar.tsx index 3f999e1c43..acb13f3b55 100644 --- a/apps/mail/components/mail/navbar.tsx +++ b/apps/mail/components/mail/navbar.tsx @@ -20,9 +20,9 @@ export function Nav({ links, isCollapsed }: NavProps) { className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2" >
diff --git a/apps/mail/components/onboarding.tsx b/apps/mail/components/onboarding.tsx index 54ae4a9b0a..ed707f73be 100644 --- a/apps/mail/components/onboarding.tsx +++ b/apps/mail/components/onboarding.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; -import { useState, useEffect, useMemo } from 'react'; import { Button } from '@/components/ui/button'; +import { useState, useEffect } from 'react'; import confetti from 'canvas-confetti'; const steps = [ @@ -84,7 +84,7 @@ export function OnboardingDialog({ (step, index) => step.video && (
{steps.map((_, index) => (
{ const [searchValue] = useSearchValue(); const { labels } = useSearchLabels(); - const labelsDebouncer = funnel( - () => queryClient.invalidateQueries({ queryKey: trpc.labels.list.queryKey() }), - { minQuietPeriodMs: DEBOUNCE_DELAY }, - ); - const threadsDebouncer = funnel( - () => queryClient.invalidateQueries({ queryKey: trpc.mail.listThreads.queryKey() }), - { minQuietPeriodMs: DEBOUNCE_DELAY }, - ); + + usePartySocket({ party: 'zero-agent', diff --git a/apps/mail/components/pricing/comparision.tsx b/apps/mail/components/pricing/comparision.tsx index a2b7dbbce9..6bf7c7e2be 100644 --- a/apps/mail/components/pricing/comparision.tsx +++ b/apps/mail/components/pricing/comparision.tsx @@ -1,4 +1,4 @@ -import { Check, Plus, PurpleThickCheck, ThickCheck } from '../icons/icons'; +import { Plus, PurpleThickCheck, ThickCheck } from '../icons/icons'; import { useSession, signIn } from '@/lib/auth-client'; import { useBilling } from '@/hooks/use-billing'; import { useNavigate } from 'react-router'; diff --git a/apps/mail/components/setup-phone.tsx b/apps/mail/components/setup-phone.tsx index 9ab85c08ac..b254d58442 100644 --- a/apps/mail/components/setup-phone.tsx +++ b/apps/mail/components/setup-phone.tsx @@ -89,6 +89,7 @@ export const SetupInboxDialog = () => { toast.error('Please enter a valid OTP'); } } catch (error) { + console.error(error); toast.error( showOtpInput ? 'Failed to verify phone number' : 'Failed to send verification code', ); diff --git a/apps/mail/components/theme/mode-toggle.tsx b/apps/mail/components/theme/mode-toggle.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/mail/components/ui/ai-sidebar.tsx b/apps/mail/components/ui/ai-sidebar.tsx index 454e6d50b7..697eee8e06 100644 --- a/apps/mail/components/ui/ai-sidebar.tsx +++ b/apps/mail/components/ui/ai-sidebar.tsx @@ -13,12 +13,12 @@ import { PromptsDialog } from './prompts-dialog'; import { Button } from '@/components/ui/button'; import { useHotkeys } from 'react-hotkeys-hook'; import { useLabels } from '@/hooks/use-labels'; -import { useSession } from '@/lib/auth-client'; + import { useAgentChat } from 'agents/ai-react'; import { X, Expand, Plus } from 'lucide-react'; import { Gauge } from '@/components/ui/gauge'; import { useParams } from 'react-router'; -import { useChat } from '@ai-sdk/react'; + import { useAgent } from 'agents/react'; import { useQueryState } from 'nuqs'; import { cn } from '@/lib/utils'; @@ -337,8 +337,6 @@ function AISidebar({ className }: AISidebarProps) { const { open, setOpen, - viewMode, - setViewMode, isFullScreen, setIsFullScreen, toggleViewMode, diff --git a/apps/mail/components/ui/app-sidebar.tsx b/apps/mail/components/ui/app-sidebar.tsx index 56ccac72b3..cdb68e79b3 100644 --- a/apps/mail/components/ui/app-sidebar.tsx +++ b/apps/mail/components/ui/app-sidebar.tsx @@ -1,37 +1,15 @@ import { Dialog, - DialogClose, DialogContent, DialogDescription, - DialogFooter, - DialogOverlay, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarHeader, - SidebarMenu, -} from '@/components/ui/sidebar'; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { SquarePenIcon, type SquarePenIconHandle } from '../icons/animated/square-pen'; -import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from './input-otp'; +import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from '@/components/ui/sidebar'; import { navigationConfig, bottomNavItems } from '@/config/navigation'; -import { useSession, authClient } from '@/lib/auth-client'; -import React, { useMemo, useRef, useState } from 'react'; -import { motion, AnimatePresence } from 'motion/react'; -import { zodResolver } from '@hookform/resolvers/zod'; +import React, { useMemo, useState } from 'react'; +import { useSession } from '@/lib/auth-client'; + import { useSidebar } from '@/components/ui/sidebar'; import { CreateEmail } from '../create/create-email'; import { PencilCompose, X } from '../icons/icons'; @@ -41,15 +19,12 @@ import { Button } from '@/components/ui/button'; import { useAIFullScreen } from './ai-sidebar'; import { useStats } from '@/hooks/use-stats'; import { useLocation } from 'react-router'; -import { useForm } from 'react-hook-form'; + import { m } from '@/paraglide/messages'; import { FOLDERS } from '@/lib/utils'; import { NavUser } from './nav-user'; import { NavMain } from './nav-main'; import { useQueryState } from 'nuqs'; -import { Input } from './input'; -import { toast } from 'sonner'; -import { z } from 'zod'; export function AppSidebar({ ...props }: React.ComponentProps) { const { isPro, isLoading } = useBilling(); @@ -66,7 +41,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { const { data: stats } = useStats(); const location = useLocation(); - const { data: session, isPending: isSessionPending } = useSession(); + const { data: session } = useSession(); const { currentSection, navItems } = useMemo(() => { // Find which section we're in based on the pathname const section = Object.entries(navigationConfig).find(([, config]) => diff --git a/apps/mail/components/ui/command-menu.tsx b/apps/mail/components/ui/command-menu.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/mail/components/ui/dialog.tsx b/apps/mail/components/ui/dialog.tsx index 014ed6bff6..945f156d60 100644 --- a/apps/mail/components/ui/dialog.tsx +++ b/apps/mail/components/ui/dialog.tsx @@ -1,5 +1,5 @@ import { Dialog as DialogPrimitive } from 'radix-ui'; -import { X } from '../icons/icons'; + import { cn } from '@/lib/utils'; import * as React from 'react'; diff --git a/apps/mail/components/ui/gauge.tsx b/apps/mail/components/ui/gauge.tsx index 219923baa5..dc2b8f1705 100644 --- a/apps/mail/components/ui/gauge.tsx +++ b/apps/mail/components/ui/gauge.tsx @@ -9,8 +9,8 @@ export const Gauge = ({ value: number; size: 'small' | 'medium' | 'large'; showValue: boolean; - color?: String; - bgcolor?: String; + color?: string; + bgcolor?: string; max?: number; }) => { const circumference = 332; //2 * Math.PI * 53; // 2 * pi * radius diff --git a/apps/mail/components/ui/nav-main.tsx b/apps/mail/components/ui/nav-main.tsx index f12ce2b650..91ee06fc9f 100644 --- a/apps/mail/components/ui/nav-main.tsx +++ b/apps/mail/components/ui/nav-main.tsx @@ -1,9 +1,9 @@ import { SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from './sidebar'; import { Collapsible, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { useActiveConnection, useConnections } from '@/hooks/use-connections'; +import { useActiveConnection, } from '@/hooks/use-connections'; import { LabelDialog } from '@/components/labels/label-dialog'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { Link, useLocation, useNavigate } from 'react-router'; +import { Link, useLocation, } from 'react-router'; import Intercom, { show } from '@intercom/messenger-js-sdk'; import { MessageSquare, OldPhone } from '../icons/icons'; import { useSidebar } from '../context/sidebar-context'; @@ -55,9 +55,9 @@ export function NavMain({ items }: NavMainProps) { const pathname = location.pathname; const searchParams = new URLSearchParams(); const [category] = useQueryState('category'); - const { data: connections } = useConnections(); - const { data: stats } = useStats(); - const { data: activeConnection } = useActiveConnection(); + + + const trpc = useTRPC(); const { data: intercomToken } = useQuery(trpc.user.getIntercomToken.queryOptions()); diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index cce4e8a97c..a8b4092b07 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -1,41 +1,35 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { HelpCircle, - LogIn, LogOut, MoonIcon, Settings, Plus, - BrainIcon, CopyCheckIcon, BadgeCheck, BanknoteIcon, } from 'lucide-react'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useActiveConnection, useConnections } from '@/hooks/use-connections'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { useLocation, useRevalidator, useSearchParams } from 'react-router'; -import { CircleCheck, Danger, OldPhone, ThreeDots } from '../icons/icons'; 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 { CircleCheck, ThreeDots } from '../icons/icons'; import { useTRPC } from '@/providers/query-provider'; import { useSidebar } from '@/components/ui/sidebar'; -import { useBrainState } from '@/hooks/use-summary'; -import { useThreads } from '@/hooks/use-threads'; import { useBilling } from '@/hooks/use-billing'; import { SunIcon } from '../icons/animated/sun'; import { clear as idbClear } from 'idb-keyval'; +import { useLocation } from 'react-router'; import { m } from '@/paraglide/messages'; import { useTheme } from 'next-themes'; import { useQueryState } from 'nuqs'; @@ -44,8 +38,8 @@ import { cn } from '@/lib/utils'; import { toast } from 'sonner'; export function NavUser() { - const { data: session, refetch: refetchSession, isPending: isSessionPending } = useSession(); - const { data, refetch: refetchConnections } = useConnections(); + const { data: session } = useSession(); + const { data } = useConnections(); const [isRendered, setIsRendered] = useState(false); const { theme, setTheme } = useTheme(); const { state } = useSidebar(); @@ -56,7 +50,6 @@ export function NavUser() { ); const { openBillingPortal, customer: billingCustomer, isPro } = useBilling(); const pathname = useLocation().pathname; - const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const { data: activeConnection, refetch: refetchActiveConnection } = useActiveConnection(); const [, setPricingDialog] = useQueryState('pricingDialog'); @@ -147,9 +140,9 @@ export function NavUser() { /> - {(activeAccount?.name || activeAccount?.email) + {(activeAccount?.name || activeAccount?.email || '') .split(' ') - .map((n) => n[0]) + .map((n: string) => n[0]) .join('') .toUpperCase() .slice(0, 2)} @@ -267,7 +260,12 @@ export function NavUser() { - +

@@ -485,7 +483,12 @@ export function NavUser() {

- +

diff --git a/apps/mail/components/ui/pricing-dialog.tsx b/apps/mail/components/ui/pricing-dialog.tsx index 69f000f225..6a0d267531 100644 --- a/apps/mail/components/ui/pricing-dialog.tsx +++ b/apps/mail/components/ui/pricing-dialog.tsx @@ -1,17 +1,16 @@ import { Dialog, DialogContent, - DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; -import { CircleCheck, PurpleThickCheck } from '@/components/icons/icons'; +import { PurpleThickCheck } from '@/components/icons/icons'; import { useBilling } from '@/hooks/use-billing'; import { PricingSwitch } from './pricing-switch'; -import { Button } from '@/components/ui/button'; -import { useState, useEffect } from 'react'; + +import { useState, } from 'react'; import { useQueryState } from 'nuqs'; -import { cn } from '@/lib/utils'; + import { Badge } from './badge'; import { toast } from 'sonner'; diff --git a/apps/mail/components/ui/recursive-folder.tsx b/apps/mail/components/ui/recursive-folder.tsx index 87713ae8d7..ce505b397b 100644 --- a/apps/mail/components/ui/recursive-folder.tsx +++ b/apps/mail/components/ui/recursive-folder.tsx @@ -1,12 +1,11 @@ -import { useActiveConnection, useConnections } from '@/hooks/use-connections'; import { LabelSidebarContextMenu } from '../context/label-sidebar-context'; -import { useSearchValue } from '@/hooks/use-search-value'; + import type { Label, Label as LabelType } from '@/types'; import { useSidebar } from '../context/sidebar-context'; import useSearchLabels from '@/hooks/use-labels-search'; import { Folder } from '../magicui/file-tree'; import { useNavigate } from 'react-router'; -import { useQueryState } from 'nuqs'; + import { useCallback } from 'react'; import * as React from 'react'; diff --git a/apps/mail/components/ui/sheet.tsx b/apps/mail/components/ui/sheet.tsx index 9de6eb744a..7bfd954c78 100644 --- a/apps/mail/components/ui/sheet.tsx +++ b/apps/mail/components/ui/sheet.tsx @@ -1,6 +1,6 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { Dialog as SheetPrimitive } from 'radix-ui'; -import { X } from 'lucide-react'; + import * as React from 'react'; import { cn } from '@/lib/utils'; diff --git a/apps/mail/components/ui/sidebar-labels.tsx b/apps/mail/components/ui/sidebar-labels.tsx index cd29b87fed..b735c0cd78 100644 --- a/apps/mail/components/ui/sidebar-labels.tsx +++ b/apps/mail/components/ui/sidebar-labels.tsx @@ -1,4 +1,4 @@ -import type { IConnection, Label as LabelType } from '@/types'; +import type { Label as LabelType } from '@/types'; import { useActiveConnection } from '@/hooks/use-connections'; import { RecursiveFolder } from './recursive-folder'; import { useStats } from '@/hooks/use-stats'; diff --git a/apps/mail/components/user/user-button.tsx b/apps/mail/components/user/user-button.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/mail/components/voice-button.tsx b/apps/mail/components/voice-button.tsx index 2f80d57c26..9e10fdbb7e 100644 --- a/apps/mail/components/voice-button.tsx +++ b/apps/mail/components/voice-button.tsx @@ -1,13 +1,11 @@ 'use client'; -import { Mic, MicOff, Volume2, VolumeX, X, Loader2, WavesIcon } from 'lucide-react'; -import { Card, CardContent } from '@/components/ui/card'; -import { AnimatePresence, motion } from 'motion/react'; +import { Mic, MicOff, Loader2, WavesIcon } from 'lucide-react'; import { useVoice } from '@/providers/voice-provider'; -import { Button } from '@/components/ui/button'; +import { motion } from 'motion/react'; + import { useSession } from '@/lib/auth-client'; import { useQueryState } from 'nuqs'; -import { cn } from '@/lib/utils'; export function VoiceButton() { const { data: session } = useSession(); diff --git a/apps/mail/config/navigation.ts b/apps/mail/config/navigation.ts index 23ed5562c0..266b9c42c3 100644 --- a/apps/mail/config/navigation.ts +++ b/apps/mail/config/navigation.ts @@ -4,11 +4,7 @@ import { ExclamationCircle, Folder, Inbox, - MessageSquare, - NotesList, - PaperPlane, SettingsGear, - Sparkles, Stars, Tabs, Users, diff --git a/apps/mail/hooks/driver/use-delete.ts b/apps/mail/hooks/driver/use-delete.ts index cc751ff36e..fb4bd9e82b 100644 --- a/apps/mail/hooks/driver/use-delete.ts +++ b/apps/mail/hooks/driver/use-delete.ts @@ -13,7 +13,7 @@ const useDelete = () => { const [mail, setMail] = useMail(); const [{ refetch: refetchThreads }] = useThreads(); const { refetch: refetchStats } = useStats(); - const { addToQueue, deleteFromQueue } = useBackgroundQueue(); + const { addToQueue, } = useBackgroundQueue(); const trpc = useTRPC(); const { mutateAsync: deleteThread } = useMutation(trpc.mail.delete.mutationOptions()); diff --git a/apps/mail/hooks/use-compose-editor.ts b/apps/mail/hooks/use-compose-editor.ts index 7852d75e4a..16e646229a 100644 --- a/apps/mail/hooks/use-compose-editor.ts +++ b/apps/mail/hooks/use-compose-editor.ts @@ -11,46 +11,6 @@ import { Markdown } from 'tiptap-markdown'; import { isObjectType } from 'remeda'; import { cn } from '@/lib/utils'; -const PreventNavigateOnDragOver = (handleFiles: (files: File[]) => void | Promise) => { - return Extension.create({ - name: 'preventNavigateOnDrop', - addProseMirrorPlugins: () => { - return [ - new Plugin({ - key: new PluginKey('preventNavigateOnDrop'), - props: { - handleDOMEvents: { - dragover: (_view, event) => { - if (event.dataTransfer?.types?.includes('Files')) { - event.preventDefault(); - - return true; - } - - return false; - }, - drop: (_view, event) => { - const fileList = event.dataTransfer?.files; - if (fileList && fileList.length) { - event.preventDefault(); - event.stopPropagation(); - - const files = Array.from(fileList); - void handleFiles(files); - - return true; - } - - return false; - }, - }, - }, - }), - ]; - }, - }); -}; - const CustomModEnter = (onModEnter: KeyboardShortcutCommand) => { return Extension.create({ name: 'handleModEnter', @@ -150,7 +110,6 @@ const useComposeEditor = ({ isReadOnly, placeholder, onChange, - onAttachmentsChange, onLengthChange, onBlur, onFocus, diff --git a/apps/mail/hooks/use-mail-navigation.ts b/apps/mail/hooks/use-mail-navigation.ts index 2b4326527c..de1eee58d4 100644 --- a/apps/mail/hooks/use-mail-navigation.ts +++ b/apps/mail/hooks/use-mail-navigation.ts @@ -1,5 +1,5 @@ -import { useCommandPalette } from '@/components/context/command-palette-context'; -import { useCallback, useEffect, useState, useRef } from 'react'; + +import { useCallback, useEffect, useRef } from 'react'; import { useOptimisticActions } from './use-optimistic-actions'; import { useMail } from '@/components/mail/use-mail'; import { useHotkeys } from 'react-hotkeys-hook'; diff --git a/apps/mail/hooks/use-notes.tsx b/apps/mail/hooks/use-notes.tsx index c67666c097..ae9f3d0269 100644 --- a/apps/mail/hooks/use-notes.tsx +++ b/apps/mail/hooks/use-notes.tsx @@ -1,12 +1,12 @@ import { useActiveConnection } from './use-connections'; import { useTRPC } from '@/providers/query-provider'; import { useQuery } from '@tanstack/react-query'; -import { useSession } from '@/lib/auth-client'; + import { m } from '@/paraglide/messages'; import type { Note } from '@/types'; export const useThreadNotes = (threadId: string) => { - const { data: session } = useSession(); + const trpc = useTRPC(); const { data: activeConnection } = useActiveConnection(); diff --git a/apps/mail/hooks/use-optimistic-actions.ts b/apps/mail/hooks/use-optimistic-actions.ts index 281c6a91b5..f61806627d 100644 --- a/apps/mail/hooks/use-optimistic-actions.ts +++ b/apps/mail/hooks/use-optimistic-actions.ts @@ -1,14 +1,14 @@ import { addOptimisticActionAtom, removeOptimisticActionAtom } from '@/store/optimistic-updates'; import { optimisticActionsManager, type PendingAction } from '@/lib/optimistic-actions-manager'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { focusedIndexAtom } from '@/hooks/use-mail-navigation'; + import { backgroundQueueAtom } from '@/store/backgroundQueue'; import type { ThreadDestination } from '@/lib/thread-actions'; import { useTRPC } from '@/providers/query-provider'; import { useMail } from '@/components/mail/use-mail'; import { moveThreadsTo } from '@/lib/thread-actions'; -import { useCallback, useRef } from 'react'; import { m } from '@/paraglide/messages'; +import { useCallback } from 'react'; import { useQueryState } from 'nuqs'; import posthog from 'posthog-js'; import { useAtom } from 'jotai'; @@ -39,15 +39,13 @@ export function useOptimisticActions() { const [, removeOptimisticAction] = useAtom(removeOptimisticActionAtom); const [threadId, setThreadId] = useQueryState('threadId'); const [, setActiveReplyId] = useQueryState('activeReplyId'); - const [, setFocusedIndex] = useAtom(focusedIndexAtom); const [mail, setMail] = useMail(); const { mutateAsync: markAsRead } = useMutation(trpc.mail.markAsRead.mutationOptions()); const { mutateAsync: markAsUnread } = useMutation(trpc.mail.markAsUnread.mutationOptions()); - const { mutateAsync: markAsImportant } = useMutation(trpc.mail.markAsImportant.mutationOptions()); + const { mutateAsync: toggleStar } = useMutation(trpc.mail.toggleStar.mutationOptions()); const { mutateAsync: toggleImportant } = useMutation(trpc.mail.toggleImportant.mutationOptions()); - const { mutateAsync: bulkArchive } = useMutation(trpc.mail.bulkArchive.mutationOptions()); - const { mutateAsync: bulkStar } = useMutation(trpc.mail.bulkStar.mutationOptions()); + const { mutateAsync: bulkDeleteThread } = useMutation(trpc.mail.bulkDelete.mutationOptions()); const { mutateAsync: modifyLabels } = useMutation(trpc.mail.modifyLabels.mutationOptions()); diff --git a/apps/mail/hooks/use-threads.ts b/apps/mail/hooks/use-threads.ts index 159c1c8de7..2968d1d7fe 100644 --- a/apps/mail/hooks/use-threads.ts +++ b/apps/mail/hooks/use-threads.ts @@ -1,6 +1,6 @@ -import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { backgroundQueueAtom, isThreadInBackgroundQueueAtom } from '@/store/backgroundQueue'; import type { IGetThreadResponse } from '../../server/src/lib/driver/types'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { useSearchValue } from '@/hooks/use-search-value'; import { useTRPC } from '@/providers/query-provider'; import useSearchLabels from './use-labels-search'; @@ -16,7 +16,7 @@ export const useThreads = () => { const [backgroundQueue] = useAtom(backgroundQueueAtom); const isInQueue = useAtomValue(isThreadInBackgroundQueueAtom); const trpc = useTRPC(); - const { labels, setLabels } = useSearchLabels(); + const { labels } = useSearchLabels(); const threadsQuery = useInfiniteQuery( trpc.mail.listThreads.infiniteQueryOptions( @@ -60,7 +60,7 @@ export const useThreads = () => { return [threadsQuery, threads, isReachingEnd, loadMore] as const; }; -export const useThread = (threadId: string | null, historyId?: string | null) => { +export const useThread = (threadId: string | null) => { const { data: session } = useSession(); const [_threadId] = useQueryState('threadId'); const id = threadId ? threadId : _threadId; diff --git a/apps/mail/lib/constants.tsx b/apps/mail/lib/constants.tsx index 981cc76603..36c23497bd 100644 --- a/apps/mail/lib/constants.tsx +++ b/apps/mail/lib/constants.tsx @@ -1,4 +1,4 @@ -import { GmailColor, OutlookColor } from '../components/icons/icons'; +import { GmailColor, } from '../components/icons/icons'; export const I18N_LOCALE_COOKIE_NAME = 'i18n:locale'; export const SIDEBAR_COOKIE_NAME = 'sidebar:state'; diff --git a/apps/mail/lib/elevenlabs-tools.ts b/apps/mail/lib/elevenlabs-tools.ts index 92e74da671..f375095825 100644 --- a/apps/mail/lib/elevenlabs-tools.ts +++ b/apps/mail/lib/elevenlabs-tools.ts @@ -1,7 +1,4 @@ import { trpcClient } from '@/providers/query-provider'; -import { perplexity } from '@ai-sdk/perplexity'; -import { generateText, tool } from 'ai'; - const getCurrentThreadId = () => { if (typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search); @@ -130,7 +127,7 @@ export const toolExecutors = { return { success: false, error: error.message }; } }, - deleteEmail: async (params: any) => { + deleteEmail: async () => { const threadId = getCurrentThreadId(); if (!threadId) { return { @@ -338,11 +335,12 @@ export const toolExecutors = { senderName: senderName, messageCount: messageCount, hasUnread: thread.hasUnread, - summary: 'this is a fake summary', + summary: text, message: `Successfully summarized email thread: ${threadId}`, }, }; } catch (error) { + console.error(error); return { success: false, error: 'Failed to fetch email for summarization', diff --git a/apps/mail/lib/email-utils.client.tsx b/apps/mail/lib/email-utils.client.tsx index 64ca336f9c..72608b13a1 100644 --- a/apps/mail/lib/email-utils.client.tsx +++ b/apps/mail/lib/email-utils.client.tsx @@ -75,7 +75,7 @@ export const highlightText = (text: string, highlight: string) => { return parts.map((part, i) => { return i % 2 === 1 ? ( {part} diff --git a/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx b/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx index b1e0a14adb..0b6f36ad82 100644 --- a/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx +++ b/apps/mail/lib/hotkeys/mail-list-hotkeys.tsx @@ -8,14 +8,14 @@ import { Categories } from '@/components/mail/mail'; import { useShortcuts } from './use-hotkey-utils'; import { useThreads } from '@/hooks/use-threads'; import { cleanSearchValue } from '@/lib/utils'; +import { m } from '@/paraglide/messages'; import { useQueryState } from 'nuqs'; import { toast } from 'sonner'; -import { m } from '@/paraglide/messages'; export function MailListHotkeys() { const scope = 'mail-list'; const [mail, setMail] = useMail(); - const [{}, items] = useThreads(); + const [, items] = useThreads(); const hoveredEmailId = useRef(null); const categories = Categories(); const [, setCategory] = useQueryState('category'); diff --git a/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx b/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx index cdcc68efcc..746bcdf8dd 100644 --- a/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx +++ b/apps/mail/lib/hotkeys/thread-display-hotkeys.tsx @@ -1,5 +1,5 @@ import { mailNavigationCommandAtom } from '@/hooks/use-mail-navigation'; -import { useThread, useThreads } from '@/hooks/use-threads'; +import { useThread, } from '@/hooks/use-threads'; import { keyboardShortcuts } from '@/config/shortcuts'; import useMoveTo from '@/hooks/driver/use-move-to'; import useDelete from '@/hooks/driver/use-delete'; diff --git a/apps/mail/lib/hotkeys/use-hotkey-utils.ts b/apps/mail/lib/hotkeys/use-hotkey-utils.ts index 6cc01a94a1..e3d059b9f0 100644 --- a/apps/mail/lib/hotkeys/use-hotkey-utils.ts +++ b/apps/mail/lib/hotkeys/use-hotkey-utils.ts @@ -3,7 +3,7 @@ import { type Shortcut, keyboardShortcuts } from '@/config/shortcuts'; import { useHotkeys } from 'react-hotkeys-hook'; import { useCallback, useMemo } from 'react'; -export const useShortcutCache = (userId?: string) => { +export const useShortcutCache = () => { // const { data: shortcuts, mutate } = useSWR( // userId ? `/hotkeys/${userId}` : null, // () => axios.get('/api/v1/shortcuts').then((res) => res.data), @@ -97,8 +97,11 @@ const dvorakToQwerty: Record = { }; const qwertyToDvorak: Record = Object.entries(dvorakToQwerty).reduce( - (acc, [dvorak, qwerty]) => ({ ...acc, [qwerty]: dvorak }), - {}, + (acc, [dvorak, qwerty]) => { + acc[qwerty] = dvorak; + return acc; + }, + {} as Record, ); export const formatKeys = (keys: string[] | undefined): string => { diff --git a/apps/mail/lib/redis.ts b/apps/mail/lib/redis.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/mail/lib/timezones.ts b/apps/mail/lib/timezones.ts index 71f02ffa88..1a33423e1f 100644 --- a/apps/mail/lib/timezones.ts +++ b/apps/mail/lib/timezones.ts @@ -4,6 +4,7 @@ export const isValidTimezone = (timezone: string) => { try { return Intl.supportedValuesOf('timeZone').includes(timezone); } catch (error) { + console.error(error); return false; } }; diff --git a/apps/mail/lib/utils.ts b/apps/mail/lib/utils.ts index 9d2494ebc6..4bcfb479f0 100644 --- a/apps/mail/lib/utils.ts +++ b/apps/mail/lib/utils.ts @@ -1,4 +1,4 @@ -import { format, isToday, isThisMonth, differenceInCalendarMonths } from 'date-fns'; +import { isToday, isThisMonth, differenceInCalendarMonths } from 'date-fns'; import { getBrowserTimezone } from './timezones'; import { formatInTimeZone } from 'date-fns-tz'; import { MAX_URL_LENGTH } from './constants'; @@ -349,7 +349,6 @@ export const constructReplyBody = ( originalDate: string, originalSender: Sender | undefined, otherRecipients: Sender[], - quotedMessage?: string, ) => { const senderName = originalSender?.name || originalSender?.email || 'Unknown Sender'; const recipientEmails = otherRecipients.map((r) => r.email).join(', '); @@ -373,7 +372,6 @@ export const constructForwardBody = ( originalDate: string, originalSender: Sender | undefined, otherRecipients: Sender[], - quotedMessage?: string, ) => { const senderName = originalSender?.name || originalSender?.email || 'Unknown Sender'; const recipientEmails = otherRecipients.map((r) => r.email).join(', '); @@ -492,7 +490,6 @@ export function parseNaturalLanguageSearch(query: string): string { export function parseNaturalLanguageDate(query: string): { from?: Date; to?: Date } | null { const now = new Date(); const currentYear = now.getFullYear(); - const currentMonth = now.getMonth(); // Common date patterns const patterns = [ diff --git a/apps/mail/middleware.ts b/apps/mail/middleware.ts deleted file mode 100644 index 847bc3c9c9..0000000000 --- a/apps/mail/middleware.ts +++ /dev/null @@ -1,36 +0,0 @@ -// import { type NextRequest, NextResponse } from 'next/server'; -// import { navigationConfig } from '@/config/navigation'; -// import { geolocation } from '@vercel/functions'; -// import { EU_COUNTRIES } from './lib/countries'; - -// const disabledRoutes = Object.values(navigationConfig) -// .flatMap((section) => section.sections) -// .flatMap((group) => group.items) -// .filter((item) => item.disabled && item.url !== '#') -// .map((item) => item.url); - -// export function middleware(request: NextRequest) { -// const response = NextResponse.next(); -// const geo = geolocation(request); -// const country = geo.countryRegion || ''; - -// response.headers.set('x-user-country', country); - -// const isEuRegion = EU_COUNTRIES.includes(country); -// response.headers.set('x-user-eu-region', String(isEuRegion)); - -// if (process.env.NODE_ENV === 'development') { -// response.headers.set('x-user-eu-region', 'true'); -// } - -// const pathname = request.nextUrl.pathname; -// if (disabledRoutes.some((route) => pathname.startsWith(route))) { -// return NextResponse.redirect(new URL('/mail/inbox', request.url)); -// } - -// return response; -// } - -// export const config = { -// matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)', -// }; diff --git a/apps/mail/package.json b/apps/mail/package.json index bd8ddb349d..c68bbb23da 100644 --- a/apps/mail/package.json +++ b/apps/mail/package.json @@ -83,6 +83,7 @@ "next-themes": "0.4.4", "novel": "1.0.2", "nuqs": "2.4.0", + "oxlint": "1.6.0", "partysocket": "^1.1.4", "pluralize": "^8.0.0", "posthog-js": "1.256.0", @@ -113,6 +114,7 @@ "vaul": "1.1.2", "virtua": "0.41.2", "vite-plugin-babel": "1.3.1", + "vite-plugin-oxlint": "1.4.0", "workers-og": "0.0.25", "zod": "catalog:" }, diff --git a/apps/mail/providers/query-provider.tsx b/apps/mail/providers/query-provider.tsx index d1986deef9..6a0b6c35cf 100644 --- a/apps/mail/providers/query-provider.tsx +++ b/apps/mail/providers/query-provider.tsx @@ -3,8 +3,8 @@ import { type PersistedClient, type Persister, } from '@tanstack/react-query-persist-client'; -import { QueryCache, QueryClient, hashKey, type InfiniteData } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client'; +import { QueryCache, QueryClient, hashKey } from '@tanstack/react-query'; import { createTRPCContext } from '@trpc/tanstack-react-query'; import { useMemo, type PropsWithChildren } from 'react'; import type { AppRouter } from '@zero/server/trpc'; @@ -12,7 +12,6 @@ import { CACHE_BURST_KEY } from '@/lib/constants'; import { signOut } from '@/lib/auth-client'; import { get, set, del } from 'idb-keyval'; import superjson from 'superjson'; -import { toast } from 'sonner'; function createIDBPersister(idbValidKey: IDBValidKey = 'zero-query-cache') { return { @@ -105,8 +104,6 @@ export const trpcClient = createTRPCClient({ ], }); -type TrpcHook = ReturnType; - export function QueryProvider({ children, connectionId, diff --git a/apps/mail/providers/voice-provider.tsx b/apps/mail/providers/voice-provider.tsx index 4bc091c45f..d2306edb5f 100644 --- a/apps/mail/providers/voice-provider.tsx +++ b/apps/mail/providers/voice-provider.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useContext, useState } from 'react'; import { toolExecutors } from '@/lib/elevenlabs-tools'; import { useConversation } from '@elevenlabs/react'; import { useSession } from '@/lib/auth-client'; @@ -42,28 +42,26 @@ export function VoiceProvider({ children }: { children: ReactNode }) { toast.error(typeof error === 'string' ? error : error.message); setIsInitializing(false); }, - clientTools: { - ...Object.entries(toolExecutors).reduce( - (acc, [name, executor]) => ({ - ...acc, - [name]: async (params: any) => { - console.log(`[Voice Tool] ${name} called with params:`, params); - setLastToolCall(`Executing: ${name}`); - - const paramsWithContext = { - ...params, - _context: currentContext, - }; - - const result = await executor(paramsWithContext); - console.log(`[Voice Tool] ${name} result:`, result); - setLastToolCall(null); - return result; - }, - }), - {}, - ), - }, + clientTools: Object.entries(toolExecutors).reduce( + (acc: Record, [name, executor]) => { + acc[name] = async (params: any) => { + console.log(`[Voice Tool] ${name} called with params:`, params); + setLastToolCall(`Executing: ${name}`); + + const paramsWithContext = { + ...params, + _context: currentContext, + }; + + const result = await executor(paramsWithContext); + console.log(`[Voice Tool] ${name} result:`, result); + setLastToolCall(null); + return result; + }; + return acc; + }, + {}, + ), }); const { status, isSpeaking } = conversation; @@ -111,7 +109,7 @@ export function VoiceProvider({ children }: { children: ReactNode }) { email_context_info: context?.hasOpenEmail ? `The user currently has an email open (thread ID: ${context.currentThreadId}). When the user refers to "this email" or "the current email", you can use the getEmail or summarizeEmail tools WITHOUT providing a threadId parameter - the tools will automatically use the currently open email.` : 'No email is currently open. If the user asks about an email, you will need to ask them to open it first or provide a specific thread ID.', - ...(context || {}), + ...context, }, }); diff --git a/apps/mail/vite.config.ts b/apps/mail/vite.config.ts index 3005da82d6..6ffa30c8f1 100644 --- a/apps/mail/vite.config.ts +++ b/apps/mail/vite.config.ts @@ -2,6 +2,7 @@ import { paraglideVitePlugin } from '@inlang/paraglide-js'; import { cloudflare } from '@cloudflare/vite-plugin'; import { reactRouter } from '@react-router/dev/vite'; import tsconfigPaths from 'vite-tsconfig-paths'; +import oxlintPlugin from 'vite-plugin-oxlint'; import babel from 'vite-plugin-babel'; import tailwindcss from 'tailwindcss'; import { defineConfig } from 'vite'; @@ -13,6 +14,7 @@ const ReactCompilerConfig = { export default defineConfig({ plugins: [ + oxlintPlugin(), reactRouter(), cloudflare(), babel({ diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts index ae1733e8e2..21e23281a4 100644 --- a/apps/server/src/lib/auth.ts +++ b/apps/server/src/lib/auth.ts @@ -1,11 +1,3 @@ -import { - connection, - user as _user, - account, - userSettings, - session, - userHotkeys, -} from '../db/schema'; import { createAuthMiddleware, phoneNumber, jwt, bearer, mcp } from 'better-auth/plugins'; import { type Account, betterAuth, type BetterAuthOptions } from 'better-auth'; import { getBrowserTimezone, isValidTimezone } from './timezones'; @@ -13,16 +5,17 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { getSocialProviders } from './auth-providers'; import { redis, resend, twilio } from './services'; import { getContext } from 'hono/context-storage'; +import { user as _user } from '../db/schema'; import { defaultUserSettings } from './schemas'; -import { getMigrations } from 'better-auth/db'; + import { disableBrainFunction } from './brain'; -import { type EProviders } from '../types'; import { APIError } from 'better-auth/api'; import { getZeroDB } from './server-utils'; +import { type EProviders } from '../types'; import type { HonoContext } from '../ctx'; import { env } from 'cloudflare:workers'; import { createDriver } from './driver'; -import { eq } from 'drizzle-orm'; + import { createDb } from '../db'; const connectionHandlerHook = async (account: Account) => { @@ -240,7 +233,7 @@ export const createAuth = () => { const createAuthConfig = () => { const cache = redis(); - const { db, conn } = createDb(env.HYPERDRIVE.connectionString); + const { db } = createDb(env.HYPERDRIVE.connectionString); return { database: drizzleAdapter(db, { provider: 'pg' }), secondaryStorage: { @@ -291,7 +284,7 @@ const createAuthConfig = () => { }, }, onAPIError: { - onError: (error, ctx) => { + onError: (error) => { console.error('API Error', error); }, errorURL: `${env.VITE_PUBLIC_APP_URL}/login`, diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index e122137540..ac02e89d4a 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -8,9 +8,9 @@ import { sanitizeContext, StandardizedError, } from './utils'; -import type { IOutgoingMessage, Label, ParsedMessage, DeleteAllSpamResponse } from '../../types'; import { mapGoogleLabelColor, mapToGoogleLabelColor } from './google-label-color-map'; import { parseAddressList, parseFrom, wasSentWithTLS } from '../email-utils'; +import type { IOutgoingMessage, Label, ParsedMessage } from '../../types'; import { sanitizeTipTapHtml } from '../sanitize-tip-tap-html'; import type { MailManager, ManagerConfig } from './types'; import { type gmail_v1, gmail } from '@googleapis/gmail'; @@ -197,7 +197,7 @@ export class GoogleMailManager implements MailManager { }); return { label: res.data.name ?? res.data.id ?? '', - count: Number(res.data.threadsUnread) ?? undefined, + count: Number(res.data.threadsUnread), }; }), ); @@ -949,7 +949,6 @@ export class GoogleMailManager implements MailManager { cc, bcc, fromEmail, - isForward = false, originalMessage = null, }: IOutgoingMessage) { const msg = createMimeMessage(); @@ -1173,6 +1172,7 @@ export class GoogleMailManager implements MailManager { body: attachmentData ?? '', }; } catch (e) { + console.error('Failed to get attachment', e); return null; } }), diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts index 45fe3c6308..bacce02bbd 100644 --- a/apps/server/src/lib/driver/microsoft.ts +++ b/apps/server/src/lib/driver/microsoft.ts @@ -932,20 +932,6 @@ export class OutlookMailManager implements MailManager { return false; } } - private async modifyThreadLabels( - threadIds: string[], - requestBody: unknown, // Gmail-specific type, replace with relevant Outlook logic - ) { - // This method is Gmail-specific (modifying thread labels). - // The equivalent in Outlook is modifying messages (read status, categories) - // or moving messages between folders. - // The logic from modifyMessageReadStatus and modifyMessageLabelsOrFolders is more relevant. - console.warn( - 'modifyThreadLabels is a Gmail-specific concept. Use modifyMessageReadStatus or modifyMessageLabelsOrFolders.', - ); - // Placeholder - return Promise.resolve(); - } public deleteAllSpam() { console.warn('deleteAllSpam is not implemented for Microsoft'); @@ -1008,12 +994,9 @@ export class OutlookMailManager implements MailManager { toRecipients, ccRecipients, bccRecipients, - sentDateTime, receivedDateTime, internetMessageId, - inferenceClassification, // Might indicate if junk categories, // Outlook categories map to tags - parentFolderId, // Can indicate folder (e.g. 'deleteditems') // headers, // Array of Header objects (name, value), doesn't exist in Outlook }: Message): Omit< ParsedMessage, @@ -1119,7 +1102,6 @@ export class OutlookMailManager implements MailManager { headers, cc, bcc, - fromEmail, // In Outlook, this is usually determined by the authenticated user unless using "send on behalf of" or "send as" }: IOutgoingMessage): Promise { // Outlook Graph API expects a Message object structure for sending/creating drafts console.log(to); diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index fc5b062ffd..bdd04c0b62 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -19,7 +19,7 @@ export const IGetThreadResponseSchema = z.object({ labels: z.array(z.object({ id: z.string(), name: z.string() })), }); -export interface ParsedDraft { +export interface ParsedDraft { id: string; to?: string[]; subject?: string; diff --git a/apps/server/src/lib/driver/utils.ts b/apps/server/src/lib/driver/utils.ts index 16f4b6e5bd..6b26a45214 100644 --- a/apps/server/src/lib/driver/utils.ts +++ b/apps/server/src/lib/driver/utils.ts @@ -2,8 +2,8 @@ import { getActiveConnection, getZeroDB } from '../server-utils'; import { getContext } from 'hono/context-storage'; import type { gmail_v1 } from '@googleapis/gmail'; import type { HonoContext } from '../../ctx'; -import { env } from 'cloudflare:workers'; -import { createDriver } from '../driver'; + + import { toByteArray } from 'base64-js'; export const FatalErrors = ['invalid_grant']; diff --git a/apps/server/src/lib/factories/base-subscription.factory.ts b/apps/server/src/lib/factories/base-subscription.factory.ts index 0d0838a38d..127e962750 100644 --- a/apps/server/src/lib/factories/base-subscription.factory.ts +++ b/apps/server/src/lib/factories/base-subscription.factory.ts @@ -1,8 +1,8 @@ -import { defaultLabels, EProviders, type AppContext } from '../../types'; -import { getContext } from 'hono/context-storage'; +import { defaultLabels, EProviders, } from '../../types'; + import { connection } from '../../db/schema'; -import type { HonoContext } from '../../ctx'; -import { getZeroDB } from '../server-utils'; + + import { env } from 'cloudflare:workers'; import { createDb } from '../../db'; import { eq } from 'drizzle-orm'; diff --git a/apps/server/src/lib/factories/google-subscription.factory.ts b/apps/server/src/lib/factories/google-subscription.factory.ts index ca989d2dae..a7d041698b 100644 --- a/apps/server/src/lib/factories/google-subscription.factory.ts +++ b/apps/server/src/lib/factories/google-subscription.factory.ts @@ -1,8 +1,4 @@ -import { - BaseSubscriptionFactory, - type SubscriptionData, - type UnsubscriptionData, -} from './base-subscription.factory'; +import { BaseSubscriptionFactory, type SubscriptionData } from './base-subscription.factory'; import { c, getNotificationsUrl } from '../../lib/utils'; import jwt from '@tsndr/cloudflare-worker-jwt'; import { env } from 'cloudflare:workers'; @@ -38,7 +34,7 @@ class GoogleSubscriptionFactory extends BaseSubscriptionFactory { try { this.serviceAccount = JSON.parse(serviceAccountJson); } catch (error) { - console.log('Invalid GOOGLE_S_ACCOUNT JSON format', serviceAccountJson); + console.log('Invalid GOOGLE_S_ACCOUNT JSON format', serviceAccountJson, error); throw new Error('Invalid GOOGLE_S_ACCOUNT JSON format'); } return this.serviceAccount as GoogleServiceAccount; diff --git a/apps/server/src/lib/factories/outlook-subscription.factory.ts b/apps/server/src/lib/factories/outlook-subscription.factory.ts index ffef97de78..171b076417 100644 --- a/apps/server/src/lib/factories/outlook-subscription.factory.ts +++ b/apps/server/src/lib/factories/outlook-subscription.factory.ts @@ -8,20 +8,20 @@ import { EProviders } from '../../types'; export class OutlookSubscriptionFactory extends BaseSubscriptionFactory { readonly providerId = EProviders.microsoft; - public async subscribe(data: { body: SubscriptionData }): Promise { + public async subscribe(_: { body: SubscriptionData }): Promise { // TODO: Implement Outlook subscription logic // This will handle Microsoft Graph API subscriptions for Outlook throw new Error('Outlook subscription not implemented yet'); } - public async unsubscribe(data: { body: UnsubscriptionData }): Promise { + public async unsubscribe(_: { body: UnsubscriptionData }): Promise { // TODO: Implement Outlook unsubscription logic throw new Error('Outlook unsubscription not implemented yet'); } - public async verifyToken(token: string): Promise { + public async verifyToken(_: string): Promise { // TODO: Implement Microsoft Graph token verification throw new Error('Outlook token verification not implemented yet'); diff --git a/apps/server/src/lib/server-utils.ts b/apps/server/src/lib/server-utils.ts index 24b0e5769e..61d6a2dfd6 100644 --- a/apps/server/src/lib/server-utils.ts +++ b/apps/server/src/lib/server-utils.ts @@ -1,5 +1,3 @@ -import { OutgoingMessageType, type OutgoingMessage } from '../routes/chat'; -import type { IGetThreadResponse } from './driver/types'; import { getContext } from 'hono/context-storage'; import { connection } from '../db/schema'; import type { HonoContext } from '../ctx'; diff --git a/apps/server/src/lib/services.ts b/apps/server/src/lib/services.ts index a96062ab85..6fbabcf1cd 100644 --- a/apps/server/src/lib/services.ts +++ b/apps/server/src/lib/services.ts @@ -9,7 +9,7 @@ export const resend = () => export const redis = () => new Redis({ url: env.REDIS_URL, token: env.REDIS_TOKEN }); -export const twilio = (forceUseRealService = false) => { +export const twilio = () => { // if (env.NODE_ENV === 'development' && !forceUseRealService) { // return { // messages: { diff --git a/apps/server/src/lib/timezones.ts b/apps/server/src/lib/timezones.ts index 71f02ffa88..1a33423e1f 100644 --- a/apps/server/src/lib/timezones.ts +++ b/apps/server/src/lib/timezones.ts @@ -4,6 +4,7 @@ export const isValidTimezone = (timezone: string) => { try { return Intl.supportedValuesOf('timeZone').includes(timezone); } catch (error) { + console.error(error); return false; } }; diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 64ba0cc48f..10fe0b4b09 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -16,7 +16,7 @@ import { } from './db/schema'; import { env, WorkerEntrypoint, DurableObject, RpcTarget } from 'cloudflare:workers'; import { EProviders, type ISubscribeBatch, type IThreadBatch } from './types'; -import { getZeroAgent, getZeroDB, verifyToken } from './lib/server-utils'; +import { getZeroDB, verifyToken } from './lib/server-utils'; import { oAuthDiscoveryMetadata } from 'better-auth/plugins'; import { eq, and, desc, asc, inArray } from 'drizzle-orm'; import { EWorkflowType, runWorkflow } from './pipelines'; @@ -24,7 +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'; diff --git a/apps/server/src/pipelines.effect.ts b/apps/server/src/pipelines.effect.ts index 1d2e1334f4..eb1502ce2a 100644 --- a/apps/server/src/pipelines.effect.ts +++ b/apps/server/src/pipelines.effect.ts @@ -18,16 +18,15 @@ import { ThreadLabels, } from './lib/brain.fallback.prompts'; import { defaultLabels, EPrompts, EProviders, type ParsedMessage, type Sender } from './types'; -import { Effect, Console, pipe, Match, Option } from 'effect'; import { getZeroAgent } from './lib/server-utils'; import { type gmail_v1 } from '@googleapis/gmail'; import { getPromptName } from './pipelines'; import { env } from 'cloudflare:workers'; import { connection } from './db/schema'; +import { Effect, Console } from 'effect'; import * as cheerio from 'cheerio'; import { eq } from 'drizzle-orm'; import { createDb } from './db'; -import { z } from 'zod'; const showLogs = true; @@ -89,7 +88,7 @@ export const runMainWorkflow = ( Effect.gen(function* () { yield* Console.log('[MAIN_WORKFLOW] Starting workflow with payload:', params); - const { providerId, historyId, subscriptionName } = params; + const { providerId, historyId } = params; let serviceAccount = null; if (override) { @@ -725,11 +724,13 @@ export const runThreadWorkflow = ( // Check delta - only modify if there are actual changes const currentLabelIds = thread.labels?.map((l) => l.id) || []; const labelsToAdd = validLabelIds.filter((id) => !currentLabelIds.includes(id)); - const aiLabelIds = userAccountLabels - .filter((l) => userLabels.some((ul) => ul.name === l.name)) - .map((l) => l.id); + const aiLabelIds = new Set( + userAccountLabels + .filter((l) => userLabels.some((ul) => ul.name === l.name)) + .map((l) => l.id), + ); const labelsToRemove = currentLabelIds.filter( - (id) => aiLabelIds.includes(id) && !validLabelIds.includes(id), + (id) => aiLabelIds.has(id) && !validLabelIds.includes(id), ); if (labelsToAdd.length > 0 || labelsToRemove.length > 0) { @@ -836,9 +837,9 @@ export const runThreadWorkflow = ( }), ); -// Helper functions for vectorization and AI processing -type VectorizeVectorMetadata = 'connection' | 'thread' | 'summary'; -type IThreadSummaryMetadata = Record; +// // Helper functions for vectorization and AI processing +// type VectorizeVectorMetadata = 'connection' | 'thread' | 'summary'; +// type IThreadSummaryMetadata = Record; export async function htmlToText(decodedBody: string): Promise { try { diff --git a/apps/server/src/routes/agent/orchestrator.ts b/apps/server/src/routes/agent/orchestrator.ts index 5e4129da83..5fca8f233e 100644 --- a/apps/server/src/routes/agent/orchestrator.ts +++ b/apps/server/src/routes/agent/orchestrator.ts @@ -1,6 +1,6 @@ import { streamText, tool, type DataStreamWriter, type ToolSet } from 'ai'; import { perplexity } from '@ai-sdk/perplexity'; -import { env } from 'cloudflare:workers'; + import { Tools } from '../../types'; import { z } from 'zod'; diff --git a/apps/server/src/routes/agent/tools.ts b/apps/server/src/routes/agent/tools.ts index 0b571eb918..751c0232b4 100644 --- a/apps/server/src/routes/agent/tools.ts +++ b/apps/server/src/routes/agent/tools.ts @@ -1,9 +1,8 @@ -import { toZodToolSet, executeOrAuthorizeZodTool } from '@arcadeai/arcadejs/lib'; import { generateText, streamText, tool, type DataStreamWriter } from 'ai'; import { composeEmail } from '../../trpc/routes/ai/compose'; import type { MailManager } from '../../lib/driver/types'; import { perplexity } from '@ai-sdk/perplexity'; -import { Arcade } from '@arcadeai/arcadejs'; + import { colors } from '../../lib/prompts'; import { env } from 'cloudflare:workers'; import { Tools } from '../../types'; @@ -40,69 +39,69 @@ export const getEmbeddingVector = async ( } }; -const askZeroMailbox = (connectionId: string) => - tool({ - description: 'Ask Zero a question about the mailbox', - parameters: z.object({ - question: z.string().describe('The question to ask Zero'), - topK: z.number().describe('The number of results to return').max(9).min(1).default(3), - }), - execute: async ({ question, topK = 3 }) => { - const embedding = await getEmbeddingVector(question, 'vectorize-load'); - if (!embedding) { - return { error: 'Failed to get embedding' }; - } - const threadResults = await env.VECTORIZE.query(embedding, { - topK, - returnMetadata: 'all', - filter: { - connection: connectionId, - }, - }); +// const askZeroMailbox = (connectionId: string) => +// tool({ +// description: 'Ask Zero a question about the mailbox', +// parameters: z.object({ +// question: z.string().describe('The question to ask Zero'), +// topK: z.number().describe('The number of results to return').max(9).min(1).default(3), +// }), +// execute: async ({ question, topK = 3 }) => { +// const embedding = await getEmbeddingVector(question, 'vectorize-load'); +// if (!embedding) { +// return { error: 'Failed to get embedding' }; +// } +// const threadResults = await env.VECTORIZE.query(embedding, { +// topK, +// returnMetadata: 'all', +// filter: { +// connection: connectionId, +// }, +// }); - if (!threadResults.matches.length) { - return { - response: [], - success: false, - }; - } - return { - response: threadResults.matches.map((e) => e.metadata?.['summary'] ?? 'no content'), - success: true, - }; - }, - }); +// if (!threadResults.matches.length) { +// return { +// response: [], +// success: false, +// }; +// } +// return { +// response: threadResults.matches.map((e) => e.metadata?.['summary'] ?? 'no content'), +// success: true, +// }; +// }, +// }); -const askZeroThread = (connectionId: string) => - tool({ - description: 'Ask Zero a question about a specific thread', - parameters: z.object({ - threadId: z.string().describe('The ID of the thread to ask Zero about'), - question: z.string().describe('The question to ask Zero'), - }), - execute: async ({ threadId, question }) => { - const response = await env.VECTORIZE.getByIds([threadId]); - if (!response.length) return { response: "I don't know, no threads found", success: false }; - const embedding = await getEmbeddingVector(question, 'vectorize-load'); - if (!embedding) { - return { error: 'Failed to get embedding' }; - } - const threadResults = await env.VECTORIZE.query(embedding, { - topK: 1, - returnMetadata: 'all', - filter: { - thread: threadId, - connection: connectionId, - }, - }); - const topThread = threadResults.matches[0]; - if (!topThread) return { response: "I don't know, no threads found", success: false }; - return { - response: topThread.metadata?.['summary'] ?? 'no content', - success: true, - }; - }, - }); +// const askZeroThread = (connectionId: string) => +// tool({ +// description: 'Ask Zero a question about a specific thread', +// parameters: z.object({ +// threadId: z.string().describe('The ID of the thread to ask Zero about'), +// question: z.string().describe('The question to ask Zero'), +// }), +// execute: async ({ threadId, question }) => { +// const response = await env.VECTORIZE.getByIds([threadId]); +// if (!response.length) return { response: "I don't know, no threads found", success: false }; +// const embedding = await getEmbeddingVector(question, 'vectorize-load'); +// if (!embedding) { +// return { error: 'Failed to get embedding' }; +// } +// const threadResults = await env.VECTORIZE.query(embedding, { +// topK: 1, +// returnMetadata: 'all', +// filter: { +// thread: threadId, +// connection: connectionId, +// }, +// }); +// const topThread = threadResults.matches[0]; +// if (!topThread) return { response: "I don't know, no threads found", success: false }; +// return { +// response: topThread.metadata?.['summary'] ?? 'no content', +// success: true, +// }; +// }, +// }); const getEmail = (driver: MailManager) => tool({ diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 85663a306c..0e04761c18 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -25,15 +25,13 @@ import { GmailSearchAssistantSystemPrompt, AiChatPrompt, } from '../lib/prompts'; -import { type Connection, type ConnectionContext, type WSMessage } from 'agents'; import { EPrompts, type IOutgoingMessage, type ParsedMessage } from '../types'; import type { IGetThreadResponse, MailManager } from '../lib/driver/types'; import { connectionToDriver, getZeroAgent } from '../lib/server-utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { createSimpleAuth, type SimpleAuth } from '../lib/auth'; +import { type Connection, type WSMessage } from 'agents'; import { ToolOrchestrator } from './agent/orchestrator'; import type { CreateDraftData } from '../lib/schemas'; -import { FOLDERS, parseHeaders } from '../lib/utils'; import { env, RpcTarget } from 'cloudflare:workers'; import { AIChatAgent } from 'agents/ai-chat-agent'; import { tools as authTools } from './agent/tools'; @@ -43,27 +41,15 @@ import { getPromptName } from '../pipelines'; import { connection } from '../db/schema'; import { getPrompt } from '../lib/brain'; import { openai } from '@ai-sdk/openai'; +import { FOLDERS } from '../lib/utils'; import { and, eq } from 'drizzle-orm'; import { McpAgent } from 'agents/mcp'; -import { groq } from '@ai-sdk/groq'; + import { createDb } from '../db'; import { z } from 'zod'; const decoder = new TextDecoder(); -interface ThreadRow { - id: string; - thread_id: string; - provider_id: string; - messages: string; - latest_sender: string; - latest_received_on: string; - latest_subject: string; - latest_label_ids: string; - created_at: string; - updated_at: string; -} - export enum IncomingMessageType { UseChatRequest = 'cf_agent_use_chat_request', ChatClear = 'cf_agent_chat_clear', @@ -364,7 +350,7 @@ export class ZeroAgent extends AIChatAgent { private getDataStreamResponse( onFinish: StreamTextOnFinishCallback<{}>, - options?: { + _?: { abortSignal: AbortSignal | undefined; }, ) { @@ -474,6 +460,7 @@ export class ZeroAgent extends AIChatAgent { try { data = JSON.parse(message) as IncomingMessage; } catch (error) { + console.warn(error); // silently ignore invalid messages for now // TODO: log errors with log levels return; @@ -969,10 +956,10 @@ export class ZeroAgent extends AIChatAgent { let totalSynced = 0; let pageToken: string | null = null; let hasMore = true; - let pageCount = 0; + let _pageCount = 0; while (hasMore) { - pageCount++; + _pageCount++; const result = await this.driver.list({ folder, @@ -1558,6 +1545,7 @@ export class ZeroMCP extends McpAgent { ], }; } catch (e) { + console.error(e); return { content: [ { @@ -1587,6 +1575,7 @@ export class ZeroMCP extends McpAgent { ], }; } catch (e) { + console.error(e); return { content: [ { @@ -1616,6 +1605,7 @@ export class ZeroMCP extends McpAgent { ], }; } catch (e) { + console.error(e); return { content: [ { diff --git a/apps/server/src/services/writing-style-service.ts b/apps/server/src/services/writing-style-service.ts index 3ff1e1f894..0a98bee9f0 100644 --- a/apps/server/src/services/writing-style-service.ts +++ b/apps/server/src/services/writing-style-service.ts @@ -1,8 +1,8 @@ import { mapToObj, pipe, entries, sortBy, take, fromEntries } from 'remeda'; -import { getContext } from 'hono/context-storage'; + import { writingStyleMatrix } from '../db/schema'; -import { getZeroDB } from '../lib/server-utils'; -import type { HonoContext } from '../ctx'; + + import { env } from 'cloudflare:workers'; import { google } from '@ai-sdk/google'; import { jsonrepair } from 'jsonrepair'; diff --git a/apps/server/src/trpc/routes/bimi.ts b/apps/server/src/trpc/routes/bimi.ts index 4276c542ce..c468f8c608 100644 --- a/apps/server/src/trpc/routes/bimi.ts +++ b/apps/server/src/trpc/routes/bimi.ts @@ -1,5 +1,4 @@ -import { router, privateProcedure, createRateLimiterMiddleware } from '../trpc'; -import { Ratelimit } from '@upstash/ratelimit'; +import { router, privateProcedure } from '../trpc'; import { TRPCError } from '@trpc/server'; import { z } from 'zod'; @@ -89,12 +88,6 @@ const fetchLogoContent = async (logoUrl: string): Promise => { export const bimiRouter = router({ getByEmail: privateProcedure - .use( - createRateLimiterMiddleware({ - generatePrefix: ({ sessionUser }) => `ratelimit:bimi-${sessionUser?.id}`, - limiter: Ratelimit.slidingWindow(30, '1m'), - }), - ) .input( z.object({ email: z.string().email(), @@ -159,12 +152,6 @@ export const bimiRouter = router({ }), getByDomain: privateProcedure - .use( - createRateLimiterMiddleware({ - generatePrefix: ({ sessionUser }) => `ratelimit:bimi-domain-${sessionUser?.id}`, - limiter: Ratelimit.slidingWindow(30, '1m'), - }), - ) .input( z.object({ domain: z.string().min(1), diff --git a/apps/server/src/trpc/routes/connections.ts b/apps/server/src/trpc/routes/connections.ts index 1ae84d9995..0ddc6ff379 100644 --- a/apps/server/src/trpc/routes/connections.ts +++ b/apps/server/src/trpc/routes/connections.ts @@ -1,17 +1,15 @@ import { createRateLimiterMiddleware, privateProcedure, publicProcedure, router } from '../trpc'; import { getActiveConnection, getZeroDB } from '../../lib/server-utils'; -import { connection, user as user_ } from '../../db/schema'; import { Ratelimit } from '@upstash/ratelimit'; -import { env } from 'cloudflare:workers'; + import { TRPCError } from '@trpc/server'; -import { and, eq } from 'drizzle-orm'; import { z } from 'zod'; export const connectionsRouter = router({ list: privateProcedure .use( createRateLimiterMiddleware({ - limiter: Ratelimit.slidingWindow(60, '1m'), + limiter: Ratelimit.slidingWindow(120, '1m'), generatePrefix: ({ sessionUser }) => `ratelimit:get-connections-${sessionUser?.id}`, }), ) diff --git a/apps/server/src/trpc/routes/label.ts b/apps/server/src/trpc/routes/label.ts index 281bf0d7cb..4bbe038176 100644 --- a/apps/server/src/trpc/routes/label.ts +++ b/apps/server/src/trpc/routes/label.ts @@ -8,7 +8,7 @@ export const labelsRouter = router({ .use( createRateLimiterMiddleware({ generatePrefix: ({ sessionUser }) => `ratelimit:get-labels-${sessionUser?.id}`, - limiter: Ratelimit.slidingWindow(60, '1m'), + limiter: Ratelimit.slidingWindow(120, '1m'), }), ) .output( diff --git a/apps/server/src/trpc/routes/mail.ts b/apps/server/src/trpc/routes/mail.ts index f56b643ebe..503f8c88bb 100644 --- a/apps/server/src/trpc/routes/mail.ts +++ b/apps/server/src/trpc/routes/mail.ts @@ -1,17 +1,16 @@ import { activeDriverProcedure, - createRateLimiterMiddleware, router, privateProcedure, } from '../trpc'; import { updateWritingStyleMatrix } from '../../services/writing-style-service'; -import { deserializeFiles, serializedFileSchema } from '../../lib/schemas'; -import { defaultPageSize, FOLDERS, LABELS } from '../../lib/utils'; +import { serializedFileSchema } from '../../lib/schemas'; +import { defaultPageSize, FOLDERS, } from '../../lib/utils'; import { IGetThreadResponseSchema } from '../../lib/driver/types'; import { processEmailHtml } from '../../lib/email-processor'; import type { DeleteAllSpamResponse } from '../../types'; import { getZeroAgent } from '../../lib/server-utils'; -import { env } from 'cloudflare:workers'; + import { TRPCError } from '@trpc/server'; import { z } from 'zod'; diff --git a/apps/server/src/trpc/routes/settings.ts b/apps/server/src/trpc/routes/settings.ts index dde22250f6..afafa63198 100644 --- a/apps/server/src/trpc/routes/settings.ts +++ b/apps/server/src/trpc/routes/settings.ts @@ -2,13 +2,12 @@ import { createRateLimiterMiddleware, privateProcedure, publicProcedure, router import { defaultUserSettings, userSettingsSchema, type UserSettings } from '../../lib/schemas'; import { getZeroDB } from '../../lib/server-utils'; import { Ratelimit } from '@upstash/ratelimit'; -import { env } from 'cloudflare:workers'; export const settingsRouter = router({ get: publicProcedure .use( createRateLimiterMiddleware({ - limiter: Ratelimit.slidingWindow(60, '1m'), + limiter: Ratelimit.slidingWindow(120, '1m'), generatePrefix: ({ sessionUser }) => `ratelimit:get-settings-${sessionUser?.id}`, }), ) diff --git a/apps/server/src/trpc/trpc.ts b/apps/server/src/trpc/trpc.ts index e2947c02af..5426e18cab 100644 --- a/apps/server/src/trpc/trpc.ts +++ b/apps/server/src/trpc/trpc.ts @@ -3,7 +3,7 @@ import { Ratelimit, type RatelimitConfig } from '@upstash/ratelimit'; import type { HonoContext, HonoVariables } from '../ctx'; import { getConnInfo } from 'hono/cloudflare-workers'; import { initTRPC, TRPCError } from '@trpc/server'; -import { env } from 'cloudflare:workers'; + import { redis } from '../lib/services'; import type { Context } from 'hono'; import superjson from 'superjson'; diff --git a/eslint.config.mjs b/eslint.config.mjs index c7b0f931b9..beb1a42753 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ import config from "@zero/tsconfig/base"; import { fileURLToPath } from "url"; -import { resolve } from "path"; + // @ts-ignore const __dirname = fileURLToPath(new URL(".", import.meta.url)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db43fb9532..62ddcc5734 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,6 +298,9 @@ importers: nuqs: specifier: 2.4.0 version: 2.4.0(react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + oxlint: + specifier: 1.6.0 + version: 1.6.0 partysocket: specifier: ^1.1.4 version: 1.1.4 @@ -388,6 +391,9 @@ importers: vite-plugin-babel: specifier: 1.3.1 version: 1.3.1(@babel/core@7.27.7)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0)) + vite-plugin-oxlint: + specifier: 1.4.0 + version: 1.4.0(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0)) workers-og: specifier: 0.0.25 version: 0.0.25 @@ -1825,6 +1831,46 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@oxlint/darwin-arm64@1.6.0': + resolution: {integrity: sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg==} + cpu: [arm64] + os: [darwin] + + '@oxlint/darwin-x64@1.6.0': + resolution: {integrity: sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg==} + cpu: [x64] + os: [darwin] + + '@oxlint/linux-arm64-gnu@1.6.0': + resolution: {integrity: sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A==} + cpu: [arm64] + os: [linux] + + '@oxlint/linux-arm64-musl@1.6.0': + resolution: {integrity: sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg==} + cpu: [arm64] + os: [linux] + + '@oxlint/linux-x64-gnu@1.6.0': + resolution: {integrity: sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w==} + cpu: [x64] + os: [linux] + + '@oxlint/linux-x64-musl@1.6.0': + resolution: {integrity: sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg==} + cpu: [x64] + os: [linux] + + '@oxlint/win32-arm64@1.6.0': + resolution: {integrity: sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ==} + cpu: [arm64] + os: [win32] + + '@oxlint/win32-x64@1.6.0': + resolution: {integrity: sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA==} + cpu: [x64] + os: [win32] + '@peculiar/asn1-android@2.3.16': resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==} @@ -6143,6 +6189,11 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxlint@1.6.0: + resolution: {integrity: sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg==} + engines: {node: '>=8.*'} + hasBin: true + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -6158,6 +6209,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -7550,6 +7604,11 @@ packages: '@babel/core': ^7.0.0 vite: ^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite-plugin-oxlint@1.4.0: + resolution: {integrity: sha512-/L0W5yldiFk65sdSYT41uhQRqBZCmteRHoLnFmqaRzzmGyNt/7yZnjruHHsuP5tpX/62Kyhl2SGERP/FMQyV4g==} + peerDependencies: + vite: ^6.0.7 + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -8737,6 +8796,30 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@oxlint/darwin-arm64@1.6.0': + optional: true + + '@oxlint/darwin-x64@1.6.0': + optional: true + + '@oxlint/linux-arm64-gnu@1.6.0': + optional: true + + '@oxlint/linux-arm64-musl@1.6.0': + optional: true + + '@oxlint/linux-x64-gnu@1.6.0': + optional: true + + '@oxlint/linux-x64-musl@1.6.0': + optional: true + + '@oxlint/win32-arm64@1.6.0': + optional: true + + '@oxlint/win32-x64@1.6.0': + optional: true + '@peculiar/asn1-android@2.3.16': dependencies: '@peculiar/asn1-schema': 2.3.15 @@ -13533,6 +13616,17 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxlint@1.6.0: + optionalDependencies: + '@oxlint/darwin-arm64': 1.6.0 + '@oxlint/darwin-x64': 1.6.0 + '@oxlint/linux-arm64-gnu': 1.6.0 + '@oxlint/linux-arm64-musl': 1.6.0 + '@oxlint/linux-x64-gnu': 1.6.0 + '@oxlint/linux-x64-musl': 1.6.0 + '@oxlint/win32-arm64': 1.6.0 + '@oxlint/win32-x64': 1.6.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -13549,6 +13643,8 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.3.0: {} + pako@0.2.9: {} pako@1.0.11: {} @@ -15196,6 +15292,12 @@ snapshots: '@babel/core': 7.27.7 vite: 6.3.5(@types/node@22.13.8)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0) + vite-plugin-oxlint@1.4.0(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0)): + dependencies: + oxlint: 1.6.0 + package-manager-detector: 1.3.0 + vite: 6.3.5(@types/node@22.13.8)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0) + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0)): dependencies: debug: 4.4.1 diff --git a/scripts/seed-style/seeder.ts b/scripts/seed-style/seeder.ts deleted file mode 100644 index e3da312258..0000000000 --- a/scripts/seed-style/seeder.ts +++ /dev/null @@ -1,200 +0,0 @@ -// import { -// command, -// string as stringType, -// number as numberType, -// flag, -// oneOf, -// option, -// boolean, -// subcommands, -// optional, -// } from 'cmd-ts'; -// import { input, select, confirm, number as numberPrompt } from '@inquirer/prompts'; -// import { updateWritingStyleMatrix } from '../services/writing-style-service'; -// import professionalEmails from './styles/professional_emails.json'; -// import persuasiveEmails from './styles/persuasive_emails.json'; -// import friendlyEmails from './styles/friendly_emails.json'; -// import conciseEmails from './styles/concise_emails.json'; -// import { writingStyleMatrix } from '@zero/db/schema'; -// import genZEmails from './styles/genz_emails.json'; -// import { keys, take } from 'remeda'; -// import { eq } from 'drizzle-orm'; -// import { db } from '@zero/db'; -// import pRetry from 'p-retry'; -// import pAll from 'p-all'; - -// const mapping = { -// professional: professionalEmails, -// persuasive: persuasiveEmails, -// genz: genZEmails, -// concise: conciseEmails, -// friendly: friendlyEmails, -// } as const; - -// const runSeeder = async (connectionId: string, style: keyof typeof mapping, size: number) => { -// console.warn( -// 'Seeding style matrix for connection', -// connectionId, -// 'based on', -// size, -// 'mock emails.', -// ); - -// const testDataSet = take(mapping[style], size); - -// await pAll( -// testDataSet.map((email, index) => async () => { -// console.warn('Seeding email', index); -// await pRetry( -// async () => { -// try { -// await updateWritingStyleMatrix(connectionId, email.body); -// } catch (error) { -// console.error(error); - -// throw error; -// } -// }, -// { -// retries: 5, -// maxTimeout: 60_000, -// minTimeout: 1_000, -// }, -// ); -// }), -// { concurrency: 1 }, -// ); - -// console.warn('Seeded style matrix for connection', connectionId); -// }; - -// const runResetStyleMatrix = async (connectionId: string) => { -// await db.delete(writingStyleMatrix).where(eq(writingStyleMatrix.connectionId, connectionId)); -// }; - -// const seed = command({ -// name: 'seed-style-matrix', -// args: { -// connectionId: option({ -// type: optional(stringType), -// long: 'connection-id', -// short: 'c', -// description: 'Connection ID to seed the generated style matrix for', -// }), -// style: option({ -// type: optional(oneOf(keys(mapping))), -// description: 'Style to seed the generated style matrix for', -// long: 'style', -// short: 's', -// }), -// size: option({ -// type: optional(numberType), -// description: 'Number of emails to seed', -// long: 'size', -// short: 'n', -// defaultValue: () => { -// return 10; -// }, -// }), -// resetStyleMatrix: flag({ -// type: optional(boolean), -// description: 'Reset the style matrix before seeding', -// long: 'reset', -// short: 'r', -// defaultValue: () => { -// return false; -// }, -// }), -// }, -// handler: async (inputs) => { -// const connectionId = inputs.connectionId ?? (await getConnectionId()); -// const style = inputs.style ?? (await getStyle()); -// const resetStyleMatrix = inputs.resetStyleMatrix ?? (await getResetStyleMatrix(connectionId)); -// const size = inputs.size ?? (await getNumberOfEmails(mapping[style].length)); - -// if (resetStyleMatrix) { -// await runResetStyleMatrix(connectionId); -// } - -// await runSeeder(connectionId, style, size); -// }, -// }); - -// const reset = command({ -// name: 'reset', -// args: { -// connectionId: option({ -// type: optional(stringType), -// long: 'connection-id', -// short: 'c', -// description: 'Connection ID to seed the generated style matrix for', -// }), -// }, -// handler: async (inputs) => { -// const connectionId = inputs.connectionId ?? (await getConnectionId()); - -// const confirmed = await confirm({ -// message: `Reset the style matrix for Connection ID (${connectionId})?`, -// }); - -// if (confirmed) { -// console.warn('Resetting style matrix for connection', connectionId); -// await runResetStyleMatrix(connectionId); -// console.warn('Reset style matrix for connection', connectionId); -// } else { -// console.warn('Aborted reset'); -// } -// }, -// }); - -// const getConnectionId = async () => { -// return input({ -// message: 'Connection ID to seed:', -// required: true, -// validate: async (value) => { -// const connection = await db.query.connection.findFirst({ -// where: (table, ops) => { -// return ops.eq(table.id, value); -// }, -// columns: { -// id: true, -// }, -// }); - -// return connection ? true : 'Invalid Connection ID'; -// }, -// }); -// }; - -// const getStyle = async () => { -// return select({ -// message: 'Style to seed the generated style matrix for', -// choices: keys(mapping), -// }); -// }; - -// const getResetStyleMatrix = async (connectionId: string) => { -// return confirm({ -// message: `Reset the style matrix for Connection ID (${connectionId}) before seeding?`, -// default: true, -// }); -// }; - -// const getNumberOfEmails = async (maxSize: number) => { -// return numberPrompt({ -// message: 'Number of emails to seed', -// default: 10, -// max: maxSize, -// min: 0, -// required: true, -// }); -// }; - -// export const seedStyleCommand = subcommands({ -// name: 'seed-style', -// description: 'Seed style matrix for a given Connection ID', -// cmds: { -// seed, -// reset, -// }, -// }); From 0030a98262ab31543ee28855a98b5813b18a647c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:00:56 -0700 Subject: [PATCH 30/38] feat: update translations via @LingoDotDev (#1698) Hey team, [**Lingo.dev**](https://lingo.dev) here with fresh translations! ### In this update - Added missing translations - Performed brand voice, context and glossary checks - Enhanced translations using Lingo.dev Localization Engine ### Next Steps - [ ] Review the changes - [ ] Merge when ready --- apps/mail/messages/ar.json | 3 ++- apps/mail/messages/ca.json | 3 ++- apps/mail/messages/cs.json | 3 ++- apps/mail/messages/de.json | 3 ++- apps/mail/messages/es.json | 3 ++- apps/mail/messages/fa.json | 3 ++- apps/mail/messages/fr.json | 3 ++- apps/mail/messages/hi.json | 3 ++- apps/mail/messages/hu.json | 3 ++- apps/mail/messages/ja.json | 3 ++- apps/mail/messages/ko.json | 3 ++- apps/mail/messages/lv.json | 3 ++- apps/mail/messages/nl.json | 3 ++- apps/mail/messages/pl.json | 3 ++- apps/mail/messages/pt.json | 3 ++- apps/mail/messages/ru.json | 3 ++- apps/mail/messages/tr.json | 3 ++- apps/mail/messages/vi.json | 3 ++- apps/mail/messages/zh_CN.json | 3 ++- apps/mail/messages/zh_TW.json | 3 ++- i18n.lock | 1 + 21 files changed, 41 insertions(+), 20 deletions(-) diff --git a/apps/mail/messages/ar.json b/apps/mail/messages/ar.json index 03a72e62f3..c2bab474e8 100644 --- a/apps/mail/messages/ar.json +++ b/apps/mail/messages/ar.json @@ -370,7 +370,8 @@ "failedToMute": "فشل الكتم", "failedToUnmute": "فشل إلغاء الكتم", "archived": "تمت الأرشفة", - "failedToArchive": "فشلت الأرشفة" + "failedToArchive": "فشلت الأرشفة", + "openInNewTab": "فتح في علامة تبويب جديدة" }, "units": { "mb": "{amount} ميجابايت" diff --git a/apps/mail/messages/ca.json b/apps/mail/messages/ca.json index 5be164ca3f..ef9715fe61 100644 --- a/apps/mail/messages/ca.json +++ b/apps/mail/messages/ca.json @@ -370,7 +370,8 @@ "failedToMute": "No s'ha pogut silenciar", "failedToUnmute": "No s'ha pogut activar el so", "archived": "Arxivat", - "failedToArchive": "No s'ha pogut arxivar" + "failedToArchive": "No s'ha pogut arxivar", + "openInNewTab": "Obre en una pestanya nova" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/cs.json b/apps/mail/messages/cs.json index a3514b9255..4afc8eb50c 100644 --- a/apps/mail/messages/cs.json +++ b/apps/mail/messages/cs.json @@ -370,7 +370,8 @@ "failedToMute": "Ztlumení se nezdařilo", "failedToUnmute": "Zrušení ztlumení se nezdařilo", "archived": "Archivováno", - "failedToArchive": "Archivace se nezdařila" + "failedToArchive": "Archivace se nezdařila", + "openInNewTab": "Otevřít v nové záložce" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/de.json b/apps/mail/messages/de.json index c52575fb44..9a5925911c 100644 --- a/apps/mail/messages/de.json +++ b/apps/mail/messages/de.json @@ -370,7 +370,8 @@ "failedToMute": "Stummschaltung fehlgeschlagen", "failedToUnmute": "Aufhebung der Stummschaltung fehlgeschlagen", "archived": "Archiviert", - "failedToArchive": "Archivierung fehlgeschlagen" + "failedToArchive": "Archivierung fehlgeschlagen", + "openInNewTab": "In neuem Tab öffnen" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/es.json b/apps/mail/messages/es.json index 4d28272c47..d2b3ec5b71 100644 --- a/apps/mail/messages/es.json +++ b/apps/mail/messages/es.json @@ -370,7 +370,8 @@ "failedToMute": "Error al silenciar", "failedToUnmute": "Error al activar sonido", "archived": "Archivado", - "failedToArchive": "Error al archivar" + "failedToArchive": "Error al archivar", + "openInNewTab": "Abrir en nueva pestaña" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/fa.json b/apps/mail/messages/fa.json index e806099f06..407cddf703 100644 --- a/apps/mail/messages/fa.json +++ b/apps/mail/messages/fa.json @@ -370,7 +370,8 @@ "failedToMute": "بی‌صدا کردن ناموفق بود", "failedToUnmute": "فعال کردن صدا ناموفق بود", "archived": "بایگانی شد", - "failedToArchive": "بایگانی کردن ناموفق بود" + "failedToArchive": "بایگانی کردن ناموفق بود", + "openInNewTab": "باز کردن در زبانه جدید" }, "units": { "mb": "{amount} مگابایت" diff --git a/apps/mail/messages/fr.json b/apps/mail/messages/fr.json index 488c979680..dca5c74f1f 100644 --- a/apps/mail/messages/fr.json +++ b/apps/mail/messages/fr.json @@ -370,7 +370,8 @@ "failedToMute": "Échec de la coupure du son", "failedToUnmute": "Échec de la réactivation du son", "archived": "Archivé", - "failedToArchive": "Échec de l'archivage" + "failedToArchive": "Échec de l'archivage", + "openInNewTab": "Ouvrir dans un nouvel onglet" }, "units": { "mb": "{amount} Mo" diff --git a/apps/mail/messages/hi.json b/apps/mail/messages/hi.json index 5eac04d0e7..1913534b7f 100644 --- a/apps/mail/messages/hi.json +++ b/apps/mail/messages/hi.json @@ -370,7 +370,8 @@ "failedToMute": "म्यूट करने में विफल", "failedToUnmute": "अनम्यूट करने में विफल", "archived": "आर्काइव किया गया", - "failedToArchive": "आर्काइव करने में विफल" + "failedToArchive": "आर्काइव करने में विफल", + "openInNewTab": "नए टैब में खोलें" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/hu.json b/apps/mail/messages/hu.json index 4b3c028aba..7ff2a472a1 100644 --- a/apps/mail/messages/hu.json +++ b/apps/mail/messages/hu.json @@ -370,7 +370,8 @@ "failedToMute": "Némítás sikertelen", "failedToUnmute": "Némítás feloldása sikertelen", "archived": "Archiválva", - "failedToArchive": "Archiválás sikertelen" + "failedToArchive": "Archiválás sikertelen", + "openInNewTab": "Megnyitás új lapon" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/ja.json b/apps/mail/messages/ja.json index 41d3d32a1e..7e13a817e3 100644 --- a/apps/mail/messages/ja.json +++ b/apps/mail/messages/ja.json @@ -370,7 +370,8 @@ "failedToMute": "ミュートに失敗しました", "failedToUnmute": "ミュート解除に失敗しました", "archived": "アーカイブしました", - "failedToArchive": "アーカイブに失敗しました" + "failedToArchive": "アーカイブに失敗しました", + "openInNewTab": "新しいタブで開く" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/ko.json b/apps/mail/messages/ko.json index e4fa26eef8..1e21b21426 100644 --- a/apps/mail/messages/ko.json +++ b/apps/mail/messages/ko.json @@ -370,7 +370,8 @@ "failedToMute": "음소거 실패", "failedToUnmute": "음소거 해제 실패", "archived": "보관됨", - "failedToArchive": "보관 실패" + "failedToArchive": "보관 실패", + "openInNewTab": "새 탭에서 열기" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/lv.json b/apps/mail/messages/lv.json index 6e6f622af7..86e938b2d4 100644 --- a/apps/mail/messages/lv.json +++ b/apps/mail/messages/lv.json @@ -370,7 +370,8 @@ "failedToMute": "Neizdevās apklusināt", "failedToUnmute": "Neizdevās atjaunot skaņu", "archived": "Arhivēts", - "failedToArchive": "Neizdevās arhivēt" + "failedToArchive": "Neizdevās arhivēt", + "openInNewTab": "Atvērt jaunā cilnē" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/nl.json b/apps/mail/messages/nl.json index 9c099e23c5..5d259db1ef 100644 --- a/apps/mail/messages/nl.json +++ b/apps/mail/messages/nl.json @@ -370,7 +370,8 @@ "failedToMute": "Dempen mislukt", "failedToUnmute": "Dempen opheffen mislukt", "archived": "Gearchiveerd", - "failedToArchive": "Archiveren mislukt" + "failedToArchive": "Archiveren mislukt", + "openInNewTab": "Openen in nieuw tabblad" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/pl.json b/apps/mail/messages/pl.json index 7f509df2e0..fa847d5c20 100644 --- a/apps/mail/messages/pl.json +++ b/apps/mail/messages/pl.json @@ -370,7 +370,8 @@ "failedToMute": "Nie udało się wyciszyć", "failedToUnmute": "Nie udało się wyłączyć wyciszenia", "archived": "Zarchiwizowano", - "failedToArchive": "Nie udało się zarchiwizować" + "failedToArchive": "Nie udało się zarchiwizować", + "openInNewTab": "Otwórz w nowej karcie" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/pt.json b/apps/mail/messages/pt.json index fcf438dcbe..f76c3c0fd7 100644 --- a/apps/mail/messages/pt.json +++ b/apps/mail/messages/pt.json @@ -370,7 +370,8 @@ "failedToMute": "Falha ao silenciar", "failedToUnmute": "Falha ao ativar som", "archived": "Arquivado", - "failedToArchive": "Falha ao arquivar" + "failedToArchive": "Falha ao arquivar", + "openInNewTab": "Abrir em nova aba" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/ru.json b/apps/mail/messages/ru.json index e2c5bb0e81..a5f1448552 100644 --- a/apps/mail/messages/ru.json +++ b/apps/mail/messages/ru.json @@ -370,7 +370,8 @@ "failedToMute": "Не удалось заглушить", "failedToUnmute": "Не удалось включить звук", "archived": "В архиве", - "failedToArchive": "Не удалось архивировать" + "failedToArchive": "Не удалось архивировать", + "openInNewTab": "Открыть в новой вкладке" }, "units": { "mb": "{amount} МБ" diff --git a/apps/mail/messages/tr.json b/apps/mail/messages/tr.json index 238b3039ae..bb8071b7fd 100644 --- a/apps/mail/messages/tr.json +++ b/apps/mail/messages/tr.json @@ -370,7 +370,8 @@ "failedToMute": "Sessize alınamadı", "failedToUnmute": "Ses açılamadı", "archived": "Arşivlendi", - "failedToArchive": "Arşivlenemedi" + "failedToArchive": "Arşivlenemedi", + "openInNewTab": "Yeni sekmede aç" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/vi.json b/apps/mail/messages/vi.json index d95ebba7b4..22b8870784 100644 --- a/apps/mail/messages/vi.json +++ b/apps/mail/messages/vi.json @@ -370,7 +370,8 @@ "failedToMute": "Không thể tắt tiếng", "failedToUnmute": "Không thể bật tiếng", "archived": "Đã lưu trữ", - "failedToArchive": "Không thể lưu trữ" + "failedToArchive": "Không thể lưu trữ", + "openInNewTab": "Mở trong tab mới" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/zh_CN.json b/apps/mail/messages/zh_CN.json index af8d4353a1..a41a91115a 100644 --- a/apps/mail/messages/zh_CN.json +++ b/apps/mail/messages/zh_CN.json @@ -370,7 +370,8 @@ "failedToMute": "静音失败", "failedToUnmute": "取消静音失败", "archived": "已归档", - "failedToArchive": "归档失败" + "failedToArchive": "归档失败", + "openInNewTab": "在新标签页中打开" }, "units": { "mb": "{amount} MB" diff --git a/apps/mail/messages/zh_TW.json b/apps/mail/messages/zh_TW.json index f412931ac7..f1fa4d5f22 100644 --- a/apps/mail/messages/zh_TW.json +++ b/apps/mail/messages/zh_TW.json @@ -370,7 +370,8 @@ "failedToMute": "靜音失敗", "failedToUnmute": "取消靜音失敗", "archived": "已封存", - "failedToArchive": "封存失敗" + "failedToArchive": "封存失敗", + "openInNewTab": "在新分頁中開啟" }, "units": { "mb": "{amount} MB" diff --git a/i18n.lock b/i18n.lock index c6d90c6fe4..8b777ebbcf 100644 --- a/i18n.lock +++ b/i18n.lock @@ -830,6 +830,7 @@ checksums: common/mail/failedToUnmute: 9a9c946ac9980a7845122a5c3bae8f13 common/mail/archived: cf5127ecfd7e43a35466a1ba5fe16450 common/mail/failedToArchive: e844b73a518542027803b7ff97e82b89 + common/mail/openInNewTab: 6844e4922a7a40a7ee25c10ea109cdeb common/units/mb: ed9c3b369a1ac40741b182458b69143a common/labels/color: 9d53d1d120e8b8954bcae9a322573748 common/labels/labelName: 682eade7443c047bc680664f821c9e0d From 6020305afde2e6e563b43bca0bcbaece0362bdb0 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:05:01 -0700 Subject: [PATCH 31/38] hotfix stuff (#1703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Optimized Gmail label fetching by implementing parallel requests with error handling using Effect.js. This change improves performance when retrieving label information by making concurrent requests instead of sequential ones. Also removed an unused `Check` icon import from the command palette context. --- ## Type of Change - [x] 🐛 Bug fix (non-breaking change which fixes an issue) - [x] ⚡ Performance improvement ## Areas Affected - [x] Email Integration (Gmail, IMAP, etc.) ## Testing Done - [x] Manual testing performed ## Checklist - [x] I have performed a self-review of my code - [x] My changes generate no new warnings ## Additional Notes The previous implementation was making sequential API calls to fetch label information, which could be slow when a user has many labels. This change uses Effect.js to make concurrent requests with proper error handling, significantly improving performance. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- .../context/command-palette-context.tsx | 1 - apps/server/src/lib/driver/google.ts | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/mail/components/context/command-palette-context.tsx b/apps/mail/components/context/command-palette-context.tsx index b9da0c4cf5..83e54698ff 100644 --- a/apps/mail/components/context/command-palette-context.tsx +++ b/apps/mail/components/context/command-palette-context.tsx @@ -1,7 +1,6 @@ import { ArrowRight, Calendar as CalendarIcon, - Check, Clock, FileText, Filter, diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index ac02e89d4a..3b052ff22c 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -8,6 +8,7 @@ import { sanitizeContext, StandardizedError, } from './utils'; +import { Effect } from 'effect'; import { mapGoogleLabelColor, mapToGoogleLabelColor } from './google-label-color-map'; import { parseAddressList, parseFrom, wasSentWithTLS } from '../email-utils'; import type { IOutgoingMessage, Label, ParsedMessage } from '../../types'; @@ -189,18 +190,25 @@ export class GoogleMailManager implements MailManager { if (!userLabels.data.labels) { return []; } - return Promise.all( - userLabels.data.labels.map(async (label) => { - const res = await this.gmail.users.labels.get({ + + const labelRequests = userLabels.data.labels.map((label) => + Effect.tryPromise({ + try: () => this.gmail.users.labels.get({ userId: 'me', id: label.id ?? undefined, - }); - return { - label: res.data.name ?? res.data.id ?? '', - count: Number(res.data.threadsUnread), - }; - }), + }), + catch: (error) => ({ _tag: 'LabelFetchFailed' as const, error }), + }) ); + + const results = await Effect.runPromise( + Effect.all(labelRequests, { concurrency: 'unbounded' }) + ); + + return results.map((res) => ({ + label: res.data.name ?? res.data.id ?? '', + count: Number(res.data.threadsUnread), + })); }, { email: this.config.auth?.email }, ); From 7bca596dc3f1ee71103d6e9d7bcc7f9151392643 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:21:19 -0700 Subject: [PATCH 32/38] minor fix (#1704) 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._ --- ## Summary by cubic Fixed an issue where microphone permission state was not updated after requesting access in the voice provider. --- apps/mail/lib/email-utils.client.tsx | 1 + apps/mail/providers/voice-provider.tsx | 1 + apps/server/src/routes/chat.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/apps/mail/lib/email-utils.client.tsx b/apps/mail/lib/email-utils.client.tsx index 72608b13a1..3094971477 100644 --- a/apps/mail/lib/email-utils.client.tsx +++ b/apps/mail/lib/email-utils.client.tsx @@ -69,6 +69,7 @@ export const highlightText = (text: string, highlight: string) => { const regex = new RegExp(`(${escapedHighlight})`, 'gi'); if (!regex.test(text)) return text; + regex.lastIndex = 0; const parts = text.split(regex); diff --git a/apps/mail/providers/voice-provider.tsx b/apps/mail/providers/voice-provider.tsx index d2306edb5f..e571bb75d6 100644 --- a/apps/mail/providers/voice-provider.tsx +++ b/apps/mail/providers/voice-provider.tsx @@ -83,6 +83,7 @@ export function VoiceProvider({ children }: { children: ReactNode }) { if (!hasPermission) { const result = await requestPermission(); if (!result) return; + setHasPermission(result); } try { diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 0e04761c18..460fe02c6d 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -920,10 +920,12 @@ export class ZeroAgent extends AIChatAgent { this.syncThreadsInProgress.delete(threadId); return { success: true, threadId, threadData }; } else { + this.syncThreadsInProgress.delete(threadId); console.log(`Skipping thread ${threadId} - no latest message`); return { success: false, threadId, reason: 'No latest message' }; } } catch (error) { + this.syncThreadsInProgress.delete(threadId); console.error(`Failed to sync thread ${threadId}:`, error); throw error; } From 00b9d8c230a729cebf350b7b3f68734890659ce6 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:43:29 -0700 Subject: [PATCH 33/38] disable workflows (#1705) 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._ --- ## Summary by cubic Added a DISABLE_WORKFLOWS environment variable to allow disabling workflow execution. When enabled, workflow-related API requests return early with a success message. - **Dependencies** - Updated wrangler.jsonc to set DISABLE_WORKFLOWS for all environments. --- apps/server/src/main.ts | 3 ++- apps/server/wrangler.jsonc | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 10fe0b4b09..e13727dfe2 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -16,8 +16,8 @@ import { } from './db/schema'; import { env, WorkerEntrypoint, DurableObject, RpcTarget } from 'cloudflare:workers'; import { EProviders, type ISubscribeBatch, type IThreadBatch } from './types'; -import { getZeroDB, verifyToken } from './lib/server-utils'; import { oAuthDiscoveryMetadata } from 'better-auth/plugins'; +import { getZeroDB, verifyToken } from './lib/server-utils'; import { eq, and, desc, asc, inArray } from 'drizzle-orm'; import { EWorkflowType, runWorkflow } from './pipelines'; import { contextStorage } from 'hono/context-storage'; @@ -644,6 +644,7 @@ export default class extends WorkerEntrypoint { .get('/', (c) => c.redirect(`${env.VITE_PUBLIC_APP_URL}`)) .post('/a8n/notify/:providerId', async (c) => { if (!c.req.header('Authorization')) return c.json({ error: 'Unauthorized' }, { status: 401 }); + if (env.DISABLE_WORKFLOWS === 'true') return c.json({ message: 'OK' }, { status: 200 }); const providerId = c.req.param('providerId'); if (providerId === EProviders.google) { const body = await c.req.json<{ historyId: string }>(); diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 2707e45349..2ddf4047e1 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -103,6 +103,7 @@ "DROP_AGENT_TABLES": "true", "THREAD_SYNC_MAX_COUNT": "40", "THREAD_SYNC_LOOP": "false", + "DISABLE_WORKFLOWS": "true", }, "kv_namespaces": [ { @@ -230,6 +231,7 @@ "DROP_AGENT_TABLES": "false", "THREAD_SYNC_MAX_COUNT": "40", "THREAD_SYNC_LOOP": "true", + "DISABLE_WORKFLOWS": "true", }, "kv_namespaces": [ { @@ -354,6 +356,7 @@ "DROP_AGENT_TABLES": "false", "THREAD_SYNC_MAX_COUNT": "40", "THREAD_SYNC_LOOP": "true", + "DISABLE_WORKFLOWS": "true", }, "kv_namespaces": [ { From c357fbf6728fb86027d976b054b3f1e393dced67 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:11:44 -0700 Subject: [PATCH 34/38] delay (#1706) 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._ --- ## Summary by cubic Added 2-second delays before and after syncing each thread in the chat route to prevent API rate limiting. --- apps/server/src/routes/chat.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 460fe02c6d..b68c47340e 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -969,9 +969,14 @@ export class ZeroAgent extends AIChatAgent { pageToken: pageToken || undefined, }); + // Need delay to avoid rate limiting + await new Promise((resolve) => setTimeout(resolve, 2000)); + for (const thread of result.threads) { try { await this.syncThread(thread.id); + // Need delay to avoid rate limiting + await new Promise((resolve) => setTimeout(resolve, 2000)); } catch (error) { console.error(`Failed to sync thread ${thread.id}:`, error); } From 5cb451161172ed24cdf0c50dd6f12abb95dc8096 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:19:28 -0700 Subject: [PATCH 35/38] Sync delay (#1707) 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._ --- ## Summary by cubic Added automatic retry logic for Gmail API rate limit errors to reduce sync failures and improve reliability. - **Bug Fixes** - Detects Gmail rate limit errors and retries failed requests up to 10 times with a 60-second delay between attempts. --- apps/server/src/lib/gmail-rate-limit.ts | 43 +++++++++++++++++++++++++ apps/server/src/routes/chat.ts | 26 +++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/lib/gmail-rate-limit.ts diff --git a/apps/server/src/lib/gmail-rate-limit.ts b/apps/server/src/lib/gmail-rate-limit.ts new file mode 100644 index 0000000000..5ca55e4112 --- /dev/null +++ b/apps/server/src/lib/gmail-rate-limit.ts @@ -0,0 +1,43 @@ +import { Effect, Duration, Schedule } from 'effect'; + +/** + * Gmail signals per-user quota problems in two ways: + * – HTTP 429 Too Many Requests + * – HTTP 403 with reason == userRateLimitExceeded or quotaExceeded + */ +export function isGmailRateLimit(err: unknown): boolean { + const e: any = err || {}; + const status = e.code ?? e.status ?? e.response?.status; + + if (status === 429) return true; + if (status === 403) { + const errors = e.errors ?? + e.response?.data?.error?.errors ?? + []; + return errors.some((x: any) => + ['userRateLimitExceeded', 'rateLimitExceeded', 'quotaExceeded', + 'dailyLimitExceeded', 'backendError', 'limitExceeded'].includes(x.reason) + ); + } + return false; +} + +/** + * A schedule that: + * – retries while the error *is* a rate-limit error (max 10 attempts) + * – waits 60 seconds between retries (conservative for Gmail user quotas) + * – stops immediately for any other error + */ +export const gmailRateLimitSchedule = Schedule + .recurWhile(isGmailRateLimit) + .pipe(Schedule.intersect(Schedule.recurs(10))) // max 10 attempts + .pipe(Schedule.addDelay(() => Duration.seconds(60))); // 60s delay between retries + +/** + * Generic wrapper that applies the schedule + */ +export function withGmailRetry( + eff: Effect.Effect, +): Effect.Effect { + return eff.pipe(Effect.retry(gmailRateLimitSchedule)); +} diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index b68c47340e..0617372ea2 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -47,6 +47,8 @@ import { McpAgent } from 'agents/mcp'; import { createDb } from '../db'; import { z } from 'zod'; +import { Effect } from 'effect'; +import { withGmailRetry } from '../lib/gmail-rate-limit'; const decoder = new TextDecoder(); @@ -881,7 +883,7 @@ export class ZeroAgent extends AIChatAgent { this.syncThreadsInProgress.set(threadId, true); try { - const threadData = await this.driver.get(threadId); + const threadData = await this.getWithRetry(threadId); const latest = threadData.latest; if (latest) { @@ -935,6 +937,26 @@ export class ZeroAgent extends AIChatAgent { return `${this.name}/${threadId}.json`; } + private async listWithRetry(params: Parameters[0]) { + if (!this.driver) throw new Error('No driver available'); + + return Effect.runPromise( + withGmailRetry( + Effect.tryPromise(() => this.driver!.list(params)) + ), + ); + } + + private async getWithRetry(threadId: string): Promise { + if (!this.driver) throw new Error('No driver available'); + + return Effect.runPromise( + withGmailRetry( + Effect.tryPromise(() => this.driver!.get(threadId)) + ), + ); + } + async syncThreads(folder: string) { if (!this.driver) { console.error('No driver available for syncThreads'); @@ -963,7 +985,7 @@ export class ZeroAgent extends AIChatAgent { while (hasMore) { _pageCount++; - const result = await this.driver.list({ + const result = await this.listWithRetry({ folder, maxResults: maxCount, pageToken: pageToken || undefined, From 699d8f26e4de85191a28c7586c7534060e306027 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:53:56 -0700 Subject: [PATCH 36/38] hotfix deployment (#1708) 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._ --- ## Summary by cubic Added the ZeroAgent class to the v4 migration in wrangler.jsonc to support new SQLite features in deployment. --- apps/server/wrangler.jsonc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 2ddf4047e1..16ea126b5f 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -78,6 +78,10 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, + { + "tag": "v4", + "new_sqlite_classes": ["ZeroAgent"], + }, ], "observability": { @@ -211,6 +215,10 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, + { + "tag": "v4", + "new_sqlite_classes": ["ZeroAgent"], + }, ], "observability": { "enabled": true, @@ -346,6 +354,10 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, + { + "tag": "v4", + "new_sqlite_classes": ["ZeroAgent"], + }, ], "vars": { "NODE_ENV": "production", From 5d7421194cd014a39aac9e878310e6c79ddc8543 Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Thu, 10 Jul 2025 18:40:26 -0700 Subject: [PATCH 37/38] no migration (#1709) 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._ --- ## Summary by cubic Removed the v4 migration steps for the ZeroAgent class from wrangler.jsonc to prevent unnecessary migrations. ## Summary by CodeRabbit * **Bug Fixes** * Improved label filtering in the mail app to display only relevant system labels with normalized names. * **Chores** * Updated migration configuration by removing an obsolete migration step related to database classes. --- apps/mail/hooks/use-labels.ts | 21 ++++++++++++++++++++- apps/server/wrangler.jsonc | 12 ------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/mail/hooks/use-labels.ts b/apps/mail/hooks/use-labels.ts index 3969b7601b..4e840a4ba6 100644 --- a/apps/mail/hooks/use-labels.ts +++ b/apps/mail/hooks/use-labels.ts @@ -2,6 +2,16 @@ import { useTRPC } from '@/providers/query-provider'; import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +const desiredSystemLabels = new Set([ + 'IMPORTANT', + 'FORUMS', + 'PROMOTIONS', + 'SOCIAL', + 'UPDATES', + 'STARRED', + 'UNREAD', +]); + export function useLabels() { const trpc = useTRPC(); const labelQuery = useQuery( @@ -12,9 +22,18 @@ export function useLabels() { const { userLabels, systemLabels } = useMemo(() => { if (!labelQuery.data) return { userLabels: [], systemLabels: [] }; + const cleanedName = labelQuery.data + .filter((label) => label.type === 'system') + .map((label) => { + return { + ...label, + name: label.name.replace('CATEGORY_', ''), + }; + }); + const cleanedSystemLabels = cleanedName.filter((label) => desiredSystemLabels.has(label.name)); return { userLabels: labelQuery.data.filter((label) => label.type === 'user'), - systemLabels: labelQuery.data.filter((label) => label.type === 'system'), + systemLabels: cleanedSystemLabels, }; }, [labelQuery.data]); diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 16ea126b5f..2ddf4047e1 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -78,10 +78,6 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, - { - "tag": "v4", - "new_sqlite_classes": ["ZeroAgent"], - }, ], "observability": { @@ -215,10 +211,6 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, - { - "tag": "v4", - "new_sqlite_classes": ["ZeroAgent"], - }, ], "observability": { "enabled": true, @@ -354,10 +346,6 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, - { - "tag": "v4", - "new_sqlite_classes": ["ZeroAgent"], - }, ], "vars": { "NODE_ENV": "production", From 992dc8079aa3c1256ddcb0ff63d30e57b25d81ef Mon Sep 17 00:00:00 2001 From: Adam <13007539+MrgSub@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:30:08 -0700 Subject: [PATCH 38/38] fix deployment (#1712) 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._ --- ## Summary by cubic Fixed deployment issues by updating concurrency settings, improving Gmail API calls, and adjusting wrangler configuration for new classes. - **Bug Fixes** - Set thread processing concurrency to 1 to prevent overload. - Improved Gmail API label and thread fetching logic. - Added delay in thread syncing to reduce rate limit errors. - **Dependencies** - Updated wrangler.jsonc to register new SQLite classes for deployment. --- apps/server/src/lib/driver/google.ts | 20 +++++++++++--------- apps/server/src/pipelines.effect.ts | 2 +- apps/server/src/routes/chat.ts | 18 ++++++------------ apps/server/wrangler.jsonc | 12 ++++++++++++ 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index 3b052ff22c..c65f7adb04 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -8,7 +8,6 @@ import { sanitizeContext, StandardizedError, } from './utils'; -import { Effect } from 'effect'; import { mapGoogleLabelColor, mapToGoogleLabelColor } from './google-label-color-map'; import { parseAddressList, parseFrom, wasSentWithTLS } from '../email-utils'; import type { IOutgoingMessage, Label, ParsedMessage } from '../../types'; @@ -21,6 +20,7 @@ import { createMimeMessage } from 'mimetext'; import { people } from '@googleapis/people'; import { cleanSearchValue } from '../utils'; import { env } from 'cloudflare:workers'; +import { Effect } from 'effect'; import * as he from 'he'; export class GoogleMailManager implements MailManager { @@ -190,19 +190,20 @@ export class GoogleMailManager implements MailManager { if (!userLabels.data.labels) { return []; } - + const labelRequests = userLabels.data.labels.map((label) => Effect.tryPromise({ - try: () => this.gmail.users.labels.get({ - userId: 'me', - id: label.id ?? undefined, - }), + try: () => + this.gmail.users.labels.get({ + userId: 'me', + id: label.id ?? undefined, + }), catch: (error) => ({ _tag: 'LabelFetchFailed' as const, error }), - }) + }), ); const results = await Effect.runPromise( - Effect.all(labelRequests, { concurrency: 'unbounded' }) + Effect.all(labelRequests, { concurrency: 'unbounded' }), ); return results.map((res) => ({ @@ -785,7 +786,8 @@ export class GoogleMailManager implements MailManager { const res = await this.gmail.users.threads.get({ userId: 'me', id: threadId, - format: 'metadata', // Fetch only metadata + format: 'metadata', // Fetch only metadata, + quotaUser: this.config.auth?.email, }); // Process res.data.messages to extract id and labelIds return { diff --git a/apps/server/src/pipelines.effect.ts b/apps/server/src/pipelines.effect.ts index eb1502ce2a..86d5e70b13 100644 --- a/apps/server/src/pipelines.effect.ts +++ b/apps/server/src/pipelines.effect.ts @@ -329,7 +329,7 @@ export const runZeroWorkflow = ( ); }), ), - { concurrency: 5 }, // Process up to 5 threads concurrently + { concurrency: 1 }, // Process up to 5 threads concurrently ); yield* Console.log('[ZERO_WORKFLOW] All thread workflows completed:', threadResults.length); diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 0617372ea2..2c97ca5490 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -45,10 +45,10 @@ import { FOLDERS } from '../lib/utils'; import { and, eq } from 'drizzle-orm'; import { McpAgent } from 'agents/mcp'; +import { withGmailRetry } from '../lib/gmail-rate-limit'; import { createDb } from '../db'; -import { z } from 'zod'; import { Effect } from 'effect'; -import { withGmailRetry } from '../lib/gmail-rate-limit'; +import { z } from 'zod'; const decoder = new TextDecoder(); @@ -940,21 +940,13 @@ export class ZeroAgent extends AIChatAgent { private async listWithRetry(params: Parameters[0]) { if (!this.driver) throw new Error('No driver available'); - return Effect.runPromise( - withGmailRetry( - Effect.tryPromise(() => this.driver!.list(params)) - ), - ); + return Effect.runPromise(withGmailRetry(Effect.tryPromise(() => this.driver!.list(params)))); } private async getWithRetry(threadId: string): Promise { if (!this.driver) throw new Error('No driver available'); - return Effect.runPromise( - withGmailRetry( - Effect.tryPromise(() => this.driver!.get(threadId)) - ), - ); + return Effect.runPromise(withGmailRetry(Effect.tryPromise(() => this.driver!.get(threadId)))); } async syncThreads(folder: string) { @@ -985,6 +977,8 @@ export class ZeroAgent extends AIChatAgent { while (hasMore) { _pageCount++; + await new Promise((resolve) => setTimeout(resolve, 2000)); + const result = await this.listWithRetry({ folder, maxResults: maxCount, diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 2ddf4047e1..16ea126b5f 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -78,6 +78,10 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, + { + "tag": "v4", + "new_sqlite_classes": ["ZeroAgent"], + }, ], "observability": { @@ -211,6 +215,10 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, + { + "tag": "v4", + "new_sqlite_classes": ["ZeroAgent"], + }, ], "observability": { "enabled": true, @@ -346,6 +354,10 @@ "tag": "v3", "new_classes": ["ZeroDB"], }, + { + "tag": "v4", + "new_sqlite_classes": ["ZeroAgent"], + }, ], "vars": { "NODE_ENV": "production",