fix: let the user delete or restore held-back deletions from the sync settings #34

When a sync held back many deletions made on the device (Android's
This commit is contained in:
Linus Rath
2026-09-27 07:19:26 +02:00
parent 2b9d19521f
commit 7a53cd5d84
4 changed files with 57 additions and 2 deletions
+4
View File
@@ -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",
@@ -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) => {
@@ -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 = [];
+17 -1
View File
@@ -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<boolean> {
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<boolean> {
const accountName = accountDeviceSync(registryId).androidAccountName;
if (!deviceSyncAvailable() || !accountName) return false;