From c1543ce859d5e4431a9fb8a44fba769a8c6d2ebf Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Fri, 4 Jul 2025 18:43:24 +0100 Subject: [PATCH 01/28] add emoji shortcodes to tiptap (#1595) # Add emoji support to mail compose editor This PR adds emoji support to the mail compose editor by integrating the TipTap emoji extension. The implementation: 1. Adds the `@tiptap/extension-emoji` package as a dependency 2. Configures the emoji extension in the compose editor with GitHub emojis 3. Enables emoticon support for automatic conversion of text like `:)` or `:smiley:` to emoji ## Summary by CodeRabbit * **New Features** * Emoji shortcodes typed in the email subject (e.g., :smile:) are now automatically converted to actual emojis. * The email editor now supports emoji input using an enhanced emoji picker. * **Style** * Improved formatting for image compression savings and attachment warning dialog for better readability. * **Chores** * Updated and reorganized package dependencies for improved project maintenance. --- .../mail/components/create/email-composer.tsx | 36 +++++++++++----- apps/mail/hooks/use-compose-editor.ts | 6 +++ apps/mail/package.json | 1 + pnpm-lock.yaml | 41 +++++++++++++++++++ 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index fbc0571c04..241823cbde 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -16,17 +16,22 @@ import { import { Check, Command, Loader, Paperclip, Plus, X as XIcon } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TextEffect } from '@/components/motion-primitives/text-effect'; +import { ImageCompressionSettings } from './image-compression-settings'; import { useActiveConnection } from '@/hooks/use-connections'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useEmailAliases } from '@/hooks/use-email-aliases'; +import type { ImageQuality } from '@/lib/image-compression'; import useComposeEditor from '@/hooks/use-compose-editor'; import { CurvedArrow, Sparkles, X } from '../icons/icons'; +import { compressImages } from '@/lib/image-compression'; +import { gitHubEmojis } from '@tiptap/extension-emoji'; import { AnimatePresence, motion } from 'motion/react'; import { zodResolver } from '@hookform/resolvers/zod'; 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'; @@ -38,10 +43,6 @@ import { useQueryState } from 'nuqs'; import pluralize from 'pluralize'; import { toast } from 'sonner'; import { z } from 'zod'; -import { ImageCompressionSettings } from './image-compression-settings'; -import { compressImages } from '@/lib/image-compression'; -import type { ImageQuality } from '@/lib/image-compression'; -import { useIsMobile } from '@/hooks/use-mobile'; type ThreadContent = { from: string; @@ -190,7 +191,10 @@ export function EmailComposer({ }); if (totalOriginalSize > totalCompressedSize) { - const savings = (((totalOriginalSize - totalCompressedSize) / totalOriginalSize) * 100).toFixed(1); + const savings = ( + ((totalOriginalSize - totalCompressedSize) / totalOriginalSize) * + 100 + ).toFixed(1); if (parseFloat(savings) > 0.1) { toast.success(`Images compressed: ${savings}% smaller`); } @@ -686,6 +690,17 @@ export function EmailComposer({ await processAndSetAttachments(originalAttachments, newQuality, true); }; + const replaceEmojiShortcodes = (text: string): string => { + const shortcodeRegex = /:([a-zA-Z0-9_+-]+):/g; + + return text.replace(shortcodeRegex, (match, shortcode): string => { + const emoji = gitHubEmojis.find( + (e) => e.shortcodes.includes(shortcode) || e.name === shortcode, + ); + return emoji?.emoji ?? match; + }); + }; + return (
{ - setValue('subject', e.target.value); + const value = replaceEmojiShortcodes(e.target.value); + setValue('subject', value); setHasUnsavedChanges(true); }} /> @@ -1337,7 +1353,7 @@ export function EmailComposer({ {pluralize('file', attachments.length, true)}

- +
- +
{attachments.map((file: File, index: number) => { const nameParts = file.name.split('.'); @@ -1538,8 +1554,8 @@ export function EmailComposer({ Attachment Warning - Looks like you mentioned an attachment in your message, but there are no files attached. - Are you sure you want to send this email? + Looks like you mentioned an attachment in your message, but there are no files + attached. Are you sure you want to send this email? diff --git a/apps/mail/hooks/use-compose-editor.ts b/apps/mail/hooks/use-compose-editor.ts index 71d0c3b79a..7852d75e4a 100644 --- a/apps/mail/hooks/use-compose-editor.ts +++ b/apps/mail/hooks/use-compose-editor.ts @@ -1,6 +1,7 @@ import { useEditor, type KeyboardShortcutCommand, Extension, generateJSON } from '@tiptap/react'; import { AutoComplete } from '@/components/create/editor-autocomplete'; import { defaultExtensions } from '@/components/create/extensions'; +import Emoji, { gitHubEmojis } from '@tiptap/extension-emoji'; import { FileHandler } from '@tiptap/extension-file-handler'; import Placeholder from '@tiptap/extension-placeholder'; import { Plugin, PluginKey } from '@tiptap/pm/state'; @@ -258,6 +259,11 @@ const useComposeEditor = ({ Placeholder.configure({ placeholder, }), + Emoji.configure({ + emojis: gitHubEmojis, + enableEmoticons: true, + // suggestion, + }), // breaks the image upload // ...(onAttachmentsChange // ? [ diff --git a/apps/mail/package.json b/apps/mail/package.json index d433fa75be..bd8ddb349d 100644 --- a/apps/mail/package.json +++ b/apps/mail/package.json @@ -35,6 +35,7 @@ "@tiptap/core": "2.23.0", "@tiptap/extension-bold": "2.23.0", "@tiptap/extension-document": "2.23.0", + "@tiptap/extension-emoji": "2.23.1", "@tiptap/extension-file-handler": "2.23.0", "@tiptap/extension-image": "2.23.0", "@tiptap/extension-link": "2.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a300f3660..2f631d8ed8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,9 @@ importers: '@tiptap/extension-document': specifier: 2.23.0 version: 2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0)) + '@tiptap/extension-emoji': + specifier: 2.23.1 + version: 2.23.1(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0)(@tiptap/suggestion@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0))(emojibase@16.0.0) '@tiptap/extension-file-handler': specifier: 2.23.0 version: 2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/extension-text-style@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))) @@ -3435,6 +3438,13 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-emoji@2.23.1': + resolution: {integrity: sha512-bqTn+hbq0bDIcrPIIjVq3GndJ/PYQfReMDlyTv0mUCtRbP7zReJ1oFx02d25RmwgS6XL3U8WW4kEFomhliwWSQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/suggestion': ^2.7.0 + '@tiptap/extension-file-handler@2.23.0': resolution: {integrity: sha512-rTimkgFtMIbYYydf2suvIpF+GnFRU80BppnrOUNfW+HzaI0i1p0gKzEDKJuPBMAEFfG/Q7Yxetk6rO6Y5Sq6Mw==} peerDependencies: @@ -4628,6 +4638,15 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojibase-data@15.3.2: + resolution: {integrity: sha512-TpDyTDDTdqWIJixV5sTA6OQ0P0JfIIeK2tFRR3q56G9LK65ylAZ7z3KyBXokpvTTJ+mLUXQXbLNyVkjvnTLE+A==} + peerDependencies: + emojibase: '*' + + emojibase@16.0.0: + resolution: {integrity: sha512-Nw2m7JLIO4Ou2X/yZPRNscHQXVbbr6SErjkJ7EooG7MbR3yDZszCv9KTizsXFc7yZl0n3WF+qUKIC/Lw6H9xaQ==} + engines: {node: '>=18.12.0'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -5339,6 +5358,9 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-emoji-supported@0.0.5: + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -10372,6 +10394,17 @@ snapshots: '@tiptap/core': 2.23.0(@tiptap/pm@2.23.0) '@tiptap/pm': 2.23.0 + '@tiptap/extension-emoji@2.23.1(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0)(@tiptap/suggestion@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0))(emojibase@16.0.0)': + dependencies: + '@tiptap/core': 2.23.0(@tiptap/pm@2.23.0) + '@tiptap/pm': 2.23.0 + '@tiptap/suggestion': 2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/pm@2.23.0) + emoji-regex: 10.4.0 + emojibase-data: 15.3.2(emojibase@16.0.0) + is-emoji-supported: 0.0.5 + transitivePeerDependencies: + - emojibase + '@tiptap/extension-file-handler@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0))(@tiptap/extension-text-style@2.23.0(@tiptap/core@2.23.0(@tiptap/pm@2.23.0)))': dependencies: '@tiptap/core': 2.23.0(@tiptap/pm@2.23.0) @@ -11581,6 +11614,12 @@ snapshots: emoji-regex@9.2.2: {} + emojibase-data@15.3.2(emojibase@16.0.0): + dependencies: + emojibase: 16.0.0 + + emojibase@16.0.0: {} + encodeurl@2.0.0: {} encoding-sniffer@0.2.1: @@ -12518,6 +12557,8 @@ snapshots: is-decimal@2.0.1: {} + is-emoji-supported@0.0.5: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: From 9ff981c05795ecacb6a622db9dc62bf8d02373ea Mon Sep 17 00:00:00 2001 From: Sargam Date: Sat, 5 Jul 2025 00:09:16 +0545 Subject: [PATCH 02/28] perf: add indexes to improve query speed (#1613) --- .../src/db/migrations/0035_uneven_shiva.sql | 46 + .../src/db/migrations/meta/0035_snapshot.json | 1610 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 7 + apps/server/src/db/schema.ts | 345 ++-- 4 files changed, 1879 insertions(+), 129 deletions(-) create mode 100644 apps/server/src/db/migrations/0035_uneven_shiva.sql create mode 100644 apps/server/src/db/migrations/meta/0035_snapshot.json diff --git a/apps/server/src/db/migrations/0035_uneven_shiva.sql b/apps/server/src/db/migrations/0035_uneven_shiva.sql new file mode 100644 index 0000000000..86335fc541 --- /dev/null +++ b/apps/server/src/db/migrations/0035_uneven_shiva.sql @@ -0,0 +1,46 @@ +ALTER TABLE "mail0_account" DROP CONSTRAINT "mail0_account_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_connection" DROP CONSTRAINT "mail0_connection_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_session" DROP CONSTRAINT "mail0_session_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_user_hotkeys" DROP CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_user_settings" DROP CONSTRAINT "mail0_user_settings_user_id_mail0_user_id_fk"; +--> statement-breakpoint +ALTER TABLE "mail0_account" ADD CONSTRAINT "mail0_account_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_connection" ADD CONSTRAINT "mail0_connection_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_session" ADD CONSTRAINT "mail0_session_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_summary" ADD CONSTRAINT "mail0_summary_connection_id_mail0_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."mail0_connection"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_user_hotkeys" ADD CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mail0_user_settings" ADD CONSTRAINT "mail0_user_settings_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "account_user_id_idx" ON "mail0_account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "account_provider_user_id_idx" ON "mail0_account" USING btree ("provider_id","user_id");--> statement-breakpoint +CREATE INDEX "account_expires_at_idx" ON "mail0_account" USING btree ("access_token_expires_at");--> statement-breakpoint +CREATE INDEX "connection_user_id_idx" ON "mail0_connection" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "connection_expires_at_idx" ON "mail0_connection" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "connection_provider_id_idx" ON "mail0_connection" USING btree ("provider_id");--> statement-breakpoint +CREATE INDEX "early_access_is_early_access_idx" ON "mail0_early_access" USING btree ("is_early_access");--> statement-breakpoint +CREATE INDEX "jwks_created_at_idx" ON "mail0_jwks" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "note_user_id_idx" ON "mail0_note" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "note_thread_id_idx" ON "mail0_note" USING btree ("thread_id");--> statement-breakpoint +CREATE INDEX "note_user_thread_idx" ON "mail0_note" USING btree ("user_id","thread_id");--> statement-breakpoint +CREATE INDEX "note_is_pinned_idx" ON "mail0_note" USING btree ("is_pinned");--> statement-breakpoint +CREATE INDEX "oauth_access_token_user_id_idx" ON "mail0_oauth_access_token" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "oauth_access_token_client_id_idx" ON "mail0_oauth_access_token" USING btree ("client_id");--> statement-breakpoint +CREATE INDEX "oauth_access_token_expires_at_idx" ON "mail0_oauth_access_token" USING btree ("access_token_expires_at");--> statement-breakpoint +CREATE INDEX "oauth_application_user_id_idx" ON "mail0_oauth_application" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "oauth_application_disabled_idx" ON "mail0_oauth_application" USING btree ("disabled");--> statement-breakpoint +CREATE INDEX "oauth_consent_user_id_idx" ON "mail0_oauth_consent" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "oauth_consent_client_id_idx" ON "mail0_oauth_consent" USING btree ("client_id");--> statement-breakpoint +CREATE INDEX "oauth_consent_given_idx" ON "mail0_oauth_consent" USING btree ("consent_given");--> statement-breakpoint +CREATE INDEX "session_user_id_idx" ON "mail0_session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_expires_at_idx" ON "mail0_session" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "summary_connection_id_idx" ON "mail0_summary" USING btree ("connection_id");--> statement-breakpoint +CREATE INDEX "summary_connection_id_saved_idx" ON "mail0_summary" USING btree ("connection_id","saved");--> statement-breakpoint +CREATE INDEX "summary_saved_idx" ON "mail0_summary" USING btree ("saved");--> statement-breakpoint +CREATE INDEX "user_hotkeys_shortcuts_idx" ON "mail0_user_hotkeys" USING btree ("shortcuts");--> statement-breakpoint +CREATE INDEX "user_settings_settings_idx" ON "mail0_user_settings" USING btree ("settings");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "mail0_verification" USING btree ("identifier");--> statement-breakpoint +CREATE INDEX "verification_expires_at_idx" ON "mail0_verification" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "writing_style_matrix_style_idx" ON "mail0_writing_style_matrix" USING btree ("style"); \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/0035_snapshot.json b/apps/server/src/db/migrations/meta/0035_snapshot.json new file mode 100644 index 0000000000..211511a509 --- /dev/null +++ b/apps/server/src/db/migrations/meta/0035_snapshot.json @@ -0,0 +1,1610 @@ +{ + "id": "0b7459fd-f13f-4111-b30a-f0bd16a5b406", + "prevId": "185ab778-1f86-44d6-ac1a-90e9399b1342", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.mail0_account": { + "name": "mail0_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_provider_user_id_idx": { + "name": "account_provider_user_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_expires_at_idx": { + "name": "account_expires_at_idx", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_account_user_id_mail0_user_id_fk": { + "name": "mail0_account_user_id_mail0_user_id_fk", + "tableFrom": "mail0_account", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_connection": { + "name": "mail0_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_user_id_idx": { + "name": "connection_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_expires_at_idx": { + "name": "connection_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_provider_id_idx": { + "name": "connection_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_connection_user_id_mail0_user_id_fk": { + "name": "mail0_connection_user_id_mail0_user_id_fk", + "tableFrom": "mail0_connection", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_connection_user_id_email_unique": { + "name": "mail0_connection_user_id_email_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_early_access": { + "name": "mail0_early_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_early_access": { + "name": "is_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "has_used_ticket": { + "name": "has_used_ticket", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": { + "early_access_is_early_access_idx": { + "name": "early_access_is_early_access_idx", + "columns": [ + { + "expression": "is_early_access", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_early_access_email_unique": { + "name": "mail0_early_access_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_jwks": { + "name": "mail0_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "jwks_created_at_idx": { + "name": "jwks_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_note": { + "name": "mail0_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "note_user_id_idx": { + "name": "note_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_thread_id_idx": { + "name": "note_thread_id_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_user_thread_idx": { + "name": "note_user_thread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_is_pinned_idx": { + "name": "note_is_pinned_idx", + "columns": [ + { + "expression": "is_pinned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_note_user_id_mail0_user_id_fk": { + "name": "mail0_note_user_id_mail0_user_id_fk", + "tableFrom": "mail0_note", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_access_token": { + "name": "mail0_oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_user_id_idx": { + "name": "oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_oauth_access_token_access_token_unique": { + "name": "mail0_oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": [ + "access_token" + ] + }, + "mail0_oauth_access_token_refresh_token_unique": { + "name": "mail0_oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": [ + "refresh_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_application": { + "name": "mail0_oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_u_r_ls": { + "name": "redirect_u_r_ls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_application_user_id_idx": { + "name": "oauth_application_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_application_disabled_idx": { + "name": "oauth_application_disabled_idx", + "columns": [ + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_oauth_application_client_id_unique": { + "name": "mail0_oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_oauth_consent": { + "name": "mail0_oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_consent_user_id_idx": { + "name": "oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_consent_given_idx": { + "name": "oauth_consent_given_idx", + "columns": [ + { + "expression": "consent_given", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_session": { + "name": "mail0_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_expires_at_idx": { + "name": "session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_session_user_id_mail0_user_id_fk": { + "name": "mail0_session_user_id_mail0_user_id_fk", + "tableFrom": "mail0_session", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_session_token_unique": { + "name": "mail0_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_summary": { + "name": "mail0_summary", + "schema": "", + "columns": { + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "saved": { + "name": "saved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_reply": { + "name": "suggested_reply", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "summary_connection_id_idx": { + "name": "summary_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_connection_id_saved_idx": { + "name": "summary_connection_id_saved_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "saved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_saved_idx": { + "name": "summary_saved_idx", + "columns": [ + { + "expression": "saved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_summary_connection_id_mail0_connection_id_fk": { + "name": "mail0_summary_connection_id_mail0_connection_id_fk", + "tableFrom": "mail0_summary", + "tableTo": "mail0_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user": { + "name": "mail0_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "default_connection_id": { + "name": "default_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_prompt": { + "name": "custom_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number_verified": { + "name": "phone_number_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_email_unique": { + "name": "mail0_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "mail0_user_phone_number_unique": { + "name": "mail0_user_phone_number_unique", + "nullsNotDistinct": false, + "columns": [ + "phone_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_hotkeys": { + "name": "mail0_user_hotkeys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "shortcuts": { + "name": "shortcuts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_hotkeys_shortcuts_idx": { + "name": "user_hotkeys_shortcuts_idx", + "columns": [ + { + "expression": "shortcuts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_user_hotkeys_user_id_mail0_user_id_fk": { + "name": "mail0_user_hotkeys_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_hotkeys", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_user_settings": { + "name": "mail0_user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"language\":\"en\",\"timezone\":\"UTC\",\"dynamicContent\":false,\"externalImages\":true,\"customPrompt\":\"\",\"trustedSenders\":[],\"isOnboarded\":false,\"colorTheme\":\"system\",\"zeroSignature\":true,\"autoRead\":true,\"defaultEmailAlias\":\"\",\"categories\":[{\"id\":\"Important\",\"name\":\"Important\",\"searchValue\":\"is:important NOT is:sent NOT is:draft\",\"order\":0,\"icon\":\"Lightning\",\"isDefault\":false},{\"id\":\"All Mail\",\"name\":\"All Mail\",\"searchValue\":\"NOT is:draft (is:inbox OR (is:sent AND to:me))\",\"order\":1,\"icon\":\"Mail\",\"isDefault\":true},{\"id\":\"Personal\",\"name\":\"Personal\",\"searchValue\":\"is:personal NOT is:sent NOT is:draft\",\"order\":2,\"icon\":\"User\",\"isDefault\":false},{\"id\":\"Promotions\",\"name\":\"Promotions\",\"searchValue\":\"is:promotions NOT is:sent NOT is:draft\",\"order\":3,\"icon\":\"Tag\",\"isDefault\":false},{\"id\":\"Updates\",\"name\":\"Updates\",\"searchValue\":\"is:updates NOT is:sent NOT is:draft\",\"order\":4,\"icon\":\"Bell\",\"isDefault\":false},{\"id\":\"Unread\",\"name\":\"Unread\",\"searchValue\":\"is:unread NOT is:sent NOT is:draft\",\"order\":5,\"icon\":\"ScanEye\",\"isDefault\":false}],\"imageCompression\":\"medium\"}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_settings_settings_idx": { + "name": "user_settings_settings_idx", + "columns": [ + { + "expression": "settings", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_user_settings_user_id_mail0_user_id_fk": { + "name": "mail0_user_settings_user_id_mail0_user_id_fk", + "tableFrom": "mail0_user_settings", + "tableTo": "mail0_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail0_user_settings_user_id_unique": { + "name": "mail0_user_settings_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_verification": { + "name": "mail0_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail0_writing_style_matrix": { + "name": "mail0_writing_style_matrix", + "schema": "", + "columns": { + "connectionId": { + "name": "connectionId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "numMessages": { + "name": "numMessages", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "writing_style_matrix_style_idx": { + "name": "writing_style_matrix_style_idx", + "columns": [ + { + "expression": "style", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk": { + "name": "mail0_writing_style_matrix_connectionId_mail0_connection_id_fk", + "tableFrom": "mail0_writing_style_matrix", + "tableTo": "mail0_connection", + "columnsFrom": [ + "connectionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mail0_writing_style_matrix_connectionId_pk": { + "name": "mail0_writing_style_matrix_connectionId_pk", + "columns": [ + "connectionId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/src/db/migrations/meta/_journal.json b/apps/server/src/db/migrations/meta/_journal.json index e1106690ec..530acb75a0 100644 --- a/apps/server/src/db/migrations/meta/_journal.json +++ b/apps/server/src/db/migrations/meta/_journal.json @@ -253,6 +253,13 @@ "when": 1751008013033, "tag": "0034_mushy_runaways", "breakpoints": true + }, + { + "idx": 35, + "version": "7", + "when": 1751568728663, + "tag": "0035_uneven_shiva", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index ffdd57a967..bbe8016dee 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -7,6 +7,7 @@ import { jsonb, primaryKey, unique, + index, } from 'drizzle-orm/pg-core'; import { defaultUserSettings } from '../lib/schemas'; @@ -26,63 +27,93 @@ export const user = createTable('user', { phoneNumberVerified: boolean('phone_number_verified'), }); -export const session = createTable('session', { - id: text('id').primaryKey(), - expiresAt: timestamp('expires_at').notNull(), - token: text('token').notNull().unique(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id') - .notNull() - .references(() => user.id), -}); +export const session = createTable( + 'session', + { + id: text('id').primaryKey(), + expiresAt: timestamp('expires_at').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + }, + (t) => [ + index('session_user_id_idx').on(t.userId), + index('session_expires_at_idx').on(t.expiresAt), + ], +); -export const account = createTable('account', { - id: text('id').primaryKey(), - accountId: text('account_id').notNull(), - providerId: text('provider_id').notNull(), - userId: text('user_id') - .notNull() - .references(() => user.id), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - idToken: text('id_token'), - accessTokenExpiresAt: timestamp('access_token_expires_at'), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), - scope: text('scope'), - password: text('password'), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); +export const account = createTable( + 'account', + { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + password: text('password'), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + }, + (t) => [ + index('account_user_id_idx').on(t.userId), + index('account_provider_user_id_idx').on(t.providerId, t.userId), + index('account_expires_at_idx').on(t.accessTokenExpiresAt), + ], +); -export const userHotkeys = createTable('user_hotkeys', { - userId: text('user_id') - .primaryKey() - .references(() => user.id), - shortcuts: jsonb('shortcuts').notNull(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); +export const userHotkeys = createTable( + 'user_hotkeys', + { + userId: text('user_id') + .primaryKey() + .references(() => user.id, { onDelete: 'cascade' }), + shortcuts: jsonb('shortcuts').notNull(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + }, + (t) => [index('user_hotkeys_shortcuts_idx').on(t.shortcuts)], +); -export const verification = createTable('verification', { - id: text('id').primaryKey(), - identifier: text('identifier').notNull(), - value: text('value').notNull(), - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), -}); +export const verification = createTable( + 'verification', + { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + }, + (t) => [ + index('verification_identifier_idx').on(t.identifier), + index('verification_expires_at_idx').on(t.expiresAt), + ], +); -export const earlyAccess = createTable('early_access', { - id: text('id').primaryKey(), - email: text('email').notNull().unique(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - isEarlyAccess: boolean('is_early_access').notNull().default(false), - hasUsedTicket: text('has_used_ticket').default(''), -}); +export const earlyAccess = createTable( + 'early_access', + { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + isEarlyAccess: boolean('is_early_access').notNull().default(false), + hasUsedTicket: text('has_used_ticket').default(''), + }, + (t) => [index('early_access_is_early_access_idx').on(t.isEarlyAccess)], +); export const connection = createTable( 'connection', @@ -90,7 +121,7 @@ export const connection = createTable( id: text('id').primaryKey(), userId: text('user_id') .notNull() - .references(() => user.id), + .references(() => user.id, { onDelete: 'cascade' }), email: text('email').notNull(), name: text('name'), picture: text('picture'), @@ -102,45 +133,73 @@ export const connection = createTable( createdAt: timestamp('created_at').notNull(), updatedAt: timestamp('updated_at').notNull(), }, - (t) => [unique().on(t.userId, t.email)], + (t) => [ + unique().on(t.userId, t.email), + index('connection_user_id_idx').on(t.userId), + index('connection_expires_at_idx').on(t.expiresAt), + index('connection_provider_id_idx').on(t.providerId), + ], ); -export const summary = createTable('summary', { - messageId: text('message_id').primaryKey(), - content: text('content').notNull(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - connectionId: text('connection_id').notNull(), - saved: boolean('saved').notNull().default(false), - tags: text('tags'), - suggestedReply: text('suggested_reply'), -}); +export const summary = createTable( + 'summary', + { + messageId: text('message_id').primaryKey(), + content: text('content').notNull(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + connectionId: text('connection_id') + .notNull() + .references(() => connection.id, { onDelete: 'cascade' }), + saved: boolean('saved').notNull().default(false), + tags: text('tags'), + suggestedReply: text('suggested_reply'), + }, + (t) => [ + index('summary_connection_id_idx').on(t.connectionId), + index('summary_connection_id_saved_idx').on(t.connectionId, t.saved), + index('summary_saved_idx').on(t.saved), + ], +); // Testing -export const note = createTable('note', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - threadId: text('thread_id').notNull(), - content: text('content').notNull(), - color: text('color').notNull().default('default'), - isPinned: boolean('is_pinned').default(false), - order: integer('order').notNull().default(0), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), -}); +export const note = createTable( + 'note', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + threadId: text('thread_id').notNull(), + content: text('content').notNull(), + color: text('color').notNull().default('default'), + isPinned: boolean('is_pinned').default(false), + order: integer('order').notNull().default(0), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (t) => [ + index('note_user_id_idx').on(t.userId), + index('note_thread_id_idx').on(t.threadId), + index('note_user_thread_idx').on(t.userId, t.threadId), + index('note_is_pinned_idx').on(t.isPinned), + ], +); -export const userSettings = createTable('user_settings', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id) - .unique(), - settings: jsonb('settings').notNull().default(defaultUserSettings), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); +export const userSettings = createTable( + 'user_settings', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }) + .unique(), + settings: jsonb('settings').notNull().default(defaultUserSettings), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + }, + (t) => [index('user_settings_settings_idx').on(t.settings)], +); export const writingStyleMatrix = createTable( 'writing_style_matrix', @@ -162,51 +221,79 @@ export const writingStyleMatrix = createTable( primaryKey({ columns: [table.connectionId], }), + index('writing_style_matrix_style_idx').on(table.style), ]; }, ); -export const jwks = createTable('jwks', { - id: text('id').primaryKey(), - publicKey: text('public_key').notNull(), - privateKey: text('private_key').notNull(), - createdAt: timestamp('created_at').notNull(), -}); +export const jwks = createTable( + 'jwks', + { + id: text('id').primaryKey(), + publicKey: text('public_key').notNull(), + privateKey: text('private_key').notNull(), + createdAt: timestamp('created_at').notNull(), + }, + (t) => [index('jwks_created_at_idx').on(t.createdAt)], +); -export const oauthApplication = createTable('oauth_application', { - id: text('id').primaryKey(), - name: text('name'), - icon: text('icon'), - metadata: text('metadata'), - clientId: text('client_id').unique(), - clientSecret: text('client_secret'), - redirectURLs: text('redirect_u_r_ls'), - type: text('type'), - disabled: boolean('disabled'), - userId: text('user_id'), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), -}); +export const oauthApplication = createTable( + 'oauth_application', + { + id: text('id').primaryKey(), + name: text('name'), + icon: text('icon'), + metadata: text('metadata'), + clientId: text('client_id').unique(), + clientSecret: text('client_secret'), + redirectURLs: text('redirect_u_r_ls'), + type: text('type'), + disabled: boolean('disabled'), + userId: text('user_id'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + }, + (t) => [ + index('oauth_application_user_id_idx').on(t.userId), + index('oauth_application_disabled_idx').on(t.disabled), + ], +); -export const oauthAccessToken = createTable('oauth_access_token', { - id: text('id').primaryKey(), - accessToken: text('access_token').unique(), - refreshToken: text('refresh_token').unique(), - accessTokenExpiresAt: timestamp('access_token_expires_at'), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), - clientId: text('client_id'), - userId: text('user_id'), - scopes: text('scopes'), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), -}); +export const oauthAccessToken = createTable( + 'oauth_access_token', + { + id: text('id').primaryKey(), + accessToken: text('access_token').unique(), + refreshToken: text('refresh_token').unique(), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + clientId: text('client_id'), + userId: text('user_id'), + scopes: text('scopes'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + }, + (t) => [ + index('oauth_access_token_user_id_idx').on(t.userId), + index('oauth_access_token_client_id_idx').on(t.clientId), + index('oauth_access_token_expires_at_idx').on(t.accessTokenExpiresAt), + ], +); -export const oauthConsent = createTable('oauth_consent', { - id: text('id').primaryKey(), - clientId: text('client_id'), - userId: text('user_id'), - scopes: text('scopes'), - createdAt: timestamp('created_at'), - updatedAt: timestamp('updated_at'), - consentGiven: boolean('consent_given'), -}); +export const oauthConsent = createTable( + 'oauth_consent', + { + id: text('id').primaryKey(), + clientId: text('client_id'), + userId: text('user_id'), + scopes: text('scopes'), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), + consentGiven: boolean('consent_given'), + }, + (t) => [ + index('oauth_consent_user_id_idx').on(t.userId), + index('oauth_consent_client_id_idx').on(t.clientId), + index('oauth_consent_given_idx').on(t.consentGiven), + ], +); From 528a986d99449831760d0292c6c809aa363144f9 Mon Sep 17 00:00:00 2001 From: Harsh Jadhav Date: Fri, 4 Jul 2025 23:58:01 +0530 Subject: [PATCH 03/28] Fix: restrict access to settings page for unauthenticated users (#1602) (#1603) Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- apps/mail/app/(routes)/settings/layout.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/mail/app/(routes)/settings/layout.tsx b/apps/mail/app/(routes)/settings/layout.tsx index 3f8978d455..bbd33d7131 100644 --- a/apps/mail/app/(routes)/settings/layout.tsx +++ b/apps/mail/app/(routes)/settings/layout.tsx @@ -1,5 +1,18 @@ import { SettingsLayoutContent } from '@/components/ui/settings-content'; import { Outlet } from 'react-router'; +import { authProxy } from '@/lib/auth-proxy'; +import type { Route } from './+types/layout'; + +export async function clientLoader({ request }: Route.ClientLoaderArgs) { + const session = await authProxy.api.getSession({ headers: request.headers }); + + if (!session) { + return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/login`); + } + + + return null; +} export default function SettingsLayout() { return ( @@ -7,4 +20,4 @@ export default function SettingsLayout() { ); -} +} \ No newline at end of file From 16125ff6306ad216cc7930035aec15fc5607f9c9 Mon Sep 17 00:00:00 2001 From: Fahad <155962781+Fahad-Dezloper@users.noreply.github.com> Date: Fri, 4 Jul 2025 23:59:40 +0530 Subject: [PATCH 04/28] text responsiveness corrected (#1594) --- apps/mail/components/ui/app-sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mail/components/ui/app-sidebar.tsx b/apps/mail/components/ui/app-sidebar.tsx index a598f375c8..11548f688d 100644 --- a/apps/mail/components/ui/app-sidebar.tsx +++ b/apps/mail/components/ui/app-sidebar.tsx @@ -157,7 +157,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { className="mt-3 inline-flex h-7 w-full items-center justify-center gap-0.5 overflow-hidden rounded-lg bg-[#8B5CF6] px-2" >
-
+
Start 7 day free trial
From 4da17dd1d039ea2f9797aae70a11dcb1afeed2e3 Mon Sep 17 00:00:00 2001 From: Anirban Singha <143536290+SinghaAnirban005@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:04:18 +0530 Subject: [PATCH 05/28] frontend: Settings: Adjust misalignments of placeholders (#1531) --- apps/mail/app/(routes)/settings/general/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mail/app/(routes)/settings/general/page.tsx b/apps/mail/app/(routes)/settings/general/page.tsx index ae96589ffc..1e16619b08 100644 --- a/apps/mail/app/(routes)/settings/general/page.tsx +++ b/apps/mail/app/(routes)/settings/general/page.tsx @@ -195,7 +195,7 @@ export default function GeneralPage() { name="language" render={({ field }) => ( - {m['pages.settings.general.language']()} + {m['pages.settings.general.language']()} {m['pages.settings.general.language']()} - + Date: Sat, 5 Jul 2025 00:20:29 +0530 Subject: [PATCH 10/28] feat: Converting empty state svg to component (#1562) --- .../mail/components/icons/empty-state-svg.tsx | 191 ++++++++++++++++++ apps/mail/components/mail/mail-list.tsx | 9 +- apps/mail/components/mail/thread-display.tsx | 8 +- 3 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 apps/mail/components/icons/empty-state-svg.tsx diff --git a/apps/mail/components/icons/empty-state-svg.tsx b/apps/mail/components/icons/empty-state-svg.tsx new file mode 100644 index 0000000000..ef2cd422a2 --- /dev/null +++ b/apps/mail/components/icons/empty-state-svg.tsx @@ -0,0 +1,191 @@ +import { useTheme } from 'next-themes'; + +interface EmptyStateSVGProps { + width?: number; + height?: number; + className?: string; +} + +interface EmptyStateBaseProps extends EmptyStateSVGProps { + isDarkTheme?: boolean; +} + +const EmptyStateBase = ({ width = 200, height = 200, className, isDarkTheme = true }: EmptyStateBaseProps) => { + // Theme-specific values + const viewBox = isDarkTheme ? "0 0 192 192" : "0 0 192 198"; + const bgFill = isDarkTheme ? "#141414" : "#FAFAFA"; + const bgOpacity = isDarkTheme ? "0.25" : "1"; + const borderColor = isDarkTheme ? "white" : "#DBDBDB"; + const borderOpacity = isDarkTheme ? "0.15" : "1"; + const borderWidth = isDarkTheme ? "1" : "0.5"; + + // Icon-specific elements - only light theme uses these + const filterElements = !isDarkTheme ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : null; + + // Configure fill colors for elements + const clipFill = isDarkTheme ? "white" : "white"; + const envelopeLetterFill = isDarkTheme ? "white" : "#B0B0B0"; + const envelopeLetterOpacity = isDarkTheme ? "0.3" : "1"; + const lineColors = !isDarkTheme + ? ["#E7E7E7", "#F0F0F0", "#F6F6F6", "#FAFAFA"] + : [ + "white", "white", "white", "white" + ]; + const lineOpacities = isDarkTheme + ? ["0.1", "0.075", "0.05", "0.025"] + : ["1", "1", "1", "1"]; + + // Paint definitions + const paint0Stop0Color = isDarkTheme ? "white" : "white"; + const paint0Stop0Opacity = isDarkTheme ? "0.1" : "1"; + const paint0Stop1Color = isDarkTheme ? "white" : "white"; + const paint0Stop1Opacity = isDarkTheme ? "0.05" : "1"; + + const paint1Stop0Color = "white"; + const paint1Stop0Opacity = "0.1"; + const paint1Stop1Color = "#323232"; + const paint1Stop1Opacity = "0"; + + const paint2Stop0Color = isDarkTheme ? "white" : "white"; + const paint2Stop0Opacity = isDarkTheme ? "0.1" : "1"; + const paint2Stop1Color = isDarkTheme ? "white" : "white"; + const paint2Stop1Opacity = isDarkTheme ? "0.05" : "1"; + + return ( + + {/* Main background circle */} + {isDarkTheme ? ( + + ) : ( + + )} + + {/* Border */} + {isDarkTheme ? ( + + ) : ( + + )} + + {/* Envelope shape - shadow layer */} + + + + + + {/* Main envelope */} + + + + + {/* Envelope details */} + + + + + + {/* Envelope content lines */} + + + + + + + {/* Envelope border */} + + + + {/* Gradients and clips */} + + {filterElements} + + {/* Gradients for coloring */} + + + + + + + + + + + + + + + + + + {/* Clip paths */} + + + + + + + + + ); +}; + +export const EmptyState = (props: EmptyStateSVGProps) => { + return ; +}; + +export const EmptyStateLight = (props: EmptyStateSVGProps) => { + return ; +}; + +export const EmptyStateIcon = ({ width = 200, height = 200, className }: EmptyStateSVGProps) => { + const { resolvedTheme } = useTheme(); + + // Explicitly check for 'dark' theme, use light theme as fallback for all other cases + return resolvedTheme === 'dark' ? ( + + ) : ( + + ); +}; \ No newline at end of file diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index ff53f96e97..3a508c15e3 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -6,6 +6,7 @@ import { getMainSearchTerm, parseNaturalLanguageSearch, } from '@/lib/utils'; +import { EmptyStateIcon } from '../icons/empty-state-svg'; import { Archive2, ExclamationCircle, @@ -983,13 +984,7 @@ export const MailList = memo( ) : !items || items.length === 0 ? (
- Empty Inbox +

It's empty here

diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index e125ebf4b3..976d6ad10b 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -14,6 +14,7 @@ import { Trash, X, } from '../icons/icons'; +import { EmptyStateIcon } from '../icons/empty-state-svg'; import { DropdownMenu, DropdownMenuContent, @@ -752,12 +753,7 @@ export function ThreadDisplay() { {!id ? (

- Empty Thread +

It's empty here

From f29e3386c6019fba159b6cf1f22cdb50b75ef1f0 Mon Sep 17 00:00:00 2001 From: abhix4 Date: Sat, 5 Jul 2025 00:21:12 +0530 Subject: [PATCH 11/28] feat: add rich text formatting toolbar (#1563) --- .../mail/components/create/email-composer.tsx | 33 ++- apps/mail/components/create/extensions.ts | 12 +- apps/mail/components/create/toolbar.tsx | 243 ++++++++++++++++++ 3 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 apps/mail/components/create/toolbar.tsx diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index 7e79cf4491..8425caa074 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -13,7 +13,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Check, Command, Loader, Paperclip, Plus, X as XIcon } from 'lucide-react'; + +import { Check, Command, Loader, Paperclip, Plus, Type, X as XIcon } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TextEffect } from '@/components/motion-primitives/text-effect'; import { ImageCompressionSettings } from './image-compression-settings'; @@ -40,9 +41,12 @@ import { EditorContent } from '@tiptap/react'; import { useForm } from 'react-hook-form'; import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; +import { Toolbar } from './toolbar'; import pluralize from 'pluralize'; import { toast } from 'sonner'; import { z } from 'zod'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; + type ThreadContent = { from: string; @@ -147,7 +151,7 @@ export function EmailComposer({ const [imageQuality, setImageQuality] = useState( settings?.settings?.imageCompression || 'medium', ); - + const [toggleToolbar, setToggleToolbar] = useState(false); const processAndSetAttachments = async ( filesToProcess: File[], quality: ImageQuality, @@ -1299,14 +1303,15 @@ export function EmailComposer({ aiGeneratedMessage !== null ? 'blur-sm' : '', )} > - +

{/* Bottom Actions */} -
-
+
+
+ {toggleToolbar && }
+ + Formatting options + + +
diff --git a/apps/mail/components/create/extensions.ts b/apps/mail/components/create/extensions.ts index a0114f5fcb..e6a6930aac 100644 --- a/apps/mail/components/create/extensions.ts +++ b/apps/mail/components/create/extensions.ts @@ -104,12 +104,12 @@ const horizontalRule = HorizontalRule.configure({ const starterKit = StarterKit.configure({ bulletList: { HTMLAttributes: { - class: cx('list-disc list-outside leading-3 -mt-2'), + class: cx('list-disc list-outside leading-2 -mt-2'), }, }, orderedList: { HTMLAttributes: { - class: cx('list-decimal list-outside leading-3 -mt-2'), + class: cx('list-decimal list-outside leading-2 -mt-2'), }, }, listItem: { @@ -119,7 +119,13 @@ const starterKit = StarterKit.configure({ }, blockquote: { HTMLAttributes: { - class: cx('border-l-4 border-primary'), + class: cx('border-l-2 border-primary'), + }, + }, + heading: { + levels: [1, 2, 3], + HTMLAttributes: { + class: cx('text-primary'), }, }, codeBlock: { diff --git a/apps/mail/components/create/toolbar.tsx b/apps/mail/components/create/toolbar.tsx new file mode 100644 index 0000000000..2e080bfaf9 --- /dev/null +++ b/apps/mail/components/create/toolbar.tsx @@ -0,0 +1,243 @@ +import { + Bold, + Italic, + Strikethrough, + Underline, + Code, + Link as LinkIcon, + List, + ListOrdered, + Heading1, + Heading2, + Heading3, + Undo2, + Redo2, + TextQuote, +} from 'lucide-react'; + +import { useTranslations } from 'use-intl'; + +import { TooltipContent, TooltipProvider, TooltipTrigger, Tooltip } from '../ui/tooltip'; +import { Separator } from '@/components/ui/separator'; +import { Button } from '../ui/button'; + +import type { Editor } from '@tiptap/core'; + +export const Toolbar = ({ editor }: { editor: Editor | null }) => { + const t = useTranslations(); + + if (!editor) return null; + + return ( +
+ +
+
+
+ + + + + Undo + + + + + + + Redo + +
+ +
+ + + + + H1 + + + + + + H2 + + + + + + H3 + +
+ +
+ + + + + {t('pages.createEmail.editor.menuBar.bold')} + + + + + + {t('pages.createEmail.editor.menuBar.italic')} + + + + + + + {t('pages.createEmail.editor.menuBar.strikethrough')} + + + + + + + {t('pages.createEmail.editor.menuBar.underline')} + +
+ + + +
+ + + + + {t('pages.createEmail.editor.menuBar.bulletList')} + + + + + + {t('pages.createEmail.editor.menuBar.orderedList')} + + + + + + Block Quote + +
+
+
+
+
+ ); +}; From 859d05fb76b4af64246047cc5e038e6e2c1815b3 Mon Sep 17 00:00:00 2001 From: Om Raval <68021378+omraval18@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:29:08 +0530 Subject: [PATCH 12/28] Fix: Example Queries Masking done Properly (#1549) --- apps/mail/app/globals.css | 32 +++++++++++++++++++++++++ apps/mail/components/create/ai-chat.tsx | 10 +------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/mail/app/globals.css b/apps/mail/app/globals.css index 4ce37a8aba..192b802ff5 100644 --- a/apps/mail/app/globals.css +++ b/apps/mail/app/globals.css @@ -9,6 +9,36 @@ .text-balance { text-wrap: balance; } + .horizontal-fade-mask { + @apply overflow-x-auto; + position: relative; + } + @supports (mask-image: linear-gradient(to right, transparent, black)) or + (-webkit-mask-image: linear-gradient(to right, transparent, black)) { + .horizontal-fade-mask::before, + .horizontal-fade-mask::after { + content: ''; + position: absolute; + top: 0; + height: 100%; + width: 15%; + pointer-events: none; + background: hsl(var(--panel)); + z-index: 1; + } + + .horizontal-fade-mask::before { + left: 0; + -webkit-mask-image: linear-gradient(to right, white, transparent); + mask-image: linear-gradient(to right, white, transparent); + } + + .horizontal-fade-mask::after { + right: 0; + -webkit-mask-image: linear-gradient(to left, white, transparent); + mask-image: linear-gradient(to left, white, transparent); + } + } } @layer base { @@ -53,6 +83,7 @@ --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; --icon-color: currentColor; + --panel: 0 0% 100%; } .dark { @@ -89,6 +120,7 @@ --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; --icon-color: currentColor; + --panel: 240 3.7% 10.2%; } } diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index 435b967b65..3ee078999a 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -84,7 +84,7 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi const secondRowQueries = ['Find all work meetings', 'What projects do i have coming up']; return ( -
+
{/* First row */}
@@ -98,10 +98,6 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi ))}
- {/* Left mask */} -
- {/* Right mask */} -
{/* Second row */} @@ -117,10 +113,6 @@ const ExampleQueries = ({ onQueryClick }: { onQueryClick: (query: string) => voi ))}
- {/* Left mask */} -
- {/* Right mask */} -
); From 1ac8dbcf6cdf75f40b5dec480981f99f875ae912 Mon Sep 17 00:00:00 2001 From: Fahad <155962781+Fahad-Dezloper@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:34:23 +0530 Subject: [PATCH 13/28] Fix/draft types (#1545) Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- apps/mail/components/mail/mail-list.tsx | 6 ++++-- apps/server/src/lib/driver/types.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index 3a508c15e3..a1a181e0ae 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -30,7 +30,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import type { MailSelectMode, ParsedMessage, ThreadProps } from '@/types'; import { ThreadContextMenu } from '@/components/context/thread-context'; import { useOptimisticActions } from '@/hooks/use-optimistic-actions'; -import { useIsFetching, useQueryClient } from '@tanstack/react-query'; +import { useIsFetching, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useMail, type Config } from '@/components/mail/use-mail'; import { type ThreadDestination } from '@/lib/thread-actions'; @@ -59,6 +59,7 @@ import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Categories } from './mail'; import { useAtom } from 'jotai'; +import type { ParsedDraft } from '../../../server/src/lib/driver/types'; const Thread = memo( function Thread({ @@ -629,7 +630,8 @@ const Thread = memo( ); const Draft = memo(({ message }: { message: { id: string } }) => { - const { data: draft } = useDraft(message.id); + const draftQuery = useDraft(message.id) as UseQueryResult; + const draft = draftQuery.data; const [, setComposeOpen] = useQueryState('isComposeOpen'); const [, setDraftId] = useQueryState('draftId'); const handleMailClick = useCallback(() => { diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index 0e442833bf..28d9b784e8 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -24,7 +24,9 @@ export interface ParsedDraft { to?: string[]; subject?: string; content?: string; - rawMessage?: T; + rawMessage?: { + internalDate?: string; + }; cc?: string[]; bcc?: string[]; } From 96f322c45a51b89612f130cd7a152c82f591dcf3 Mon Sep 17 00:00:00 2001 From: Umang Singh <67512605+Pheewww@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:42:04 +0530 Subject: [PATCH 14/28] Fix: Attachment Dropdown Trigger Fix (#1543) Co-authored-by: Umang Singh --- apps/mail/components/mail/mail-display.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index 53306ced90..ccbc191687 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -1633,6 +1633,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: { e.stopPropagation(); e.preventDefault(); From 373bac81214d9a521bf3bcd10ca9e69c9972c888 Mon Sep 17 00:00:00 2001 From: Pankaj jaat <125138274+Pankajkumar2608@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:48:14 +0530 Subject: [PATCH 15/28] =?UTF-8?q?on=20clicking=20download=20all=20attachem?= =?UTF-8?q?ent=20its=20makes=20the=20email=20ui=20collaspe=20=E2=80=A6=20(?= =?UTF-8?q?#1541)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam --- apps/mail/components/mail/mail-display.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index ccbc191687..960dfa9571 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -1631,6 +1631,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: {m['common.mailDisplay.print']()} + {(emailData.attachments?.length ?? 0) > 0 && ( Download All Attachments + )}
From 58d19dad929a226b614b86a8f978dd8a05c096df Mon Sep 17 00:00:00 2001 From: Fahad <155962781+Fahad-Dezloper@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:50:03 +0530 Subject: [PATCH 16/28] Fix/noname placeholder (#1527) --- apps/mail/components/mail/mail-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index a1a181e0ae..dada7cc4d6 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -698,7 +698,7 @@ const Draft = memo(({ message }: { message: { id: string } }) => { )} > - {cleanNameDisplay(draft?.to?.[0] || 'noname') || ''} + {cleanNameDisplay(draft?.to?.[0] || 'No Recipient') || ''}
From 4b6d1baf1f0c0f1c1b55a6c1eb8c368d15585b1f Mon Sep 17 00:00:00 2001 From: Samrath <102617759+samrathreddy@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:52:15 +0530 Subject: [PATCH 17/28] feat: Moved openAI model configuration to .env (#1540) --- .env.example | 4 ++++ apps/server/src/routes/ai.ts | 4 ++-- apps/server/src/routes/chat.ts | 8 ++++---- apps/server/src/services/mcp-service/mcp.ts | 2 +- apps/server/src/trpc/routes/ai/compose.ts | 5 +++-- apps/server/src/trpc/routes/ai/search.ts | 3 ++- docker-compose.prod.yaml | 4 ++++ 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index af9fa2775c..8c4f2052ab 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,10 @@ RESEND_API_KEY= OPENAI_API_KEY= PERPLEXITY_API_KEY= +# OpenAI Model names (gpt-4o, gpt-4o-mini etc) +OPENAI_MODEL= +OPENAI_MINI_MODEL= + #AI PROMPT AI_SYSTEM_PROMPT="" diff --git a/apps/server/src/routes/ai.ts b/apps/server/src/routes/ai.ts index 7234dd2a45..a1d3c21eeb 100644 --- a/apps/server/src/routes/ai.ts +++ b/apps/server/src/routes/ai.ts @@ -145,7 +145,7 @@ aiRouter.post('/call', async (c) => { const driver = connectionToDriver(connection); const { text } = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: systemPrompt, prompt: data.query, tools: { @@ -158,7 +158,7 @@ aiRouter.post('/call', async (c) => { console.log('[DEBUG] buildGmailSearchQuery', params); const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: params.query, }); diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 6df33dc171..5b9671c800 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -363,7 +363,7 @@ export class ZeroAgent extends AIChatAgent { ); const result = streamText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), messages: processedMessages, tools, onFinish, @@ -689,7 +689,7 @@ export class ZeroAgent extends AIChatAgent { async buildGmailSearchQuery(query: string) { const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: query, }); @@ -1242,7 +1242,7 @@ export class ZeroMCP extends McpAgent { }, async (s) => { const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: s.query, }); @@ -1587,7 +1587,7 @@ const buildGmailSearchQuery = tool({ }), execute: async ({ query }) => { const result = await generateObject({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: query, schema: z.object({ diff --git a/apps/server/src/services/mcp-service/mcp.ts b/apps/server/src/services/mcp-service/mcp.ts index 71bb913509..822d29dc32 100644 --- a/apps/server/src/services/mcp-service/mcp.ts +++ b/apps/server/src/services/mcp-service/mcp.ts @@ -55,7 +55,7 @@ export class ZeroMCP extends McpAgent }, async (s) => { const result = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: GmailSearchAssistantSystemPrompt(), prompt: s.query, }); diff --git a/apps/server/src/trpc/routes/ai/compose.ts b/apps/server/src/trpc/routes/ai/compose.ts index 9ab24a7cc3..f0fbc5b890 100644 --- a/apps/server/src/trpc/routes/ai/compose.ts +++ b/apps/server/src/trpc/routes/ai/compose.ts @@ -11,6 +11,7 @@ import { stripHtml } from 'string-strip-html'; import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; import { z } from 'zod'; +import { env } from 'cloudflare:workers'; type ComposeEmailInput = { prompt: string; @@ -84,7 +85,7 @@ export async function composeEmail(input: ComposeEmailInput) { ]; const { text } = await generateText({ - model: openai('gpt-4o-mini'), + model: openai(env.OPENAI_MINI_MODEL || 'gpt-4o-mini'), messages: [ { role: 'system', @@ -273,7 +274,7 @@ const generateSubject = async (message: string, styleProfile?: WritingStyleMatri ); const { text } = await generateText({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), messages: [ { role: 'system', diff --git a/apps/server/src/trpc/routes/ai/search.ts b/apps/server/src/trpc/routes/ai/search.ts index 51f26aba1f..88c0c4c24d 100644 --- a/apps/server/src/trpc/routes/ai/search.ts +++ b/apps/server/src/trpc/routes/ai/search.ts @@ -5,6 +5,7 @@ import { import { activeDriverProcedure } from '../../trpc'; import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; +import { env } from 'cloudflare:workers'; import { z } from 'zod'; export const generateSearchQuery = activeDriverProcedure @@ -21,7 +22,7 @@ export const generateSearchQuery = activeDriverProcedure : ''; const result = await generateObject({ - model: openai('gpt-4o'), + model: openai(env.OPENAI_MODEL || 'gpt-4o'), system: systemPrompt, prompt: input.query, schema: z.object({ diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index 9df2dee965..780b4b313f 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -12,6 +12,10 @@ services: RESEND_API_KEY: ${RESEND_API_KEY} AI_SYSTEM_PROMPT: ${AI_SYSTEM_PROMPT} GROQ_API_KEY: ${GROQ_API_KEY} + PERPLEXITY_API_KEY: ${PERPLEXITY_API_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + OPENAI_MODEL: ${OPENAI_MODEL} + OPENAI_MINI_MODEL: ${OPENAI_MINI_MODEL} NEXT_PUBLIC_ELEVENLABS_AGENT_ID: ${NEXT_PUBLIC_ELEVENLABS_AGENT_ID} NEXT_PUBLIC_IMAGE_PROXY: ${NEXT_PUBLIC_IMAGE_PROXY} NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY} From c8117ba17b4e3fc1ffbf6bbe3e96526bfc05c072 Mon Sep 17 00:00:00 2001 From: abhix4 Date: Sat, 5 Jul 2025 00:53:26 +0530 Subject: [PATCH 18/28] fix thread popover opacity (#1537) --- apps/mail/components/mail/mail-list.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index dada7cc4d6..df79b882dd 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -296,7 +296,6 @@ const Thread = memo(
{ @@ -420,7 +419,7 @@ const Thread = memo( ) : null}
-
+
Date: Sat, 5 Jul 2025 00:55:38 +0530 Subject: [PATCH 19/28] add attachments support to drafts (#1536) --- apps/mail/components/create/create-email.tsx | 32 ++++++++- apps/server/src/lib/driver/google.ts | 72 +++++++++++++++----- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index efe86ca501..9265469c00 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -18,6 +18,7 @@ import { X } from '../icons/icons'; import posthog from 'posthog-js'; import { toast } from 'sonner'; import './prosemirror.css'; +import type { Attachment } from '@/types'; // Define the draft type to include CC and BCC fields type DraftType = { @@ -27,8 +28,11 @@ type DraftType = { to?: string[]; cc?: string[]; bcc?: string[]; + attachments?: File[] }; + + // Define the connection type type Connection = { id: string; @@ -101,9 +105,9 @@ export function CreateEmail({ : ''; await sendEmail({ - to: data.to.map((email) => ({ email, name: email.split('@')[0] || email })), - cc: data.cc?.map((email) => ({ email, name: email.split('@')[0] || email })), - bcc: data.bcc?.map((email) => ({ email, name: email.split('@')[0] || email })), + to: data.to.map((email) => ({ email, name: email?.split('@')[0] || email })), + cc: data.cc?.map((email) => ({ email, name: email?.split('@')[0] || email })), + bcc: data.bcc?.map((email) => ({ email, name: email?.split('@')[0] || email })), subject: data.subject, message: data.message + zeroSignature, attachments: await serializeFiles(data.attachments), @@ -144,6 +148,7 @@ export function CreateEmail({ // Cast draft to our extended type that includes CC and BCC const typedDraft = draft as unknown as DraftType; + const handleDialogClose = (open: boolean) => { setIsComposeOpen(open ? 'true' : null); if (!open) { @@ -151,6 +156,26 @@ export function CreateEmail({ } }; + const base64ToFile = (base64: string, filename: string, mimeType: string): File | null => { + try { + const byteString = atob(base64); + const byteArray = new Uint8Array(byteString.length); + for (let i = 0; i < byteString.length; i++) { + byteArray[i] = byteString.charCodeAt(i); + } + return new File([byteArray], filename, { type: mimeType }); + } catch (error) { + console.error('Failed to convert base64 to file', error) + return null; + } + } + + // convert the attachments into File[] + const files: File[] = ((typedDraft?.attachments as Attachment[] | undefined) || []) + .map((att: Attachment) => base64ToFile(att.body, att.filename, att.mimeType)) + .filter((file): file is File => file !== null); + + return ( <> @@ -196,6 +221,7 @@ export function CreateEmail({ setIsComposeOpen(null); setDraftId(null); }} + initialAttachments={files} initialSubject={typedDraft?.subject || initialSubject} autofocus={false} settingsLoading={settingsLoading} diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index 8fcc4d5406..b42c5d10ee 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -484,7 +484,7 @@ export class GoogleMailManager implements MailManager { throw new Error('Draft not found'); } - const parsedDraft = this.parseDraft(res.data); + const parsedDraft = await this.parseDraft(res.data); if (!parsedDraft) { throw new Error('Failed to parse draft'); } @@ -1111,7 +1111,8 @@ export class GoogleMailManager implements MailManager { raw: encodedMessage, }; } - private parseDraft(draft: gmail_v1.Schema$Draft) { + + private async parseDraft(draft: gmail_v1.Schema$Draft) { if (!draft.message) return null; const headers = draft.message.payload?.headers || []; @@ -1121,26 +1122,61 @@ export class GoogleMailManager implements MailManager { ?.value?.split(',') .map((e) => e.trim()) .filter(Boolean) || []; - const subject = headers.find((h) => h.name === 'Subject')?.value; - - let content = ''; - const payload = draft.message.payload; - - if (payload) { - if (payload.parts) { - const textPart = payload.parts.find((part) => part.mimeType === 'text/html'); - if (textPart?.body?.data) { - content = fromBinary(textPart.body.data); - } - } else if (payload.body?.data) { - content = fromBinary(payload.body.data); - } - } + const subject = headers.find((h) => h.name === 'Subject')?.value; + const cc = draft.message.payload?.headers?.find((h) => h.name === 'Cc')?.value?.split(',') || []; const bcc = draft.message.payload?.headers?.find((h) => h.name === 'Bcc')?.value?.split(',') || []; + + const payload = draft.message.payload; + let content = ''; + let attachments: { + filename: string; + mimeType: string; + size: number; + attachmentId: string; + headers: { name: string; value: string }[]; + body: string; + }[] = []; + + if (payload?.parts) { + // Get body + const htmlPart = payload.parts.find((part) => part.mimeType === 'text/html'); + if (htmlPart?.body?.data) { + content = fromBinary(htmlPart.body.data); + } + + // Get attachments + const attachmentParts = payload.parts.filter( + (part) => !!part.filename && !!part.body?.attachmentId + ); + + attachments = await Promise.all( + attachmentParts.map(async (part) => { + try { + const attachmentData = await this.getAttachment(draft.message!.id!, part.body!.attachmentId!); + return { + filename: part.filename || '', + mimeType: part.mimeType || '', + size: Number(part.body?.size || 0), + attachmentId: part.body!.attachmentId!, + headers: + part.headers?.map((h) => ({ + name: h.name ?? '', + value: h.value ?? '', + })) ?? [], + body: attachmentData ?? '', + }; + } catch (e) { + return null; + } + }) + ).then((a) => a.filter((a): a is NonNullable => a !== null)); + } else if (payload?.body?.data) { + content = fromBinary(payload.body.data); + } return { id: draft.id || '', @@ -1150,8 +1186,10 @@ export class GoogleMailManager implements MailManager { rawMessage: draft.message, cc, bcc, + attachments, }; } + private async withErrorHandler( operation: string, fn: () => Promise | T, From 3263d880085a8895eeb50b421c761f5b1dfa0b59 Mon Sep 17 00:00:00 2001 From: Om Raval <68021378+omraval18@users.noreply.github.com> Date: Sat, 5 Jul 2025 00:57:52 +0530 Subject: [PATCH 20/28] Fix/email printing (#1529) Co-authored-by: Adam --- apps/mail/components/mail/mail-display.tsx | 1 + apps/mail/components/mail/thread-display.tsx | 3 ++- apps/mail/lib/email-utils.ts | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index 960dfa9571..c0285c8c14 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -55,6 +55,7 @@ import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Badge } from '../ui/badge'; import { format } from 'date-fns'; +import { cleanHtml } from '@/lib/email-utils'; import { toast } from 'sonner'; // HTML escaping function to prevent XSS attacks diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index 976d6ad10b..d9bb5ef4db 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -39,6 +39,7 @@ import type { ParsedMessage, Attachment } from '@/types'; import { MailDisplaySkeleton } from './mail-skeleton'; import { useTRPC } from '@/providers/query-provider'; import { useIsMobile } from '@/hooks/use-mobile'; +import { cleanHtml } from '@/lib/email-utils'; import { Button } from '@/components/ui/button'; import { useStats } from '@/hooks/use-stats'; import ReplyCompose from './reply-composer'; @@ -611,7 +612,7 @@ export function ThreadDisplay() { diff --git a/apps/mail/lib/email-utils.ts b/apps/mail/lib/email-utils.ts index af84480ded..5ef6683b55 100644 --- a/apps/mail/lib/email-utils.ts +++ b/apps/mail/lib/email-utils.ts @@ -1,5 +1,6 @@ import * as emailAddresses from 'email-addresses'; import type { Sender } from '@/types'; +import DOMPurify from 'dompurify'; import Color from 'color'; export const fixNonReadableColors = ( @@ -207,3 +208,15 @@ export const wasSentWithTLS = (receivedHeaders: string[]) => { return false; }; + +// cleans up html string for xss attacks and returns html +export const cleanHtml = (html: string) => { + if (!html) return '

No email content available

'; + + try { + return DOMPurify.sanitize(html); + } catch (error) { + console.warn('DOMPurify Failed or not Available, falling back to Default HTML ', error); + return '

No email content available

'; + } +}; From 1f20712d5f0f13c7abc0cf45993fdfa7e5839c28 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 12:37:54 -0700 Subject: [PATCH 21/28] Hotfix (#1627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/components/create/toolbar.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/mail/components/create/toolbar.tsx b/apps/mail/components/create/toolbar.tsx index 2e080bfaf9..1c090f6661 100644 --- a/apps/mail/components/create/toolbar.tsx +++ b/apps/mail/components/create/toolbar.tsx @@ -15,17 +15,14 @@ import { TextQuote, } from 'lucide-react'; -import { useTranslations } from 'use-intl'; - import { TooltipContent, TooltipProvider, TooltipTrigger, Tooltip } from '../ui/tooltip'; import { Separator } from '@/components/ui/separator'; import { Button } from '../ui/button'; import type { Editor } from '@tiptap/core'; +import { m } from '@/paraglide/messages'; export const Toolbar = ({ editor }: { editor: Editor | null }) => { - const t = useTranslations(); - if (!editor) return null; return ( @@ -133,7 +130,7 @@ export const Toolbar = ({ editor }: { editor: Editor | null }) => { - {t('pages.createEmail.editor.menuBar.bold')} + {m['pages.createEmail.editor.menuBar.bold']()} @@ -149,7 +146,7 @@ export const Toolbar = ({ editor }: { editor: Editor | null }) => { - {t('pages.createEmail.editor.menuBar.italic')} + {m['pages.createEmail.editor.menuBar.italic']()} @@ -166,7 +163,7 @@ export const Toolbar = ({ editor }: { editor: Editor | null }) => { - {t('pages.createEmail.editor.menuBar.strikethrough')} + {m['pages.createEmail.editor.menuBar.strikethrough']()} @@ -182,7 +179,7 @@ export const Toolbar = ({ editor }: { editor: Editor | null }) => { - {t('pages.createEmail.editor.menuBar.underline')} + {m['pages.createEmail.editor.menuBar.underline']()}
@@ -202,7 +199,9 @@ export const Toolbar = ({ editor }: { editor: Editor | null }) => { - {t('pages.createEmail.editor.menuBar.bulletList')} + + {m['pages.createEmail.editor.menuBar.bulletList']()} + @@ -217,7 +216,9 @@ export const Toolbar = ({ editor }: { editor: Editor | null }) => { - {t('pages.createEmail.editor.menuBar.orderedList')} + + {m['pages.createEmail.editor.menuBar.orderedList']()} + From 033b8cd6dcab3aeec9acf90eb3a84abde10dac75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:01:35 -0700 Subject: [PATCH 22/28] feat: update translations via @LingoDotDev (#1625) Hey team, [**Lingo.dev**](https://lingo.dev) here with fresh translations! ### In this update - Added missing translations - Performed brand voice, context and glossary checks - Enhanced translations using Lingo.dev Localization Engine ### Next Steps - [ ] Review the changes - [ ] Merge when ready --- apps/mail/messages/ar.json | 1 + apps/mail/messages/ca.json | 1 + apps/mail/messages/cs.json | 1 + apps/mail/messages/de.json | 1 + apps/mail/messages/es.json | 1 + apps/mail/messages/fa.json | 1 + apps/mail/messages/fr.json | 1 + apps/mail/messages/hi.json | 1 + apps/mail/messages/hu.json | 1 + apps/mail/messages/ja.json | 1 + apps/mail/messages/ko.json | 1 + apps/mail/messages/lv.json | 1 + apps/mail/messages/nl.json | 1 + apps/mail/messages/pl.json | 1 + apps/mail/messages/pt.json | 1 + apps/mail/messages/ru.json | 1 + apps/mail/messages/tr.json | 1 + apps/mail/messages/vi.json | 1 + apps/mail/messages/zh_CN.json | 1 + apps/mail/messages/zh_TW.json | 1 + i18n.lock | 1 + 21 files changed, 21 insertions(+) diff --git a/apps/mail/messages/ar.json b/apps/mail/messages/ar.json index f84e448423..03a72e62f3 100644 --- a/apps/mail/messages/ar.json +++ b/apps/mail/messages/ar.json @@ -401,6 +401,7 @@ "spam": "البريد المزعج", "archive": "الأرشيف", "bin": "سلة المحذوفات", + "livesupport": "الدعم المباشر", "feedback": "الملاحظات", "settings": "الإعدادات", "voice": "المساعد الصوتي" diff --git a/apps/mail/messages/ca.json b/apps/mail/messages/ca.json index 9dc677cd6b..5be164ca3f 100644 --- a/apps/mail/messages/ca.json +++ b/apps/mail/messages/ca.json @@ -401,6 +401,7 @@ "spam": "Contingut no desitjat", "archive": "Arxivats", "bin": "Paperera", + "livesupport": "Suport en directe", "feedback": "Feedback", "settings": "Configuració", "voice": "Assistent de veu" diff --git a/apps/mail/messages/cs.json b/apps/mail/messages/cs.json index 939f3fefe0..a3514b9255 100644 --- a/apps/mail/messages/cs.json +++ b/apps/mail/messages/cs.json @@ -401,6 +401,7 @@ "spam": "Nevyžádaná pošta", "archive": "Archiv", "bin": "Odstraněná pošta", + "livesupport": "Živá podpora", "feedback": "Zpětná vazba", "settings": "Nastavení", "voice": "Hlasový asistent" diff --git a/apps/mail/messages/de.json b/apps/mail/messages/de.json index 42793938a0..c52575fb44 100644 --- a/apps/mail/messages/de.json +++ b/apps/mail/messages/de.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archiv", "bin": "Papierkorb", + "livesupport": "Live-Support", "feedback": "Rückmeldung", "settings": "Einstellungen", "voice": "Sprachassistent" diff --git a/apps/mail/messages/es.json b/apps/mail/messages/es.json index 7f7b466df3..4d28272c47 100644 --- a/apps/mail/messages/es.json +++ b/apps/mail/messages/es.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archivados", "bin": "Papelera de reciclaje", + "livesupport": "Soporte en vivo", "feedback": "Sugerencias", "settings": "Configuración", "voice": "Asistente de voz" diff --git a/apps/mail/messages/fa.json b/apps/mail/messages/fa.json index 6a9ce00e78..e806099f06 100644 --- a/apps/mail/messages/fa.json +++ b/apps/mail/messages/fa.json @@ -401,6 +401,7 @@ "spam": "هرزنامه", "archive": "آرشیو", "bin": "سطل زباله", + "livesupport": "پشتیبانی زنده", "feedback": "بازخورد", "settings": "تنظیمات", "voice": "دستیار صوتی" diff --git a/apps/mail/messages/fr.json b/apps/mail/messages/fr.json index 123cc97f34..488c979680 100644 --- a/apps/mail/messages/fr.json +++ b/apps/mail/messages/fr.json @@ -401,6 +401,7 @@ "spam": "Pourriels", "archive": "Archives", "bin": "Corbeille", + "livesupport": "Support en direct", "feedback": "Vos commentaires", "settings": "Paramètres", "voice": "Assistant vocal" diff --git a/apps/mail/messages/hi.json b/apps/mail/messages/hi.json index 3665a6aefa..5eac04d0e7 100644 --- a/apps/mail/messages/hi.json +++ b/apps/mail/messages/hi.json @@ -401,6 +401,7 @@ "spam": "स्पैम", "archive": "आर्काइव", "bin": "बिन", + "livesupport": "लाइव सपोर्ट", "feedback": "प्रतिक्रिया", "settings": "सेटिंग्स", "voice": "वॉइस असिस्टेंट" diff --git a/apps/mail/messages/hu.json b/apps/mail/messages/hu.json index e2af343132..4b3c028aba 100644 --- a/apps/mail/messages/hu.json +++ b/apps/mail/messages/hu.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archívum", "bin": "Lomtár", + "livesupport": "Élő támogatás", "feedback": "Visszajelzés", "settings": "Beállítások", "voice": "Hangasszisztens" diff --git a/apps/mail/messages/ja.json b/apps/mail/messages/ja.json index a14e6e4b03..41d3d32a1e 100644 --- a/apps/mail/messages/ja.json +++ b/apps/mail/messages/ja.json @@ -401,6 +401,7 @@ "spam": "迷惑メール", "archive": "アーカイブ", "bin": "ごみ箱", + "livesupport": "ライブサポート", "feedback": "フィードバック", "settings": "設定", "voice": "音声アシスタント" diff --git a/apps/mail/messages/ko.json b/apps/mail/messages/ko.json index c2550440b6..e4fa26eef8 100644 --- a/apps/mail/messages/ko.json +++ b/apps/mail/messages/ko.json @@ -401,6 +401,7 @@ "spam": "스팸", "archive": "보관함", "bin": "휴지통", + "livesupport": "실시간 지원", "feedback": "피드백", "settings": "설정", "voice": "음성 비서" diff --git a/apps/mail/messages/lv.json b/apps/mail/messages/lv.json index 340796a12f..6e6f622af7 100644 --- a/apps/mail/messages/lv.json +++ b/apps/mail/messages/lv.json @@ -401,6 +401,7 @@ "spam": "Mēstules", "archive": "Arhīvs", "bin": "Miskaste", + "livesupport": "Tiešsaistes atbalsts", "feedback": "Atsauksmes", "settings": "Iestatījumi", "voice": "Balss asistents" diff --git a/apps/mail/messages/nl.json b/apps/mail/messages/nl.json index 836138b47f..9c099e23c5 100644 --- a/apps/mail/messages/nl.json +++ b/apps/mail/messages/nl.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archief", "bin": "Prullenbak", + "livesupport": "Live ondersteuning", "feedback": "Feedback", "settings": "Instellingen", "voice": "Spraakassistent" diff --git a/apps/mail/messages/pl.json b/apps/mail/messages/pl.json index 8682ba4f14..7f509df2e0 100644 --- a/apps/mail/messages/pl.json +++ b/apps/mail/messages/pl.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Archiwum", "bin": "Kosz", + "livesupport": "Wsparcie na żywo", "feedback": "Opinie", "settings": "Ustawienia", "voice": "Asystent głosowy" diff --git a/apps/mail/messages/pt.json b/apps/mail/messages/pt.json index 61ea4190ff..fcf438dcbe 100644 --- a/apps/mail/messages/pt.json +++ b/apps/mail/messages/pt.json @@ -401,6 +401,7 @@ "spam": "Spam", "archive": "Arquivados", "bin": "Lixeira", + "livesupport": "Suporte ao vivo", "feedback": "Feedback", "settings": "Configurações", "voice": "Assistente de voz" diff --git a/apps/mail/messages/ru.json b/apps/mail/messages/ru.json index 5f391841e1..e2c5bb0e81 100644 --- a/apps/mail/messages/ru.json +++ b/apps/mail/messages/ru.json @@ -401,6 +401,7 @@ "spam": "Спам", "archive": "Архив", "bin": "Корзина", + "livesupport": "Поддержка в реальном времени", "feedback": "Отзывы", "settings": "Настройки", "voice": "Голосовой помощник" diff --git a/apps/mail/messages/tr.json b/apps/mail/messages/tr.json index 33c2e87491..238b3039ae 100644 --- a/apps/mail/messages/tr.json +++ b/apps/mail/messages/tr.json @@ -401,6 +401,7 @@ "spam": "İstenmeyen E-posta", "archive": "Arşiv", "bin": "Çöp", + "livesupport": "Canlı Destek", "feedback": "Geribildirim", "settings": "Ayarlar", "voice": "Sesli Asistan" diff --git a/apps/mail/messages/vi.json b/apps/mail/messages/vi.json index 00b4007ed1..d95ebba7b4 100644 --- a/apps/mail/messages/vi.json +++ b/apps/mail/messages/vi.json @@ -401,6 +401,7 @@ "spam": "Thư rác", "archive": "Lưu trữ", "bin": "Thùng rác", + "livesupport": "Hỗ trợ trực tuyến", "feedback": "Phản hồi", "settings": "Cài đặt", "voice": "Trợ lý giọng nói" diff --git a/apps/mail/messages/zh_CN.json b/apps/mail/messages/zh_CN.json index b96166f41b..af8d4353a1 100644 --- a/apps/mail/messages/zh_CN.json +++ b/apps/mail/messages/zh_CN.json @@ -401,6 +401,7 @@ "spam": "垃圾邮件", "archive": "存档", "bin": "回收站", + "livesupport": "在线支持", "feedback": "反馈", "settings": "设置", "voice": "语音助手" diff --git a/apps/mail/messages/zh_TW.json b/apps/mail/messages/zh_TW.json index 44b61ebf7a..f412931ac7 100644 --- a/apps/mail/messages/zh_TW.json +++ b/apps/mail/messages/zh_TW.json @@ -401,6 +401,7 @@ "spam": "垃圾郵件", "archive": "封存", "bin": "垃圾桶", + "livesupport": "即時支援", "feedback": "意見反饋", "settings": "設定", "voice": "語音助理" diff --git a/i18n.lock b/i18n.lock index 33136c09e7..c6d90c6fe4 100644 --- a/i18n.lock +++ b/i18n.lock @@ -852,6 +852,7 @@ checksums: navigation/sidebar/spam: 904064026d3ce87cd872e0b819a15310 navigation/sidebar/archive: fa813ab3074103e5daad07462af25789 navigation/sidebar/bin: e95691895f3a89d896838716e48290bd + navigation/sidebar/livesupport: 087e6998c099b3c08c5ade57c5f68752 navigation/sidebar/feedback: 6fac88806e0c269a30777b283988c61c navigation/sidebar/settings: 8df6777277469c1fd88cc18dde2f1cc3 navigation/sidebar/voice: 81a94ad8770dca9c3cbb5a88329b6a6f From c1ff7be9490c5fe212cc86bc3748ae34d5898190 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 13:16:22 -0700 Subject: [PATCH 23/28] No prefetch (#1628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/hooks/use-threads.ts | 68 +++++++++++++++++----------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/mail/hooks/use-threads.ts b/apps/mail/hooks/use-threads.ts index f524701597..a9877373da 100644 --- a/apps/mail/hooks/use-threads.ts +++ b/apps/mail/hooks/use-threads.ts @@ -69,8 +69,8 @@ export const useThread = (threadId: string | null, historyId?: string | null) => const [_threadId] = useQueryState('threadId'); const id = threadId ? threadId : _threadId; const trpc = useTRPC(); - const { data } = useSettings(); - const { resolvedTheme } = useTheme(); + // const { data } = useSettings(); + // const { resolvedTheme } = useTheme(); const previousHistoryId = usePrevious(historyId ?? null); const queryClient = useQueryClient(); @@ -92,44 +92,44 @@ export const useThread = (threadId: string | null, historyId?: string | null) => ), ); - const isTrustedSender = useMemo( - () => - !!data?.settings?.externalImages || - !!data?.settings?.trustedSenders?.includes(threadQuery.data?.latest?.sender.email ?? ''), - [data?.settings, threadQuery.data?.latest?.sender.email], - ); + // const isTrustedSender = useMemo( + // () => + // !!data?.settings?.externalImages || + // !!data?.settings?.trustedSenders?.includes(threadQuery.data?.latest?.sender.email ?? ''), + // [data?.settings, threadQuery.data?.latest?.sender.email], + // ); const latestDraft = useMemo(() => { if (!threadQuery.data?.latest?.id) return undefined; return threadQuery.data.messages.findLast((e) => e.isDraft); }, [threadQuery]); - const { mutateAsync: processEmailContent } = useMutation( - trpc.mail.processEmailContent.mutationOptions(), - ); - - const prefetchEmailContent = async (message: ParsedMessage) => { - return queryClient.prefetchQuery({ - queryKey: ['email-content', message.id, isTrustedSender, resolvedTheme], - queryFn: async () => { - const result = await processEmailContent({ - html: message.decodedBody ?? '', - shouldLoadImages: isTrustedSender, - theme: (resolvedTheme as 'light' | 'dark') || 'light', - }); - - return { - html: result.processedHtml, - hasBlockedImages: result.hasBlockedImages, - }; - }, - }); - }; - - useEffect(() => { - if (!threadQuery.data?.latest?.id) return; - prefetchEmailContent(threadQuery.data.latest); - }, [threadQuery.data?.latest]); + // const { mutateAsync: processEmailContent } = useMutation( + // trpc.mail.processEmailContent.mutationOptions(), + // ); + + // const prefetchEmailContent = async (message: ParsedMessage) => { + // return queryClient.prefetchQuery({ + // queryKey: ['email-content', message.id, isTrustedSender, resolvedTheme], + // queryFn: async () => { + // const result = await processEmailContent({ + // html: message.decodedBody ?? '', + // shouldLoadImages: isTrustedSender, + // theme: (resolvedTheme as 'light' | 'dark') || 'light', + // }); + + // return { + // html: result.processedHtml, + // hasBlockedImages: result.hasBlockedImages, + // }; + // }, + // }); + // }; + + // useEffect(() => { + // if (!threadQuery.data?.latest?.id) return; + // prefetchEmailContent(threadQuery.data.latest); + // }, [threadQuery.data?.latest]); const isGroupThread = useMemo(() => { if (!threadQuery.data?.latest?.id) return false; From 0b19bdad03e6283de88e833adc1e63e685bfd4e8 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 13:23:45 -0700 Subject: [PATCH 24/28] No batching (#1629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/app/root.tsx | 2 +- apps/mail/lib/trpc.server.ts | 2 +- apps/mail/providers/query-provider.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mail/app/root.tsx b/apps/mail/app/root.tsx index c36afb2739..22acfdacd1 100644 --- a/apps/mail/app/root.tsx +++ b/apps/mail/app/root.tsx @@ -32,7 +32,7 @@ export const getServerTrpc = (req: Request) => createTRPCClient({ links: [ httpBatchLink({ - maxItems: 8, + maxItems: 1, url: getUrl(), transformer: superjson, headers: req.headers, diff --git a/apps/mail/lib/trpc.server.ts b/apps/mail/lib/trpc.server.ts index 6210a468ac..78a5b127a7 100644 --- a/apps/mail/lib/trpc.server.ts +++ b/apps/mail/lib/trpc.server.ts @@ -8,7 +8,7 @@ export const getServerTrpc = (req: Request) => createTRPCClient({ links: [ httpBatchLink({ - maxItems: 8, + maxItems: 1, url: getUrl(), transformer: superjson, headers: req.headers, diff --git a/apps/mail/providers/query-provider.tsx b/apps/mail/providers/query-provider.tsx index 0e498c1789..2c0f429955 100644 --- a/apps/mail/providers/query-provider.tsx +++ b/apps/mail/providers/query-provider.tsx @@ -90,7 +90,7 @@ export const trpcClient = createTRPCClient({ transformer: superjson, url: getUrl(), methodOverride: 'POST', - maxItems: 8, + maxItems: 1, fetch: (url, options) => fetch(url, { ...options, credentials: 'include' }).then((res) => { const currentPath = new URL(window.location.href).pathname; From f60b0fcba00ba8072476ad25fd8d6d2054b59961 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 15:34:27 -0700 Subject: [PATCH 25/28] mail-list performance (#1631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/components/mail/mail-list.tsx | 153 +++++++++++------------- apps/mail/hooks/use-connections.ts | 10 +- 2 files changed, 81 insertions(+), 82 deletions(-) diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index df79b882dd..ff9ddd4467 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -6,7 +6,6 @@ import { getMainSearchTerm, parseNaturalLanguageSearch, } from '@/lib/utils'; -import { EmptyStateIcon } from '../icons/empty-state-svg'; import { Archive2, ExclamationCircle, @@ -24,18 +23,20 @@ import { useState, type ComponentProps, } from 'react'; +import { useIsFetching, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state'; import { focusedIndexAtom, useMailNavigation } from '@/hooks/use-mail-navigation'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import type { MailSelectMode, ParsedMessage, ThreadProps } from '@/types'; +import type { ParsedDraft } from '../../../server/src/lib/driver/types'; import { ThreadContextMenu } from '@/components/context/thread-context'; import { useOptimisticActions } from '@/hooks/use-optimistic-actions'; -import { useIsFetching, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useMail, type Config } from '@/components/mail/use-mail'; import { type ThreadDestination } from '@/lib/thread-actions'; import { useThread, useThreads } from '@/hooks/use-threads'; import { useSearchValue } from '@/hooks/use-search-value'; +import { EmptyStateIcon } from '../icons/empty-state-svg'; import { highlightText } from '@/lib/email-utils.client'; import { useHotkeysContext } from 'react-hotkeys-hook'; import { AnimatePresence, motion } from 'motion/react'; @@ -59,7 +60,6 @@ import { Button } from '../ui/button'; import { useQueryState } from 'nuqs'; import { Categories } from './mail'; import { useAtom } from 'jotai'; -import type { ParsedDraft } from '../../../server/src/lib/driver/types'; const Thread = memo( function Thread({ @@ -81,23 +81,15 @@ const Thread = memo( const [, setActiveReplyId] = useQueryState('activeReplyId'); const [focusedIndex, setFocusedIndex] = useAtom(focusedIndexAtom); - // const latestReceivedMessage = useMemo(() => { - // if (!getThreadData?.messages) return getThreadData?.latest; - - // const nonDraftMessages = getThreadData.messages.filter((msg) => !msg.isDraft); - // if (nonDraftMessages.length === 0) return getThreadData?.latest; - - // return ( - // nonDraftMessages.sort((a, b) => { - // const dateA = new Date(a.receivedOn).getTime(); - // const dateB = new Date(b.receivedOn).getTime(); - // return dateB - dateA; - // })[0] || getThreadData?.latest - // ); - // }, [getThreadData?.messages, getThreadData?.latest]); + const { latestMessage, idToUse, cleanName } = useMemo(() => { + const latestMessage = getThreadData?.latest; + const idToUse = latestMessage?.threadId ?? latestMessage?.id; + const cleanName = latestMessage?.sender?.name + ? latestMessage.sender.name.trim().replace(/^['"]|['"]$/g, '') + : ''; - const latestMessage = getThreadData?.latest; - const idToUse = useMemo(() => latestMessage?.threadId ?? latestMessage?.id, [latestMessage]); + return { latestMessage, idToUse, cleanName }; + }, [getThreadData?.latest]); const { data: settingsData } = useSettings(); const queryClient = useQueryClient(); @@ -109,57 +101,56 @@ const Thread = memo( const optimisticState = useOptimisticThreadState(idToUse ?? ''); - const displayStarred = useMemo(() => { - if (optimisticState.optimisticStarred !== null) { - return optimisticState.optimisticStarred; - } - return getThreadData?.latest?.tags?.some((tag) => tag.name === 'STARRED') ?? false; - }, [optimisticState.optimisticStarred, getThreadData?.latest?.tags]); - - const displayImportant = useMemo(() => { - if (optimisticState.optimisticImportant !== null) { - return optimisticState.optimisticImportant; - } - return getThreadData?.latest?.tags?.some((tag) => tag.name === 'IMPORTANT') ?? false; - }, [optimisticState.optimisticImportant, getThreadData?.latest?.tags]); - - const displayUnread = useMemo(() => { - if (optimisticState.optimisticRead !== null) { - return !optimisticState.optimisticRead; - } - return getThreadData?.hasUnread ?? false; - }, [optimisticState.optimisticRead, getThreadData?.hasUnread]); - - const optimisticLabels = useMemo(() => { - if (!getThreadData?.labels) return []; - - let labels = [...getThreadData.labels]; - const hasStarredLabel = labels.some((label) => label.name === 'STARRED'); - - if (optimisticState.optimisticStarred !== null) { - if (optimisticState.optimisticStarred && !hasStarredLabel) { - labels.push({ id: 'starred-optimistic', name: 'STARRED' }); - } else if (!optimisticState.optimisticStarred && hasStarredLabel) { - labels = labels.filter((label) => label.name !== 'STARRED'); + const { displayStarred, displayImportant, displayUnread, optimisticLabels } = useMemo(() => { + const displayStarred = + optimisticState.optimisticStarred !== null + ? optimisticState.optimisticStarred + : (getThreadData?.latest?.tags?.some((tag) => tag.name === 'STARRED') ?? false); + + const displayImportant = + optimisticState.optimisticImportant !== null + ? optimisticState.optimisticImportant + : (getThreadData?.latest?.tags?.some((tag) => tag.name === 'IMPORTANT') ?? false); + + const displayUnread = + optimisticState.optimisticRead !== null + ? !optimisticState.optimisticRead + : (getThreadData?.hasUnread ?? false); + + let labels: { id: string; name: string }[] = []; + if (getThreadData?.labels) { + labels = [...getThreadData.labels]; + const hasStarredLabel = labels.some((label) => label.name === 'STARRED'); + + if (optimisticState.optimisticStarred !== null) { + if (optimisticState.optimisticStarred && !hasStarredLabel) { + labels.push({ id: 'starred-optimistic', name: 'STARRED' }); + } else if (!optimisticState.optimisticStarred && hasStarredLabel) { + labels = labels.filter((label) => label.name !== 'STARRED'); + } } - } - if (optimisticState.optimisticLabels) { - labels = labels.filter( - (label) => !optimisticState.optimisticLabels.removedLabelIds.includes(label.id), - ); + if (optimisticState.optimisticLabels) { + labels = labels.filter( + (label) => !optimisticState.optimisticLabels.removedLabelIds.includes(label.id), + ); - optimisticState.optimisticLabels.addedLabelIds.forEach((labelId) => { - if (!labels.some((label) => label.id === labelId)) { - labels.push({ id: labelId, name: labelId }); - } - }); + optimisticState.optimisticLabels.addedLabelIds.forEach((labelId) => { + if (!labels.some((label) => label.id === labelId)) { + labels.push({ id: labelId, name: labelId }); + } + }); + } } - return labels; + return { displayStarred, displayImportant, displayUnread, optimisticLabels: labels }; }, [ - getThreadData?.labels, optimisticState.optimisticStarred, + optimisticState.optimisticImportant, + optimisticState.optimisticRead, + getThreadData?.latest?.tags, + getThreadData?.hasUnread, + getThreadData?.labels, optimisticState.optimisticLabels, ]); @@ -276,15 +267,15 @@ const Thread = memo( const isMailBulkSelected = idToUse ? mailState.bulkSelected.includes(idToUse) : false; - const isFolderInbox = folder === FOLDERS.INBOX || !folder; - const isFolderSpam = folder === FOLDERS.SPAM; - const isFolderSent = folder === FOLDERS.SENT; - const isFolderBin = folder === FOLDERS.BIN; - - const cleanName = useMemo(() => { - if (!latestMessage?.sender?.name) return ''; - return latestMessage.sender.name.trim().replace(/^['"]|['"]$/g, ''); - }, [latestMessage?.sender?.name]); + const { isFolderInbox, isFolderSpam, isFolderSent, isFolderBin } = useMemo( + () => ({ + isFolderInbox: folder === FOLDERS.INBOX || !folder, + isFolderSpam: folder === FOLDERS.SPAM, + isFolderSent: folder === FOLDERS.SENT, + isFolderBin: folder === FOLDERS.BIN, + }), + [folder], + ); // Check if thread has a draft const hasDraft = useMemo(() => { @@ -294,9 +285,7 @@ const Thread = memo( const content = latestMessage && getThreadData ? (
{ window.dispatchEvent(new CustomEvent('emailHover', { detail: { id: idToUse } })); @@ -419,7 +408,9 @@ const Thread = memo( ) : null}
-
+
0; - const itemsRef = useRef(items); + const parentRef = useRef(null); + const vListRef = useRef(null); + useEffect(() => { itemsRef.current = items; }, [items]); @@ -778,9 +771,6 @@ export const MailList = memo( return () => window.removeEventListener('refreshMailList', handleRefresh); }, [refetch]); - const parentRef = useRef(null); - const vListRef = useRef(null); - const handleNavigateToThread = useCallback( (threadId: string | null) => { setThreadId(threadId); @@ -931,7 +921,7 @@ export const MailList = memo( const filteredItems = useMemo(() => items.filter((item) => item.id), [items]); - const Comp = folder === FOLDERS.DRAFT ? Draft : Thread; + const Comp = useMemo(() => (folder === FOLDERS.DRAFT ? Draft : Thread), [folder]); const vListRenderer = useCallback( (index: number) => { @@ -956,6 +946,7 @@ export const MailList = memo( ); }, [ + folder, filteredItems, focusedIndex, keyboardActive, diff --git a/apps/mail/hooks/use-connections.ts b/apps/mail/hooks/use-connections.ts index 2242e0e386..999f4f0323 100644 --- a/apps/mail/hooks/use-connections.ts +++ b/apps/mail/hooks/use-connections.ts @@ -9,6 +9,14 @@ export const useConnections = () => { export const useActiveConnection = () => { const trpc = useTRPC(); - const connectionsQuery = useQuery(trpc.connections.getDefault.queryOptions()); + const connectionsQuery = useQuery( + trpc.connections.getDefault.queryOptions(void 0, { + staleTime: 1000 * 60 * 60, // 1 hour, + gcTime: 1000 * 60 * 60 * 24, // 24 hours + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + }), + ); return connectionsQuery; }; From 8845ee728078f8a3757689690c7c671cade9621a Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 15:41:25 -0700 Subject: [PATCH 26/28] no clear cache on logout - revisit later (#1632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/components/ui/nav-user.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mail/components/ui/nav-user.tsx b/apps/mail/components/ui/nav-user.tsx index 9f558cd7ea..4b35d91960 100644 --- a/apps/mail/components/ui/nav-user.tsx +++ b/apps/mail/components/ui/nav-user.tsx @@ -152,7 +152,7 @@ export function NavUser() { success: () => 'Signed out successfully!', error: 'Error signing out', async finally() { - await handleClearCache(); + // await handleClearCache(); window.location.href = '/login'; }, }); From a64532a18566d1ed5c20ab4a52eaf62041e36e39 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 18:14:02 -0700 Subject: [PATCH 27/28] minor fixes (#1634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- .../mail/components/create/email-composer.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index 8425caa074..e46a379fc8 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -14,6 +14,7 @@ import { SelectValue, } from '@/components/ui/select'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; import { Check, Command, Loader, Paperclip, Plus, Type, X as XIcon } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TextEffect } from '@/components/motion-primitives/text-effect'; @@ -45,8 +46,7 @@ import { Toolbar } from './toolbar'; import pluralize from 'pluralize'; import { toast } from 'sonner'; import { z } from 'zod'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; - +const shortcodeRegex = /:([a-zA-Z0-9_+-]+):/g; type ThreadContent = { from: string; @@ -82,10 +82,10 @@ interface EmailComposerProps { const isValidEmail = (email: string): boolean => { // for format like test@example.com - const simpleEmailRegex = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const simpleEmailRegex = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; // for format like name - const displayNameEmailRegex = /^.+\s*<\s*[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\s*>$/; + const displayNameEmailRegex = /^.+\s*<\s*[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\s*>$/; return simpleEmailRegex.test(email) || displayNameEmailRegex.test(email); }; @@ -700,8 +700,7 @@ export function EmailComposer({ }; const replaceEmojiShortcodes = (text: string): string => { - const shortcodeRegex = /:([a-zA-Z0-9_+-]+):/g; - + if (!text.trim().length || !text.includes(':')) return text; return text.replace(shortcodeRegex, (match, shortcode): string => { const emoji = gitHubEmojis.find( (e) => e.shortcodes.includes(shortcode) || e.name === shortcode, @@ -746,7 +745,7 @@ export function EmailComposer({ {email.charAt(0).toUpperCase()} - + {email} @@ -880,7 +879,7 @@ export function EmailComposer({ {email.charAt(0).toUpperCase()} - + {/* for email format: "Display Name" */} {email.match(/^"?(.*?)"?\s*<[^>]+>$/)?.[1] ?? email} @@ -1406,10 +1405,10 @@ export function EmailComposer({ {file.type.includes('pdf') ? '📄' : file.type.includes('excel') || - file.type.includes('spreadsheetml') + file.type.includes('spreadsheetml') ? '📊' : file.type.includes('word') || - file.type.includes('wordprocessingml') + file.type.includes('wordprocessingml') ? '📝' : '📎'} @@ -1468,13 +1467,12 @@ export function EmailComposer({ onClick={() => setToggleToolbar(!toggleToolbar)} className={`h-auto w-auto rounded p-1.5 ${toggleToolbar ? 'bg-muted' : 'bg-background'} border`} > - + Formatting options -
From cf88826e39c7ba73f750df3a975b994e148f5c0c Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 4 Jul 2025 20:52:29 -0700 Subject: [PATCH 28/28] ai work (#1635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ --- apps/mail/components/create/ai-chat.tsx | 87 ++++++++++++-------- apps/mail/components/mail/mail-list.tsx | 14 ++-- apps/mail/components/mail/mail.tsx | 9 +- apps/mail/components/ui/ai-sidebar.tsx | 4 +- apps/server/package.json | 7 +- apps/server/src/routes/agent/orchestrator.ts | 84 +++++++++++++++++++ apps/server/src/routes/agent/tools.ts | 80 +++++++++++------- apps/server/src/routes/chat.ts | 8 +- pnpm-lock.yaml | 81 ++++++++++++++++++ 9 files changed, 292 insertions(+), 82 deletions(-) create mode 100644 apps/server/src/routes/agent/orchestrator.ts diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index 3ee078999a..ce0b000300 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -1,4 +1,5 @@ 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 useComposeEditor from '@/hooks/use-compose-editor'; @@ -185,15 +186,6 @@ const ToolResponse = ({ toolName, result, args }: { toolName: string; result: an
) : null; - case Tools.WebSearch: - return ( -
-
-

{result}

-
-
- ); - case Tools.ComposeEmail: return result?.newBody ? (
@@ -204,13 +196,6 @@ const ToolResponse = ({ toolName, result, args }: { toolName: string; result: an ) : null; default: - if (result?.success) { - return ( -
- Operation completed successfully -
- ); - } return null; } }; @@ -262,19 +247,22 @@ export function AIChat({ } }, []); + useEffect(() => { + if (!['submitted', 'streaming'].includes(status)) { + scrollToBottom(); + } + }, [status, scrollToBottom]); + const editor = useComposeEditor({ placeholder: 'Ask Zero to do anything...', onLengthChange: () => setInput(editor.getText()), onKeydown(event) { - // Cmd+0 to toggle the AI sidebar (Added explicitly since TipTap editor doesn't bubble up the event) if (event.key === '0' && event.metaKey) { return toggleOpen(); } if (event.key === 'Enter' && !event.metaKey && !event.shiftKey) { - event.preventDefault(); - handleSubmit(event as unknown as React.FormEvent); - editor.commands.clearContent(true); + onSubmit(event as unknown as React.FormEvent); } }, }); @@ -283,12 +271,11 @@ export function AIChat({ e.preventDefault(); handleSubmit(e); editor.commands.clearContent(true); + setTimeout(() => { + scrollToBottom(); + }, 100); }; - useEffect(() => { - scrollToBottom(); - }, [messages, scrollToBottom]); - useEffect(() => { if (aiSidebarOpen === 'true') { editor.commands.focus(); @@ -298,7 +285,7 @@ export function AIChat({ return (
-
+
{chatMessages && !chatMessages.enabled ? (
setPricingDialog('true')} @@ -334,14 +321,18 @@ export function AIChat({ messages.map((message, index) => { const textParts = message.parts.filter((part) => part.type === 'text'); const toolParts = message.parts.filter((part) => part.type === 'tool-invocation'); - const toolResultOnlyTools = [Tools.WebSearch]; - const doesIncludeToolResult = toolParts.some((part) => - toolResultOnlyTools.includes(part.toolInvocation?.toolName as Tools), + const streamingTools = [Tools.WebSearch]; + const doesIncludeStreamingTool = toolParts.some( + (part) => + streamingTools.includes(part.toolInvocation?.toolName as Tools) && + part.toolInvocation?.result, ); return ( -
+
{toolParts.map((part, idx) => - part.toolInvocation && part.toolInvocation.result ? ( + part.toolInvocation && + part.toolInvocation.result && + !streamingTools.includes(part.toolInvocation.toolName as Tools) ? ( ) : null, )} - {!doesIncludeToolResult && textParts.length > 0 && ( + {!doesIncludeStreamingTool && textParts.length > 0 && (

{textParts.map( - (part) => part.text && {part.text || ' '}, + (part) => + part.text && ( + + {part.text || ' '} + + ), )}

)} @@ -368,7 +389,6 @@ export function AIChat({ ); }) )} -
{(status === 'submitted' || status === 'streaming') && (
@@ -382,6 +402,7 @@ export function AIChat({ {(status === 'error' || !!error) && (
Error, please try again later
)} +
diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index ff9ddd4467..fbce38fedb 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -93,11 +93,11 @@ const Thread = memo( const { data: settingsData } = useSettings(); const queryClient = useQueryClient(); - // Check if thread has notes - const { data: threadNotes } = useThreadNotes(idToUse || ''); - const hasNotes = useMemo(() => { - return (threadNotes?.notes && threadNotes.notes.length > 0) || false; - }, [threadNotes?.notes]); + // // Check if thread has notes + // const { data: threadNotes } = useThreadNotes(idToUse || ''); + // const hasNotes = useMemo(() => { + // return (threadNotes?.notes && threadNotes.notes.length > 0) || false; + // }, [threadNotes?.notes]); const optimisticState = useOptimisticThreadState(idToUse ?? ''); @@ -530,11 +530,11 @@ const Thread = memo( Draft ) : null} - {hasNotes ? ( + {/* {hasNotes ? ( - ) : null} + ) : null} */}
{latestMessage.receivedOn ? ( diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index cd638b1e4f..353df3b5f7 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -385,14 +385,11 @@ export function MailLayout() { const isMobile = useIsMobile(); const navigate = useNavigate(); const { data: session, isPending } = useSession(); - const { data: connections } = useConnections(); const prevFolderRef = useRef(folder); const { enableScope, disableScope } = useHotkeysContext(); const { data: activeConnection } = useActiveConnection(); const { activeFilters, clearAllFilters } = useCommandPalette(); - const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useQueryState('isCommandPaletteOpen'); - - const { data: activeAccount } = useActiveConnection(); + const [, setIsCommandPaletteOpen] = useQueryState('isCommandPaletteOpen'); useEffect(() => { if (prevFolderRef.current !== folder && mail.bulkSelected.length > 0) { @@ -632,8 +629,8 @@ export function MailLayout() {
)} - - + {activeConnection?.id ? : null} + {activeConnection?.id ? : null}
diff --git a/apps/mail/components/ui/ai-sidebar.tsx b/apps/mail/components/ui/ai-sidebar.tsx index 2fce4311f3..b14b3c3628 100644 --- a/apps/mail/components/ui/ai-sidebar.tsx +++ b/apps/mail/components/ui/ai-sidebar.tsx @@ -348,11 +348,10 @@ function AISidebar({ className }: AISidebarProps) { const { isPro, track, refetch: refetchBilling } = useBilling(); const queryClient = useQueryClient(); const trpc = useTRPC(); - const [threadId, setThreadId] = useQueryState('threadId'); + const [threadId] = useQueryState('threadId'); const { folder } = useParams<{ folder: string }>(); const { refetch: refetchLabels } = useLabels(); const [searchValue] = useSearchValue(); - const { data: session } = useSession(); const { data: activeConnection } = useActiveConnection(); const agent = useAgent({ @@ -363,7 +362,6 @@ function AISidebar({ className }: AISidebarProps) { const chatState = useAgentChat({ agent, - initialMessages: [], maxSteps: 5, body: { threadId: threadId ?? undefined, diff --git a/apps/server/package.json b/apps/server/package.json index 4bf3f70f06..ee6834eda4 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,6 +24,7 @@ "@ai-sdk/openai": "^1.3.21", "@ai-sdk/perplexity": "1.1.9", "@ai-sdk/ui-utils": "1.2.11", + "@arcadeai/arcadejs": "1.8.1", "@coinbase/cookie-manager": "1.1.8", "@googleapis/gmail": "12.0.0", "@googleapis/people": "3.0.9", @@ -56,6 +57,7 @@ "hono-party": "^0.0.12", "jose": "6.0.11", "jsonrepair": "^3.12.0", + "mime-types": "3.0.1", "mimetext": "^3.0.27", "p-retry": "6.2.1", "partyserver": "^0.0.71", @@ -67,10 +69,9 @@ "string-strip-html": "^13.4.12", "superjson": "catalog:", "twilio": "5.7.0", - "wrangler": "catalog:", - "zod": "catalog:", "uuid": "11.1.0", - "mime-types": "3.0.1" + "wrangler": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@types/he": "1.2.3", diff --git a/apps/server/src/routes/agent/orchestrator.ts b/apps/server/src/routes/agent/orchestrator.ts new file mode 100644 index 0000000000..5e4129da83 --- /dev/null +++ b/apps/server/src/routes/agent/orchestrator.ts @@ -0,0 +1,84 @@ +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'; + +/** + * Orchestrator that handles the distinction between tools and agents. + * Tools execute and return results, while agents stream responses directly. + */ +export class ToolOrchestrator { + private dataStream: DataStreamWriter; + private streamingTools: Set = new Set([Tools.WebSearch]); + + constructor(dataStream: DataStreamWriter) { + this.dataStream = dataStream; + } + + /** + * Determines if a tool should be treated as an agent that streams + */ + isStreamingTool(toolName: string): boolean { + return this.streamingTools.has(toolName); + } + + /** + * Creates a streaming agent wrapper for tools that should stream responses directly + */ + createStreamingAgent(toolName: string, originalTool: any) { + if (!this.isStreamingTool(toolName)) { + return originalTool; + } + + // For webSearch, we want to stream the response directly without wrapping it as a tool result + if (toolName === Tools.WebSearch) { + return tool({ + description: 'Search the web for information using Perplexity AI', + parameters: z.object({ + query: z.string().describe('The query to search the web for'), + }), + execute: async ({ query }) => { + try { + const response = streamText({ + model: perplexity('sonar'), + messages: [ + { role: 'system', content: 'Be precise and concise.' }, + { role: 'system', content: 'Do not include sources in your response.' }, + { role: 'system', content: 'Do not use markdown formatting in your response.' }, + { role: 'user', content: query }, + ], + maxTokens: 1024, + }); + + // Stream the response directly to the data stream + response.mergeIntoDataStream(this.dataStream); + + // Return a placeholder result since the actual streaming happens above + return { type: 'streaming_response', toolName }; + } catch (error) { + console.error('Error searching the web:', error); + throw new Error('Failed to search the web'); + } + }, + }); + } + + return originalTool; + } + + /** + * Processes all tools and returns modified versions for streaming tools + */ + processTools(tools: T): T { + const processedTools = { ...tools }; + + for (const [toolName, toolInstance] of Object.entries(tools)) { + if (this.isStreamingTool(toolName)) { + processedTools[toolName as keyof T] = this.createStreamingAgent(toolName, toolInstance); + } + } + + return processedTools; + } +} diff --git a/apps/server/src/routes/agent/tools.ts b/apps/server/src/routes/agent/tools.ts index fd07d48d71..1147e3e268 100644 --- a/apps/server/src/routes/agent/tools.ts +++ b/apps/server/src/routes/agent/tools.ts @@ -1,9 +1,11 @@ +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 { generateText, tool } from 'ai'; import { Tools } from '../../types'; import { z } from 'zod'; @@ -328,33 +330,52 @@ const deleteLabel = (driver: MailManager) => }, }); -export const webSearch = tool({ - description: 'Search the web for information using Perplexity AI', - parameters: z.object({ - query: z.string().describe('The query to search the web for'), - }), - execute: async ({ query }) => { - try { - const { text } = await generateText({ - model: perplexity('sonar'), - messages: [ - { role: 'system', content: 'Be precise and concise.' }, - { role: 'system', content: 'Do not include sources in your response.' }, - { role: 'system', content: 'Do not use markdown formatting in your response.' }, - { role: 'user', content: query }, - ], - maxTokens: 1024, - }); +const getGoogleTools = async (connectionId: string) => { + const arcade = new Arcade(); + const googleToolkit = await arcade.tools.list({ toolkit: 'google', limit: 30 }); + const googleTools = toZodToolSet({ + tools: googleToolkit.items, + client: arcade, + userId: connectionId, // Your app's internal ID for the user (an email, UUID, etc). It's used internally to identify your user in Arcade + executeFactory: executeOrAuthorizeZodTool, // Checks if tool is authorized and executes it, or returns authorization URL if needed + }); + return googleTools; +}; + +export const webSearch = (dataStream: DataStreamWriter) => + tool({ + description: 'Search the web for information using Perplexity AI', + parameters: z.object({ + query: z.string().describe('The query to search the web for'), + }), + execute: async ({ query }) => { + try { + const response = streamText({ + model: perplexity('sonar'), + messages: [ + { role: 'system', content: 'Be precise and concise.' }, + { role: 'system', content: 'Do not include sources in your response.' }, + { role: 'system', content: 'Do not use markdown formatting in your response.' }, + { role: 'user', content: query }, + ], + maxTokens: 1024, + }); - return text; - } catch (error) { - console.error('Error searching the web:', error); - throw new Error('Failed to search the web'); - } - }, -}); + response.mergeIntoDataStream(dataStream); -export const tools = (driver: MailManager, connectionId: string) => { + return { type: 'streaming_response', query }; + } catch (error) { + console.error('Error searching the web:', error); + throw new Error('Failed to search the web'); + } + }, + }); + +export const tools = async ( + driver: MailManager, + connectionId: string, + dataStream: DataStreamWriter, +) => { return { [Tools.GetThread]: getEmail(driver), [Tools.ComposeEmail]: composeEmailTool(connectionId), @@ -368,8 +389,9 @@ export const tools = (driver: MailManager, connectionId: string) => { [Tools.BulkDelete]: bulkDelete(driver), [Tools.BulkArchive]: bulkArchive(driver), [Tools.DeleteLabel]: deleteLabel(driver), - [Tools.AskZeroMailbox]: askZeroMailbox(connectionId), - [Tools.AskZeroThread]: askZeroThread(connectionId), - [Tools.WebSearch]: webSearch, + // [Tools.AskZeroMailbox]: askZeroMailbox(connectionId), + // [Tools.AskZeroThread]: askZeroThread(connectionId), + [Tools.WebSearch]: webSearch(dataStream), + // ...(await getGoogleTools(connectionId)), }; }; diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 5b9671c800..0722432247 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -18,6 +18,7 @@ import type { IGetThreadResponse, MailManager } from '../lib/driver/types'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createSimpleAuth, type SimpleAuth } from '../lib/auth'; import { connectionToDriver } from '../lib/server-utils'; +import { ToolOrchestrator } from './agent/orchestrator'; import type { CreateDraftData } from '../lib/schemas'; import { FOLDERS, parseHeaders } from '../lib/utils'; import { env, RpcTarget } from 'cloudflare:workers'; @@ -352,7 +353,12 @@ export class ZeroAgent extends AIChatAgent { throw new Error('Unauthorized no driver or connectionId [2]'); } } - const tools = { ...authTools(this.driver, connectionId), buildGmailSearchQuery }; + const orchestrator = new ToolOrchestrator(dataStream); + const rawTools = { + ...(await authTools(this.driver, connectionId, dataStream)), + buildGmailSearchQuery, + }; + const tools = orchestrator.processTools(rawTools); const processedMessages = await processToolCalls( { messages: this.messages, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f631d8ed8..1a1df2feab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,9 @@ importers: '@ai-sdk/ui-utils': specifier: 1.2.11 version: 1.2.11(zod@3.25.67) + '@arcadeai/arcadejs': + specifier: 1.8.1 + version: 1.8.1 '@coinbase/cookie-manager': specifier: 1.1.8 version: 1.1.8 @@ -762,6 +765,9 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@arcadeai/arcadejs@1.8.1': + resolution: {integrity: sha512-ZTj2UvdfFmFn1as4gdDiZD8nbnEFZcZUzH9XtTmjRbgf/1V8s1wEtlzlI3vct+dA+KZ+NhS79AEw5lx/Ki0xSw==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -3703,6 +3709,12 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node@18.19.115': + resolution: {integrity: sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==} + '@types/node@22.13.8': resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==} @@ -3851,6 +3863,10 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + agents@0.0.93: resolution: {integrity: sha512-W25kx492Txn5XYY9gx2YBhGmfC8C/N3JQzfjbmq9GjhYtAFCsJdIw6C5xbLt/ev2x3Uor/8XMHXYiw/2YbTSkQ==} peerDependencies: @@ -4975,6 +4991,9 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data-encoder@4.1.0: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} @@ -4983,6 +5002,10 @@ packages: resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + formdata-node@6.0.3: resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} engines: {node: '>= 18'} @@ -5243,6 +5266,9 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -5964,6 +5990,11 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -7331,6 +7362,9 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -7551,6 +7585,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -7764,6 +7802,19 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@arcadeai/arcadejs@1.8.1': + dependencies: + '@types/node': 18.19.115 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + zod: 3.25.67 + transitivePeerDependencies: + - encoding + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -10673,6 +10724,15 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.15.29 + form-data: 4.0.3 + + '@types/node@18.19.115': + dependencies: + undici-types: 5.26.5 + '@types/node@22.13.8': dependencies: undici-types: 6.20.0 @@ -10837,6 +10897,10 @@ snapshots: agent-base@7.1.3: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + agents@0.0.93(@cloudflare/workers-types@4.20250628.0)(react@19.1.0): dependencies: '@modelcontextprotocol/sdk': 1.12.0 @@ -12140,6 +12204,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} + form-data-encoder@4.1.0: {} form-data@4.0.3: @@ -12150,6 +12216,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + formdata-node@6.0.3: {} forwarded@0.2.0: {} @@ -12452,6 +12523,10 @@ snapshots: human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + husky@9.1.7: {} hyphenate-style-name@1.1.0: {} @@ -13254,6 +13329,8 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -14901,6 +14978,8 @@ snapshots: uncrypto@0.1.3: {} + undici-types@5.26.5: {} + undici-types@6.20.0: {} undici-types@6.21.0: {} @@ -15116,6 +15195,8 @@ snapshots: w3c-keyname@2.2.8: {} + web-streams-polyfill@4.0.0-beta.3: {} + web-vitals@4.2.4: {} webidl-conversions@3.0.1: {}