Merge branch 'main' into feature/scheduled-send

# Conflicts:
#	app/(main)/[locale]/page.tsx
#	components/email/email-list.tsx
#	components/email/email-viewer.tsx
This commit is contained in:
Lucas Gaitzsch
2026-05-26 18:30:45 +02:00
53 changed files with 864 additions and 358 deletions
+3 -3
View File
@@ -50,7 +50,9 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte
const currentMailboxRole = mailboxes.find(mb => mb.id === selectedMailbox)?.role;
const showRecipient = currentMailboxRole === 'sent' || currentMailboxRole === 'drafts';
const sender = showRecipient ? (email.to?.[0] ?? email.from?.[0]) : email.from?.[0];
const isFocusedMailLayout = mailLayout === 'focus';
const isMobile = useUIStore((state) => state.isMobile);
// The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile.
const isFocusedMailLayout = mailLayout === 'focus' && !isMobile;
const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk;
const trimmedPreview = stripInvisibleLeading(email.preview ?? '');
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
@@ -68,8 +70,6 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte
sourceMailboxId: selectedMailbox,
});
const isMobile = useUIStore((state) => state.isMobile);
const { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed } = useLongPress(
useCallback((pos) => {
onContextMenu?.(
+4 -1
View File
@@ -11,6 +11,7 @@ import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { useEmailStore } from "@/stores/email-store";
import { useAuthStore } from "@/stores/auth-store";
import { useSettingsStore } from "@/stores/settings-store";
import { useUIStore } from "@/stores/ui-store";
import { groupEmailsByThread, sortThreadGroups } from "@/lib/thread-utils";
import { useContextMenu } from "@/hooks/use-context-menu";
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
@@ -121,9 +122,11 @@ export function EmailList({
const density = useSettingsStore((state) => state.density);
const showPreview = useSettingsStore((state) => state.showPreview);
const mailLayout = useSettingsStore((state) => state.mailLayout);
const isFocusedMailLayout = mailLayout === 'focus';
const footerHasMore = hasMore ?? hasMoreEmails;
const footerIsLoadingMore = isLoadingMoreItems ?? isLoadingMore;
const isMobile = useUIStore((state) => state.isMobile);
// Match the list items: focus layout collapses to multi-line on mobile, so virtualizer estimates must match.
const isFocusedMailLayout = mailLayout === 'focus' && !isMobile;
const estimateSize = useCallback(() => {
if (isFocusedMailLayout) {
+9 -4
View File
@@ -7,6 +7,7 @@ import { emailExportFilename, attachmentDownloadFilename, DEFAULT_EMAIL_TEMPLATE
import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import";
import { EMAIL_IFRAME_SANITIZE_CONFIG, collapseBlockedImageContainers, escapeHtml, plainTextToSafeHtml, sanitizeEmailHtml, sanitizePlainTextRenderedHtml } from "@/lib/email-sanitization";
import { hasMeaningfulHtmlBody } from "@/lib/signature-utils";
import { withBasePath } from "@/lib/browser-navigation";
import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar";
import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime, generateUUID } from "@/lib/utils";
@@ -76,6 +77,7 @@ import { useContactStore, getContactDisplayName, getContactPrimaryEmail } from "
import { toast } from "@/stores/toast-store";
import { useDeviceDetection } from "@/hooks/use-media-query";
import { useAuthStore } from "@/stores/auth-store";
import { useAccountStore } from "@/stores/account-store";
import { useEmailStore } from "@/stores/email-store";
import { useThemeStore } from "@/stores/theme-store";
import { EmailIdentityBadge } from "./email-identity-badge";
@@ -954,6 +956,7 @@ export function EmailViewer({
const { isTablet, isMobile } = useDeviceDetection();
const { tabletListVisible } = useUIStore();
const { identities, client, isDemoMode, activeAccountId } = useAuthStore();
const activeAccount = useAccountStore((s) => s.accounts.find((a) => a.id === activeAccountId));
const promptForRescheduleDelayedUntil = useCallback((): string | null => {
const value = window.prompt(t('reschedule_prompt'));
if (!value) return null;
@@ -3301,9 +3304,9 @@ export function EmailViewer({
if (!email) {
if (isDemoMode) {
const logoSrc = resolvedTheme === 'dark'
const logoSrc = withBasePath(resolvedTheme === 'dark'
? '/branding/Bulwark_Logo_with_Lettering_White_and_Color.svg'
: '/branding/Bulwark_Logo_with_Lettering_Dark_Color.svg';
: '/branding/Bulwark_Logo_with_Lettering_Dark_Color.svg');
return (
<div className={cn("flex-1 flex flex-col items-center justify-center bg-gradient-to-br from-muted/30 to-muted/50", className)}>
<div className="text-center p-8 max-w-md">
@@ -5384,10 +5387,12 @@ export function EmailViewer({
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
<div className="flex-shrink-0">
<Avatar
name={currentUserName || "You"}
email={currentUserEmail || ""}
name={activeAccount?.displayName || currentUserName || "You"}
email={activeAccount?.email || activeAccount?.username || currentUserEmail || ""}
size="lg"
className="shadow-sm w-10 h-10"
disableFavicon
fallbackColor={activeAccount?.avatarColor}
/>
</div>
<div className="flex-1 min-w-0 space-y-3">
+5 -4
View File
@@ -74,7 +74,9 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
const getAccountById = useAccountStore((state) => state.getAccountById);
const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined;
const isChecked = selectedEmailIds.has(email.id);
const isFocusedMailLayout = mailLayout === 'focus';
const isMobile = useUIStore((state) => state.isMobile);
// The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile.
const isFocusedMailLayout = mailLayout === 'focus' && !isMobile;
const trimmedPreview = stripInvisibleLeading(email.preview ?? '');
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
const scheduledSendLabel = email.isScheduled && email.scheduledSendAt
@@ -95,8 +97,6 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
sourceMailboxId: selectedMailbox,
});
const isMobile = useUIStore((state) => state.isMobile);
const { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed } = useLongPress(
useCallback((pos) => {
onContextMenu?.(
@@ -404,7 +404,8 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk);
const isMobile = useUIStore((state) => state.isMobile);
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread;
const isFocusedMailLayout = mailLayout === 'focus';
// The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile.
const isFocusedMailLayout = mailLayout === 'focus' && !isMobile;
const trimmedPreview = stripInvisibleLeading(latestEmail.preview ?? '');
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
const scheduledSendLabel = latestEmail.isScheduled && latestEmail.scheduledSendAt