diff --git a/locales/rn/en.json b/locales/rn/en.json index ff5d09ef..dff3fc6b 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -646,6 +646,10 @@ "choose_calendars_title": "Calendars to sync", "collections_count": "{selected} of {total} sync", "continue": "Continue", + "deletions_delete": "Delete on server", + "deletions_message": "Many items were deleted on this device at once, so they were not deleted on the server. Delete them there as well, or bring them back on this device.", + "deletions_restore": "Bring them back", + "deletions_title": "Delete them on the server too?", "denied_calendar": "Without access to calendars, Bulwark cannot put your calendars on this device.", "denied_contacts": "Without access to contacts, Bulwark cannot put your address books on this device.", "denied_title": "Sync stays off", diff --git a/src/components/settings/device-sync/DeviceSyncAccountRow.tsx b/src/components/settings/device-sync/DeviceSyncAccountRow.tsx index dc3d68c3..a2b35c30 100644 --- a/src/components/settings/device-sync/DeviceSyncAccountRow.tsx +++ b/src/components/settings/device-sync/DeviceSyncAccountRow.tsx @@ -29,6 +29,7 @@ import { disableDeviceSync, enableDeviceSync, openAndroidAccountSettings, + resolveDeletions, resumeDeviceSync, syncNow, } from '../../../device-sync/app/lifecycle'; @@ -224,10 +225,30 @@ export function DeviceSyncAccountRow({ account, authority }: Props) { void refresh(); }; + const reviewDeletions = () => { + const resolve = (choice: 'delete' | 'restore') => { + void resolveDeletions(account.id, authority, choice) + .catch((err: unknown) => alertFailed(err instanceof Error ? err.message : String(err))) + .finally(() => { void refresh(); }); + }; + Alert.alert( + t('settings.device_sync.deletions_title', 'Delete them on the server too?'), + t( + 'settings.device_sync.deletions_message', + 'Many items were deleted on this device at once, so they were not deleted on the server. Delete them there as well, or bring them back on this device.', + ), + [ + { text: t('common.cancel', 'Cancel'), style: 'cancel' }, + { text: t('settings.device_sync.deletions_restore', 'Bring them back'), onPress: () => resolve('restore') }, + { text: t('settings.device_sync.deletions_delete', 'Delete on server'), style: 'destructive', onPress: () => resolve('delete') }, + ], + ); + }; + const runAction = (action: StatusAction) => { if (action === 'signIn') navigation.navigate('AddAccount'); else if (action === 'grantAccess') void grantAccess(); - else void openAndroidAccountSettings(account.id); + else reviewDeletions(); }; const closeChooser = (changed: boolean) => { diff --git a/src/device-sync/__tests__/app/lifecycle.test.ts b/src/device-sync/__tests__/app/lifecycle.test.ts index 019dd5c9..c61d4a85 100644 --- a/src/device-sync/__tests__/app/lifecycle.test.ts +++ b/src/device-sync/__tests__/app/lifecycle.test.ts @@ -108,6 +108,7 @@ import { reconcileDeviceAccounts, releaseDeviceSyncBeforeSignOut, removeSuspendedAccount, + resolveDeletions, resumeDeviceSync, TEARDOWN_TIMEOUT_MS, } from '../../app/lifecycle'; @@ -483,6 +484,19 @@ describe('sign-in', () => { }); describe('settings actions', () => { + it('deletes held-back deletions on the server, or brings the items back, with a sync', async () => { + await enableDeviceSync(ALICE, CONTACTS_AUTHORITY); + h.state.log = []; + expect(await resolveDeletions(ALICE, CONTACTS_AUTHORITY, 'delete')).toBe(true); + expect(await resolveDeletions(ALICE, CONTACTS_AUTHORITY, 'restore')).toBe(true); + expect(h.state.log).toEqual([ + `sync alice ${CONTACTS_AUTHORITY} {"manual":true,"overrideTooManyDeletions":true}`, + `sync alice ${CONTACTS_AUTHORITY} {"manual":true,"discardLocalDeletions":true}`, + ]); + // Without an Android account there is nothing to resolve. + expect(await resolveDeletions(BOB, CONTACTS_AUTHORITY, 'delete')).toBe(false); + }); + it('applies a new interval to Android while the authority syncs', async () => { await enableDeviceSync(ALICE, CONTACTS_AUTHORITY); h.state.log = []; diff --git a/src/device-sync/app/lifecycle.ts b/src/device-sync/app/lifecycle.ts index 91fd5364..e873595e 100644 --- a/src/device-sync/app/lifecycle.ts +++ b/src/device-sync/app/lifecycle.ts @@ -623,7 +623,23 @@ export async function syncNow(registryId: string, authority: Authority): Promise return true; } -/** Android's sync screen of the account ("Android account settings", "Review deletions"). */ +/** + * After a run held back many deletions made on the device + * (`tooManyDeletions`): delete those items on the server too, or bring them + * back on the device from the server. Android's own screens offer neither. + */ +export async function resolveDeletions(registryId: string, authority: Authority, choice: 'delete' | 'restore'): Promise { + const accountName = accountDeviceSync(registryId).androidAccountName; + if (!deviceSyncAvailable() || !accountName) return false; + await requestSync( + accountName, + authority, + choice === 'delete' ? { manual: true, overrideTooManyDeletions: true } : { manual: true, discardLocalDeletions: true }, + ); + return true; +} + +/** Android's sync screen of the account ("Android account settings"). */ export async function openAndroidAccountSettings(registryId: string): Promise { const accountName = accountDeviceSync(registryId).androidAccountName; if (!deviceSyncAvailable() || !accountName) return false;