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] 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 | 53 +- 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 | 42 +- 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, 758 insertions(+), 2743 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 ad4f16ab..ebc0a558 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 a11c0453..952f3a46 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 00000000..2af0b8ba --- /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 b97c35e3..7df1f637 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 5bef425a..d7248fc5 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 8cd0f225..693c04d6 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, - }, -]; - -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.', - }, -]; 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 63d5785d..99f31b00 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 1bc11bcf..c570e2e1 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 8558675f..95780ce9 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 d1ea0028..d6805729 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 52642548..94a44453 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 c9838474..c58e905f 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 db630980..189f749f 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 89e8dadd..75a787f2 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 76429a79..b9bf5a29 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 059356a0..e0e4a906 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 e3eee76d..8ddaefe3 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 377967fc..c4266667 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 31188096..b9da0c4c 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 580af18d..d98ca528 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 ca61dedf..967505ac 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 50faed6b..e9c40510 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 1f55686b..a3422452 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 44b67079..00000000 --- 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 6bca3870..00000000 --- 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 adb241db..ee903f81 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 fbf8b925..dcd64c5d 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 aa3ac4a6..5a1f6764 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 681c749c..bf040596 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 e69de29b..00000000 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 f3b21c8e..00000000 --- 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 0b9d4e08..00000000 --- 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 d44bb17f..00000000 --- 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 fb51b528..713449d2 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 1c090f66..1730bfce 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 db54360f..5e79f871 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 2f05e1a3..49741635 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 5efc310e..1b056250 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() {
-
+
Resources
@@ -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 9774098b..725e8423 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 85c5a715..25fd7028 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 3b867dc2..0f275ced 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 3f999e1c..acb13f3b 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 54ae4a9b..ed707f73 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 a2b7dbbc..6bf7c7e2 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 9ab85c08..b254d584 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 e69de29b..00000000 diff --git a/apps/mail/components/ui/ai-sidebar.tsx b/apps/mail/components/ui/ai-sidebar.tsx index 454e6d50..697eee8e 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 56ccac72..cdb68e79 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 e69de29b..00000000 diff --git a/apps/mail/components/ui/dialog.tsx b/apps/mail/components/ui/dialog.tsx index 014ed6bf..945f156d 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 219923ba..dc2b8f17 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 f12ce2b6..91ee06fc 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 cce4e8a9..a8b4092b 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -1,15 +1,3 @@ -import { - HelpCircle, - LogIn, - LogOut, - MoonIcon, - Settings, - Plus, - BrainIcon, - CopyCheckIcon, - BadgeCheck, - BanknoteIcon, -} from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -17,25 +5,31 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { + HelpCircle, + LogOut, + MoonIcon, + Settings, + Plus, + CopyCheckIcon, + BadgeCheck, + BanknoteIcon, +} from 'lucide-react'; 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 69f000f2..6a0d2675 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 87713ae8..ce505b39 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 9de6eb74..7bfd954c 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 cd29b87f..b735c0cd 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 e69de29b..00000000 diff --git a/apps/mail/components/voice-button.tsx b/apps/mail/components/voice-button.tsx index 2f80d57c..9e10fdbb 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 23ed5562..266b9c42 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 cc751ff3..fb4bd9e8 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 7852d75e..16e64622 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 2b432652..de1eee58 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 c67666c0..ae9f3d02 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 281c6a91..f6180662 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 159c1c8d..2968d1d7 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 981cc766..36c23497 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 92e74da6..f3750958 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 64ca336f..72608b13 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 b1e0a14a..0b6f36ad 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 cdcc68ef..746bcdf8 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 6cc01a94..e3d059b9 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 e69de29b..00000000 diff --git a/apps/mail/lib/timezones.ts b/apps/mail/lib/timezones.ts index 71f02ffa..1a33423e 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 9d2494eb..4bcfb479 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 847bc3c9..00000000 --- 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 bd8ddb34..c68bbb23 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 d1986dee..6a0b6c35 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 4bc091c4..d2306edb 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}`); + 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 paramsWithContext = { + ...params, + _context: currentContext, + }; - const result = await executor(paramsWithContext); - console.log(`[Voice Tool] ${name} result:`, result); - setLastToolCall(null); - return result; - }, - }), - {}, - ), - }, + 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 3005da82..6ffa30c8 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 ae1733e8..21e23281 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 e1221375..ac02e89d 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 45fe3c63..bacce02b 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 fc5b062f..bdd04c0b 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 16f4b6e5..6b26a452 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 0d0838a3..127e9627 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 ca989d2d..a7d04169 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 ffef97de..171b0764 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 24b0e576..61d6a2df 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 a96062ab..6fbabcf1 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 71f02ffa..1a33423e 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 64ba0cc4..10fe0b4b 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 1d2e1334..eb1502ce 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 5e4129da..5fca8f23 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 0b571eb9..751c0232 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 85663a30..0e04761c 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 3ff1e1f8..0a98bee9 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 4276c542..c468f8c6 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 1ae84d99..0ddc6ff3 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 281bf0d7..4bbe0381 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 f56b643e..503f8c88 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 dde22250..afafa631 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 e2947c02..5426e18c 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 c7b0f931..beb1a427 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 db43fb95..62ddcc57 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 e3da3122..00000000 --- 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, -// }, -// });