fix: Prevent Update Clearing From Freezing App (Closes #1955)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user