feat: add "Move to Trash and mark as read" delete action #323
This commit is contained in:
@@ -28,7 +28,7 @@ const RESTRICTABLE_SETTINGS = [
|
|||||||
{ key: 'density', label: 'Density', category: 'Appearance', type: 'enum', allowedValues: ['compact', 'regular', 'spacious'] },
|
{ key: 'density', label: 'Density', category: 'Appearance', type: 'enum', allowedValues: ['compact', 'regular', 'spacious'] },
|
||||||
{ key: 'animationsEnabled', label: 'Animations', category: 'Appearance', type: 'boolean' },
|
{ key: 'animationsEnabled', label: 'Animations', category: 'Appearance', type: 'boolean' },
|
||||||
{ key: 'markAsReadDelay', label: 'Mark as Read Delay', category: 'Email', type: 'number' },
|
{ key: 'markAsReadDelay', label: 'Mark as Read Delay', category: 'Email', type: 'number' },
|
||||||
{ key: 'deleteAction', label: 'Delete Action', category: 'Email', type: 'enum', allowedValues: ['trash', 'permanent'] },
|
{ key: 'deleteAction', label: 'Delete Action', category: 'Email', type: 'enum', allowedValues: ['trash', 'trash-and-read', 'permanent'] },
|
||||||
{ key: 'showPreview', label: 'Show Preview', category: 'Email', type: 'boolean' },
|
{ key: 'showPreview', label: 'Show Preview', category: 'Email', type: 'boolean' },
|
||||||
{ key: 'mailLayout', label: 'Mail Layout', category: 'Email', type: 'enum', allowedValues: ['split', 'focus', 'horizontal'] },
|
{ key: 'mailLayout', label: 'Mail Layout', category: 'Email', type: 'enum', allowedValues: ['split', 'focus', 'horizontal'] },
|
||||||
{ key: 'emailsPerPage', label: 'Emails Per Page', category: 'Email', type: 'number' },
|
{ key: 'emailsPerPage', label: 'Emails Per Page', category: 'Email', type: 'number' },
|
||||||
|
|||||||
@@ -120,9 +120,10 @@ export function ReadingSettings() {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={deleteAction}
|
value={deleteAction}
|
||||||
onChange={(value) => updateSetting('deleteAction', value as 'trash' | 'permanent')}
|
onChange={(value) => updateSetting('deleteAction', value as 'trash' | 'trash-and-read' | 'permanent')}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'trash', label: t('delete_action.trash') },
|
{ value: 'trash', label: t('delete_action.trash') },
|
||||||
|
{ value: 'trash-and-read', label: t('delete_action.trash_and_read') },
|
||||||
{ value: 'permanent', label: t('delete_action.permanent') },
|
{ value: 'permanent', label: t('delete_action.permanent') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+12
-5
@@ -264,10 +264,11 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
this.recalcMailboxCounts();
|
this.recalcMailboxCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveToTrash(emailId: string, trashMailboxId: string): Promise<void> {
|
async moveToTrash(emailId: string, trashMailboxId: string, _accountId?: string, markAsRead?: boolean): Promise<void> {
|
||||||
const email = this.data.emails.find(e => e.id === emailId);
|
const email = this.data.emails.find(e => e.id === emailId);
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
email.mailboxIds = { [trashMailboxId]: true };
|
email.mailboxIds = { [trashMailboxId]: true };
|
||||||
|
if (markAsRead) email.keywords.$seen = true;
|
||||||
this.recalcMailboxCounts();
|
this.recalcMailboxCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,10 +278,13 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
this.recalcMailboxCounts();
|
this.recalcMailboxCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchMoveEmails(emailIds: string[], toMailboxId: string): Promise<void> {
|
async batchMoveEmails(emailIds: string[], toMailboxId: string, _accountId?: string, markAsRead?: boolean): Promise<void> {
|
||||||
for (const id of emailIds) {
|
for (const id of emailIds) {
|
||||||
const email = this.data.emails.find(e => e.id === id);
|
const email = this.data.emails.find(e => e.id === id);
|
||||||
if (email) email.mailboxIds = { [toMailboxId]: true };
|
if (email) {
|
||||||
|
email.mailboxIds = { [toMailboxId]: true };
|
||||||
|
if (markAsRead) email.keywords.$seen = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.recalcMailboxCounts();
|
this.recalcMailboxCounts();
|
||||||
}
|
}
|
||||||
@@ -355,10 +359,13 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAsSpam(emailId: string): Promise<void> {
|
async markAsSpam(emailId: string, _accountId?: string, markAsRead?: boolean): Promise<void> {
|
||||||
const email = this.data.emails.find(e => e.id === emailId);
|
const email = this.data.emails.find(e => e.id === emailId);
|
||||||
const junkMb = this.data.mailboxes.find(m => m.role === 'junk');
|
const junkMb = this.data.mailboxes.find(m => m.role === 'junk');
|
||||||
if (email && junkMb) email.mailboxIds = { [junkMb.id]: true };
|
if (email && junkMb) {
|
||||||
|
email.mailboxIds = { [junkMb.id]: true };
|
||||||
|
if (markAsRead) email.keywords.$seen = true;
|
||||||
|
}
|
||||||
this.recalcMailboxCounts();
|
this.recalcMailboxCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ export interface IJMAPClient {
|
|||||||
setKeyword(emailId: string, keyword: string): Promise<void>;
|
setKeyword(emailId: string, keyword: string): Promise<void>;
|
||||||
migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number>;
|
migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number>;
|
||||||
deleteEmail(emailId: string): Promise<void>;
|
deleteEmail(emailId: string): Promise<void>;
|
||||||
moveToTrash(emailId: string, trashMailboxId: string, accountId?: string): Promise<void>;
|
moveToTrash(emailId: string, trashMailboxId: string, accountId?: string, markAsRead?: boolean): Promise<void>;
|
||||||
batchDeleteEmails(emailIds: string[]): Promise<void>;
|
batchDeleteEmails(emailIds: string[]): Promise<void>;
|
||||||
batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string): Promise<void>;
|
batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string, markAsRead?: boolean): Promise<void>;
|
||||||
batchArchiveEmails(
|
batchArchiveEmails(
|
||||||
emails: Array<{ id: string; receivedAt: string }>,
|
emails: Array<{ id: string; receivedAt: string }>,
|
||||||
archiveMailboxId: string,
|
archiveMailboxId: string,
|
||||||
@@ -110,7 +110,7 @@ export interface IJMAPClient {
|
|||||||
emptyMailbox(mailboxId: string): Promise<number>;
|
emptyMailbox(mailboxId: string): Promise<number>;
|
||||||
markMailboxAsRead(mailboxId: string, accountId?: string): Promise<number>;
|
markMailboxAsRead(mailboxId: string, accountId?: string): Promise<number>;
|
||||||
markAllAsRead(excludeMailboxIds?: string[], accountId?: string): Promise<number>;
|
markAllAsRead(excludeMailboxIds?: string[], accountId?: string): Promise<number>;
|
||||||
markAsSpam(emailId: string, accountId?: string): Promise<void>;
|
markAsSpam(emailId: string, accountId?: string, markAsRead?: boolean): Promise<void>;
|
||||||
undoSpam(emailId: string, originalMailboxId: string, accountId?: string): Promise<void>;
|
undoSpam(emailId: string, originalMailboxId: string, accountId?: string): Promise<void>;
|
||||||
|
|
||||||
// ── Threads ───────────────────────────────────────────────────
|
// ── Threads ───────────────────────────────────────────────────
|
||||||
|
|||||||
+16
-14
@@ -1226,16 +1226,14 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveToTrash(emailId: string, trashMailboxId: string, accountId?: string): Promise<void> {
|
async moveToTrash(emailId: string, trashMailboxId: string, accountId?: string, markAsRead?: boolean): Promise<void> {
|
||||||
const targetAccountId = accountId || this.accountId;
|
const targetAccountId = accountId || this.accountId;
|
||||||
|
const patch: Record<string, unknown> = { mailboxIds: { [trashMailboxId]: true } };
|
||||||
|
if (markAsRead) patch["keywords/$seen"] = true;
|
||||||
await this.request([
|
await this.request([
|
||||||
["Email/set", {
|
["Email/set", {
|
||||||
accountId: targetAccountId,
|
accountId: targetAccountId,
|
||||||
update: {
|
update: { [emailId]: patch },
|
||||||
[emailId]: {
|
|
||||||
mailboxIds: { [trashMailboxId]: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, "0"],
|
}, "0"],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1251,10 +1249,15 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string): Promise<void> {
|
async batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string, markAsRead?: boolean): Promise<void> {
|
||||||
if (emailIds.length === 0) return;
|
if (emailIds.length === 0) return;
|
||||||
|
|
||||||
const updates = Object.fromEntries(emailIds.map(id => [id, { mailboxIds: { [toMailboxId]: true } }]));
|
const buildPatch = () => {
|
||||||
|
const patch: Record<string, unknown> = { mailboxIds: { [toMailboxId]: true } };
|
||||||
|
if (markAsRead) patch["keywords/$seen"] = true;
|
||||||
|
return patch;
|
||||||
|
};
|
||||||
|
const updates = Object.fromEntries(emailIds.map(id => [id, buildPatch()]));
|
||||||
await this.request([
|
await this.request([
|
||||||
["Email/set", { accountId: accountId || this.accountId, update: updates }, "0"],
|
["Email/set", { accountId: accountId || this.accountId, update: updates }, "0"],
|
||||||
]);
|
]);
|
||||||
@@ -1507,7 +1510,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
return totalMarked;
|
return totalMarked;
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAsSpam(emailId: string, accountId?: string): Promise<void> {
|
async markAsSpam(emailId: string, accountId?: string, markAsRead?: boolean): Promise<void> {
|
||||||
const targetAccountId = accountId || this.accountId;
|
const targetAccountId = accountId || this.accountId;
|
||||||
|
|
||||||
const mailboxes = await this.getMailboxes();
|
const mailboxes = await this.getMailboxes();
|
||||||
@@ -1526,14 +1529,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
? junkMailbox.originalId
|
? junkMailbox.originalId
|
||||||
: junkMailbox.id;
|
: junkMailbox.id;
|
||||||
|
|
||||||
|
const patch: Record<string, unknown> = { mailboxIds: { [mailboxId]: true } };
|
||||||
|
if (markAsRead) patch["keywords/$seen"] = true;
|
||||||
|
|
||||||
await this.request([
|
await this.request([
|
||||||
["Email/set", {
|
["Email/set", {
|
||||||
accountId: targetAccountId,
|
accountId: targetAccountId,
|
||||||
update: {
|
update: { [emailId]: patch },
|
||||||
[emailId]: {
|
|
||||||
mailboxIds: { [mailboxId]: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, "0"],
|
}, "0"],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -974,6 +974,7 @@
|
|||||||
"label": "Delete Action",
|
"label": "Delete Action",
|
||||||
"description": "What happens when you delete an email",
|
"description": "What happens when you delete an email",
|
||||||
"trash": "Move to Trash",
|
"trash": "Move to Trash",
|
||||||
|
"trash_and_read": "Move to Trash and mark as read",
|
||||||
"permanent": "Delete Permanently",
|
"permanent": "Delete Permanently",
|
||||||
"warning": "Emails will be permanently deleted and cannot be recovered. This action is irreversible."
|
"warning": "Emails will be permanently deleted and cannot be recovered. This action is irreversible."
|
||||||
},
|
},
|
||||||
|
|||||||
+20
-9
@@ -773,14 +773,18 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
forceDelete = true;
|
forceDelete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If deleteAction is 'trash' and not forced permanent delete, try to move to trash mailbox
|
// If deleteAction is 'trash' or 'trash-and-read' and not forced permanent delete, try to move to trash mailbox
|
||||||
if (deleteAction === 'trash' && !forceDelete) {
|
if ((deleteAction === 'trash' || deleteAction === 'trash-and-read') && !forceDelete) {
|
||||||
const trashMailbox = findTrashMailbox(mailboxes, { accountId });
|
const trashMailbox = findTrashMailbox(mailboxes, { accountId });
|
||||||
|
const alsoMarkRead = deleteAction === 'trash-and-read' && isUnread;
|
||||||
|
|
||||||
if (trashMailbox) {
|
if (trashMailbox) {
|
||||||
// Use originalId for shared mailboxes if available
|
// Use originalId for shared mailboxes if available
|
||||||
const trashId = trashMailbox.originalId || trashMailbox.id;
|
const trashId = trashMailbox.originalId || trashMailbox.id;
|
||||||
await effectiveClient.moveToTrash(emailId, trashId, accountId);
|
await effectiveClient.moveToTrash(emailId, trashId, accountId, alsoMarkRead);
|
||||||
|
|
||||||
|
// After marking read in the same request, the email arrives in trash as read.
|
||||||
|
const arrivesUnread = isUnread && !alsoMarkRead;
|
||||||
|
|
||||||
// Remove from local state (email moved to trash, not in current view)
|
// Remove from local state (email moved to trash, not in current view)
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -803,9 +807,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
...mailbox,
|
...mailbox,
|
||||||
totalEmails: mailbox.totalEmails + 1,
|
totalEmails: mailbox.totalEmails + 1,
|
||||||
unreadEmails: isUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
unreadEmails: arrivesUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
||||||
totalThreads: mailbox.totalThreads + 1,
|
totalThreads: mailbox.totalThreads + 1,
|
||||||
unreadThreads: isUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads
|
unreadThreads: arrivesUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return mailbox;
|
return mailbox;
|
||||||
@@ -1514,6 +1518,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk;
|
const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk;
|
||||||
const isInJunk = currentMailbox?.role === 'junk';
|
const isInJunk = currentMailbox?.role === 'junk';
|
||||||
const forceDestroy = permanent || isInTrash || (isInJunk && permanentlyDeleteJunk);
|
const forceDestroy = permanent || isInTrash || (isInJunk && permanentlyDeleteJunk);
|
||||||
|
const alsoMarkRead = useSettingsStore.getState().deleteAction === 'trash-and-read';
|
||||||
|
|
||||||
// Group emails by accountId (handles unified view and search results spanning accounts).
|
// Group emails by accountId (handles unified view and search results spanning accounts).
|
||||||
const emailsByAccount = new Map<string, string[]>();
|
const emailsByAccount = new Map<string, string[]>();
|
||||||
@@ -1554,7 +1559,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const trashId = trashMailbox.originalId || trashMailbox.id;
|
const trashId = trashMailbox.originalId || trashMailbox.id;
|
||||||
await acctClient.batchMoveEmails(ids, trashId, trashMailbox.accountId);
|
await acctClient.batchMoveEmails(ids, trashId, trashMailbox.accountId, alsoMarkRead);
|
||||||
ids.forEach(id => movedEmailIds.add(id));
|
ids.forEach(id => movedEmailIds.add(id));
|
||||||
});
|
});
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
@@ -1745,7 +1750,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resolveActionClient(client).markAsSpam(emailId, currentMailbox.accountId);
|
const isUnread = !email.keywords?.$seen;
|
||||||
|
const alsoMarkRead = useSettingsStore.getState().deleteAction === 'trash-and-read' && isUnread;
|
||||||
|
await resolveActionClient(client).markAsSpam(emailId, currentMailbox.accountId, alsoMarkRead);
|
||||||
|
|
||||||
set(state => ({
|
set(state => ({
|
||||||
emails: state.emails.filter(e => e.id !== emailId),
|
emails: state.emails.filter(e => e.id !== emailId),
|
||||||
@@ -1800,16 +1807,20 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
batchMarkAsSpam: async (client, emailIds) => {
|
batchMarkAsSpam: async (client, emailIds) => {
|
||||||
const { selectedMailbox } = get();
|
const { selectedMailbox, emails } = get();
|
||||||
const mailboxes = resolveActionMailboxes();
|
const mailboxes = resolveActionMailboxes();
|
||||||
const effectiveClient = resolveActionClient(client);
|
const effectiveClient = resolveActionClient(client);
|
||||||
|
|
||||||
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||||
if (!currentMailbox) return;
|
if (!currentMailbox) return;
|
||||||
|
|
||||||
|
const alsoMarkRead = useSettingsStore.getState().deleteAction === 'trash-and-read';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const emailId of emailIds) {
|
for (const emailId of emailIds) {
|
||||||
await effectiveClient.markAsSpam(emailId, currentMailbox.accountId);
|
const email = emails.find(e => e.id === emailId);
|
||||||
|
const markRead = alsoMarkRead && !!email && !email.keywords?.$seen;
|
||||||
|
await effectiveClient.markAsSpam(emailId, currentMailbox.accountId, markRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(state => ({
|
set(state => ({
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export type FontSize = 'small' | 'medium' | 'large';
|
|||||||
export type Density = 'extra-compact' | 'compact' | 'regular' | 'comfortable';
|
export type Density = 'extra-compact' | 'compact' | 'regular' | 'comfortable';
|
||||||
/** @deprecated Use Density instead */
|
/** @deprecated Use Density instead */
|
||||||
export type ListDensity = Density;
|
export type ListDensity = Density;
|
||||||
export type DeleteAction = 'trash' | 'permanent';
|
export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent';
|
||||||
export type ReplyMode = 'reply' | 'replyAll';
|
export type ReplyMode = 'reply' | 'replyAll';
|
||||||
export type SignaturePosition = 'above_quote' | 'below_quote';
|
export type SignaturePosition = 'above_quote' | 'below_quote';
|
||||||
export type DateFormat = 'regional' | 'iso' | 'custom';
|
export type DateFormat = 'regional' | 'iso' | 'custom';
|
||||||
|
|||||||
Reference in New Issue
Block a user