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/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 69b64c12..d7ee5136 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": { @@ -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.", @@ -620,10 +622,19 @@ "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.", + "managePostsLoading": "# I'm checking what posts are left to remove.", + "checkBatchActionWallPosts": "# I'm looking for a batch of posts to **{action}**...", + "removeActionWallPosts": "# I'm **{action} {count} posts**....", + "actionDelete": "delete", + "actionDeletePresent": "deleting", + "actionUntag": "untag", + "actionUntagPresent": "untagging", + "actionHide": "hide", + "actionHidePresent": "hiding" }, "progress": { - "wallPostsDeleted": "Removed {count} wall posts." + "wallPostsProgress": "Deleted **{deleteCount}**, untagged **{untagCount}**, and hid **{hideCount}** 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 de8dc3cc..51fdc0f9 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 @@ -465,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. @@ -556,9 +684,9 @@ 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); + 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 4bb97989..7c4b577c 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, @@ -418,7 +430,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 +449,7 @@ async function selectDeletePostsOption( } } } - + console.log('Could not find delete posts option'); return false; })()`, @@ -545,7 +557,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"); @@ -559,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; @@ -640,62 +654,69 @@ 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"; + + // loop through different actions + for (const action of batchActions) { + batchAction = action; + 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) { + // Check for rate limits + await checkRateLimit(vm); - // 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); + if (checkedCount >= maxToCheck) { + vm.log( + "runJobDeleteWallPosts", + `Reached maximum of ${maxToCheck} items`, + ); + break; + } - if (checkedCount >= maxToCheck) { - vm.log( - "runJobDeleteWallPosts", - `Reached maximum of ${maxToCheck} items`, - ); - break; - } + await vm.waitForPause(); - 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 +726,81 @@ 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 go to next item + vm.log( + "runJobDeleteWallPosts", + `Item [${listIndex}][${itemIndex}] changes priority from "${batchAction}" to "${combinedPriority}", unchecking`, + ); + 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; + } + continue; } - 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 + vm.instructions = vm.t( + "viewModels.facebook.jobs.removeActionWallPosts", + { + action: vm.t(actionPresentKeys[batchAction]), + count: checkedCount, + }, + ); + 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; } @@ -897,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}`); @@ -921,6 +969,7 @@ export async function runJobDeleteWallPosts( // Reload the profile page to see any newly available posts vm.log("runJobDeleteWallPosts", "Reloading profile page for next batch"); + vm.instructions = vm.t("viewModels.facebook.jobs.managePostsLoading"); await vm.loadURL(FACEBOOK_PROFILE_URL); await vm.waitForLoadingToFinish(); } 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..415b540d 100644 --- a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue +++ b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue @@ -1,12 +1,25 @@