From 49c2216a5d2a9a1010cd30d2092ded43f7d80f35 Mon Sep 17 00:00:00 2001 From: Saptak S Date: Tue, 14 Apr 2026 20:04:09 +0530 Subject: [PATCH 1/8] Does batchActions in the priority order set by us Goes through batchAction based on the priority order set by us, that is first try to delete all items. If there are 0 checked item with batchAction set as delete, then try to untag, and then try to hide. These prevents anomalies where the first item might have just "hide" action since it will always try to delete all items first and only move to next priority action if there is 0 checked item. --- .../FacebookViewModel/jobs_delete.test.ts | 5 +- .../FacebookViewModel/jobs_delete.ts | 211 +++++++++--------- 2 files changed, 110 insertions(+), 106 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts index de8dc3cc..a62a64d0 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts @@ -188,6 +188,9 @@ describe("FacebookViewModel Delete Jobs", () => { // Simulate: button clicked, dialog appears, but no items to delete vi.mocked(mockWebview.executeJavaScript) + .mockResolvedValueOnce(true) // clickManagePostsButton + .mockResolvedValueOnce(true) // waitForManagePostsDialog (first check) + .mockResolvedValue([]) .mockResolvedValueOnce(true) // clickManagePostsButton .mockResolvedValueOnce(true) // waitForManagePostsDialog (first check) .mockResolvedValue([]); // getListsAndItems returns empty @@ -556,7 +559,7 @@ describe("FacebookViewModel Delete Jobs", () => { expect(vm.log).toHaveBeenCalledWith( "runJobDeleteWallPosts", - 'First item sets batch action to "untag", checked 1/10', + 'Item keeps batch action "untag", checked 1/10', ); expect(vm.progress.wallPostsDeleted).toBe(1); }); diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index 4bb97989..a6745059 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -418,7 +418,7 @@ async function selectDeletePostsOption( // Find all divs that might contain the delete posts option const divs = dialog.querySelectorAll('div[aria-disabled]'); - + for (const div of divs) { // Check if this div or its children contain text about deleting posts const text = div.textContent?.toLowerCase() || ''; @@ -437,7 +437,7 @@ async function selectDeletePostsOption( } } } - + console.log('Could not find delete posts option'); return false; })()`, @@ -640,62 +640,63 @@ export async function runJobDeleteWallPosts( ); let checkedCount = 0; - let batchAction: PostAction | null = null; + const batchActions: PostAction[] = ["delete", "untag", "hide"] // Check all actions in priority order + let batchAction: PostAction = "delete"; + + // infinite loop to loop through different actions + for (const action of batchActions) { + batchAction = action + // Loop through items, checking if any item match the current batchAction priority action. + // Stop when adding a new item would reduce the priority (e.g. from delete -> hide). + for (const { listIndex, itemIndex } of allItems) { + // Check for rate limits + await checkRateLimit(vm); + + if (checkedCount >= maxToCheck) { + vm.log( + "runJobDeleteWallPosts", + `Reached maximum of ${maxToCheck} items`, + ); + break; + } - // Loop through items, checking each one. Track the highest-priority action - // available for all checked items. Stop when adding a new item would reduce - // the priority (e.g. from delete -> hide). - for (const { listIndex, itemIndex } of allItems) { - // Check for rate limits - await checkRateLimit(vm); + await vm.waitForPause(); - if (checkedCount >= maxToCheck) { - vm.log( - "runJobDeleteWallPosts", - `Reached maximum of ${maxToCheck} items`, - ); - break; - } - - await vm.waitForPause(); + // Check this checkbox + const toggled = await toggleCheckbox(vm, listIndex, itemIndex, true); + if (!toggled) { + vm.log( + "runJobDeleteWallPosts", + `Failed to check item [${listIndex}][${itemIndex}]`, + ); + continue; + } - // Check this checkbox - const toggled = await toggleCheckbox(vm, listIndex, itemIndex, true); - if (!toggled) { - vm.log( - "runJobDeleteWallPosts", - `Failed to check item [${listIndex}][${itemIndex}]`, + const checkboxChecked = await waitForCheckboxState( + vm, + listIndex, + itemIndex, + true, ); - continue; - } + if (!checkboxChecked) { + vm.log( + "runJobDeleteWallPosts", + `Timed out waiting for item [${listIndex}][${itemIndex}] to become checked`, + ); + continue; + } - const checkboxChecked = await waitForCheckboxState( - vm, - listIndex, - itemIndex, - true, - ); - if (!checkboxChecked) { + // Read the combined action description (reflects all currently-checked items) + const actionDescription = await waitForActionDescriptionStable(vm); vm.log( "runJobDeleteWallPosts", - `Timed out waiting for item [${listIndex}][${itemIndex}] to become checked`, + `Action description: "${actionDescription}"`, ); - continue; - } - // Read the combined action description (reflects all currently-checked items) - const actionDescription = await waitForActionDescriptionStable(vm); - vm.log( - "runJobDeleteWallPosts", - `Action description: "${actionDescription}"`, - ); - - const combinedPriority = getHighestPriority( - parseActions(actionDescription), - ); + const combinedPriority = getHighestPriority( + parseActions(actionDescription), + ); - if (batchAction === null) { - // First item: establish the batch action if (combinedPriority === null) { // Unrecognized description, skip this item vm.log( @@ -705,74 +706,74 @@ export async function runJobDeleteWallPosts( await toggleCheckbox(vm, listIndex, itemIndex, false); await waitForCheckboxState(vm, listIndex, itemIndex, false); continue; - } - batchAction = combinedPriority; - checkedCount++; - vm.log( - "runJobDeleteWallPosts", - `First item sets batch action to "${batchAction}", checked ${checkedCount}/${maxToCheck}`, - ); - } else if (combinedPriority === batchAction) { - // Same priority: keep this item checked and continue - checkedCount++; - vm.log( - "runJobDeleteWallPosts", - `Item keeps batch action "${batchAction}", checked ${checkedCount}/${maxToCheck}`, - ); - } else { - // Adding this item changes the priority — uncheck it and stop - vm.log( - "runJobDeleteWallPosts", - `Item [${listIndex}][${itemIndex}] changes priority from "${batchAction}" to "${combinedPriority}", unchecking and stopping`, - ); - await toggleCheckbox(vm, listIndex, itemIndex, false); - const checkboxUnchecked = await waitForCheckboxState( - vm, - listIndex, - itemIndex, - false, - ); - if (!checkboxUnchecked) { + } else if (combinedPriority === batchAction) { + // Same priority: keep this item checked and continue + checkedCount++; vm.log( "runJobDeleteWallPosts", - `Timed out waiting for item [${listIndex}][${itemIndex}] to become unchecked`, + `Item keeps batch action "${batchAction}", checked ${checkedCount}/${maxToCheck}`, ); - } - - const batchActionRestored = await waitForBatchAction(vm, batchAction); - if (!batchActionRestored.success) { - await reportDeleteWallPostsError( + } else { + // Adding this item changes the priority — uncheck it and stop + vm.log( + "runJobDeleteWallPosts", + `Item [${listIndex}][${itemIndex}] changes priority from "${batchAction}" to "${combinedPriority}", unchecking and stopping`, + ); + await toggleCheckbox(vm, listIndex, itemIndex, false); + const checkboxUnchecked = await waitForCheckboxState( vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed, - { - batchNumber, - message: `Batch action did not return to "${batchAction}" after unchecking item [${listIndex}][${itemIndex}]`, - actionDescription: batchActionRestored.actionDescription, - }, + listIndex, + itemIndex, + false, ); - return; + if (!checkboxUnchecked) { + vm.log( + "runJobDeleteWallPosts", + `Timed out waiting for item [${listIndex}][${itemIndex}] to become unchecked`, + ); + } + + const batchActionRestored = await waitForBatchAction(vm, batchAction); + if (!batchActionRestored.success && checkedCount !== 0) { + await reportDeleteWallPostsError( + vm, + jobIndex, + AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed, + { + batchNumber, + message: `Batch action did not return to "${batchAction}" after unchecking item [${listIndex}][${itemIndex}]`, + actionDescription: batchActionRestored.actionDescription, + }, + ); + return; + } + break; } - break; } - } - - vm.log( - "runJobDeleteWallPosts", - `Selected ${checkedCount} items for action "${batchAction}"`, - ); - - // If nothing was checked, we're done - if (checkedCount === 0) { - vm.log("runJobDeleteWallPosts", "No actionable items found, finishing"); - break; - } - if (batchAction === null) { vm.log( "runJobDeleteWallPosts", - "Checked items were selected but no batch action was determined", + `Selected ${checkedCount} items for action "${batchAction}"`, ); + + if (checkedCount !== 0) { + // If actionable items found, no need to loop through other actions + break; + } + + // If nothing was checked, see if more items get selected by next priority action in the list + if (batchAction !== "hide") { + vm.log( + "runJobDeleteWallPosts", + `No actionable items found for action "${batchAction}", checking next priority action` + ) + } + } + + if (checkedCount === 0 && batchAction === "hide") { + // If the current action is hide and still checked item is 0, means all priority actions have + // been checked and nothing left to do. + vm.log("runJobDeleteWallPosts", "No actionable items found, finishing"); break; } From e65ec5ec0f8efe543de1434b2aafaea5352dbedc Mon Sep 17 00:00:00 2001 From: Saptak S Date: Wed, 15 Apr 2026 17:42:42 +0530 Subject: [PATCH 2/8] Adds tests to check if first element is hide and second is delete, it still deletes the second item --- .../FacebookViewModel/jobs_delete.test.ts | 125 ++++++++++++++++++ .../FacebookViewModel/jobs_delete.ts | 14 +- 2 files changed, 132 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts index a62a64d0..fb7a6858 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts @@ -468,6 +468,131 @@ describe("FacebookViewModel Delete Jobs", () => { expect(vm.progress.wallPostsDeleted).toBe(1); }); + it("deletes second item even if first item is hide and second item is delete", async () => { + // Items: item 0 supports hide, item 1 supports delete+hide only. + // Expected: check item 0 (priority=hide) -> uncheck, check item 1 -> priority=delete. + // Then proceed to delete item 1. On 2nd batch, clickManagePostsButton fails -> exit. + const vm = createMockFacebookViewModel(); + const mockWebview = vm.getWebview()!; + + let managePostsClicks = 0; + let isDialogOpen = false; + let isActionOptionsVisible = false; + const checkedItems = new Set(); + + vi.mocked(mockWebview.executeJavaScript).mockImplementation( + async (code: string) => { + if ( + code.includes( + `querySelectorAll('div[aria-label="Manage posts"][role="button"]')`, + ) + ) { + managePostsClicks++; + isDialogOpen = managePostsClicks === 1; + isActionOptionsVisible = false; + return managePostsClicks <= 2; + } + + if ( + code.includes( + `document.querySelector('div[aria-label="Manage posts"][role="dialog"]')`, + ) && + code.includes("return !!dialog;") + ) { + return isDialogOpen; + } + + if (code.includes("result.push({ listIndex, itemIndex });")) { + return managePostsClicks === 1 + ? [ + { listIndex: 0, itemIndex: 0 }, + { listIndex: 0, itemIndex: 1 }, + ] + : []; + } + + if (code.includes("const shouldCheck = ")) { + const listMatch = code.match(/const list = lists\[(\d+)\];/); + const itemMatch = code.match(/const item = items\[(\d+)\];/); + const shouldCheckMatch = code.match( + /const shouldCheck = (true|false);/, + ); + + if (!listMatch || !itemMatch || !shouldCheckMatch) { + return false; + } + + const key = `${listMatch[1]}-${itemMatch[1]}`; + const shouldCheck = shouldCheckMatch[1] === "true"; + + if (shouldCheck) { + checkedItems.add(key); + } else { + checkedItems.delete(key); + } + + return true; + } + + if (code.includes("checkbox instanceof HTMLInputElement")) { + const listMatch = code.match(/const list = lists\[(\d+)\];/); + const itemMatch = code.match(/const item = items\[(\d+)\];/); + + if (!listMatch || !itemMatch) { + return null; + } + + return checkedItems.has(`${listMatch[1]}-${itemMatch[1]}`); + } + + if (code.includes('text.startsWith("You can")')) { + if (checkedItems.has("0-0") && !checkedItems.has("0-1")) { + return "You can hide the posts selected."; + } + + if (!checkedItems.has("0-0") && checkedItems.has("0-1")) { + // Unchecked hide item but checked the deleteable item + return "You can hide or delete the posts selected."; + } + + return ""; + } + + if (code.includes(`aria-label="Next"`)) { + isActionOptionsVisible = true; + return true; + } + + if ( + code.includes("const hasActionOptions =") && + code.includes(`aria-label="Done"`) + ) { + return isActionOptionsVisible; + } + + if (code.includes("text.includes('delete posts')")) { + return checkedItems.size === 1 && checkedItems.has("0-1"); + } + + if (code.includes(`aria-label="Done"`)) { + isDialogOpen = false; + isActionOptionsVisible = false; + return true; + } + + return false; + }, + ); + + await DeleteJobs.runJobDeleteWallPosts(vm, 3); + + expect(vm.log).toHaveBeenCalledWith( + "runJobDeleteWallPosts", + expect.stringContaining('Selected 1 items for action "delete"'), + ); + expect(vm.progress.wallPostsDeleted).toBe(1); + }); + it("performs untag action when highest priority is untag", async () => { // Item supports untag+hide. Expected: batch action = untag. // On 2nd batch, clickManagePostsButton fails -> exit. diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index a6745059..cdc5dd86 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -640,12 +640,12 @@ export async function runJobDeleteWallPosts( ); let checkedCount = 0; - const batchActions: PostAction[] = ["delete", "untag", "hide"] // Check all actions in priority order + const batchActions: PostAction[] = ["delete", "untag", "hide"]; // Check all actions in priority order let batchAction: PostAction = "delete"; // infinite loop to loop through different actions for (const action of batchActions) { - batchAction = action + batchAction = action; // Loop through items, checking if any item match the current batchAction priority action. // Stop when adding a new item would reduce the priority (e.g. from delete -> hide). for (const { listIndex, itemIndex } of allItems) { @@ -714,10 +714,10 @@ export async function runJobDeleteWallPosts( `Item keeps batch action "${batchAction}", checked ${checkedCount}/${maxToCheck}`, ); } else { - // Adding this item changes the priority — uncheck it and stop + // Adding this item changes the priority — uncheck it and go to next item vm.log( "runJobDeleteWallPosts", - `Item [${listIndex}][${itemIndex}] changes priority from "${batchAction}" to "${combinedPriority}", unchecking and stopping`, + `Item [${listIndex}][${itemIndex}] changes priority from "${batchAction}" to "${combinedPriority}", unchecking`, ); await toggleCheckbox(vm, listIndex, itemIndex, false); const checkboxUnchecked = await waitForCheckboxState( @@ -747,7 +747,7 @@ export async function runJobDeleteWallPosts( ); return; } - break; + continue; } } @@ -765,8 +765,8 @@ export async function runJobDeleteWallPosts( if (batchAction !== "hide") { vm.log( "runJobDeleteWallPosts", - `No actionable items found for action "${batchAction}", checking next priority action` - ) + `No actionable items found for action "${batchAction}", checking next priority action`, + ); } } From 49b8db38570da8db683312c39d0c258294e53aab Mon Sep 17 00:00:00 2001 From: Saptak S Date: Fri, 17 Apr 2026 23:05:38 +0530 Subject: [PATCH 3/8] Makes bubble text more verbose --- src/renderer/src/i18n/locales/en.json | 4 +++- .../src/view_models/FacebookViewModel/jobs_delete.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 69b64c12..08b62c0b 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -620,7 +620,9 @@ "savingLanguage": "I'm checking your language settings.", "settingLanguageToEnglish": "I'm temporarily changing your language to English (US) for automation.", "restoringLanguage": "I'm restoring your original language setting.", - "deletingWallPosts": "# I'm removing all posts from your Facebook wall." + "removingWallPosts": "# I'm removing all posts from your Facebook wall.", + "checkBatchActionWallPosts": "# I'm looking for a batch of posts to {action}...", + "removeActionWallPosts": "# I'm going to {action} {count} posts...." }, "progress": { "wallPostsDeleted": "Removed {count} wall posts." diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index cdc5dd86..692db201 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -545,7 +545,7 @@ export async function runJobDeleteWallPosts( vm.showBrowser = true; vm.showAutomationNotice = true; - vm.instructions = vm.t("viewModels.facebook.jobs.deletingWallPosts"); + vm.instructions = vm.t("viewModels.facebook.jobs.removingWallPosts"); vm.log("runJobDeleteWallPosts", "Loading profile page"); @@ -646,6 +646,9 @@ export async function runJobDeleteWallPosts( // infinite loop to loop through different actions for (const action of batchActions) { batchAction = action; + vm.instructions = vm.t("viewModels.facebook.jobs.checkBatchActionWallPosts", { + action: batchAction, + }); // Loop through items, checking if any item match the current batchAction priority action. // Stop when adding a new item would reduce the priority (e.g. from delete -> hide). for (const { listIndex, itemIndex } of allItems) { @@ -758,6 +761,10 @@ export async function runJobDeleteWallPosts( if (checkedCount !== 0) { // If actionable items found, no need to loop through other actions + vm.instructions = vm.t("viewModels.facebook.jobs.removeActionWallPosts", { + action: batchAction, + count: checkedCount, + }); break; } From be7510addfabe7b7888b2312d7c0ac48b4c3cfe0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 17 Apr 2026 12:28:34 -0700 Subject: [PATCH 4/8] Localize actions (delete, untag, hide) and support present tense versions of actions --- package-lock.json | 3 +- src/renderer/src/i18n/locales/en.json | 8 ++++- .../FacebookViewModel/jobs_delete.ts | 32 +++++++++++++++---- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07e1c7df..b6a17f97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "hasInstallScript": true, "license": "proprietary", "workspaces": [ - "archive-static-sites/x-archive", - "docs" + "archive-static-sites/x-archive" ], "dependencies": { "@atproto/api": "^0.18.21", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 08b62c0b..0a69d96f 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -622,7 +622,13 @@ "restoringLanguage": "I'm restoring your original language setting.", "removingWallPosts": "# I'm removing all posts from your Facebook wall.", "checkBatchActionWallPosts": "# I'm looking for a batch of posts to {action}...", - "removeActionWallPosts": "# I'm going to {action} {count} posts...." + "removeActionWallPosts": "# I'm {action} {count} posts....", + "actionDelete": "delete", + "actionDeletePresent": "deleting", + "actionUntag": "untag", + "actionUntagPresent": "untagging", + "actionHide": "hide", + "actionHidePresent": "hiding" }, "progress": { "wallPostsDeleted": "Removed {count} wall posts." diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index 692db201..73be4b64 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -130,6 +130,18 @@ async function getActionDescription(vm: FacebookViewModel): Promise { type PostAction = "delete" | "untag" | "hide"; +const actionVerbKeys: Record = { + delete: "viewModels.facebook.jobs.actionDelete", + untag: "viewModels.facebook.jobs.actionUntag", + hide: "viewModels.facebook.jobs.actionHide", +}; + +const actionPresentKeys: Record = { + delete: "viewModels.facebook.jobs.actionDeletePresent", + untag: "viewModels.facebook.jobs.actionUntagPresent", + hide: "viewModels.facebook.jobs.actionHidePresent", +}; + async function getCheckboxState( vm: FacebookViewModel, listIndex: number, @@ -646,9 +658,12 @@ export async function runJobDeleteWallPosts( // infinite loop to loop through different actions for (const action of batchActions) { batchAction = action; - vm.instructions = vm.t("viewModels.facebook.jobs.checkBatchActionWallPosts", { - action: batchAction, - }); + vm.instructions = vm.t( + "viewModels.facebook.jobs.checkBatchActionWallPosts", + { + action: vm.t(actionVerbKeys[batchAction]), + }, + ); // Loop through items, checking if any item match the current batchAction priority action. // Stop when adding a new item would reduce the priority (e.g. from delete -> hide). for (const { listIndex, itemIndex } of allItems) { @@ -761,10 +776,13 @@ export async function runJobDeleteWallPosts( if (checkedCount !== 0) { // If actionable items found, no need to loop through other actions - vm.instructions = vm.t("viewModels.facebook.jobs.removeActionWallPosts", { - action: batchAction, - count: checkedCount, - }); + vm.instructions = vm.t( + "viewModels.facebook.jobs.removeActionWallPosts", + { + action: vm.t(actionPresentKeys[batchAction]), + count: checkedCount, + }, + ); break; } From 607e4cbd6b401088c82c90e27f6e5e28ab25d3a0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 17 Apr 2026 15:17:08 -0700 Subject: [PATCH 5/8] Track deleting, untagging, and hiding separately --- .../controller/stats/getProgressInfo.ts | 17 +++++++++ .../facebook_account_controller.ts | 14 ++++++++ src/account_facebook/ipc.ts | 24 +++++++++++++ src/cyd-api-client.ts | 2 ++ src/preload.ts | 20 +++++++++++ src/renderer/src/i18n/locales/en.json | 14 +++++--- src/renderer/src/test_util.ts | 4 +++ src/renderer/src/util_facebook.test.ts | 14 ++++++++ src/renderer/src/util_facebook.ts | 2 ++ .../FacebookViewModel/jobs_delete.test.ts | 2 +- .../FacebookViewModel/jobs_delete.ts | 36 +++++++++++++++---- .../view_models/FacebookViewModel/types.ts | 4 +++ .../FacebookViewModel/view_model.test.ts | 2 ++ .../FacebookProgressComponent.test.ts | 2 ++ .../components/FacebookProgressComponent.vue | 22 +++++++++--- .../wizard/FacebookFinishedPage.test.ts | 36 ++++++++++++++++++- .../facebook/wizard/FacebookFinishedPage.vue | 18 ++++++++-- src/shared_types/account.ts | 4 +++ src/shared_types/facebook.ts | 4 +++ 19 files changed, 221 insertions(+), 20 deletions(-) diff --git a/src/account_facebook/controller/stats/getProgressInfo.ts b/src/account_facebook/controller/stats/getProgressInfo.ts index 72866662..173b45c6 100644 --- a/src/account_facebook/controller/stats/getProgressInfo.ts +++ b/src/account_facebook/controller/stats/getProgressInfo.ts @@ -19,8 +19,25 @@ export async function getProgressInfo( totalWallPostsDeleted = parseInt(totalWallPostsDeletedConfig); } + const totalWallPostsUntaggedConfig: string | null = + await controller.getConfig("totalWallPostsUntagged"); + let totalWallPostsUntagged: number = 0; + if (totalWallPostsUntaggedConfig) { + totalWallPostsUntagged = parseInt(totalWallPostsUntaggedConfig); + } + + const totalWallPostsHiddenConfig: string | null = await controller.getConfig( + "totalWallPostsHidden", + ); + let totalWallPostsHidden: number = 0; + if (totalWallPostsHiddenConfig) { + totalWallPostsHidden = parseInt(totalWallPostsHiddenConfig); + } + const progressInfo = emptyFacebookProgressInfo(); progressInfo.accountUUID = controller.accountUUID; progressInfo.totalWallPostsDeleted = totalWallPostsDeleted; + progressInfo.totalWallPostsUntagged = totalWallPostsUntagged; + progressInfo.totalWallPostsHidden = totalWallPostsHidden; return progressInfo; } diff --git a/src/account_facebook/facebook_account_controller.ts b/src/account_facebook/facebook_account_controller.ts index 1a644fcc..1e0951be 100644 --- a/src/account_facebook/facebook_account_controller.ts +++ b/src/account_facebook/facebook_account_controller.ts @@ -222,4 +222,18 @@ export class FacebookAccountController extends BaseAccountController { + const currentValue = await this.getConfig("totalWallPostsUntagged"); + const newValue = (currentValue ? parseInt(currentValue) : 0) + count; + await this.setConfig("totalWallPostsUntagged", newValue.toString()); + } + + // Increment the total wall posts hidden counter + async incrementTotalWallPostsHidden(count: number): Promise { + const currentValue = await this.getConfig("totalWallPostsHidden"); + const newValue = (currentValue ? parseInt(currentValue) : 0) + count; + await this.setConfig("totalWallPostsHidden", newValue.toString()); + } } diff --git a/src/account_facebook/ipc.ts b/src/account_facebook/ipc.ts index be1083ff..b9c23aef 100644 --- a/src/account_facebook/ipc.ts +++ b/src/account_facebook/ipc.ts @@ -147,6 +147,30 @@ export const defineIPCFacebook = () => { }, ); + ipcMain.handle( + "Facebook:incrementTotalWallPostsUntagged", + async (_, accountID: number, count: number): Promise => { + try { + const controller = getFacebookAccountController(accountID); + return await controller.incrementTotalWallPostsUntagged(count); + } catch (error) { + throw new Error(packageExceptionForReport(error as Error)); + } + }, + ); + + ipcMain.handle( + "Facebook:incrementTotalWallPostsHidden", + async (_, accountID: number, count: number): Promise => { + try { + const controller = getFacebookAccountController(accountID); + return await controller.incrementTotalWallPostsHidden(count); + } catch (error) { + throw new Error(packageExceptionForReport(error as Error)); + } + }, + ); + ipcMain.handle("Facebook:isRateLimited", async (_, accountID: number) => { try { const controller = getFacebookAccountController(accountID); diff --git a/src/cyd-api-client.ts b/src/cyd-api-client.ts index 71af3a39..402402bf 100644 --- a/src/cyd-api-client.ts +++ b/src/cyd-api-client.ts @@ -73,6 +73,8 @@ export type PostXProgressAPIRequest = { export type PostFacebookProgressAPIRequest = { account_uuid: string; total_wall_posts_deleted: number; + total_wall_posts_untagged: number; + total_wall_posts_hidden: number; }; // API models for GET /user/premium diff --git a/src/preload.ts b/src/preload.ts index 0018f60c..dbb31ab1 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -537,6 +537,26 @@ const electronAPI = { count, ); }, + incrementTotalWallPostsUntagged: ( + accountID: number, + count: number, + ): Promise => { + return ipcRenderer.invoke( + "Facebook:incrementTotalWallPostsUntagged", + accountID, + count, + ); + }, + incrementTotalWallPostsHidden: ( + accountID: number, + count: number, + ): Promise => { + return ipcRenderer.invoke( + "Facebook:incrementTotalWallPostsHidden", + accountID, + count, + ); + }, isRateLimited: (accountID: number): Promise => { return ipcRenderer.invoke("Facebook:isRateLimited", accountID); }, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 0a69d96f..40a23787 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -407,12 +407,12 @@ "getArchiveTitle": "Get Archive from Meta", "getArchiveDescription": "If you want, export your Facebook archive BEFORE you delete all your posts.", "deleteWallTitle": "Delete My Wall", - "deleteWallDescription": "Delete all posts from your Facebook wall." + "deleteWallDescription": "Remove all posts from your Facebook wall." }, "deleteOptions": { "title": "Delete My Wall", - "description": "Select the data you want to delete from your Facebook wall.", - "deleteWallPosts": "Delete all posts from my wall" + "description": "Select the data you want to remove from your Facebook wall.", + "deleteWallPosts": "Remove all posts from my wall" }, "review": { "deleteWallPosts": "All posts from your Facebook wall", @@ -421,7 +421,9 @@ }, "finished": { "title": "Jobs Completed", - "wallPosts": "wall posts removed (deleted, untagged, or hidden)" + "wallPostsDeleted": "wall posts deleted", + "wallPostsUntagged": "wall posts untagged", + "wallPostsHidden": "wall posts hidden" }, "premium": { "readyToDelete": "You're all set! Let's continue to configure what you want to delete.", @@ -631,7 +633,9 @@ "actionHidePresent": "hiding" }, "progress": { - "wallPostsDeleted": "Removed {count} wall posts." + "wallPostsDeleted": "Deleted {count} wall posts.", + "wallPostsUntagged": "Untagged {count} wall posts.", + "wallPostsHidden": "Hidden {count} wall posts." } } } diff --git a/src/renderer/src/test_util.ts b/src/renderer/src/test_util.ts index 38c697cb..1223de92 100644 --- a/src/renderer/src/test_util.ts +++ b/src/renderer/src/test_util.ts @@ -308,12 +308,16 @@ export function mockElectronAPI() { getProgressInfo: vi.fn().mockResolvedValue({ accountUUID: "test-uuid-123", totalWallPostsDeleted: 0, + totalWallPostsUntagged: 0, + totalWallPostsHidden: 0, }), getConfig: vi.fn().mockResolvedValue(null), setConfig: vi.fn().mockResolvedValue(undefined), deleteConfig: vi.fn().mockResolvedValue(undefined), deleteConfigLike: vi.fn().mockResolvedValue(undefined), incrementTotalWallPostsDeleted: vi.fn().mockResolvedValue(undefined), + incrementTotalWallPostsUntagged: vi.fn().mockResolvedValue(undefined), + incrementTotalWallPostsHidden: vi.fn().mockResolvedValue(undefined), isRateLimited: vi .fn() .mockResolvedValue({ isRateLimited: false, rateLimitReset: 0 }), diff --git a/src/renderer/src/util_facebook.test.ts b/src/renderer/src/util_facebook.test.ts index 26f9561f..e94082fb 100644 --- a/src/renderer/src/util_facebook.test.ts +++ b/src/renderer/src/util_facebook.test.ts @@ -60,6 +60,8 @@ describe("util_facebook", () => { const mockProgressInfo = { accountUUID: "test-uuid-123", totalWallPostsDeleted: 42, + totalWallPostsUntagged: 0, + totalWallPostsHidden: 0, }; mockFacebookGetProgressInfo.mockResolvedValue(mockProgressInfo); @@ -79,6 +81,8 @@ describe("util_facebook", () => { { account_uuid: "test-uuid-123", total_wall_posts_deleted: 42, + total_wall_posts_untagged: 0, + total_wall_posts_hidden: 0, }, true, ); @@ -93,6 +97,8 @@ describe("util_facebook", () => { const mockProgressInfo = { accountUUID: "test-uuid-456", totalWallPostsDeleted: 100, + totalWallPostsUntagged: 0, + totalWallPostsHidden: 0, }; mockFacebookGetProgressInfo.mockResolvedValue(mockProgressInfo); @@ -111,6 +117,8 @@ describe("util_facebook", () => { { account_uuid: "test-uuid-456", total_wall_posts_deleted: 100, + total_wall_posts_untagged: 0, + total_wall_posts_hidden: 0, }, false, ); @@ -125,6 +133,8 @@ describe("util_facebook", () => { const mockProgressInfo = { accountUUID: "test-uuid-789", totalWallPostsDeleted: 0, + totalWallPostsUntagged: 0, + totalWallPostsHidden: 0, }; mockFacebookGetProgressInfo.mockResolvedValue(mockProgressInfo); @@ -134,6 +144,8 @@ describe("util_facebook", () => { { account_uuid: "test-uuid-789", total_wall_posts_deleted: 0, + total_wall_posts_untagged: 0, + total_wall_posts_hidden: 0, }, false, ); @@ -154,6 +166,8 @@ describe("util_facebook", () => { const mockProgressInfo = { accountUUID: "test-uuid", totalWallPostsDeleted: 10, + totalWallPostsUntagged: 0, + totalWallPostsHidden: 0, }; mockFacebookGetProgressInfo.mockResolvedValue(mockProgressInfo); diff --git a/src/renderer/src/util_facebook.ts b/src/renderer/src/util_facebook.ts index f768dc73..e747e0f6 100644 --- a/src/renderer/src/util_facebook.ts +++ b/src/renderer/src/util_facebook.ts @@ -13,6 +13,8 @@ export async function facebookPostProgress( { account_uuid: progressInfo.accountUUID, total_wall_posts_deleted: progressInfo.totalWallPostsDeleted, + total_wall_posts_untagged: progressInfo.totalWallPostsUntagged, + total_wall_posts_hidden: progressInfo.totalWallPostsHidden, }, deviceInfo?.valid ? true : false, ); diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts index fb7a6858..51fdc0f9 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.test.ts @@ -686,7 +686,7 @@ describe("FacebookViewModel Delete Jobs", () => { "runJobDeleteWallPosts", 'Item keeps batch action "untag", checked 1/10', ); - expect(vm.progress.wallPostsDeleted).toBe(1); + expect(vm.progress.wallPostsUntagged).toBe(1); }); it("unchecks the last item before clicking Next when delete is no longer allowed", async () => { diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index 73be4b64..11a3622c 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -571,6 +571,8 @@ export async function runJobDeleteWallPosts( // Keep deleting posts until there are no more to delete let totalDeleted = 0; + let totalUntagged = 0; + let totalHidden = 0; let batchNumber = 0; const maxToCheck = 10; @@ -923,18 +925,38 @@ export async function runJobDeleteWallPosts( } // Update progress - totalDeleted += checkedCount; - vm.progress.wallPostsDeleted = totalDeleted; + if (batchAction === "delete") { + totalDeleted += checkedCount; + vm.progress.wallPostsDeleted = totalDeleted; + } else if (batchAction === "untag") { + totalUntagged += checkedCount; + vm.progress.wallPostsUntagged = totalUntagged; + } else { + totalHidden += checkedCount; + vm.progress.wallPostsHidden = totalHidden; + } vm.log( "runJobDeleteWallPosts", - `Batch ${batchNumber} complete: deleted ${checkedCount} posts, total: ${totalDeleted}`, + `Batch ${batchNumber} complete: ${batchAction} ${checkedCount} posts (deleted: ${totalDeleted}, untagged: ${totalUntagged}, hidden: ${totalHidden})`, ); // Update the persistent counter in the database - await window.electron.Facebook.incrementTotalWallPostsDeleted( - vm.account.id, - checkedCount, - ); + if (batchAction === "delete") { + await window.electron.Facebook.incrementTotalWallPostsDeleted( + vm.account.id, + checkedCount, + ); + } else if (batchAction === "untag") { + await window.electron.Facebook.incrementTotalWallPostsUntagged( + vm.account.id, + checkedCount, + ); + } else { + await window.electron.Facebook.incrementTotalWallPostsHidden( + vm.account.id, + checkedCount, + ); + } // Submit progress to the API vm.emitter?.emit(`facebook-submit-progress-${vm.account.id}`); diff --git a/src/renderer/src/view_models/FacebookViewModel/types.ts b/src/renderer/src/view_models/FacebookViewModel/types.ts index 91594d5c..8edae668 100644 --- a/src/renderer/src/view_models/FacebookViewModel/types.ts +++ b/src/renderer/src/view_models/FacebookViewModel/types.ts @@ -35,6 +35,8 @@ export type FacebookJob = { export type FacebookProgress = { currentJob: string; wallPostsDeleted: number; + wallPostsUntagged: number; + wallPostsHidden: number; isDeleteWallPostsFinished: boolean; }; @@ -42,6 +44,8 @@ export function emptyFacebookProgress(): FacebookProgress { return { currentJob: "", wallPostsDeleted: 0, + wallPostsUntagged: 0, + wallPostsHidden: 0, isDeleteWallPostsFinished: false, }; } diff --git a/src/renderer/src/view_models/FacebookViewModel/view_model.test.ts b/src/renderer/src/view_models/FacebookViewModel/view_model.test.ts index 3c233afe..5c790caa 100644 --- a/src/renderer/src/view_models/FacebookViewModel/view_model.test.ts +++ b/src/renderer/src/view_models/FacebookViewModel/view_model.test.ts @@ -463,6 +463,8 @@ describe("FacebookViewModel", () => { expect(progress.currentJob).toBe(""); expect(progress.wallPostsDeleted).toBe(0); + expect(progress.wallPostsUntagged).toBe(0); + expect(progress.wallPostsHidden).toBe(0); expect(progress.isDeleteWallPostsFinished).toBe(false); }); diff --git a/src/renderer/src/views/facebook/components/FacebookProgressComponent.test.ts b/src/renderer/src/views/facebook/components/FacebookProgressComponent.test.ts index 50caccc4..7aa090f4 100644 --- a/src/renderer/src/views/facebook/components/FacebookProgressComponent.test.ts +++ b/src/renderer/src/views/facebook/components/FacebookProgressComponent.test.ts @@ -21,6 +21,8 @@ function createMockProgress( return { currentJob: "", wallPostsDeleted: 0, + wallPostsUntagged: 0, + wallPostsHidden: 0, isDeleteWallPostsFinished: false, ...overrides, }; diff --git a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue index 19b90c42..70df92e3 100644 --- a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue +++ b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue @@ -14,15 +14,29 @@ defineProps<{
diff --git a/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.test.ts b/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.test.ts index d4ca4345..2e10daad 100644 --- a/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.test.ts +++ b/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.test.ts @@ -105,7 +105,41 @@ describe("FacebookFinishedPage", () => { }); expect(wrapper.text()).toContain("42"); - expect(wrapper.text()).toContain("wall posts"); + expect(wrapper.text()).toContain("wall posts deleted"); + }); + + it("shows wall posts untagged count", () => { + const vm = createMockFacebookViewModel(); + vm.progress.wallPostsUntagged = 15; + + const wrapper = mount(FacebookFinishedPage, { + global: { + plugins: [i18n], + }, + props: { + model: vm, + }, + }); + + expect(wrapper.text()).toContain("15"); + expect(wrapper.text()).toContain("wall posts untagged"); + }); + + it("shows wall posts hidden count", () => { + const vm = createMockFacebookViewModel(); + vm.progress.wallPostsHidden = 8; + + const wrapper = mount(FacebookFinishedPage, { + global: { + plugins: [i18n], + }, + props: { + model: vm, + }, + }); + + expect(wrapper.text()).toContain("8"); + expect(wrapper.text()).toContain("wall posts hidden"); }); it("formats large numbers with locale string", () => { diff --git a/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.vue b/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.vue index 5add8fbe..f67129e3 100644 --- a/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.vue +++ b/src/renderer/src/views/facebook/wizard/FacebookFinishedPage.vue @@ -49,12 +49,26 @@ const backToDashboard = () => {

{{ t("finished.youJustDeleted") }}

    -
  • +
  • {{ model.progress.wallPostsDeleted.toLocaleString() }} - {{ t("facebook.finished.wallPosts") }} + {{ t("facebook.finished.wallPostsDeleted") }} +
  • +
  • + + {{ + model.progress.wallPostsUntagged.toLocaleString() + }} + {{ t("facebook.finished.wallPostsUntagged") }} +
  • +
  • + + {{ + model.progress.wallPostsHidden.toLocaleString() + }} + {{ t("facebook.finished.wallPostsHidden") }}
diff --git a/src/shared_types/account.ts b/src/shared_types/account.ts index f3c55c16..3075eee2 100644 --- a/src/shared_types/account.ts +++ b/src/shared_types/account.ts @@ -102,11 +102,15 @@ export type FacebookAccount = { export type FacebookProgressInfo = { accountUUID: string; totalWallPostsDeleted: number; + totalWallPostsUntagged: number; + totalWallPostsHidden: number; }; export function emptyFacebookProgressInfo(): FacebookProgressInfo { return { accountUUID: "", totalWallPostsDeleted: 0, + totalWallPostsUntagged: 0, + totalWallPostsHidden: 0, }; } diff --git a/src/shared_types/facebook.ts b/src/shared_types/facebook.ts index 4d7d3119..345c418c 100644 --- a/src/shared_types/facebook.ts +++ b/src/shared_types/facebook.ts @@ -10,11 +10,15 @@ export type FacebookJob = PlatformJob & { export type FacebookProgress = { wallPostsDeleted: number; + wallPostsUntagged: number; + wallPostsHidden: number; }; export function emptyFacebookProgress(): FacebookProgress { return { wallPostsDeleted: 0, + wallPostsUntagged: 0, + wallPostsHidden: 0, }; } From 1aa6e5818e7e9ac4997b895ff3b38458fee6d27a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 17 Apr 2026 15:48:59 -0700 Subject: [PATCH 6/8] Better FB progress --- src/renderer/src/i18n/locales/en.json | 10 ++--- .../components/FacebookProgressComponent.vue | 38 ++++++++----------- .../facebook/wizard/FacebookFinishedPage.vue | 27 ++++++------- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 40a23787..9830225b 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -44,7 +44,7 @@ "tombstoneLockAccount": "Locking account", "savePosts": "Saving posts", "savePostsHTML": "Saving posts HTML", - "deleteWallPosts": "Deleting wall posts", + "deleteWallPosts": "Removing wall posts", "restoreUserLang": "Restoring language" }, "progress": { @@ -623,8 +623,8 @@ "settingLanguageToEnglish": "I'm temporarily changing your language to English (US) for automation.", "restoringLanguage": "I'm restoring your original language setting.", "removingWallPosts": "# I'm removing all posts from your Facebook wall.", - "checkBatchActionWallPosts": "# I'm looking for a batch of posts to {action}...", - "removeActionWallPosts": "# I'm {action} {count} posts....", + "checkBatchActionWallPosts": "# I'm looking for a batch of posts to **{action}**...", + "removeActionWallPosts": "# I'm **{action} {count} posts**....", "actionDelete": "delete", "actionDeletePresent": "deleting", "actionUntag": "untag", @@ -633,9 +633,7 @@ "actionHidePresent": "hiding" }, "progress": { - "wallPostsDeleted": "Deleted {count} wall posts.", - "wallPostsUntagged": "Untagged {count} wall posts.", - "wallPostsHidden": "Hidden {count} wall posts." + "wallPostsProgress": "Deleted **{deleteCount}**, untagged **{untagCount}**, and hid **{hideCount}** wall posts." } } } diff --git a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue index 70df92e3..647ec3f2 100644 --- a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue +++ b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue @@ -1,12 +1,25 @@