From 0f948f0930fbcd5ebd3f1e0f984c5a4c60b6a56a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:00:23 +0200 Subject: [PATCH 1/8] Optimistically remove threads from list view when deletion/move tasks are queued (#2) * feat: optimistically remove threads from list view when deletion/move tasks are queued Co-authored-by: csfercoci <3026315+csfercoci@users.noreply.github.com> * Merge branch 'master' into copilot/fix-message-deletion-ui --- .../models/query-subscription-pool-spec.ts | 64 +++++++++++++++++++ app/spec/models/query-subscription-spec.ts | 45 ++++++++++++- .../flux/models/query-subscription-pool.ts | 53 +++++++++++++++ app/src/flux/models/query-subscription.ts | 27 ++++++-- 4 files changed, 181 insertions(+), 8 deletions(-) diff --git a/app/spec/models/query-subscription-pool-spec.ts b/app/spec/models/query-subscription-pool-spec.ts index f4561ed2df..c1a61342e9 100644 --- a/app/spec/models/query-subscription-pool-spec.ts +++ b/app/spec/models/query-subscription-pool-spec.ts @@ -1,6 +1,10 @@ import QuerySubscriptionPool from '../../src/flux/models/query-subscription-pool'; import DatabaseStore from '../../src/flux/stores/database-store'; import { Label } from '../../src/flux/models/label'; +import { Thread } from '../../src/flux/models/thread'; +import { Folder } from '../../src/flux/models/folder'; +import { ChangeFolderTask } from '../../src/flux/tasks/change-folder-task'; +import { ChangeLabelsTask } from '../../src/flux/tasks/change-labels-task'; describe('QuerySubscriptionPool', function QuerySubscriptionPoolSpecs() { beforeEach(() => { @@ -57,4 +61,64 @@ describe('QuerySubscriptionPool', function QuerySubscriptionPoolSpecs() { }); }); }); + + describe('_threadIdsForRemovalTask', () => { + it('should return threadIds for a ChangeFolderTask', () => { + const threads = [ + new Thread({ id: 't1', accountId: 'a1', folders: [new Folder({ id: 'f1' })] }), + ]; + const task = new ChangeFolderTask({ + threads, + folder: new Folder({ id: 'trash-folder', role: 'trash', accountId: 'a1' }), + }); + const result = QuerySubscriptionPool._threadIdsForRemovalTask(task); + expect(result).toEqual(['t1']); + }); + + it('should return threadIds for a ChangeLabelsTask with only removals', () => { + const task = new ChangeLabelsTask({ + threads: [new Thread({ id: 't2', accountId: 'a1' })], + labelsToRemove: [new Label({ id: 'inbox', role: 'inbox', accountId: 'a1' })], + labelsToAdd: [], + }); + const result = QuerySubscriptionPool._threadIdsForRemovalTask(task); + expect(result).toEqual(['t2']); + }); + + it('should return null for a ChangeLabelsTask with additions', () => { + const task = new ChangeLabelsTask({ + threads: [new Thread({ id: 't3', accountId: 'a1' })], + labelsToRemove: [new Label({ id: 'inbox', role: 'inbox', accountId: 'a1' })], + labelsToAdd: [new Label({ id: 'archive', role: 'all', accountId: 'a1' })], + }); + const result = QuerySubscriptionPool._threadIdsForRemovalTask(task); + expect(result).toBeNull(); + }); + }); + + describe('_optimisticallyRemoveThreads', () => { + it('should call optimisticallyRemoveItemsById on Thread subscriptions', () => { + const threadQuery = DatabaseStore.findAll(Thread); + const threadKey = threadQuery.sql(); + const callback = jasmine.createSpy('callback'); + QuerySubscriptionPool.add(threadQuery, callback); + const subscription = QuerySubscriptionPool._subscriptions[threadKey]; + spyOn(subscription, 'optimisticallyRemoveItemsById'); + + QuerySubscriptionPool._optimisticallyRemoveThreads(['t1', 't2']); + expect(subscription.optimisticallyRemoveItemsById).toHaveBeenCalledWith(['t1', 't2']); + }); + + it('should not call optimisticallyRemoveItemsById on non-Thread subscriptions', () => { + const labelQuery = DatabaseStore.findAll