Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/unsubscriber/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/(app)/cold-email-blocker/TestRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ function TestRulesContentRow(props: {
snippet: message.snippet || null,
threadId: message.threadId,
messageId: message.id,
date: message.internalDate || undefined,
});
}}
>
Expand Down
89 changes: 47 additions & 42 deletions apps/web/app/(app)/reply-tracker/ReplyTrackerEmails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,46 +169,51 @@ export function ReplyTrackerEmails({
}

return (
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={35} minSize={0}>
{listView}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={65} minSize={0} className="bg-slate-100">
<ThreadContent
threadId={selectedEmail.threadId}
showReplyButton={true}
autoOpenReplyForMessageId={selectedEmail.messageId}
topRightComponent={
<div className="flex items-center gap-1">
{trackers.find((t) => t.threadId === selectedEmail.threadId)
?.resolved ? (
<UnresolveButton
threadId={selectedEmail.threadId}
onResolve={handleResolve}
isLoading={resolvingThreads.has(selectedEmail.threadId)}
showShortcut={false}
/>
) : (
<ResolveButton
threadId={selectedEmail.threadId}
onResolve={handleResolve}
isLoading={resolvingThreads.has(selectedEmail.threadId)}
showShortcut={false}
/>
)}
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedEmail(null)}
>
<XIcon className="size-4" />
</Button>
</div>
}
/>
</ResizablePanel>
</ResizablePanelGroup>
// hacky. this will break if other parts of the layout change
<div className="h-[calc(100vh-7.5rem)]">
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={35} minSize={0}>
<div className="h-full overflow-y-auto">{listView}</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={65} minSize={0} className="bg-slate-100">
<div className="h-full overflow-y-auto">
<ThreadContent
threadId={selectedEmail.threadId}
showReplyButton={true}
autoOpenReplyForMessageId={selectedEmail.messageId}
topRightComponent={
<div className="flex items-center gap-1">
{trackers.find((t) => t.threadId === selectedEmail.threadId)
?.resolved ? (
<UnresolveButton
threadId={selectedEmail.threadId}
onResolve={handleResolve}
isLoading={resolvingThreads.has(selectedEmail.threadId)}
showShortcut={false}
/>
) : (
<ResolveButton
threadId={selectedEmail.threadId}
onResolve={handleResolve}
isLoading={resolvingThreads.has(selectedEmail.threadId)}
showShortcut={false}
/>
)}
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedEmail(null)}
>
<XIcon className="size-4" />
</Button>
</div>
}
/>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}

Expand Down Expand Up @@ -250,7 +255,7 @@ function Row({
)}
onMouseEnter={onSelect}
>
<TableCell onClick={openSplitView}>
<TableCell onClick={openSplitView} className="py-8 pl-8 pr-6">
<div className="flex items-center justify-between">
<EmailMessageCell
sender={
Expand All @@ -268,7 +273,7 @@ function Row({
{/* biome-ignore lint/a11y/useKeyWithClickEvents: buttons inside handle keyboard events */}
<div
className={cn(
"ml-4 flex items-center gap-1",
"ml-4 flex items-center gap-1.5",
isSplitViewOpen && "flex-col",
)}
onClick={(e) => e.stopPropagation()}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(app)/reply-tracker/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default async function ReplyTrackerPage({

<TabsTrigger value="resolved" className="flex items-center gap-2">
<CheckCircleIcon className="h-4 w-4" />
Resolved
Done
</TabsTrigger>
</TabsList>
<TimeRangeFilter />
Expand Down
4 changes: 0 additions & 4 deletions apps/web/app/(app)/simple/SimpleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,6 @@ function SimpleListRow({
</Link>
</Button>
)}

{/* <div className="mt-2 text-sm text-gray-500">
{new Date(message.headers.date).toLocaleString()}
</div> */}
</div>

{!expanded && <div className="mt-2 sm:mt-0">{actionButtons}</div>}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/ai/categorize/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof categorizeBodyWithHtml>;
1 change: 0 additions & 1 deletion apps/web/app/api/ai/summarise/controller.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/google/webhook/process-history-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down Expand Up @@ -138,7 +139,7 @@ export async function processHistoryItem(
content,
messageId,
threadId,
date: message.headers.date,
date: internalDateToDate(message.internalDate),
},
gmail,
user,
Expand Down
5 changes: 3 additions & 2 deletions apps/web/app/api/user/stats/tinybird/load/load-emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -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;
}
Expand Down
69 changes: 69 additions & 0 deletions apps/web/components/email-list/EmailAttachments.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-4 grid grid-cols-2 gap-2 xl:grid-cols-3">
{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 (
<Card key={attachment.filename} className="p-4">
<div className="text-gray-600">{attachment.filename}</div>
<div className="mt-4 flex items-center justify-between">
<div className="text-gray-600">
{mimeTypeToString(attachment.mimeType)}
</div>
<Button variant="outline" size="sm" asChild>
<Link href={url} target="_blank">
<>
<DownloadIcon className="mr-2 h-4 w-4" />
Download
</>
</Link>
</Button>
</div>
</Card>
);
})}
</div>
);
}

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;
}
}
67 changes: 67 additions & 0 deletions apps/web/components/email-list/EmailContents.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLIFrameElement, Event>) => {
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 (
<div>
{isLoading && <Loading />}
<iframe
srcDoc={srcDoc}
onLoad={onLoad}
className="h-0 min-h-0 w-full"
title="Email content preview"
/>
</div>
);
}

export function PlainEmail({ text }: { text: string }) {
return <pre className="whitespace-pre-wrap">{text}</pre>;
}

function getIframeHtml(html: string) {
// Always inject our default font styles with lower specificity
// This ensures styled elements keep their fonts while unstyled ones get our defaults
const defaultFontStyles = `
<style>
/* Base styles with low specificity */
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
}
</style>
`;

let htmlWithHead = "";
if (html.indexOf("</head>") === -1) {
htmlWithHead = `<head>${defaultFontStyles}<base target="_blank"></head>${html}`;
} else {
htmlWithHead = html.replace(
"</head>",
`${defaultFontStyles}<base target="_blank" rel="noopener noreferrer"></head>`,
);
}

return htmlWithHead;
}
31 changes: 31 additions & 0 deletions apps/web/components/email-list/EmailDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ThreadMessage } from "@/components/email-list/types";

export function EmailDetails({ message }: { message: ThreadMessage }) {
const details = [
{ label: "From", value: message.headers.from },
{ label: "To", value: message.headers.to },
{ label: "CC", value: message.headers.cc },
{ label: "BCC", value: message.headers.bcc },
{
label: "Date",
value: new Date(message.headers.date).toLocaleString(),
},
// { label: "Subject", value: message.headers.subject },
];

return (
<div className="mb-4 rounded-md bg-gray-50 p-3 text-sm">
<div className="grid gap-1">
{details.map(
({ label, value }) =>
value && (
<div key={label} className="grid grid-cols-[auto,1fr] gap-2">
<span className="font-medium">{label}:</span>
<span>{value}</span>
</div>
),
)}
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion apps/web/components/email-list/EmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function EmailList({
snippet: thread.snippet,
threadId: message.threadId,
messageId: message.id,
date: message.headers.date,
internalDate: message.internalDate || "",
});

if (isActionError(result)) {
Expand Down
Loading
Loading