diff --git a/apps/unsubscriber/package.json b/apps/unsubscriber/package.json index 7b58d8fb4b..aeceeec7ed 100644 --- a/apps/unsubscriber/package.json +++ b/apps/unsubscriber/package.json @@ -18,13 +18,13 @@ "typescript": "5.7.2" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "^1.0.6", - "@ai-sdk/anthropic": "^1.0.8", - "@ai-sdk/google": "^1.0.12", - "@ai-sdk/openai": "^1.0.11", + "@ai-sdk/amazon-bedrock": "1.1.6", + "@ai-sdk/anthropic": "1.1.6", + "@ai-sdk/google": "1.1.11", + "@ai-sdk/openai": "1.1.9", "@fastify/cors": "^10.0.1", "@t3-oss/env-core": "^0.11.1", - "ai": "^4.0.31", + "ai": "4.1.32", "dotenv": "^16.4.7", "fastify": "^5.2.0", "zod": "^3.24.1" diff --git a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx index e587b18a57..fd24f1bfcc 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx @@ -168,6 +168,7 @@ function TestRulesContentRow(props: { snippet: message.snippet || null, threadId: message.threadId, messageId: message.id, + date: message.internalDate || undefined, }); }} > diff --git a/apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx b/apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx index c6d5abceed..ffcf032d88 100644 --- a/apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx +++ b/apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx @@ -169,46 +169,51 @@ export function ReplyTrackerEmails({ } return ( - - - {listView} - - - - - {trackers.find((t) => t.threadId === selectedEmail.threadId) - ?.resolved ? ( - - ) : ( - - )} - - - } - /> - - + // hacky. this will break if other parts of the layout change +
+ + +
{listView}
+
+ + +
+ + {trackers.find((t) => t.threadId === selectedEmail.threadId) + ?.resolved ? ( + + ) : ( + + )} + +
+ } + /> +
+ + + ); } @@ -250,7 +255,7 @@ function Row({ )} onMouseEnter={onSelect} > - +
e.stopPropagation()} diff --git a/apps/web/app/(app)/reply-tracker/page.tsx b/apps/web/app/(app)/reply-tracker/page.tsx index 15e8c19350..c3913ad1fd 100644 --- a/apps/web/app/(app)/reply-tracker/page.tsx +++ b/apps/web/app/(app)/reply-tracker/page.tsx @@ -68,7 +68,7 @@ export default async function ReplyTrackerPage({ - Resolved + Done diff --git a/apps/web/app/(app)/simple/SimpleList.tsx b/apps/web/app/(app)/simple/SimpleList.tsx index 513f57e376..6532406ec5 100644 --- a/apps/web/app/(app)/simple/SimpleList.tsx +++ b/apps/web/app/(app)/simple/SimpleList.tsx @@ -281,10 +281,6 @@ function SimpleListRow({ )} - - {/*
- {new Date(message.headers.date).toLocaleString()} -
*/}
{!expanded &&
{actionButtons}
} diff --git a/apps/web/app/api/ai/categorize/validation.ts b/apps/web/app/api/ai/categorize/validation.ts index 7f7b6ccdef..1d8762c074 100644 --- a/apps/web/app/api/ai/categorize/validation.ts +++ b/apps/web/app/api/ai/categorize/validation.ts @@ -17,6 +17,6 @@ export const categorizeBodyWithHtml = categorizeBody.extend({ textPlain: z.string().nullable(), textHtml: z.string().nullable(), snippet: z.string().nullable(), - date: z.string(), + internalDate: z.string(), }); export type CategorizeBodyWithHtml = z.infer; diff --git a/apps/web/app/api/ai/summarise/controller.ts b/apps/web/app/api/ai/summarise/controller.ts index b6080a622f..0d01d9be5b 100644 --- a/apps/web/app/api/ai/summarise/controller.ts +++ b/apps/web/app/api/ai/summarise/controller.ts @@ -1,5 +1,4 @@ import { chatCompletionStream } from "@/utils/llms"; -import { Provider } from "@/utils/llms/config"; import type { UserAIFields } from "@/utils/llms/types"; import { expire } from "@/utils/redis"; import { saveSummary } from "@/utils/redis/summary"; diff --git a/apps/web/app/api/google/webhook/process-history-item.ts b/apps/web/app/api/google/webhook/process-history-item.ts index 356faab303..6be59434aa 100644 --- a/apps/web/app/api/google/webhook/process-history-item.ts +++ b/apps/web/app/api/google/webhook/process-history-item.ts @@ -15,6 +15,7 @@ import type { ProcessHistoryOptions } from "@/app/api/google/webhook/types"; import { ColdEmailSetting } from "@prisma/client"; import { logger } from "@/app/api/google/webhook/logger"; import { isIgnoredSender } from "@/utils/filter-ignored-senders"; +import { internalDateToDate } from "@/utils/date"; export async function processHistoryItem( { @@ -138,7 +139,7 @@ export async function processHistoryItem( content, messageId, threadId, - date: message.headers.date, + date: internalDateToDate(message.internalDate), }, gmail, user, diff --git a/apps/web/app/api/user/stats/tinybird/load/load-emails.ts b/apps/web/app/api/user/stats/tinybird/load/load-emails.ts index 658cf6b51a..02a1ced232 100644 --- a/apps/web/app/api/user/stats/tinybird/load/load-emails.ts +++ b/apps/web/app/api/user/stats/tinybird/load/load-emails.ts @@ -10,6 +10,7 @@ import { findUnsubscribeLink } from "@/utils/parse/parseHtml.server"; import { env } from "@/env"; import { GmailLabel } from "@/utils/gmail/label"; import { createScopedLogger } from "@/utils/logger"; +import { internalDateToDate } from "@/utils/date"; const PAGE_SIZE = 20; // avoid setting too high because it will hit the rate limit const PAUSE_AFTER_RATE_LIMIT = 10_000; @@ -169,7 +170,7 @@ async function saveBatch( ? extractDomainFromEmail(m.headers.to) : "Missing", subject: m.headers.subject, - timestamp: +new Date(m.headers.date), + timestamp: +internalDateToDate(m.internalDate), unsubscribeLink, read: !m.labelIds?.includes(GmailLabel.UNREAD), sent: !!m.labelIds?.includes(GmailLabel.SENT), @@ -182,7 +183,7 @@ async function saveBatch( logger.error("No timestamp for email", { ownerEmail: tinybirdEmail.ownerEmail, gmailMessageId: tinybirdEmail.gmailMessageId, - date: m.headers.date, + date: m.internalDate, }); return; } diff --git a/apps/web/components/email-list/EmailAttachments.tsx b/apps/web/components/email-list/EmailAttachments.tsx new file mode 100644 index 0000000000..46f27abaca --- /dev/null +++ b/apps/web/components/email-list/EmailAttachments.tsx @@ -0,0 +1,69 @@ +import { Card } from "@/components/Card"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { DownloadIcon } from "lucide-react"; +import type { ThreadMessage } from "@/components/email-list/types"; + +export function EmailAttachments({ message }: { message: ThreadMessage }) { + return ( +
+ {message.attachments?.map((attachment) => { + const searchParams = new URLSearchParams({ + messageId: message.id, + attachmentId: attachment.attachmentId, + mimeType: attachment.mimeType, + filename: attachment.filename, + }); + + const url = `/api/google/messages/attachment?${searchParams.toString()}`; + + return ( + +
{attachment.filename}
+
+
+ {mimeTypeToString(attachment.mimeType)} +
+ +
+
+ ); + })} +
+ ); +} + +function mimeTypeToString(mimeType: string): string { + switch (mimeType) { + case "application/pdf": + return "PDF"; + case "application/zip": + return "ZIP"; + case "image/png": + return "PNG"; + case "image/jpeg": + return "JPEG"; + // LLM generated. Need to check they're actually needed + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + return "DOCX"; + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + return "XLSX"; + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + return "PPTX"; + case "application/vnd.ms-excel": + return "XLS"; + case "application/vnd.ms-powerpoint": + return "PPT"; + case "application/msword": + return "DOC"; + default: + return mimeType; + } +} diff --git a/apps/web/components/email-list/EmailContents.tsx b/apps/web/components/email-list/EmailContents.tsx new file mode 100644 index 0000000000..0c284f690c --- /dev/null +++ b/apps/web/components/email-list/EmailContents.tsx @@ -0,0 +1,67 @@ +import { type SyntheticEvent, useCallback, useMemo, useState } from "react"; +import { Loading } from "@/components/Loading"; + +export function HtmlEmail({ html }: { html: string }) { + const srcDoc = useMemo(() => getIframeHtml(html), [html]); + const [isLoading, setIsLoading] = useState(true); + + const onLoad = useCallback( + (event: SyntheticEvent) => { + if (event.currentTarget.contentWindow) { + // sometimes we see minimal scrollbar, so add a buffer + const BUFFER = 5; + + const height = `${ + event.currentTarget.contentWindow.document.documentElement + .scrollHeight + BUFFER + }px`; + + event.currentTarget.style.height = height; + setIsLoading(false); + } + }, + [], + ); + + return ( +
+ {isLoading && } +