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
46 changes: 44 additions & 2 deletions apps/mail/app/(routes)/settings/general/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,22 @@ import { saveUserSettings } from '@/actions/settings';
import { getBrowserTimezone } from '@/lib/timezones';
import { Textarea } from '@/components/ui/textarea';
import { useSettings } from '@/hooks/use-settings';
import { Globe, Clock, XIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Globe, Clock } from 'lucide-react';
import { changeLocale } from '@/i18n/utils';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import * as z from 'zod';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

const formSchema = z.object({
language: z.enum(locales as [string, ...string[]]),
timezone: z.string(),
dynamicContent: z.boolean(),
externalImages: z.boolean(),
customPrompt: z.string(),
trustedSenders: z.string().array(),
signature: z.object({
enabled: z.boolean(),
content: z.string(),
Expand Down Expand Up @@ -140,6 +142,7 @@ export default function GeneralPage() {
dynamicContent: false,
externalImages: true,
customPrompt: '',
trustedSenders: [],
signature: {
enabled: false,
content: '',
Expand All @@ -148,6 +151,8 @@ export default function GeneralPage() {
},
});

const externalImages = form.watch("externalImages")

useEffect(() => {
if (settings) {
form.reset(settings);
Expand Down Expand Up @@ -229,7 +234,7 @@ export default function GeneralPage() {
)}
/>
</div>
<div className="flex w-full flex-col items-center gap-5 md:flex-row">
<div className="flex w-full w-max flex-col items-start gap-5">
{/* <FormField
control={form.control}
name="dynamicContent"
Expand Down Expand Up @@ -268,6 +273,43 @@ export default function GeneralPage() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="trustedSenders"
render={({ field}) => field.value.length > 0 && !externalImages ? (
<FormItem className="bg-popover flex w-full flex-col rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
{t('pages.settings.general.trustedSenders')}
</FormLabel>
<FormDescription>
{t('pages.settings.general.trustedSendersDescription')}
</FormDescription>
</div>
<ScrollArea className="flex flex-col max-h-32 pr-3">
{field.value.map((senderEmail) => (
<div className="flex items-center justify-between mt-1.5 first:mt-0" key={senderEmail}>
<span>{senderEmail}</span>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() =>
field.onChange(field.value.filter((e) => e !== senderEmail))
}
>
<XIcon className="h-4 w-4 transition hover:opacity-80" />
</button>
</TooltipTrigger>
<TooltipContent>
{t('common.actions.remove')}
</TooltipContent>
</Tooltip>
</div>
))}
</ScrollArea>
</FormItem>
) : <></>}
/>
</div>
<FormField
control={form.control}
Expand Down
2 changes: 1 addition & 1 deletion apps/mail/components/mail/mail-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>

<div className="h-fit w-full p-0">
{emailData?.decodedBody ? (
<MailIframe html={emailData?.decodedBody} />
<MailIframe html={emailData?.decodedBody} senderEmail={emailData.sender.email} />
) : (
<div
className="flex h-[500px] w-full items-center justify-center"
Expand Down
45 changes: 40 additions & 5 deletions apps/mail/components/mail/mail-iframe.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { defaultUserSettings } from '@zero/db/user_settings_default';
import { fixNonReadableColors, template } from '@/lib/email-utils';
import { saveUserSettings } from '@/actions/settings';
import { getBrowserTimezone } from '@/lib/timezones';
import { useSettings } from '@/hooks/use-settings';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import DOMPurify from 'dompurify';

export function MailIframe({ html }: { html: string }) {
const { settings } = useSettings();
const [imagesEnabled, setImagesEnabled] = useState(settings?.externalImages || false);
export function MailIframe({ html, senderEmail }: { html: string; senderEmail: string }) {
const { settings, mutate } = useSettings();
const isTrustedSender = settings?.trustedSenders?.includes(senderEmail);
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 existingSettings = settings ?? {
...defaultUserSettings,
timezone: getBrowserTimezone(),
};

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]);

const t = useTranslations();
Expand Down Expand Up @@ -64,12 +93,18 @@ export function MailIframe({ html }: { html: string }) {
{!imagesEnabled && !settings?.externalImages && (
<div className="flex items-center justify-start bg-amber-500 p-2 text-sm text-amber-900">
<p>{t('common.actions.hiddenImagesWarning')}</p>
<p
<button
onClick={() => setImagesEnabled(!imagesEnabled)}
className="ml-2 cursor-pointer underline"
>
{imagesEnabled ? t('common.actions.disableImages') : t('common.actions.showImages')}
</p>
</button>
<button
onClick={() => void onTrustSender(senderEmail)}
className="ml-2 cursor-pointer underline"
>
{t('common.actions.trustSender')}
</button>
</div>
)}
{/* {!loaded && (
Expand Down
6 changes: 5 additions & 1 deletion apps/mail/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
"hiddenImagesWarning": "Images are hidden by default for security reasons.",
"showImages": "Show Images",
"disableImages": "Hide Images",
"trustSender": "Trust Sender",
"cancel": "Cancel",
"save": "Save"
"save": "Save",
"remove": "Remove"
},
"themes": {
"dark": "Dark",
Expand Down Expand Up @@ -304,6 +306,8 @@
"dynamicContentDescription": "Allow emails to display dynamic content.",
"externalImages": "Display External Images",
"externalImagesDescription": "Allow emails to display images from external sources.",
"trustedSenders": "Trusted Senders",
"trustedSendersDescription": "Always display images for these senders.",
"languageChangedTo": "Language changed to {locale}",
"customPrompt": "Custom AI Prompt",
"customPromptPlaceholder": "Enter your custom prompt for the AI...",
Expand Down
6 changes: 4 additions & 2 deletions packages/db/src/user_settings_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@ export const defaultUserSettings = {
dynamicContent: false,
externalImages: true,
customPrompt: "",
trustedSenders: [],
signature: {
enabled: false,
content: "",
includeByDefault: true,
},
};
} satisfies UserSettings;

export const userSettingsSchema = z.object({
language: z.string(),
timezone: z.string(),
dynamicContent: z.boolean(),
externalImages: z.boolean(),
customPrompt: z.string(),
trustedSenders: z.string().array(),
signature: z.object({
enabled: z.boolean(),
content: z.string(),
includeByDefault: z.boolean(),
}),
});

export type UserSettings = typeof defaultUserSettings;
export type UserSettings = z.infer<typeof userSettingsSchema>