feat: add hover actions for emails and update settings for quick actions
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Email } from "@/lib/jmap/types";
|
||||||
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
import type { HoverAction } from "@/stores/settings-store";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Trash2, Star, Mail, MailOpen, Archive, Tag, ShieldAlert } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
interface EmailHoverActionsProps {
|
||||||
|
email: Email;
|
||||||
|
onToggleStar?: () => void;
|
||||||
|
onMarkAsRead?: (read: boolean) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onArchive?: () => void;
|
||||||
|
onSetColorTag?: (color: string | null) => void;
|
||||||
|
onMarkAsSpam?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_CONFIG: Record<HoverAction, {
|
||||||
|
icon: typeof Trash2;
|
||||||
|
titleKey: string;
|
||||||
|
className?: string;
|
||||||
|
}> = {
|
||||||
|
delete: {
|
||||||
|
icon: Trash2,
|
||||||
|
titleKey: "delete",
|
||||||
|
className: "hover:text-red-600 dark:hover:text-red-400",
|
||||||
|
},
|
||||||
|
star: {
|
||||||
|
icon: Star,
|
||||||
|
titleKey: "star",
|
||||||
|
className: "hover:text-amber-500 dark:hover:text-amber-400",
|
||||||
|
},
|
||||||
|
markRead: {
|
||||||
|
icon: Mail,
|
||||||
|
titleKey: "mark_read",
|
||||||
|
className: "hover:text-blue-600 dark:hover:text-blue-400",
|
||||||
|
},
|
||||||
|
archive: {
|
||||||
|
icon: Archive,
|
||||||
|
titleKey: "archive",
|
||||||
|
className: "hover:text-green-600 dark:hover:text-green-400",
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
icon: Tag,
|
||||||
|
titleKey: "tag",
|
||||||
|
className: "hover:text-purple-600 dark:hover:text-purple-400",
|
||||||
|
},
|
||||||
|
spam: {
|
||||||
|
icon: ShieldAlert,
|
||||||
|
titleKey: "spam",
|
||||||
|
className: "hover:text-orange-600 dark:hover:text-orange-400",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EmailHoverActions({
|
||||||
|
email,
|
||||||
|
onToggleStar,
|
||||||
|
onMarkAsRead,
|
||||||
|
onDelete,
|
||||||
|
onArchive,
|
||||||
|
onSetColorTag,
|
||||||
|
onMarkAsSpam,
|
||||||
|
}: EmailHoverActionsProps) {
|
||||||
|
const hoverActions = useSettingsStore((state) => state.hoverActions);
|
||||||
|
const t = useTranslations("settings.email_behavior.hover_actions");
|
||||||
|
|
||||||
|
const isUnread = !email.keywords?.$seen;
|
||||||
|
const isStarred = email.keywords?.$flagged;
|
||||||
|
|
||||||
|
if (hoverActions.length === 0) return null;
|
||||||
|
|
||||||
|
const handleAction = (e: React.MouseEvent, action: HoverAction) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
switch (action) {
|
||||||
|
case "delete":
|
||||||
|
onDelete?.();
|
||||||
|
break;
|
||||||
|
case "star":
|
||||||
|
onToggleStar?.();
|
||||||
|
break;
|
||||||
|
case "markRead":
|
||||||
|
onMarkAsRead?.(!isUnread);
|
||||||
|
break;
|
||||||
|
case "archive":
|
||||||
|
onArchive?.();
|
||||||
|
break;
|
||||||
|
case "tag":
|
||||||
|
onSetColorTag?.(null);
|
||||||
|
break;
|
||||||
|
case "spam":
|
||||||
|
onMarkAsSpam?.();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute right-0 top-0 bottom-0 z-10 hidden group-hover:flex items-center"
|
||||||
|
>
|
||||||
|
<div className="w-8 h-full bg-gradient-to-r from-transparent to-muted" />
|
||||||
|
<div className="flex items-center gap-0.5 h-full bg-muted pr-3 pl-0.5">
|
||||||
|
{hoverActions.map((actionId) => {
|
||||||
|
const config = ACTION_CONFIG[actionId];
|
||||||
|
if (!config) return null;
|
||||||
|
const Icon = config.icon;
|
||||||
|
|
||||||
|
const DisplayIcon = actionId === "markRead"
|
||||||
|
? (isUnread ? MailOpen : Mail)
|
||||||
|
: actionId === "star" && isStarred
|
||||||
|
? Star
|
||||||
|
: Icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={actionId}
|
||||||
|
onClick={(e) => handleAction(e, actionId)}
|
||||||
|
title={t(config.titleKey)}
|
||||||
|
className={cn(
|
||||||
|
"p-1.5 rounded-md transition-colors duration-100 text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10",
|
||||||
|
config.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DisplayIcon
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4",
|
||||||
|
actionId === "star" && isStarred && "fill-amber-400 text-amber-400",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { useEmailDrag } from "@/hooks/use-email-drag";
|
|||||||
import { useLongPress } from "@/hooks/use-long-press";
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
import { useUIStore } from "@/stores/ui-store";
|
import { useUIStore } from "@/stores/ui-store";
|
||||||
import { EmailIdentityBadge } from "./email-identity-badge";
|
import { EmailIdentityBadge } from "./email-identity-badge";
|
||||||
|
import { EmailHoverActions } from "./email-hover-actions";
|
||||||
import { getEmailColorTag } from "@/lib/thread-utils";
|
import { getEmailColorTag } from "@/lib/thread-utils";
|
||||||
|
|
||||||
interface EmailListItemProps {
|
interface EmailListItemProps {
|
||||||
@@ -21,9 +22,15 @@ interface EmailListItemProps {
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
|
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
|
||||||
|
onToggleStar?: () => void;
|
||||||
|
onMarkAsRead?: (read: boolean) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onArchive?: () => void;
|
||||||
|
onSetColorTag?: (color: string | null) => void;
|
||||||
|
onMarkAsSpam?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmailListItem({ email, selected, onClick, onContextMenu }: EmailListItemProps) {
|
export function EmailListItem({ email, selected, onClick, onContextMenu, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }: EmailListItemProps) {
|
||||||
const t = useTranslations('email_viewer');
|
const t = useTranslations('email_viewer');
|
||||||
const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox, clearSelection } = useEmailStore();
|
const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox, clearSelection } = useEmailStore();
|
||||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||||
@@ -74,7 +81,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
{...longPressHandlers}
|
{...longPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group cursor-pointer select-none transition-all duration-200 border-b border-border",
|
"relative group cursor-pointer select-none transition-shadow duration-200 border-b border-border overflow-hidden",
|
||||||
// Apply color tag as background, with selected and unread states
|
// Apply color tag as background, with selected and unread states
|
||||||
colorTag ? colorTag : (
|
colorTag ? colorTag : (
|
||||||
selected
|
selected
|
||||||
@@ -217,6 +224,17 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hover Quick Actions */}
|
||||||
|
<EmailHoverActions
|
||||||
|
email={email}
|
||||||
|
onToggleStar={onToggleStar}
|
||||||
|
onMarkAsRead={onMarkAsRead}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onArchive={onArchive}
|
||||||
|
onSetColorTag={onSetColorTag}
|
||||||
|
onMarkAsSpam={onMarkAsSpam}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -422,6 +422,12 @@ export function EmailList({
|
|||||||
onEmailSelect={(email) => onEmailSelect?.(email)}
|
onEmailSelect={(email) => onEmailSelect?.(email)}
|
||||||
onContextMenu={openContextMenu}
|
onContextMenu={openContextMenu}
|
||||||
onOpenConversation={onOpenConversation}
|
onOpenConversation={onOpenConversation}
|
||||||
|
onToggleStar={onToggleStar ? (email) => onToggleStar(email) : undefined}
|
||||||
|
onMarkAsRead={onMarkAsRead ? (email, read) => onMarkAsRead(email, read) : undefined}
|
||||||
|
onDelete={onDelete ? (email) => onDelete(email) : undefined}
|
||||||
|
onArchive={onArchive ? (email) => onArchive(email) : undefined}
|
||||||
|
onSetColorTag={onSetColorTag}
|
||||||
|
onMarkAsSpam={onMarkAsSpam ? (email) => onMarkAsSpam(email) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getThreadColorTag, getEmailColorTag } from "@/lib/thread-utils";
|
|||||||
import { useEmailDrag } from "@/hooks/use-email-drag";
|
import { useEmailDrag } from "@/hooks/use-email-drag";
|
||||||
import { useLongPress } from "@/hooks/use-long-press";
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
import { ThreadEmailItem } from "./thread-email-item";
|
import { ThreadEmailItem } from "./thread-email-item";
|
||||||
|
import { EmailHoverActions } from "./email-hover-actions";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface ThreadListItemProps {
|
interface ThreadListItemProps {
|
||||||
@@ -25,6 +26,12 @@ interface ThreadListItemProps {
|
|||||||
onEmailSelect: (email: Email) => void;
|
onEmailSelect: (email: Email) => void;
|
||||||
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
|
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
|
||||||
onOpenConversation?: (thread: ThreadGroup) => void;
|
onOpenConversation?: (thread: ThreadGroup) => void;
|
||||||
|
onToggleStar?: (email: Email) => void;
|
||||||
|
onMarkAsRead?: (email: Email, read: boolean) => void;
|
||||||
|
onDelete?: (email: Email) => void;
|
||||||
|
onArchive?: (email: Email) => void;
|
||||||
|
onSetColorTag?: (emailId: string, color: string | null) => void;
|
||||||
|
onMarkAsSpam?: (email: Email) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SingleEmailItemProps {
|
interface SingleEmailItemProps {
|
||||||
@@ -34,10 +41,16 @@ interface SingleEmailItemProps {
|
|||||||
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
|
onContextMenu?: (e: React.MouseEvent, email: Email) => void;
|
||||||
showPreview: boolean;
|
showPreview: boolean;
|
||||||
colorTag: string | null;
|
colorTag: string | null;
|
||||||
|
onToggleStar?: () => void;
|
||||||
|
onMarkAsRead?: (read: boolean) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onArchive?: () => void;
|
||||||
|
onSetColorTag?: (color: string | null) => void;
|
||||||
|
onMarkAsSpam?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||||
function SingleEmailItem({ email, selected, onClick, onContextMenu, showPreview, colorTag }, ref) {
|
function SingleEmailItem({ email, selected, onClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, ref) {
|
||||||
const isUnread = !email.keywords?.$seen;
|
const isUnread = !email.keywords?.$seen;
|
||||||
const isStarred = email.keywords?.$flagged;
|
const isStarred = email.keywords?.$flagged;
|
||||||
const sender = email.from?.[0];
|
const sender = email.from?.[0];
|
||||||
@@ -100,7 +113,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
{...longPressHandlers}
|
{...longPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group cursor-pointer select-none transition-all duration-200 border-b border-border",
|
"relative group cursor-pointer select-none transition-shadow duration-200 border-b border-border overflow-hidden",
|
||||||
resolvedColorTag ? resolvedColorTag : (
|
resolvedColorTag ? resolvedColorTag : (
|
||||||
selected
|
selected
|
||||||
? "bg-accent"
|
? "bg-accent"
|
||||||
@@ -216,6 +229,17 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hover Quick Actions */}
|
||||||
|
<EmailHoverActions
|
||||||
|
email={email}
|
||||||
|
onToggleStar={onToggleStar}
|
||||||
|
onMarkAsRead={onMarkAsRead}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onArchive={onArchive}
|
||||||
|
onSetColorTag={onSetColorTag}
|
||||||
|
onMarkAsSpam={onMarkAsSpam}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -232,6 +256,12 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
onEmailSelect,
|
onEmailSelect,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
onOpenConversation,
|
onOpenConversation,
|
||||||
|
onToggleStar,
|
||||||
|
onMarkAsRead,
|
||||||
|
onDelete,
|
||||||
|
onArchive,
|
||||||
|
onSetColorTag,
|
||||||
|
onMarkAsSpam,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const t = useTranslations('threads');
|
const t = useTranslations('threads');
|
||||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||||
@@ -278,6 +308,12 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
showPreview={showPreview}
|
showPreview={showPreview}
|
||||||
colorTag={colorTag}
|
colorTag={colorTag}
|
||||||
|
onToggleStar={onToggleStar ? () => onToggleStar(latestEmail) : undefined}
|
||||||
|
onMarkAsRead={onMarkAsRead ? (read) => onMarkAsRead(latestEmail, read) : undefined}
|
||||||
|
onDelete={onDelete ? () => onDelete(latestEmail) : undefined}
|
||||||
|
onArchive={onArchive ? () => onArchive(latestEmail) : undefined}
|
||||||
|
onSetColorTag={onSetColorTag ? (color) => onSetColorTag(latestEmail.id, color) : undefined}
|
||||||
|
onMarkAsSpam={onMarkAsSpam ? () => onMarkAsSpam(latestEmail) : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -339,7 +375,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
{...threadLongPressHandlers}
|
{...threadLongPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group cursor-pointer select-none transition-all duration-200",
|
"relative group cursor-pointer select-none transition-shadow duration-200 overflow-hidden",
|
||||||
colorTag ? colorTag : (
|
colorTag ? colorTag : (
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-accent"
|
? "bg-accent"
|
||||||
@@ -493,6 +529,17 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hover Quick Actions for thread header */}
|
||||||
|
<EmailHoverActions
|
||||||
|
email={latestEmail}
|
||||||
|
onToggleStar={onToggleStar ? () => onToggleStar(latestEmail) : undefined}
|
||||||
|
onMarkAsRead={onMarkAsRead ? (read) => onMarkAsRead(latestEmail, read) : undefined}
|
||||||
|
onDelete={onDelete ? () => onDelete(latestEmail) : undefined}
|
||||||
|
onArchive={onArchive ? () => onArchive(latestEmail) : undefined}
|
||||||
|
onSetColorTag={onSetColorTag ? (color) => onSetColorTag(latestEmail.id, color) : undefined}
|
||||||
|
onMarkAsSpam={onMarkAsSpam ? () => onMarkAsSpam(latestEmail) : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && !isMobile && (
|
{isExpanded && !isMobile && (
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import type { ArchiveMode } from '@/stores/settings-store';
|
import type { ArchiveMode, HoverAction } from '@/stores/settings-store';
|
||||||
|
import { ALL_HOVER_ACTIONS } from '@/stores/settings-store';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { useEmailStore } from '@/stores/email-store';
|
import { useEmailStore } from '@/stores/email-store';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||||
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
|
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
|
||||||
import { ChevronRight, AlertTriangle, FolderSync, Loader2 } from 'lucide-react';
|
import { ChevronRight, AlertTriangle, FolderSync, Loader2 } from 'lucide-react';
|
||||||
@@ -27,6 +29,7 @@ export function EmailSettings() {
|
|||||||
attachmentPosition,
|
attachmentPosition,
|
||||||
emailAlwaysLightMode,
|
emailAlwaysLightMode,
|
||||||
archiveMode,
|
archiveMode,
|
||||||
|
hoverActions,
|
||||||
trustedSenders,
|
trustedSenders,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSettingsStore();
|
} = useSettingsStore();
|
||||||
@@ -185,6 +188,39 @@ export function EmailSettings() {
|
|||||||
<ToggleSwitch checked={showPreview} onChange={(checked) => updateSetting('showPreview', checked)} />
|
<ToggleSwitch checked={showPreview} onChange={(checked) => updateSetting('showPreview', checked)} />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Quick Hover Actions */}
|
||||||
|
<div className="py-3 border-b border-border space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-foreground">{t('hover_actions.label')}</label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{t('hover_actions.description')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{ALL_HOVER_ACTIONS.map((action) => {
|
||||||
|
const isEnabled = hoverActions.includes(action.id);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={action.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const newActions = isEnabled
|
||||||
|
? hoverActions.filter((a: HoverAction) => a !== action.id)
|
||||||
|
: [...hoverActions, action.id];
|
||||||
|
updateSetting('hoverActions', newActions);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 text-xs rounded-md transition-colors duration-150',
|
||||||
|
isEnabled
|
||||||
|
? 'bg-primary text-primary-foreground font-medium'
|
||||||
|
: 'bg-muted hover:bg-accent text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(`hover_actions.${action.labelKey}`)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingItem label={t('attachment_click_action.label')} description={t('attachment_click_action.description')}>
|
<SettingItem label={t('attachment_click_action.label')} description={t('attachment_click_action.description')}>
|
||||||
<Select
|
<Select
|
||||||
value={mailAttachmentAction}
|
value={mailAttachmentAction}
|
||||||
|
|||||||
@@ -808,6 +808,17 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"invalid_email": "Please enter a valid email address",
|
"invalid_email": "Please enter a valid email address",
|
||||||
"already_added": "This sender is already trusted"
|
"already_added": "This sender is already trusted"
|
||||||
|
},
|
||||||
|
"hover_actions": {
|
||||||
|
"label": "Quick Hover Actions",
|
||||||
|
"description": "Choose which quick actions appear when hovering over an email in the list",
|
||||||
|
"delete": "Delete",
|
||||||
|
"star": "Star / Unstar",
|
||||||
|
"mark_read": "Mark Read / Unread",
|
||||||
|
"archive": "Archive",
|
||||||
|
"tag": "Tag",
|
||||||
|
"spam": "Mark as Spam",
|
||||||
|
"none_selected": "No actions selected"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ export type AttachmentPosition = 'beside-sender' | 'below-header';
|
|||||||
export type ToolbarPosition = 'top' | 'below-subject';
|
export type ToolbarPosition = 'top' | 'below-subject';
|
||||||
export type ArchiveMode = 'single' | 'year' | 'month';
|
export type ArchiveMode = 'single' | 'year' | 'month';
|
||||||
|
|
||||||
|
export type HoverAction = 'delete' | 'star' | 'markRead' | 'archive' | 'tag' | 'spam';
|
||||||
|
|
||||||
|
export const ALL_HOVER_ACTIONS: { id: HoverAction; labelKey: string }[] = [
|
||||||
|
{ id: 'delete', labelKey: 'delete' },
|
||||||
|
{ id: 'star', labelKey: 'star' },
|
||||||
|
{ id: 'markRead', labelKey: 'mark_read' },
|
||||||
|
{ id: 'archive', labelKey: 'archive' },
|
||||||
|
{ id: 'tag', labelKey: 'tag' },
|
||||||
|
{ id: 'spam', labelKey: 'spam' },
|
||||||
|
];
|
||||||
|
|
||||||
export interface KeywordDefinition {
|
export interface KeywordDefinition {
|
||||||
id: string; // Used as JMAP keyword suffix: $label:<id>
|
id: string; // Used as JMAP keyword suffix: $label:<id>
|
||||||
label: string; // Display name
|
label: string; // Display name
|
||||||
@@ -97,6 +108,7 @@ interface SettingsState {
|
|||||||
attachmentPosition: AttachmentPosition;
|
attachmentPosition: AttachmentPosition;
|
||||||
emailAlwaysLightMode: boolean; // Always render email content in light mode
|
emailAlwaysLightMode: boolean; // Always render email content in light mode
|
||||||
archiveMode: ArchiveMode; // How to organize archived emails: single folder, by year, or by year+month
|
archiveMode: ArchiveMode; // How to organize archived emails: single folder, by year, or by year+month
|
||||||
|
hoverActions: HoverAction[]; // Quick actions shown on hover in mail list
|
||||||
|
|
||||||
// Composer
|
// Composer
|
||||||
autoSaveDraftInterval: number; // milliseconds
|
autoSaveDraftInterval: number; // milliseconds
|
||||||
@@ -196,6 +208,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
attachmentPosition: 'beside-sender' as AttachmentPosition,
|
attachmentPosition: 'beside-sender' as AttachmentPosition,
|
||||||
emailAlwaysLightMode: false,
|
emailAlwaysLightMode: false,
|
||||||
archiveMode: 'single' as ArchiveMode,
|
archiveMode: 'single' as ArchiveMode,
|
||||||
|
hoverActions: ['delete', 'star', 'markRead', 'archive'] as HoverAction[],
|
||||||
|
|
||||||
// Composer
|
// Composer
|
||||||
autoSaveDraftInterval: 60000, // 1 minute
|
autoSaveDraftInterval: 60000, // 1 minute
|
||||||
@@ -284,6 +297,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
mailAttachmentAction: state.mailAttachmentAction,
|
mailAttachmentAction: state.mailAttachmentAction,
|
||||||
attachmentPosition: state.attachmentPosition,
|
attachmentPosition: state.attachmentPosition,
|
||||||
archiveMode: state.archiveMode,
|
archiveMode: state.archiveMode,
|
||||||
|
hoverActions: state.hoverActions,
|
||||||
trustedSenders: state.trustedSenders,
|
trustedSenders: state.trustedSenders,
|
||||||
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
||||||
sendConfirmation: state.sendConfirmation,
|
sendConfirmation: state.sendConfirmation,
|
||||||
|
|||||||
Reference in New Issue
Block a user