feat: add support for marking emails as answered or forwarded and update UI accordingly
This commit is contained in:
@@ -444,9 +444,27 @@ export default function Home() {
|
|||||||
if (!client) return;
|
if (!client) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const effectiveMode = pendingDraft?.mode ?? composerMode;
|
||||||
|
const originalEmailId = selectedEmail?.id;
|
||||||
|
|
||||||
await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody, data.attachments);
|
await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody, data.attachments);
|
||||||
setShowComposer(false);
|
setShowComposer(false);
|
||||||
|
|
||||||
|
// Mark the original email with $answered or $forwarded keyword
|
||||||
|
if (originalEmailId && (effectiveMode === 'reply' || effectiveMode === 'replyAll')) {
|
||||||
|
try {
|
||||||
|
await client.setKeyword(originalEmailId, '$answered');
|
||||||
|
} catch (e) {
|
||||||
|
debug.error('Failed to set $answered keyword:', e);
|
||||||
|
}
|
||||||
|
} else if (originalEmailId && effectiveMode === 'forward') {
|
||||||
|
try {
|
||||||
|
await client.setKeyword(originalEmailId, '$forwarded');
|
||||||
|
} catch (e) {
|
||||||
|
debug.error('Failed to set $forwarded keyword:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh the current mailbox to update the UI
|
// Refresh the current mailbox to update the UI
|
||||||
await fetchEmails(client, selectedMailbox);
|
await fetchEmails(client, selectedMailbox);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -861,6 +879,8 @@ export default function Home() {
|
|||||||
// Append signature from the primary identity
|
// Append signature from the primary identity
|
||||||
const finalBody = appendPlainTextSignature(body, primaryIdentity);
|
const finalBody = appendPlainTextSignature(body, primaryIdentity);
|
||||||
|
|
||||||
|
const originalEmailId = selectedEmail.id;
|
||||||
|
|
||||||
// Send reply with just the body text
|
// Send reply with just the body text
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
client,
|
client,
|
||||||
@@ -875,6 +895,13 @@ export default function Home() {
|
|||||||
primaryIdentity?.name || undefined
|
primaryIdentity?.name || undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Mark the original email as answered
|
||||||
|
try {
|
||||||
|
await client.setKeyword(originalEmailId, '$answered');
|
||||||
|
} catch (e) {
|
||||||
|
debug.error('Failed to set $answered keyword:', e);
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh emails to show the sent reply
|
// Refresh emails to show the sent reply
|
||||||
await fetchEmails(client, selectedMailbox);
|
await fetchEmails(client, selectedMailbox);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { formatDate } from "@/lib/utils";
|
|||||||
import { Email } from "@/lib/jmap/types";
|
import { Email } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { Paperclip, Star, Circle, CheckSquare, Square, Tag } from "lucide-react";
|
import { Paperclip, Star, Circle, CheckSquare, Square, Tag, Reply, Forward } from "lucide-react";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
@@ -41,6 +41,8 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
|
|||||||
const isUnread = !email.keywords?.$seen;
|
const isUnread = !email.keywords?.$seen;
|
||||||
const isStarred = email.keywords?.$flagged;
|
const isStarred = email.keywords?.$flagged;
|
||||||
const isImportant = email.keywords?.["$important"];
|
const isImportant = email.keywords?.["$important"];
|
||||||
|
const isAnswered = email.keywords?.$answered;
|
||||||
|
const isForwarded = email.keywords?.$forwarded;
|
||||||
const sender = email.from?.[0];
|
const sender = email.from?.[0];
|
||||||
|
|
||||||
// Resolve color tag using keyword definitions from settings
|
// Resolve color tag using keyword definitions from settings
|
||||||
@@ -175,6 +177,18 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<EmailIdentityBadge email={email} identities={identities} compact={true} />
|
<EmailIdentityBadge email={email} identities={identities} compact={true} />
|
||||||
|
{isAnswered && !isForwarded && (
|
||||||
|
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{isForwarded && !isAnswered && (
|
||||||
|
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{isAnswered && isForwarded && (
|
||||||
|
<>
|
||||||
|
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{email.hasAttachment && (
|
{email.hasAttachment && (
|
||||||
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { formatDate } from "@/lib/utils";
|
|||||||
import { Email } from "@/lib/jmap/types";
|
import { Email } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react";
|
import { Paperclip, Star, Circle, CheckSquare, Square, Reply, Forward } from "lucide-react";
|
||||||
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 { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
@@ -29,6 +29,8 @@ export function ThreadEmailItem({
|
|||||||
}: ThreadEmailItemProps) {
|
}: ThreadEmailItemProps) {
|
||||||
const isUnread = !email.keywords?.$seen;
|
const isUnread = !email.keywords?.$seen;
|
||||||
const isStarred = email.keywords?.$flagged;
|
const isStarred = email.keywords?.$flagged;
|
||||||
|
const isAnswered = email.keywords?.$answered;
|
||||||
|
const isForwarded = email.keywords?.$forwarded;
|
||||||
const sender = email.from?.[0];
|
const sender = email.from?.[0];
|
||||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||||
const density = useSettingsStore((state) => state.density);
|
const density = useSettingsStore((state) => state.density);
|
||||||
@@ -151,6 +153,18 @@ export function ThreadEmailItem({
|
|||||||
{isStarred && (
|
{isStarred && (
|
||||||
<Star className="w-3 h-3 fill-amber-400 text-amber-400" />
|
<Star className="w-3 h-3 fill-amber-400 text-amber-400" />
|
||||||
)}
|
)}
|
||||||
|
{isAnswered && !isForwarded && (
|
||||||
|
<Reply className="w-3 h-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{isForwarded && !isAnswered && (
|
||||||
|
<Forward className="w-3 h-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{isAnswered && isForwarded && (
|
||||||
|
<>
|
||||||
|
<Reply className="w-3 h-3 text-muted-foreground" />
|
||||||
|
<Forward className="w-3 h-3 text-muted-foreground" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{email.hasAttachment && (
|
{email.hasAttachment && (
|
||||||
<Paperclip className="w-3 h-3 text-muted-foreground" />
|
<Paperclip className="w-3 h-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { formatDate } from "@/lib/utils";
|
|||||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square } from "lucide-react";
|
import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward } from "lucide-react";
|
||||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||||
import { useUIStore } from "@/stores/ui-store";
|
import { useUIStore } from "@/stores/ui-store";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
@@ -53,6 +53,8 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
function SingleEmailItem({ email, selected, onClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, 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 isAnswered = email.keywords?.$answered;
|
||||||
|
const isForwarded = email.keywords?.$forwarded;
|
||||||
const sender = email.from?.[0];
|
const sender = email.from?.[0];
|
||||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||||
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
|
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
|
||||||
@@ -182,6 +184,18 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
{isStarred && (
|
{isStarred && (
|
||||||
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
||||||
)}
|
)}
|
||||||
|
{isAnswered && !isForwarded && (
|
||||||
|
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{isForwarded && !isAnswered && (
|
||||||
|
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{isAnswered && isForwarded && (
|
||||||
|
<>
|
||||||
|
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{email.hasAttachment && (
|
{email.hasAttachment && (
|
||||||
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
@@ -267,7 +281,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||||
const density = useSettingsStore((state) => state.density);
|
const density = useSettingsStore((state) => state.density);
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, emailCount } = thread;
|
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread;
|
||||||
|
|
||||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||||
|
|
||||||
@@ -482,6 +496,18 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
{hasStarred && (
|
{hasStarred && (
|
||||||
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
||||||
)}
|
)}
|
||||||
|
{hasAnswered && !hasForwarded && (
|
||||||
|
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{hasForwarded && !hasAnswered && (
|
||||||
|
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{hasAnswered && hasForwarded && (
|
||||||
|
<>
|
||||||
|
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{hasAttachment && (
|
{hasAttachment && (
|
||||||
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -89,6 +89,22 @@ describe('groupEmailsByThread', () => {
|
|||||||
expect(groupEmailsByThread(emails)[0].hasAttachment).toBe(true);
|
expect(groupEmailsByThread(emails)[0].hasAttachment).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detects hasAnswered when an email has $answered', () => {
|
||||||
|
const emails = [
|
||||||
|
makeEmail({ id: 'e1', keywords: { $seen: true } }),
|
||||||
|
makeEmail({ id: 'e2', keywords: { $seen: true, $answered: true } }),
|
||||||
|
];
|
||||||
|
expect(groupEmailsByThread(emails)[0].hasAnswered).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects hasForwarded when an email has $forwarded', () => {
|
||||||
|
const emails = [
|
||||||
|
makeEmail({ id: 'e1', keywords: { $seen: true } }),
|
||||||
|
makeEmail({ id: 'e2', keywords: { $seen: true, $forwarded: true } }),
|
||||||
|
];
|
||||||
|
expect(groupEmailsByThread(emails)[0].hasForwarded).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns empty array for empty input', () => {
|
it('returns empty array for empty input', () => {
|
||||||
expect(groupEmailsByThread([])).toEqual([]);
|
expect(groupEmailsByThread([])).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -110,6 +126,8 @@ describe('sortThreadGroups', () => {
|
|||||||
hasUnread: false,
|
hasUnread: false,
|
||||||
hasStarred: false,
|
hasStarred: false,
|
||||||
hasAttachment: false,
|
hasAttachment: false,
|
||||||
|
hasAnswered: false,
|
||||||
|
hasForwarded: false,
|
||||||
emailCount: 1,
|
emailCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,6 +138,8 @@ describe('sortThreadGroups', () => {
|
|||||||
hasUnread: false,
|
hasUnread: false,
|
||||||
hasStarred: false,
|
hasStarred: false,
|
||||||
hasAttachment: false,
|
hasAttachment: false,
|
||||||
|
hasAnswered: false,
|
||||||
|
hasForwarded: false,
|
||||||
emailCount: 1,
|
emailCount: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -169,6 +189,8 @@ describe('mergeThreadEmails', () => {
|
|||||||
hasUnread: false,
|
hasUnread: false,
|
||||||
hasStarred: false,
|
hasStarred: false,
|
||||||
hasAttachment: false,
|
hasAttachment: false,
|
||||||
|
hasAnswered: false,
|
||||||
|
hasForwarded: false,
|
||||||
emailCount: 2,
|
emailCount: 2,
|
||||||
};
|
};
|
||||||
const fetched = [
|
const fetched = [
|
||||||
@@ -189,6 +211,8 @@ describe('mergeThreadEmails', () => {
|
|||||||
hasUnread: false,
|
hasUnread: false,
|
||||||
hasStarred: false,
|
hasStarred: false,
|
||||||
hasAttachment: false,
|
hasAttachment: false,
|
||||||
|
hasAnswered: false,
|
||||||
|
hasForwarded: false,
|
||||||
emailCount: 1,
|
emailCount: 1,
|
||||||
};
|
};
|
||||||
const fetched = [
|
const fetched = [
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
if (email) email.keywords = { ...email.keywords, ...keywords };
|
if (email) email.keywords = { ...email.keywords, ...keywords };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setKeyword(emailId: string, keyword: string): Promise<void> {
|
||||||
|
const email = this.data.emails.find(e => e.id === emailId);
|
||||||
|
if (email) email.keywords[keyword] = true;
|
||||||
|
}
|
||||||
|
|
||||||
async migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number> {
|
async migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number> {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const email of this.data.emails) {
|
for (const email of this.data.emails) {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export interface IJMAPClient {
|
|||||||
batchMarkAsRead(emailIds: string[], read?: boolean): Promise<void>;
|
batchMarkAsRead(emailIds: string[], read?: boolean): Promise<void>;
|
||||||
toggleStar(emailId: string, starred: boolean): Promise<void>;
|
toggleStar(emailId: string, starred: boolean): Promise<void>;
|
||||||
updateEmailKeywords(emailId: string, keywords: Record<string, boolean>): Promise<void>;
|
updateEmailKeywords(emailId: string, keywords: Record<string, boolean>): 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): Promise<void>;
|
||||||
|
|||||||
@@ -763,6 +763,19 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setKeyword(emailId: string, keyword: string): Promise<void> {
|
||||||
|
await this.request([
|
||||||
|
["Email/set", {
|
||||||
|
accountId: this.accountId,
|
||||||
|
update: {
|
||||||
|
[emailId]: {
|
||||||
|
[`keywords/${keyword}`]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, "0"],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
async migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number> {
|
async migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number> {
|
||||||
// Query all email IDs that have the old keyword
|
// Query all email IDs that have the old keyword
|
||||||
const allIds: string[] = [];
|
const allIds: string[] = [];
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ export interface ThreadGroup {
|
|||||||
hasUnread: boolean; // Any unread emails in thread
|
hasUnread: boolean; // Any unread emails in thread
|
||||||
hasStarred: boolean; // Any starred emails in thread
|
hasStarred: boolean; // Any starred emails in thread
|
||||||
hasAttachment: boolean; // Any email has attachment
|
hasAttachment: boolean; // Any email has attachment
|
||||||
|
hasAnswered: boolean; // Any email has been replied to
|
||||||
|
hasForwarded: boolean; // Any email has been forwarded
|
||||||
emailCount: number; // Total emails in thread
|
emailCount: number; // Total emails in thread
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export function groupEmailsByThread(emails: Email[]): ThreadGroup[] {
|
|||||||
const hasUnread = sortedEmails.some(e => !e.keywords?.$seen);
|
const hasUnread = sortedEmails.some(e => !e.keywords?.$seen);
|
||||||
const hasStarred = sortedEmails.some(e => e.keywords?.$flagged);
|
const hasStarred = sortedEmails.some(e => e.keywords?.$flagged);
|
||||||
const hasAttachment = sortedEmails.some(e => e.hasAttachment);
|
const hasAttachment = sortedEmails.some(e => e.hasAttachment);
|
||||||
|
const hasAnswered = sortedEmails.some(e => e.keywords?.$answered);
|
||||||
|
const hasForwarded = sortedEmails.some(e => e.keywords?.$forwarded);
|
||||||
|
|
||||||
threadGroups.push({
|
threadGroups.push({
|
||||||
threadId,
|
threadId,
|
||||||
@@ -47,6 +49,8 @@ export function groupEmailsByThread(emails: Email[]): ThreadGroup[] {
|
|||||||
hasUnread,
|
hasUnread,
|
||||||
hasStarred,
|
hasStarred,
|
||||||
hasAttachment,
|
hasAttachment,
|
||||||
|
hasAnswered,
|
||||||
|
hasForwarded,
|
||||||
emailCount: sortedEmails.length,
|
emailCount: sortedEmails.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -123,6 +127,8 @@ export function mergeThreadEmails(
|
|||||||
const hasUnread = mergedEmails.some(e => !e.keywords?.$seen);
|
const hasUnread = mergedEmails.some(e => !e.keywords?.$seen);
|
||||||
const hasStarred = mergedEmails.some(e => e.keywords?.$flagged);
|
const hasStarred = mergedEmails.some(e => e.keywords?.$flagged);
|
||||||
const hasAttachment = mergedEmails.some(e => e.hasAttachment);
|
const hasAttachment = mergedEmails.some(e => e.hasAttachment);
|
||||||
|
const hasAnswered = mergedEmails.some(e => e.keywords?.$answered);
|
||||||
|
const hasForwarded = mergedEmails.some(e => e.keywords?.$forwarded);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threadId: existingGroup.threadId,
|
threadId: existingGroup.threadId,
|
||||||
@@ -132,6 +138,8 @@ export function mergeThreadEmails(
|
|||||||
hasUnread,
|
hasUnread,
|
||||||
hasStarred,
|
hasStarred,
|
||||||
hasAttachment,
|
hasAttachment,
|
||||||
|
hasAnswered,
|
||||||
|
hasForwarded,
|
||||||
emailCount: mergedEmails.length,
|
emailCount: mergedEmails.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -86,7 +86,10 @@
|
|||||||
"vitest": "^4.0.16"
|
"vitest": "^4.0.16"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"elliptic": "^6.6.1",
|
"elliptic": {
|
||||||
|
".": "^6.6.1",
|
||||||
|
"webcrypto-liner": "$elliptic"
|
||||||
|
},
|
||||||
"flatted": "^3.4.2",
|
"flatted": "^3.4.2",
|
||||||
"undici": "^7.24.0"
|
"undici": "^7.24.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user