fix: Prevent Update Clearing From Freezing App (Closes #1955)

This commit is contained in:
Rajarshee Chatterjee
2026-08-04 19:52:11 +05:30
parent c3482a8510
commit 8a12529ba8
5 changed files with 102 additions and 17 deletions
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { getString } from '@i18n/translations';
@@ -25,20 +25,37 @@ const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
onDismiss,
onConfirm,
}) => {
const handleConfirm = () => {
void onConfirm();
onDismiss();
const [isConfirming, setIsConfirming] = useState(false);
const handleConfirm = async () => {
setIsConfirming(true);
try {
await onConfirm();
onDismiss();
} finally {
setIsConfirming(false);
}
};
return (
<Dialog.Root visible={visible} onDismiss={onDismiss}>
<Dialog.Root
visible={visible}
onDismiss={isConfirming ? () => {} : onDismiss}
>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
{message ? <Dialog.Description>{message}</Dialog.Description> : null}
</Dialog.Header>
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>{cancelLabel}</Dialog.Action>
<Dialog.Action tone={confirmTone} onPress={handleConfirm}>
<Dialog.Action disabled={isConfirming} onPress={onDismiss}>
{cancelLabel}
</Dialog.Action>
<Dialog.Action
disabled={isConfirming}
loading={isConfirming}
tone={confirmTone}
onPress={handleConfirm}
>
{confirmLabel}
</Dialog.Action>
</Dialog.Actions>
+37 -3
View File
@@ -1,5 +1,10 @@
import './mocks';
import { fireEvent, render, screen } from '@testing-library/react-native';
import {
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react-native';
import { Dialog } from '../Dialog';
import ConfirmationDialog from '../ConfirmationDialog/ConfirmationDialog';
@@ -104,7 +109,7 @@ describe('Dialog', () => {
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('runs and dismisses a confirmation action', () => {
it('runs and dismisses a confirmation action', async () => {
const onConfirm = jest.fn();
const onDismiss = jest.fn();
@@ -121,6 +126,35 @@ describe('Dialog', () => {
fireEvent.press(screen.getByText('Delete'));
expect(onConfirm).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
await waitFor(() => expect(onDismiss).toHaveBeenCalledTimes(1));
});
it('waits for an async confirmation action before dismissing', async () => {
let resolveConfirmation: () => void = () => {};
const onConfirm = jest.fn(
() =>
new Promise<void>(resolve => {
resolveConfirmation = resolve;
}),
);
const onDismiss = jest.fn();
render(
<ConfirmationDialog
title="Delete item?"
confirmLabel="Delete"
visible
onConfirm={onConfirm}
onDismiss={onDismiss}
/>,
);
fireEvent.press(screen.getByText('Delete'));
expect(onConfirm).toHaveBeenCalledTimes(1);
expect(onDismiss).not.toHaveBeenCalled();
resolveConfirmation();
await waitFor(() => expect(onDismiss).toHaveBeenCalledTimes(1));
});
});
+9
View File
@@ -28,6 +28,7 @@ import NativeFile from '@modules/native-file';
import { ChapterFilterKey, ChapterOrderKey } from '@database/constants';
import { chapterFilterToSQL, chapterOrderToSQL } from '@database/utils/parser';
import { castInt } from '@database/manager/manager';
import { createNovelTriggerQueryUpdate } from '@database/queryStrings/triggers';
// #region Mutations
@@ -366,7 +367,15 @@ export const markPreviousChaptersUnread = async (
export const clearUpdates = async (): Promise<void> => {
await dbManager.write(async tx => {
// The chapter update trigger recalculates novel aggregates once per row.
// Bypass it for this database-wide operation and update the one affected
// aggregate in bulk instead.
await tx.run(
sql.raw('DROP TRIGGER IF EXISTS update_novel_stats_on_update'),
);
await tx.update(chapterSchema).set({ updatedTime: null }).run();
await tx.update(novelSchema).set({ lastUpdatedAt: null }).run();
await tx.run(sql.raw(createNovelTriggerQueryUpdate));
});
};
@@ -7,6 +7,8 @@
import './mockDb';
import { setupTestDatabase, getTestDb, teardownTestDatabase } from './setup';
import { insertTestNovel, insertTestChapter, clearAllTables } from './testData';
import { chapterSchema } from '@database/schema';
import { eq } from 'drizzle-orm';
import {
markChapterRead,
@@ -661,10 +663,10 @@ describe('ChapterQueries', () => {
});
describe('clearUpdates', () => {
it('should clear all update timestamps', async () => {
it('should clear update timestamps and preserve the update trigger', async () => {
const testDb = getTestDb();
const novelId = await insertTestNovel(testDb, { inLibrary: true });
await insertTestChapter(testDb, novelId, {
const chapterId = await insertTestChapter(testDb, novelId, {
updatedTime: '2024-01-01',
});
await insertTestChapter(testDb, novelId, {
@@ -675,6 +677,25 @@ describe('ChapterQueries', () => {
const chapters = await getNovelChapters(novelId);
expect(chapters.every(c => c.updatedTime === null)).toBe(true);
expect(
testDb.sqlite.executeSync(
'SELECT lastUpdatedAt FROM Novel WHERE id = ?',
[novelId],
).rows[0]?.lastUpdatedAt,
).toBeNull();
await testDb.drizzleDb
.update(chapterSchema)
.set({ updatedTime: '2024-02-01' })
.where(eq(chapterSchema.id, chapterId))
.run();
expect(
testDb.sqlite.executeSync(
'SELECT lastUpdatedAt FROM Novel WHERE id = ?',
[novelId],
).rows[0]?.lastUpdatedAt,
).toBe('2024-02-01');
});
});
@@ -45,6 +45,14 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
const [clearUpdatesDialog, setClearUpdatesDialog] = useState(false);
const showClearUpdatesDialog = () => setClearUpdatesDialog(true);
const hideClearUpdatesDialog = () => setClearUpdatesDialog(false);
const handleClearUpdates = async () => {
try {
await clearUpdates();
showToast(getString('advancedSettingsScreen.clearUpdatesMessage'));
} catch (error) {
showToast(error instanceof Error ? error.message : String(error));
}
};
const {
value: deleteReadChaptersDialog,
@@ -150,11 +158,7 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
confirmLabel={getString('common.clear')}
message={getString('advancedSettingsScreen.clearUpdatesWarning')}
visible={clearUpdatesDialog}
onConfirm={() => {
clearUpdates();
showToast(getString('advancedSettingsScreen.clearUpdatesMessage'));
hideClearUpdatesDialog();
}}
onConfirm={handleClearUpdates}
onDismiss={hideClearUpdatesDialog}
/>