feat: send email notifications for missed messages

- New cron job in cluster
- Handle presence and other concerns
- Toggle in app to disable email notifications
- Handles other edge cases such as cooldown period
- Simple html email with simple message

Closes #117
This commit is contained in:
talksik
2026-04-09 14:15:34 -07:00
parent 6fe5af8d8d
commit 51c4c6c822
25 changed files with 725 additions and 29 deletions
+6
View File
@@ -125,6 +125,12 @@ class ApiClient {
return data.url;
}
// --- Settings ---
async updateSettings(data: { email_notifications_enabled?: boolean }): Promise<void> {
await this.requestVoid("PATCH", "/humans/me/settings", data);
}
// --- Depot ---
async prepareUpload(data: PrepareUploadRequest) {
+1
View File
@@ -5,6 +5,7 @@ export const HumanSchema = z.object({
created_at: z.coerce.date(),
email: z.string().email(),
email_prefix: z.string(),
email_notifications_enabled: z.boolean(),
});
export type Human = z.infer<typeof HumanSchema>;
+41 -1
View File
@@ -1,12 +1,15 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRight, LogOut, User, Info, Shield } from "lucide-react";
import { ChevronRight, LogOut, User, Info, Shield, Mail } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WindowControls } from "@/components/window-controls";
import { Button } from "@/components/ui/button";
import { Muted } from "@/components/ui/typography";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useAuthStore } from "@/stores/auth-store";
import { apiClient } from "@/api/client";
import { ArrowLeft } from "lucide-react";
interface SettingsRowProps {
@@ -63,6 +66,25 @@ export default function SettingsPage() {
const navigate = useNavigate();
const user = useAuthStore((s) => s.user);
const signOut = useAuthStore((s) => s.signOut);
const [emailNotifications, setEmailNotifications] = useState(
user?.email_notifications_enabled ?? true,
);
const handleToggleEmailNotifications = async (checked: boolean) => {
setEmailNotifications(checked);
useAuthStore.setState((state) => ({
user: state.user ? { ...state.user, email_notifications_enabled: checked } : null,
}));
try {
await apiClient.updateSettings({ email_notifications_enabled: checked });
} catch {
// Revert on failure
setEmailNotifications(!checked);
useAuthStore.setState((state) => ({
user: state.user ? { ...state.user, email_notifications_enabled: !checked } : null,
}));
}
};
const initials = user?.email_prefix?.slice(0, 2).toUpperCase() ?? "?";
@@ -115,6 +137,24 @@ export default function SettingsPage() {
<Separator className="mt-4" />
<SettingsGroup title="Notifications">
<div className="flex w-full items-center gap-3 px-4 py-3">
<span className="text-muted-foreground flex size-5 items-center justify-center">
<Mail className="size-4" />
</span>
<span className="min-w-0 flex-1 text-sm font-medium">
Email notifications
</span>
<Switch
size="sm"
checked={emailNotifications}
onCheckedChange={handleToggleEmailNotifications}
/>
</div>
</SettingsGroup>
<Separator className="mt-4" />
<SettingsGroup title="About">
<SettingsRow
icon={<Info className="size-4" />}