Feature: pin emails to the top of the folder list
Outlook-Web-style pinning: a context-menu Pin/Unpin action stores a $pinned keyword on the message (plain IMAP-compatible flag, survives other clients), and pinned mails stay at the top of the folder list regardless of age, marked with a pin icon. Ordering is done server-side via the hasKeyword sort comparator (RFC 8621), applied consistently to the folder fetch, pagination and the push-refresh so page windows stay stable. The client-side safety sort in getEmails mirrors it, and sortThreadGroups keeps threads containing a pinned mail on top so the client-side thread grouping does not undo the order. The new-mail notification in refreshCurrentMailbox now checks the first non-pinned entry: with pinned mails on top, the newest mail is no longer at index 0 and arrivals would never have notified. The toggle reuses the color-tag pathway (routed keyword write for unified views, in-place local patch), then refetches the first page so the mail floats or sinks immediately. Search and unified views keep their existing order. Pin/Unpin strings are added to all 21 locales.
This commit is contained in:
@@ -1652,6 +1652,45 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTogglePinned = async (emailToPin: Email) => {
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
const email = emails.find(e => e.id === emailToPin.id) ?? emailToPin;
|
||||
const isPinned = email.keywords?.['$pinned'] === true;
|
||||
// JMAP keywords are a set of present keys - drop the key to unpin
|
||||
// rather than writing a false value.
|
||||
const keywords = { ...email.keywords };
|
||||
if (isPinned) {
|
||||
delete keywords['$pinned'];
|
||||
} else {
|
||||
keywords['$pinned'] = true;
|
||||
}
|
||||
|
||||
// Same unified-view routing as color tags: write to the email's own
|
||||
// account via the login it is reachable through. (#281)
|
||||
const pinClientId = isUnifiedView ? email.sourceClientAccountId : undefined;
|
||||
const pinAccountId = isUnifiedView ? email.sourceAccountId : undefined;
|
||||
const pinClient = pinClientId
|
||||
? (useAuthStore.getState().getClientForAccount(pinClientId) ?? client)
|
||||
: client;
|
||||
|
||||
await pinClient.updateEmailKeywords(email.id, keywords, pinAccountId);
|
||||
|
||||
// Patch in place so the icon flips immediately, then refetch the first
|
||||
// page so the mail floats/sinks per the server's pinned-first sort.
|
||||
// Skip the refetch where that sort does not apply (unified views) or
|
||||
// where it would replace a tag-filtered list (refreshCurrentMailbox
|
||||
// fetches by folder only).
|
||||
setEmailKeywordsLocal(email.id, keywords);
|
||||
if (!isUnifiedView && !useEmailStore.getState().selectedKeyword) {
|
||||
void refreshCurrentMailbox(client);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle pin:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetColorTag = async (emailId: string, color: string | null) => {
|
||||
if (!client) return;
|
||||
|
||||
@@ -3042,6 +3081,9 @@ export default function Home() {
|
||||
await toggleStar(client, email.id);
|
||||
}
|
||||
}}
|
||||
onTogglePinned={async (email) => {
|
||||
await handleTogglePinned(email);
|
||||
}}
|
||||
onDelete={async (email) => {
|
||||
await handleDelete(email);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user