Skip to content

Commit 5b4fa18

Browse files
authored
367 add support for scheduling for later send of emails (#374)
* added ability to schedule and send emails * docs(changeset): added ability to schedule and send emails
1 parent d838bd5 commit 5b4fa18

File tree

20 files changed

+759
-126
lines changed

20 files changed

+759
-126
lines changed

.changeset/good-papers-visit.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@kurrier/schema": patch
3+
"@kurrier/worker": patch
4+
"@kurrier/db": patch
5+
"@kurrier/web": patch
6+
"@kurrier/repo": patch
7+
---
8+
9+
added ability to schedule and send emails

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Kurrier
22
The self-hosted open-source workspace for **email, calendars, contacts and storage**.
33

4-
---
54

65
## ✨ What's New
76

apps/web/app/dashboard/(unified)/(mail)/layout.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { SidebarInset } from "@/components/ui/sidebar";
22
import { AppSidebar } from "@/components/ui/dashboards/unified/default/app-sidebar";
33
import {
4-
fetchIdentityMailboxList,
5-
fetchMailboxUnreadCounts,
4+
fetchIdentityMailboxList,
5+
fetchMailboxUnreadCounts, fetchScheduledCount,
66
} from "@/lib/actions/mailbox";
7-
import { fetchLabelsWithCounts } from "@/lib/actions/labels";
7+
import {fetchLabelsWithCounts} from "@/lib/actions/labels";
88
import { getPublicEnv, LabelScope } from "@schema";
99
import { isSignedIn } from "@/lib/actions/auth";
1010
import { DynamicContextProvider } from "@/hooks/use-dynamic-context";
@@ -19,12 +19,13 @@ export default async function DashboardLayout({
1919
children: React.ReactNode;
2020
}) {
2121
const publicConfig = getPublicEnv();
22-
const [identityMailboxes, unreadCounts, user, globalLabels] =
22+
const [identityMailboxes, unreadCounts, user, globalLabels, scheduledCounts] =
2323
await Promise.all([
2424
fetchIdentityMailboxList(),
2525
fetchMailboxUnreadCounts(),
2626
isSignedIn(),
2727
fetchLabelsWithCounts(),
28+
fetchScheduledCount()
2829
]);
2930

3031
return (
@@ -34,7 +35,7 @@ export default async function DashboardLayout({
3435
user={user}
3536
identityMailboxes={identityMailboxes}
3637
sidebarTopContent={
37-
<div className={"-mt-1"}>
38+
<div className={"-mt-1"} key={"mail-sidebar-compose"}>
3839
{identityMailboxes.length > 0 && (
3940
<ComposeMail publicConfig={publicConfig} />
4041
)}
@@ -45,6 +46,7 @@ export default async function DashboardLayout({
4546
<IdentityMailboxesList
4647
identityMailboxes={identityMailboxes}
4748
unreadCounts={unreadCounts}
49+
scheduledCounts={scheduledCounts}
4850
/>
4951
<DynamicContextProvider
5052
initialState={{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { SidebarTrigger } from "@/components/ui/sidebar";
2+
import { Separator } from "@/components/ui/separator";
3+
import MailboxSearch from "@/components/mailbox/default/mailbox-search";
4+
import React, { ReactNode } from "react";
5+
import { isSignedIn } from "@/lib/actions/auth";
6+
7+
type LayoutProps = {
8+
children: ReactNode;
9+
params: Promise<Record<string, string>>;
10+
};
11+
12+
export default async function DashboardLayout({
13+
children,
14+
params,
15+
}: LayoutProps) {
16+
const { identityPublicId, mailboxSlug } = await params;
17+
18+
const user = await isSignedIn();
19+
20+
return (
21+
<>
22+
<header className="bg-background sticky top-0 flex shrink-0 items-center gap-2 border-b p-4">
23+
<SidebarTrigger className="-ml-1" />
24+
<Separator
25+
orientation="vertical"
26+
className="mr-2 data-[orientation=vertical]:h-4"
27+
/>
28+
29+
<MailboxSearch
30+
user={user}
31+
publicId={identityPublicId}
32+
mailboxSlug={mailboxSlug}
33+
/>
34+
</header>
35+
36+
{children}
37+
</>
38+
);
39+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import ScheduledList from "@/components/mailbox/default/scheduled-list";
3+
import {fetchScheduledDrafts} from "@/lib/actions/mailbox";
4+
5+
async function Page() {
6+
const scheduledDrafts = await fetchScheduledDrafts()
7+
return <>
8+
9+
<div className="p-4">
10+
<ul role="list" className="divide-y rounded-4xl">
11+
<ScheduledList drafts={scheduledDrafts} />
12+
</ul>
13+
</div>
14+
15+
16+
</>
17+
}
18+
19+
export default Page;

apps/web/components/dashboard/identity-mailboxes-list.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { IdentityEntity, MailboxEntity } from "@db";
2525
import AddNewFolder from "@/components/mailbox/default/add-new-folder";
2626
import { Menu } from "@mantine/core";
2727
import DeleteMailboxFolder from "@/components/mailbox/default/delete-folder";
28+
import {IconMailFast} from "@tabler/icons-react";
2829

2930
const ORDER: MailboxKind[] = [
3031
"inbox",
@@ -114,17 +115,23 @@ function buildTree(
114115
export default function IdentityMailboxesList({
115116
identityMailboxes,
116117
unreadCounts,
118+
scheduledCounts,
117119
onComplete,
118120
}: {
119121
identityMailboxes: FetchIdentityMailboxListResult;
120122
unreadCounts: FetchMailboxUnreadCountsResult;
123+
scheduledCounts: number;
121124
onComplete?: () => void;
122125
}) {
123126
const pathname = usePathname();
124127
const params = useParams() as {
125128
identityPublicId?: string;
126129
mailboxSlug?: string;
127130
};
131+
const currentSlug = React.useMemo(() => {
132+
const parts = pathname.split("/").filter(Boolean);
133+
return parts.at(-1) ?? "inbox";
134+
}, [pathname]);
128135

129136
const Item = ({
130137
m,
@@ -140,10 +147,9 @@ export default function IdentityMailboxesList({
140147
const Icon = ICON[m.kind] ?? Folder;
141148
const slug = m.slug ?? "inbox";
142149
const href = `/dashboard/mail/${identityPublicId}/${slug}`;
143-
const isActive =
144-
pathname === href ||
145-
(params.identityPublicId === identityPublicId &&
146-
(params.mailboxSlug ?? "inbox") === slug);
150+
const isActive =
151+
pathname === href ||
152+
(params.identityPublicId === identityPublicId && currentSlug === slug);
147153

148154
const [open, setOpen] = React.useState(true);
149155
const hasChildren = m.children.length > 0;
@@ -257,6 +263,11 @@ export default function IdentityMailboxesList({
257263
/>
258264
))}
259265
</div>
266+
<Link href={`/dashboard/mail/${params.identityPublicId}/scheduled`} className={`my-4 rounded hover:dark:bg-neutral-800 ${currentSlug === "scheduled" ? "dark:bg-neutral-800 dark:text-brand-foreground bg-brand-200 text-brand" : ""} flex justify-start gap-1 w-full p-1.5`}>
267+
<IconMailFast size={22}/>
268+
<span className={"font-normal text-sm"}>Scheduled ({scheduledCounts})</span>
269+
</Link>
270+
260271
</div>
261272
);
262273
})}

apps/web/components/mailbox/default/compose-mail.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export default function ComposeMail({
151151
aria-modal="true"
152152
className={[
153153
"fixed z-[1000] bg-background border shadow-xl rounded-lg overflow-hidden",
154-
"right-4 bottom-4",
154+
"right-12 bottom-4",
155155
// expanded ? "w-[720px] h-[70vh]" : "w-[520px] h-auto",
156156
expanded ? "w-[720px] h-[70vh]" : "w-[520px] h-auto",
157157
"transition-[width,height] duration-200 ease-out",
@@ -195,9 +195,6 @@ export default function ComposeMail({
195195
ref={editorRef}
196196
publicConfig={publicConfig}
197197
message={null}
198-
// onReady={() =>
199-
// requestAnimationFrame(() => editorRef.current?.focus())
200-
// }
201198
showEditorMode={showEditorMode}
202199
handleClose={handleClose}
203200
/>

apps/web/components/mailbox/default/editor/editor-footer.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { useRef, useState } from "react";
22
import { ActionIcon, Button, Popover, Progress } from "@mantine/core";
3-
import { Baseline, Paperclip, X as IconX } from "lucide-react";
3+
import {Baseline, Paperclip, X as IconX} from "lucide-react";
44
import { RichTextEditor } from "@mantine/tiptap";
55
import { useDynamicContext } from "@/hooks/use-dynamic-context";
66
import { createClient } from "@/lib/supabase/client";
77
import type { PublicConfig } from "@schema";
88
import { v4 as uuidv4 } from "uuid";
99
import { extension } from "mime-types";
1010
import { MessageEntity } from "@db";
11+
import ScheduleSend from "@/components/mailbox/default/editor/schedule-send";
1112

1213
type UploadItem = {
1314
name: string;
@@ -240,15 +241,16 @@ export default function EditorFooter() {
240241
)}
241242

242243
<div className="border-t items-center flex py-2 px-2">
243-
<div className="mx-2">
244+
<div className="mx-2 flex items-center gap-[1px]">
244245
<Button
245246
loading={!!state.isPending}
246-
size="xs"
247-
radius="xl"
247+
size="sm"
248248
type="submit"
249+
className={"!rounded-l-4xl !rounded-r-xs"}
249250
>
250251
Send
251252
</Button>
253+
<ScheduleSend />
252254
</div>
253255

254256
<Popover position="top-start" withArrow shadow="md">

0 commit comments

Comments
 (0)