feat: calendar invitations RSVP, trust assessment, file preview

This commit is contained in:
Linus Rath
2026-03-16 22:00:11 +01:00
parent cde1d61d02
commit 0f5d030d5f
32 changed files with 21143 additions and 279 deletions
+2 -2
View File
@@ -452,8 +452,8 @@ export default function FilesPage() {
<FilePreviewModal
name={previewFile}
onClose={() => setPreviewFile(null)}
onDownload={handleDownload}
getFileContent={getFileContent}
onDownload={() => handleDownload(previewFile)}
getFileContent={() => getFileContent(previewFile)}
/>
)}
+38
View File
@@ -36,6 +36,8 @@ import { isFilterEmpty, activeFilterCount } from "@/lib/jmap/search-utils";
import { WelcomeBanner } from "@/components/ui/welcome-banner";
import { NavigationRail } from "@/components/layout/navigation-rail";
import { Input } from "@/components/ui/input";
import { FilePreviewModal } from "@/components/files/file-preview-modal";
import { isFilePreviewable } from "@/lib/file-preview";
import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine, CheckSquare, Square } from "lucide-react";
import { ResizeHandle } from "@/components/layout/resize-handle";
import { Button } from "@/components/ui/button";
@@ -59,6 +61,7 @@ export default function Home() {
const [conversationThread, setConversationThread] = useState<ThreadGroup | null>(null);
const [conversationEmails, setConversationEmails] = useState<Email[]>([]);
const [isLoadingConversation, setIsLoadingConversation] = useState(false);
const [previewAttachment, setPreviewAttachment] = useState<{ blobId: string; name: string; type?: string } | null>(null);
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading, connectionLost } = useAuthStore();
const { identities } = useIdentityStore();
@@ -746,12 +749,38 @@ export default function Home() {
if (!client) return;
try {
const { mailAttachmentAction } = useSettingsStore.getState();
if (mailAttachmentAction === 'preview' && isFilePreviewable(name, type)) {
setPreviewAttachment({ blobId, name, type });
return;
}
await client.downloadBlob(blobId, name, type);
} catch (error) {
console.error("Failed to download attachment:", error);
}
};
const handlePreviewAttachmentDownload = useCallback(async () => {
if (!client || !previewAttachment) return;
await client.downloadBlob(previewAttachment.blobId, previewAttachment.name, previewAttachment.type);
}, [client, previewAttachment]);
const getPreviewAttachmentContent = useCallback(async () => {
if (!client || !previewAttachment) {
throw new Error('No attachment selected');
}
const blob = await client.fetchBlob(previewAttachment.blobId, previewAttachment.name, previewAttachment.type);
return {
blob,
contentType: previewAttachment.type || blob.type || 'application/octet-stream',
};
}, [client, previewAttachment]);
const handleQuickReply = async (body: string) => {
if (!client || !selectedEmail) return;
@@ -1498,6 +1527,15 @@ export default function Home() {
onClose={() => setShowShortcutsModal(false)}
/>
{previewAttachment && (
<FilePreviewModal
name={previewAttachment.name}
onClose={() => setPreviewAttachment(null)}
onDownload={handlePreviewAttachmentDownload}
getFileContent={getPreviewAttachmentContent}
/>
)}
{/* Screen reader live region for dynamic status announcements */}
<div className="sr-only" aria-live="polite" aria-atomic="true" id="sr-status" />