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:
@@ -116,37 +116,47 @@ describe('groupEmailsByThread', () => {
|
||||
});
|
||||
|
||||
describe('sortThreadGroups', () => {
|
||||
const makeGroup = (threadId: string, receivedAt: string, hasPinned = false): ThreadGroup => ({
|
||||
threadId,
|
||||
emails: [makeEmail({ receivedAt })],
|
||||
latestEmail: makeEmail({ receivedAt }),
|
||||
participantNames: ['A'],
|
||||
hasUnread: false,
|
||||
hasStarred: false,
|
||||
hasPinned,
|
||||
hasAttachment: false,
|
||||
hasAnswered: false,
|
||||
hasForwarded: false,
|
||||
emailCount: 1,
|
||||
});
|
||||
|
||||
it('sorts groups by latestEmail.receivedAt descending', () => {
|
||||
const groups: ThreadGroup[] = [
|
||||
{
|
||||
threadId: 'old',
|
||||
emails: [makeEmail({ receivedAt: '2024-01-01T00:00:00Z' })],
|
||||
latestEmail: makeEmail({ receivedAt: '2024-01-01T00:00:00Z' }),
|
||||
participantNames: ['A'],
|
||||
hasUnread: false,
|
||||
hasStarred: false,
|
||||
hasAttachment: false,
|
||||
hasAnswered: false,
|
||||
hasForwarded: false,
|
||||
emailCount: 1,
|
||||
},
|
||||
{
|
||||
threadId: 'new',
|
||||
emails: [makeEmail({ receivedAt: '2024-06-01T00:00:00Z' })],
|
||||
latestEmail: makeEmail({ receivedAt: '2024-06-01T00:00:00Z' }),
|
||||
participantNames: ['B'],
|
||||
hasUnread: false,
|
||||
hasStarred: false,
|
||||
hasAttachment: false,
|
||||
hasAnswered: false,
|
||||
hasForwarded: false,
|
||||
emailCount: 1,
|
||||
},
|
||||
const groups = [
|
||||
makeGroup('old', '2024-01-01T00:00:00Z'),
|
||||
makeGroup('new', '2024-06-01T00:00:00Z'),
|
||||
];
|
||||
const sorted = sortThreadGroups(groups);
|
||||
expect(sorted[0].threadId).toBe('new');
|
||||
expect(sorted[1].threadId).toBe('old');
|
||||
});
|
||||
|
||||
it('keeps pinned threads on top regardless of date', () => {
|
||||
const groups = [
|
||||
makeGroup('newest', '2024-06-01T00:00:00Z'),
|
||||
makeGroup('old-pinned', '2024-01-01T00:00:00Z', true),
|
||||
makeGroup('mid', '2024-03-01T00:00:00Z'),
|
||||
];
|
||||
const sorted = sortThreadGroups(groups);
|
||||
expect(sorted.map(g => g.threadId)).toEqual(['old-pinned', 'newest', 'mid']);
|
||||
});
|
||||
|
||||
it('detects hasPinned from the $pinned keyword', () => {
|
||||
const emails = [
|
||||
makeEmail({ id: 'e1', keywords: { $seen: true } }),
|
||||
makeEmail({ id: 'e2', keywords: { $seen: true, '$pinned': true } }),
|
||||
];
|
||||
expect(groupEmailsByThread(emails)[0].hasPinned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThreadParticipants', () => {
|
||||
@@ -188,6 +198,7 @@ describe('mergeThreadEmails', () => {
|
||||
participantNames: ['Alice'],
|
||||
hasUnread: false,
|
||||
hasStarred: false,
|
||||
hasPinned: false,
|
||||
hasAttachment: false,
|
||||
hasAnswered: false,
|
||||
hasForwarded: false,
|
||||
@@ -210,6 +221,7 @@ describe('mergeThreadEmails', () => {
|
||||
participantNames: ['Alice'],
|
||||
hasUnread: false,
|
||||
hasStarred: false,
|
||||
hasPinned: false,
|
||||
hasAttachment: false,
|
||||
hasAnswered: false,
|
||||
hasForwarded: false,
|
||||
|
||||
Reference in New Issue
Block a user