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
5 changes: 5 additions & 0 deletions apps/mail/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ function validateSettings(settings: unknown): UserSettings {
export async function saveUserSettings(settings: UserSettings) {
try {
const userId = await getAuthenticatedUserId();
console.log(settings, 'before');

settings = validateSettings(settings);

console.log(settings, 'after');

const timestamp = new Date();

const [existingSettings] = await db
Expand Down
18 changes: 14 additions & 4 deletions apps/mail/app/api/driver/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,20 @@ export const driver = async (config: IConfig): Promise<MailManager> => {
}
return false;
})
.map((recipient) => ({
name: recipient.name || '',
addr: recipient.email,
}));
.map((recipient) => {
// Parse the email address from the recipient string
const emailMatch = recipient.email.match(/<([^>]+)>/);
const email = emailMatch ? emailMatch[1] : recipient.email;
// Ensure we have a valid email address
if (!email) {
console.error('Debug - Invalid email address:', recipient.email);
throw new Error('Invalid email address');
}
return {
name: recipient.name || '',
addr: email,
};
});

console.log('Debug - Filtered to recipients:', JSON.stringify(toRecipients, null, 2));

Expand Down
69 changes: 37 additions & 32 deletions apps/mail/components/mail/mail-iframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,47 @@ import { getBrowserTimezone } from '@/lib/timezones';
import { useSettings } from '@/hooks/use-settings';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import DOMPurify from 'dompurify';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import DOMPurify from 'dompurify';

export function MailIframe({ html, senderEmail }: { html: string; senderEmail: string }) {
const { settings, mutate } = useSettings();
const isTrustedSender = settings?.trustedSenders?.includes(senderEmail);
const [cspViolation, setCspViolation] = useState(false)
const [cspViolation, setCspViolation] = useState(false);
const [imagesEnabled, setImagesEnabled] = useState(
isTrustedSender || settings?.externalImages || false,
);
const iframeRef = useRef<HTMLIFrameElement>(null);
const [height, setHeight] = useState(300);
const { resolvedTheme } = useTheme();

const onTrustSender = useCallback(async (senderEmail: string) => {
setImagesEnabled(true);
const onTrustSender = useCallback(
async (senderEmail: string) => {
setImagesEnabled(true);

const existingSettings = settings ?? {
...defaultUserSettings,
timezone: getBrowserTimezone(),
};
console.log(settings);

const { success } = await saveUserSettings({
...existingSettings,
trustedSenders: settings?.trustedSenders
? settings.trustedSenders.concat(senderEmail)
: [senderEmail],
});
const existingSettings = settings ?? {
...defaultUserSettings,
timezone: getBrowserTimezone(),
};

if (!success) {
toast.error('Failed to trust sender');
} else {
mutate();
}
}, [settings, mutate]);
const { success } = await saveUserSettings({
...existingSettings,
trustedSenders: settings?.trustedSenders
? settings.trustedSenders.concat(senderEmail)
: [senderEmail],
});

if (!success) {
toast.error('Failed to trust sender');
} else {
mutate();
}
},
[settings, mutate],
);

const iframeDoc = useMemo(() => template(html, imagesEnabled), [html, imagesEnabled]);

Expand Down Expand Up @@ -95,7 +100,7 @@ export function MailIframe({ html, senderEmail }: { html: string; senderEmail: s
'message',
(event) => {
if (event.data.type === 'csp-violation') {
setCspViolation(true)
setCspViolation(true);
}
},
{ signal: ctrl.signal },
Expand Down Expand Up @@ -254,7 +259,7 @@ export function DynamicIframe({
const parentStyle = window.getComputedStyle(iframe.parentElement || document.body);
const parentColor = parentStyle.color;
const parentBg = parentStyle.backgroundColor;

const styleElement = iframeDoc.createElement('style');
styleElement.textContent = `
body {
Expand All @@ -265,21 +270,21 @@ export function DynamicIframe({
iframeDoc.head.appendChild(styleElement);
}
};

updateStyles();

// Size adjustment
const resizeIframe = () => {
if (!iframe.contentWindow || !iframeDoc.body) return;

const newHeight = iframeDoc.body.scrollHeight;
const newWidth = iframeDoc.body.scrollWidth;

if (newHeight !== height) {
setHeight(newHeight);
iframe.style.height = `${newHeight}px`;
}

if (newWidth !== width && newWidth > iframe.clientWidth) {
setWidth(newWidth);
}
Expand All @@ -289,12 +294,12 @@ export function DynamicIframe({
const resizeObserver = new ResizeObserver(() => {
resizeIframe();
});

resizeObserver.observe(iframeDoc.body);

// Additional event listeners for images loading
const images = iframeDoc.querySelectorAll('img');
images.forEach(img => {
images.forEach((img) => {
img.addEventListener('load', resizeIframe);
img.addEventListener('error', (e) => {
console.error('Image failed to load:', e);
Expand All @@ -304,7 +309,7 @@ export function DynamicIframe({

// Handle link clicks to open in new tab
const links = iframeDoc.querySelectorAll('a');
links.forEach(link => {
links.forEach((link) => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
});
Expand All @@ -316,7 +321,7 @@ export function DynamicIframe({
// Cleanup
return () => {
resizeObserver.disconnect();
images.forEach(img => {
images.forEach((img) => {
img.removeEventListener('load', resizeIframe);
img.removeEventListener('error', resizeIframe);
});
Expand All @@ -333,4 +338,4 @@ export function DynamicIframe({
{...props}
/>
);
}
}
28 changes: 16 additions & 12 deletions apps/mail/components/mail/mail-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,18 +220,22 @@ const Thread = memo(
) : null}
</p>
<MailLabels labels={threadLabels} />
{Math.random() > 0.5 ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="rounded-md border border-dotted px-[5px] py-[1px] text-xs opacity-70">
{Math.random() * 10}
</span>
</TooltipTrigger>
<TooltipContent className="px-1 py-0 text-xs">
{t('common.mail.replies', { count: Math.random() * 10 })}
</TooltipContent>
</Tooltip>
) : null}
{Math.random() > 0.5 &&
(() => {
const count = Math.floor(Math.random() * 10) + 1;
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="rounded-md border border-dotted px-[5px] py-[1px] text-xs opacity-70">
{count}
</span>
</TooltipTrigger>
<TooltipContent className="px-1 py-0 text-xs">
{t('common.mail.replies', { count })}
</TooltipContent>
</Tooltip>
);
})()}
</div>
{latestMessage.receivedOn ? (
<p
Expand Down
19 changes: 7 additions & 12 deletions apps/mail/components/mail/thread-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,23 +167,18 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
useEffect(() => {
if (!emailData || !id) return;
const unreadEmails = emailData.messages.filter((e) => e.unread);
if (unreadEmails.length === 0) {
console.log('Marking email as read:', id);
markAsRead({ ids: [id] })
.catch((error) => {
console.error('Failed to mark email as read:', error);
toast.error(t('common.mail.failedToMarkAsRead'));
})
.then(() => Promise.all([mutateThread(), mutateStats()]));
} else {
console.log('Marking email as read:', id, ...unreadEmails.map((e) => e.id));
console.log({
totalReplies: emailData.totalReplies,
unreadEmails: unreadEmails.length,
});
if (unreadEmails.length > 0) {
const ids = [id, ...unreadEmails.map((e) => e.id)];
markAsRead({ ids })
.catch((error) => {
console.error('Failed to mark email as read:', error);
toast.error(t('common.mail.failedToMarkAsRead'));
})
.then(() => Promise.all([mutateThread(), mutateStats()]));
.then(() => Promise.allSettled([mutateThread(), mutateStats()]));
}
}, [emailData, id]);

Expand Down Expand Up @@ -232,7 +227,7 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
if (!result.success) throw new Error('Failed to mark as unread');

setMail((prev) => ({ ...prev, bulkSelected: [] }));
await Promise.all([mutateStats(), mutateThread()]);
await Promise.allSettled([mutateStats(), mutateThread()]);
handleClose();
};

Expand Down
38 changes: 21 additions & 17 deletions apps/mail/components/ui/nav-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { Collapsible, CollapsibleTrigger } from '@/components/ui/collapsible';
import { usePathname, useSearchParams } from 'next/navigation';
import { clearBulkSelectionAtom } from '../mail/use-mail';
import { type MessageKey } from '@/config/navigation';
import { type NavItem } from '@/config/navigation';
import { useSession } from '@/lib/auth-client';
import { Badge } from '@/components/ui/badge';
import { GoldenTicketModal } from '../golden';
import { useStats } from '@/hooks/use-stats';
import { useTranslations } from 'next-intl';
import { useRef, useCallback } from 'react';
Expand All @@ -20,9 +23,6 @@ import { cn } from '@/lib/utils';
import { useAtom } from 'jotai';
import * as React from 'react';
import Link from 'next/link';
import {type NavItem} from '@/config/navigation'
import { GoldenTicketModal } from '../golden';
import { useSession } from '@/lib/auth-client';

interface IconProps extends React.SVGProps<SVGSVGElement> {
ref?: React.Ref<SVGSVGElement>;
Expand Down Expand Up @@ -86,7 +86,7 @@ export function NavMain({ items }: NavMainProps) {

// Handle settings navigation
// if (item.isSettingsButton) {
// Include current path with category query parameter if present
// Include current path with category query parameter if present
// const currentPath = category
// ? `${pathname}?category=${encodeURIComponent(category)}`
// : pathname;
Expand Down Expand Up @@ -179,9 +179,9 @@ export function NavMain({ items }: NavMainProps) {
}

function NavItem(item: NavItemProps & { href: string }) {
const iconRef = useRef<IconRefType>(null);
const { data: stats } = useStats();
const t = useTranslations();
const iconRef = useRef<IconRefType>(null);
const { data: stats } = useStats();
const t = useTranslations();

if (item.disabled) {
return (
Expand All @@ -190,7 +190,7 @@ function NavItem(item: NavItemProps & { href: string }) {
className="flex cursor-not-allowed items-center opacity-50"
>
{item.icon && <item.icon ref={iconRef} className="relative mr-2.5 h-3 w-3.5" />}
<p className="mt-0.5 text-[13px] truncate">{t(item.title as MessageKey)}</p>
<p className="mt-0.5 truncate text-[13px]">{t(item.title as MessageKey)}</p>
</SidebarMenuButton>
);
}
Expand All @@ -214,14 +214,16 @@ function NavItem(item: NavItemProps & { href: string }) {
onClick={() => setOpenMobile(false)}
>
{item.icon && <item.icon ref={iconRef} className="mr-2 shrink-0" />}
<p className="mt-0.5 text-[13px] truncate min-w-0 flex-1">{t(item.title as MessageKey)}</p>
{stats && stats.find((stat) => stat.label?.toLowerCase() === item.id?.toLowerCase()) && (
<Badge className="ml-auto rounded-md shrink-0" variant="outline">
{stats
.find((stat) => stat.label?.toLowerCase() === item.id?.toLowerCase())
?.count?.toLocaleString() || '0'}
</Badge>
)}
<p className="mt-0.5 min-w-0 flex-1 truncate text-[13px]">{t(item.title as MessageKey)}</p>
{stats
? stats.find((stat) => stat.label?.toLowerCase() === item.id?.toLowerCase()) && (
<Badge className="ml-auto shrink-0 rounded-md" variant="outline">
{stats
.find((stat) => stat.label?.toLowerCase() === item.id?.toLowerCase())
?.count?.toLocaleString() || '0'}
</Badge>
)
: null}
</SidebarMenuButton>
);

Expand All @@ -232,7 +234,9 @@ function NavItem(item: NavItemProps & { href: string }) {
return (
<Collapsible defaultOpen={item.isActive}>
<CollapsibleTrigger asChild>
<Link {...linkProps} prefetch target={item.target}>{buttonContent}</Link>
<Link {...linkProps} prefetch target={item.target}>
{buttonContent}
</Link>
</CollapsibleTrigger>
</Collapsible>
);
Expand Down