mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-28 14:56:48 +00:00
rebased
This commit is contained in:
@@ -906,26 +906,6 @@ function EmailComposerBase({
|
||||
<div className="inline-flex w-full shrink-0 items-end justify-between self-stretch rounded-b-2xl bg-[#FFFFFF] px-3.5 py-2.5 outline-white/5 dark:bg-[#313131]">
|
||||
<div className="flex flex-col items-start justify-start gap-2">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Button
|
||||
size={'xs'}
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || settingsLoading || !isScheduleValid}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="text-center text-sm leading-none text-white dark:text-black">
|
||||
<span>Send </span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-5 items-center justify-center gap-1 rounded-sm bg-white/10 px-1 dark:bg-black/10">
|
||||
<Command className="h-3.5 w-3.5 text-white dark:text-black" />
|
||||
<CurvedArrow className="mt-1.5 h-4 w-4 fill-white dark:fill-black" />
|
||||
</div>
|
||||
</Button>
|
||||
<ScheduleSendPicker
|
||||
value={scheduleAt}
|
||||
onChange={handleScheduleChange}
|
||||
onValidityChange={handleScheduleValidityChange}
|
||||
/>
|
||||
<div className="relative">
|
||||
<AnimatePresence>
|
||||
{aiGeneratedMessage !== null ? (
|
||||
@@ -949,32 +929,6 @@ function EmailComposerBase({
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<Button
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
className="border border-[#8B5CF6]"
|
||||
onClick={async () => {
|
||||
if (!subjectInput.trim()) {
|
||||
await handleGenerateSubject();
|
||||
}
|
||||
setAiGeneratedMessage(null);
|
||||
await handleAiGenerate();
|
||||
}}
|
||||
disabled={isLoading || aiIsLoading || messageLength < 1}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2.5 pl-0.5">
|
||||
<div className="flex h-5 items-center justify-center gap-1 rounded-sm">
|
||||
{aiIsLoading ? (
|
||||
<Loader className="h-3.5 w-3.5 animate-spin fill-black dark:fill-white" />
|
||||
) : (
|
||||
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden text-center text-sm leading-none text-black md:block dark:text-white">
|
||||
Generate
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
@@ -1128,14 +1082,22 @@ function EmailComposerBase({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-start gap-2">
|
||||
<Button size={'xs'} onClick={handleSend} disabled={isLoading || settingsLoading}>
|
||||
<ScheduleSendPicker
|
||||
value={scheduleAt}
|
||||
onChange={handleScheduleChange}
|
||||
onValidityChange={handleScheduleValidityChange}
|
||||
/>
|
||||
<Button
|
||||
size={'xs'}
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || settingsLoading || !isScheduleValid}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="text-center text-sm leading-none text-white dark:text-black">
|
||||
<span>Send </span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button variant={'secondary'} size={'xs'}>
|
||||
|
||||
<div className="flex h-5 items-center justify-center gap-1 rounded-sm bg-white/10 px-1 dark:bg-black/10">
|
||||
<Command className="h-3.5 w-3.5 text-white dark:text-black" />
|
||||
<CurvedArrow className="mt-1.5 h-4 w-4 fill-white dark:fill-black" />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { format, isValid } from 'date-fns';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format, isValid } from 'date-fns';
|
||||
import { Button } from '../ui/button';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -79,14 +80,15 @@ export const ScheduleSendPicker: React.FC<ScheduleSendPickerProps> = ({
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
size={'xs'}
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-md border px-2 py-1 text-sm hover:bg-accent',
|
||||
'hover:bg-accent flex items-center gap-1 rounded-md bg-white/10 px-2 py-1 text-sm text-black dark:bg-black/10 dark:text-white',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
{/* <Clock className="h-4 w-4" /> */}
|
||||
<span>
|
||||
{(() => {
|
||||
if (!localValue) return 'Send later';
|
||||
@@ -100,7 +102,7 @@ export const ScheduleSendPicker: React.FC<ScheduleSendPickerProps> = ({
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-[100] w-64 p-4" align="start" side="top" sideOffset={8}>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -109,10 +111,10 @@ export const ScheduleSendPicker: React.FC<ScheduleSendPickerProps> = ({
|
||||
type="datetime-local"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:opacity-0"
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { useTemplates } from '@/hooks/use-templates';
|
||||
import { useTRPC } from '@/providers/query-provider';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,10 +7,6 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { toast } from 'sonner';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { FileText, Save, Trash2 } from 'lucide-react';
|
||||
import React, { useState, useMemo, useDeferredValue, useCallback, startTransition } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,8 +14,16 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import React, { useState, useMemo, useDeferredValue, useCallback, startTransition } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { FileText, Save, Trash2 } from 'lucide-react';
|
||||
import { useTRPC } from '@/providers/query-provider';
|
||||
import { useTemplates } from '@/hooks/use-templates';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TRPCClientError } from '@trpc/client';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type RecipientField = 'to' | 'cc' | 'bcc';
|
||||
|
||||
@@ -37,9 +37,11 @@ type Template = {
|
||||
bcc?: string[] | null;
|
||||
};
|
||||
|
||||
type TemplatesQueryData = {
|
||||
templates: Template[];
|
||||
} | undefined;
|
||||
type TemplatesQueryData =
|
||||
| {
|
||||
templates: Template[];
|
||||
}
|
||||
| undefined;
|
||||
|
||||
interface TemplateButtonProps {
|
||||
editor: Editor | null;
|
||||
@@ -63,7 +65,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
const { data } = useTemplates();
|
||||
|
||||
|
||||
const templates: Template[] = data?.templates ?? [];
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
@@ -76,9 +78,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
|
||||
const filteredTemplates = useMemo(() => {
|
||||
if (!deferredSearch.trim()) return templates;
|
||||
return templates.filter((t) =>
|
||||
t.name.toLowerCase().includes(deferredSearch.toLowerCase()),
|
||||
);
|
||||
return templates.filter((t) => t.name.toLowerCase().includes(deferredSearch.toLowerCase()));
|
||||
}, [deferredSearch, templates]);
|
||||
|
||||
const { mutateAsync: createTemplate } = useMutation(trpc.templates.create.mutationOptions());
|
||||
@@ -123,16 +123,19 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyTemplate = useCallback((template: Template) => {
|
||||
if (!editor) return;
|
||||
startTransition(() => {
|
||||
if (template.subject) setSubject(template.subject);
|
||||
if (template.body) editor.commands.setContent(template.body, false);
|
||||
if (template.to) setRecipients('to', template.to);
|
||||
if (template.cc) setRecipients('cc', template.cc);
|
||||
if (template.bcc) setRecipients('bcc', template.bcc);
|
||||
});
|
||||
}, [editor, setSubject, setRecipients]);
|
||||
const handleApplyTemplate = useCallback(
|
||||
(template: Template) => {
|
||||
if (!editor) return;
|
||||
startTransition(() => {
|
||||
if (template.subject) setSubject(template.subject);
|
||||
if (template.body) editor.commands.setContent(template.body, false);
|
||||
if (template.to) setRecipients('to', template.to);
|
||||
if (template.cc) setRecipients('cc', template.cc);
|
||||
if (template.bcc) setRecipients('bcc', template.bcc);
|
||||
});
|
||||
},
|
||||
[editor, setSubject, setRecipients],
|
||||
);
|
||||
|
||||
const handleDeleteTemplate = useCallback(
|
||||
async (templateId: string) => {
|
||||
@@ -157,7 +160,13 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
<>
|
||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size={'xs'} variant={'secondary'} disabled={isSaving}>
|
||||
<Button
|
||||
disabled={isSaving}
|
||||
type="button"
|
||||
size={'xs'}
|
||||
variant={'secondary'}
|
||||
className="hover:bg-accent flex items-center gap-1 rounded-md bg-white/10 px-2 py-1 text-sm text-black dark:bg-black/10 dark:text-white"
|
||||
>
|
||||
Templates
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -177,7 +186,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
<FileText className="mr-2 h-3.5 w-3.5" /> Use template
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="z-99999 w-60">
|
||||
<div className="p-2 border-b border-border sticky top-0 bg-background">
|
||||
<div className="border-border bg-background sticky top-0 border-b p-2">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
@@ -195,10 +204,10 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
>
|
||||
<span className="flex-1 truncate text-left">{t.name}</span>
|
||||
<button
|
||||
className="p-0.5 text-muted-foreground hover:text-destructive"
|
||||
className="text-muted-foreground hover:text-destructive p-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMenuOpen(false);
|
||||
setMenuOpen(false);
|
||||
toast(`Delete template "${t.name}"?`, {
|
||||
duration: 10000,
|
||||
action: {
|
||||
@@ -217,7 +226,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{filteredTemplates.length === 0 && (
|
||||
<div className="p-2 text-xs text-muted-foreground">No templates</div>
|
||||
<div className="text-muted-foreground p-2 text-xs">No templates</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuSubContent>
|
||||
@@ -231,7 +240,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save as Template</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4 space-y-2">
|
||||
<div className="space-y-2 py-4">
|
||||
<Input
|
||||
placeholder="Template name"
|
||||
value={templateName}
|
||||
@@ -240,11 +249,7 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSaveDialogOpen(false)}
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSaveDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSaveTemplate} disabled={isSaving}>
|
||||
@@ -257,4 +262,4 @@ const TemplateButtonComponent: React.FC<TemplateButtonProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateButton = React.memo(TemplateButtonComponent);
|
||||
export const TemplateButton = React.memo(TemplateButtonComponent);
|
||||
|
||||
@@ -197,7 +197,7 @@ export const CurvedArrow = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="2em"
|
||||
height="2em"
|
||||
fill="none"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
|
||||
Reference in New Issue
Block a user