diff --git a/package-lock.json b/package-lock.json index b02a6f9e..d0ae8a18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49955,7 +49955,7 @@ }, "packages/web": { "name": "@oboku/web", - "version": "0.27.0", + "version": "0.26.0", "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index f8730585..445b4f4c 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -12,7 +12,6 @@ import { useObservers } from "./rxdb/sync/useObservers" import { PreloadQueries } from "./PreloadQueries" import { SplashScreen } from "./SplashScreen" import { FirstTimeExperienceTours } from "./firstTimeExperience/FirstTimeExperienceTours" -import { DialogProvider } from "./common/dialog" import { BlurContainer } from "./books/BlurContainer" import "./i18n" import { ErrorBoundary } from "@sentry/react" @@ -32,6 +31,7 @@ import { AuthorizeActionDialog } from "./auth/AuthorizeActionDialog" import { profileStorageSignal } from "./profile/storage" import { authSignalStorageAdapter } from "./auth/storage" import { authStateSignal } from "./auth/authState" +import { DialogProvider } from "./common/dialogs/DialogProvider" declare module "@mui/styles/defaultTheme" { // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/web/src/auth/AuthorizeActionDialog.tsx b/packages/web/src/auth/AuthorizeActionDialog.tsx index 674ae1c5..8ed8779c 100644 --- a/packages/web/src/auth/AuthorizeActionDialog.tsx +++ b/packages/web/src/auth/AuthorizeActionDialog.tsx @@ -13,7 +13,7 @@ import { Controller, useForm } from "react-hook-form" import { errorToHelperText } from "../common/forms/errorToHelperText" import { signal, useSignalValue } from "reactjrx" import { Observable, from, mergeMap } from "rxjs" -import { CancelError } from "../errors" +import { CancelError } from "../common/errors/errors" const FORM_ID = "LockActionBehindUserPasswordDialog" @@ -31,6 +31,9 @@ export const authorizeAction = (action: () => void, onCancel?: () => void) => onCancel }) +/** + * add check if user has pass code or not and if not just ignore + */ export function withAuthorization(stream: Observable) { return stream.pipe( mergeMap(() => diff --git a/packages/web/src/auth/LoginScreen.tsx b/packages/web/src/auth/LoginScreen.tsx index a4d8b081..14971e13 100644 --- a/packages/web/src/auth/LoginScreen.tsx +++ b/packages/web/src/auth/LoginScreen.tsx @@ -3,7 +3,7 @@ import { Alert } from "@mui/material" import { useTranslation } from "react-i18next" import { Google } from "@mui/icons-material" import { useSignIn } from "./useSignIn" -import { ErrorMessage, isCancelError } from "../errors" +import { ErrorMessage, isCancelError } from "../common/errors/errors" import { OrDivider } from "../common/OrDivider" import { links } from "@oboku/shared" import { useMutation } from "reactjrx" diff --git a/packages/web/src/auth/useSignIn.ts b/packages/web/src/auth/useSignIn.ts index 718409e7..b60c7043 100644 --- a/packages/web/src/auth/useSignIn.ts +++ b/packages/web/src/auth/useSignIn.ts @@ -3,7 +3,7 @@ import { useCallback } from "react" import { catchError, finalize, from, of, switchMap, tap } from "rxjs" import { lock, unlock } from "../common/BlockingBackdrop" import { API_URI } from "../constants" -import { CancelError } from "../errors" +import { CancelError } from "../common/errors/errors" import { useReCreateDb } from "../rxdb" import { authStateSignal } from "./authState" import { httpClient } from "../http/httpClient" diff --git a/packages/web/src/books/details/DataSourceSection.tsx b/packages/web/src/books/details/DataSourceSection.tsx index 7846573c..2ee8854e 100644 --- a/packages/web/src/books/details/DataSourceSection.tsx +++ b/packages/web/src/books/details/DataSourceSection.tsx @@ -11,17 +11,16 @@ import { MoreVertRounded } from "@mui/icons-material" import { FC, useState } from "react" import { useDataSourcePlugin } from "../../dataSources/helpers" import { Report } from "../../debug/report.shared" -import { useDialogManager } from "../../common/dialog" import { useBookLinksState } from "../states" import { useCreateRequestPopupDialog } from "../../plugins/useCreateRequestPopupDialog" import { upsertBookLink } from "../triggers" import { useTagsByIds } from "../../tags/helpers" +import { createDialog } from "../../common/dialogs/createDialog" export const DataSourceSection: FC<{ bookId: string }> = ({ bookId }) => { const link = useBookLinksState({ bookId, tags: useTagsByIds().data })[0] const dataSourcePlugin = useDataSourcePlugin(link?.type) const [isSelectItemOpened, setIsSelectItemOpened] = useState(false) - const dialog = useDialogManager() const createRequestPopupDialog = useCreateRequestPopupDialog() return ( @@ -48,7 +47,7 @@ export const DataSourceSection: FC<{ bookId: string }> = ({ bookId }) => { }} onClick={() => { if (!dataSourcePlugin?.SelectItemComponent) { - dialog({ preset: "NOT_IMPLEMENTED" }) + createDialog({ preset: "NOT_IMPLEMENTED" }) } else { setIsSelectItemOpened(true) } diff --git a/packages/web/src/books/drawer/BookActionsDrawer.tsx b/packages/web/src/books/drawer/BookActionsDrawer.tsx index 1cc6bcd0..1f705b00 100644 --- a/packages/web/src/books/drawer/BookActionsDrawer.tsx +++ b/packages/web/src/books/drawer/BookActionsDrawer.tsx @@ -104,16 +104,11 @@ export const BookActionsDrawer = memo(() => { bookId ) - const { mutate: onRemovePress, ...rest } = useRemoveHandler({ - onError: () => { - handleClose() - }, - onSuccess: ({ isDeleted }) => { - if (isDeleted) { - handleClose(() => { - onDeleteBook?.() - }) - } + const { mutate: onRemovePress } = useRemoveHandler({ + onSuccess: () => { + handleClose(() => { + onDeleteBook?.() + }) } }) diff --git a/packages/web/src/books/drawer/useRemoveHandler.ts b/packages/web/src/books/drawer/useRemoveHandler.ts index c121cbbb..833f0614 100644 --- a/packages/web/src/books/drawer/useRemoveHandler.ts +++ b/packages/web/src/books/drawer/useRemoveHandler.ts @@ -1,25 +1,22 @@ import { getBookById, useRemoveBook } from "../helpers" -import { useDialogManager } from "../../common/dialog" import { useMutation } from "reactjrx" import { getLatestDatabase } from "../../rxdb/useCreateDatabase" -import { catchError, from, map, mergeMap } from "rxjs" +import { combineLatest, from, map, mergeMap, of } from "rxjs" import { isRemovableFromDataSource } from "../../links/isRemovableFromDataSource" import { getDataSourcePlugin } from "../../dataSources/getDataSourcePlugin" import { getLinkById } from "../../links/helpers" - -type Return = { - isDeleted: boolean -} +import { createDialog } from "../../common/dialogs/createDialog" +import { withUnknownErrorDialog } from "../../common/errors/withUnknownErrorDialog" +import { withOfflineErrorDialog } from "../../common/network/withOfflineErrorDialog" export const useRemoveHandler = ( - options: { onSuccess?: (data: Return) => void; onError?: () => void } = {} + options: { onSuccess?: () => void; onError?: () => void } = {} ) => { - const removeBook = useRemoveBook() - const dialog = useDialogManager() + const { mutateAsync: removeBook } = useRemoveBook() return useMutation({ mutationFn: ({ bookId }: { bookId: string }) => { - return getLatestDatabase().pipe( + const mutation$ = getLatestDatabase().pipe( mergeMap((database) => { return getBookById({ database, id: bookId }).pipe( mergeMap((book) => { @@ -28,31 +25,21 @@ export const useRemoveHandler = ( const linkId = book.links[0] if (!book?.isAttachedToDataSource || !linkId) { - return from( - new Promise((resolve, reject) => { - dialog({ - preset: "CONFIRM", - title: "Delete a book", - content: `You are about to delete a book, are you sure ?`, - onConfirm: () => { - removeBook({ id: book._id }) - .then(() => resolve({ isDeleted: true })) - .catch(reject) - }, - onCancel: () => { - resolve({ isDeleted: false }) - } - }) - }) - ) + return combineLatest([ + of(book), + createDialog({ + preset: "CONFIRM", + title: "Delete a book", + content: `You are about to delete a book, are you sure ?`, + onConfirm: () => ({ deleteFromDataSource: false }) + }).$ + ]) } return getLinkById(database, linkId).pipe( mergeMap((firstLink) => { if (!firstLink) { - return from(removeBook({ id: book._id })).pipe( - map(() => ({ isDeleted: true })) - ) + return of({ deleteFromDataSource: false }) } const plugin = getDataSourcePlugin(firstLink?.type) @@ -61,71 +48,53 @@ export const useRemoveHandler = ( book?.isAttachedToDataSource && !isRemovableFromDataSource({ link: firstLink }) ) { - return from( - new Promise((resolve, reject) => { - dialog({ - preset: "CONFIRM", - title: "Delete a book", - content: `This book has been synchronized with one of your ${plugin?.name} data source. Oboku does not support deletion from ${plugin?.name} directly so consider deleting it there manually if you don't want the book to be synced again`, - onConfirm: () => { - removeBook({ id: book._id }) - .then(() => resolve({ isDeleted: true })) - .catch(reject) - }, - onCancel: () => { - resolve({ isDeleted: false }) - } - }) - }) - ) + return createDialog({ + preset: "CONFIRM", + title: "Delete a book", + content: `This book has been synchronized with one of your ${plugin?.name} data source. Oboku does not support deletion from ${plugin?.name} directly so consider deleting it there manually if you don't want the book to be synced again`, + onConfirm: () => ({ deleteFromDataSource: false }) + }).$ } else { - return from( - new Promise((resolve, reject) => { - dialog({ - preset: "CONFIRM", - title: "Delete a book", - content: `This book has been synchronized with one of your ${plugin?.name} data source. You can delete it from both oboku and ${plugin?.name} which will prevent the book to be synced again`, - actions: [ - { - type: "confirm", - title: "both", - onClick: () => { - removeBook({ - id: book._id, - deleteFromDataSource: true - }) - .then(() => resolve({ isDeleted: true })) - .catch(reject) - } - }, - { - type: "confirm", - title: "only oboku", - onClick: () => { - removeBook({ id: book._id }) - .then(() => resolve({ isDeleted: true })) - .catch(reject) - } - } - ], - onCancel: () => { - resolve({ isDeleted: false }) - } - }) - }) - ) + return createDialog({ + preset: "CONFIRM", + title: "Delete a book", + content: `This book has been synchronized with one of your ${plugin?.name} data source. You can delete it from both oboku and ${plugin?.name} which will prevent the book to be synced again`, + actions: [ + { + type: "confirm", + title: "both", + onConfirm: () => ({ deleteFromDataSource: true }) + }, + { + type: "confirm", + title: "only oboku", + onConfirm: () => ({ deleteFromDataSource: false }) + } + ] + }).$ } + }), + map( + ({ deleteFromDataSource }) => + [book, { deleteFromDataSource }] as const + ) + ) + }), + mergeMap(([book, { deleteFromDataSource }]) => + from( + removeBook({ + id: book._id, + deleteFromDataSource: deleteFromDataSource }) ) - }) + ) ) }), - catchError((e) => { - console.error(e) - - throw e - }) + withOfflineErrorDialog(), + withUnknownErrorDialog() ) + + return mutation$ }, ...options }) diff --git a/packages/web/src/books/helpers.ts b/packages/web/src/books/helpers.ts index f07119ff..479496ad 100644 --- a/packages/web/src/books/helpers.ts +++ b/packages/web/src/books/helpers.ts @@ -10,65 +10,51 @@ import { useCallback, useMemo } from "react" import { PromiseReturnType } from "../types" import { BookQueryResult, useBooksDic } from "./states" import { AtomicUpdateFunction } from "rxdb" -import { useLock } from "../common/BlockingBackdrop" import { useNetworkState } from "react-use" -import { useDialogManager } from "../common/dialog" import { from } from "rxjs" import { useRemoveBookFromDataSource } from "../plugins/useRemoveBookFromDataSource" import { useMutation } from "reactjrx" import { isPluginError } from "../plugins/plugin-front" import { getMetadataFromBook } from "./getMetadataFromBook" import { useRefreshBookMetadata } from "./useRefreshBookMetadata" +import { CancelError, OfflineError } from "../common/errors/errors" export const useRemoveBook = () => { const removeDownload = useRemoveDownloadFile() const { db } = useDatabase() - const dialog = useDialogManager() - const [lock] = useLock() const removeBookFromDataSource = useRemoveBookFromDataSource() const network = useNetworkState() - return useCallback( - async ({ + return useMutation({ + mutationFn: async ({ id, deleteFromDataSource }: { id: string deleteFromDataSource?: boolean }) => { - let unlock = () => {} + if (deleteFromDataSource) { + if (!network.online) { + throw new OfflineError() + } - try { - if (deleteFromDataSource) { - if (!network.online) { - return dialog({ preset: "OFFLINE" }) + try { + await removeBookFromDataSource(id) + } catch (e) { + if (isPluginError(e) && e.code === "cancelled") { + throw new CancelError() } - try { - await removeBookFromDataSource(id) - } catch (e) { - if (isPluginError(e) && e.code === "cancelled") { - return - } - - Report.error(e) - - return dialog({ preset: "UNKNOWN_ERROR" }) - } finally { - unlock() - } + throw e } - - await Promise.all([ - removeDownload(id), - db?.book.findOne({ selector: { _id: id } }).remove() - ]) - } catch (e) { - Report.error(e) } - }, - [removeDownload, removeBookFromDataSource, network, dialog, db] - ) + + await Promise.all([ + removeDownload(id), + db?.book.findOne({ selector: { _id: id } }).remove() + ]) + } + }) } export const useRemoveTagFromBook = () => { diff --git a/packages/web/src/books/useRefreshBookMetadata.ts b/packages/web/src/books/useRefreshBookMetadata.ts index d0a43928..3e4ee30a 100644 --- a/packages/web/src/books/useRefreshBookMetadata.ts +++ b/packages/web/src/books/useRefreshBookMetadata.ts @@ -1,6 +1,5 @@ import { useNetworkState } from "react-use" import { from, switchMap, catchError, map, EMPTY } from "rxjs" -import { useDialogManager } from "../common/dialog" import { httpClient } from "../http/httpClient" import { isPluginError } from "../plugins/plugin-front" import { usePluginRefreshMetadata } from "../plugins/usePluginRefreshMetadata" @@ -8,11 +7,11 @@ import { useDatabase } from "../rxdb" import { useSyncReplicate } from "../rxdb/replication/useSyncReplicate" import { useAtomicUpdateBook } from "./helpers" import { Report } from "../debug/report.shared" +import { createDialog } from "../common/dialogs/createDialog" export const useRefreshBookMetadata = () => { const { db: database } = useDatabase() const [updateBook] = useAtomicUpdateBook() - const dialog = useDialogManager() const network = useNetworkState() const { mutateAsync: sync } = useSyncReplicate() const refreshPluginMetadata = usePluginRefreshMetadata() @@ -20,7 +19,7 @@ export const useRefreshBookMetadata = () => { return async (bookId: string) => { try { if (!network.online) { - return dialog({ preset: "OFFLINE" }) + return createDialog({ preset: "OFFLINE" }) } const book = await database?.book diff --git a/packages/web/src/collections/useRefreshCollectionMetadata.ts b/packages/web/src/collections/useRefreshCollectionMetadata.ts index f9edc05d..30c488e3 100644 --- a/packages/web/src/collections/useRefreshCollectionMetadata.ts +++ b/packages/web/src/collections/useRefreshCollectionMetadata.ts @@ -7,7 +7,7 @@ import { isPluginError } from "../plugins/plugin-front" import { useMutation } from "reactjrx" import { useWithNetwork } from "../common/network/useWithNetwork" import { getLatestDatabase } from "../rxdb/useCreateDatabase" -import { OfflineError } from "../errors" +import { OfflineError } from "../common/errors/errors" import { getCollectionById } from "./databaseHelpers" export const useRefreshCollectionMetadata = () => { diff --git a/packages/web/src/common/dialog.tsx b/packages/web/src/common/dialog.tsx deleted file mode 100644 index 8a361ab1..00000000 --- a/packages/web/src/common/dialog.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle -} from "@mui/material" -import { - createContext, - FC, - ReactNode, - useCallback, - useContext, - useMemo, - useState -} from "react" -import { Subject, lastValueFrom, map, merge, of } from "rxjs" -import { CancelError } from "../errors" - -type Preset = "NOT_IMPLEMENTED" | "OFFLINE" | "CONFIRM" | "UNKNOWN_ERROR" - -type DialogType = { - title?: string - content?: string - id: string - preset?: Preset - cancellable?: boolean - canEscape?: boolean - cancelTitle?: string - confirmTitle?: string - actions?: { title: string; type: "confirm"; onClick: () => void }[] - onConfirm?: () => void - onClose?: () => void - onCancel?: () => void -} - -const DialogContext = createContext([]) - -const ManageDialogContext = createContext({ - remove: (id: string) => {}, - add: (options: Omit) => ({ - id: "-1" as string, - $: of({}) - }) -}) - -export const useDialogManager = () => { - const { add } = useContext(ManageDialogContext) - - return useCallback(add, [add]) -} - -const enrichDialogWithPreset = ( - dialog?: DialogType -): DialogType | undefined => { - if (!dialog) return undefined - - switch (dialog.preset) { - case "NOT_IMPLEMENTED": - return { - ...dialog, - title: "Not implemented", - content: "Sorry this feature is not yet implemented", - canEscape: true - } - case "OFFLINE": - return { - ...dialog, - title: "Offline is great but...", - content: "You need to be online to proceed with this action" - } - case "UNKNOWN_ERROR": - return { - title: "Oups, something went wrong!", - content: - "Something unexpected happened and oboku could not proceed with your action. Maybe you can try again?", - ...dialog - } - case "CONFIRM": - return { - ...dialog, - title: dialog.title || "Hold on a minute!", - content: - dialog.content || - "Are you sure you want to proceed with this action?", - cancellable: - dialog.cancellable !== undefined ? dialog.cancellable : true - } - default: - return dialog - } -} - -const InnerDialog = () => { - const dialogs = useContext(DialogContext) - const { remove } = useContext(ManageDialogContext) - - const currentDialog = enrichDialogWithPreset(dialogs[0]) - - const handleClose = useCallback(() => { - currentDialog?.onClose && currentDialog.onClose() - if (currentDialog) { - remove(currentDialog.id) - } - }, [remove, currentDialog]) - - const onCancel = useCallback(() => { - currentDialog?.onCancel && currentDialog.onCancel() - handleClose() - }, [handleClose, currentDialog]) - - const onConfirm = useCallback(() => { - currentDialog?.onConfirm && currentDialog.onConfirm() - handleClose() - }, [handleClose, currentDialog]) - - const actions = currentDialog?.actions || [ - { - title: currentDialog?.confirmTitle || "Ok", - onClick: () => {}, - type: "confirm" - } - ] - - return ( - - {currentDialog?.title} - - {currentDialog?.content} - - - {currentDialog?.cancellable === true && ( - - )} - {actions.map((action, id) => ( - - ))} - - - ) -} - -let generatedId = 0 - -/** - * @todo use recoil or another way to not re-render all children - * whenever dialog changes - */ -export const DialogProvider: FC<{ children: ReactNode }> = ({ children }) => { - const [dialogs, setDialogs] = useState([]) - - const remove = useCallback((id: string) => { - setDialogs((old) => old.filter((dialog) => id !== dialog.id)) - }, []) - - const add = useCallback((options: Omit) => { - generatedId++ - - const cancel = new Subject() - const confirm = new Subject() - - const newDialog: DialogType = { - ...options, - id: generatedId.toString(), - onCancel: () => { - cancel.next() - options.onCancel?.() - }, - onConfirm: () => { - confirm.next() - options.onConfirm?.() - }, - onClose: () => { - confirm.complete() - cancel.complete() - options.onClose?.() - } - } - - setDialogs((old) => [...old, newDialog]) - - const $ = merge( - cancel.pipe( - map(() => { - throw new CancelError() - }) - ), - confirm.pipe(map(() => ({}))) - ) - - return { id: newDialog.id, $ } - }, []) - - const controls = useMemo( - () => ({ - remove, - add - }), - [add, remove] - ) - - return ( - <> - - {children} - - {dialogs.length > 0 && } - - - - ) -} diff --git a/packages/web/src/common/dialogs/DialogProvider.tsx b/packages/web/src/common/dialogs/DialogProvider.tsx new file mode 100644 index 00000000..5000b312 --- /dev/null +++ b/packages/web/src/common/dialogs/DialogProvider.tsx @@ -0,0 +1,125 @@ +import { useSignalValue } from "reactjrx" +import { DialogType, dialogSignal } from "./state" +import { removeDialog } from "./removeDialog" +import { ReactNode, useCallback } from "react" +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle +} from "@mui/material" + +const enrichDialogWithPreset = ( + dialog?: DialogType +): DialogType | undefined => { + if (!dialog) return undefined + + switch (dialog.preset) { + case "NOT_IMPLEMENTED": + return { + ...dialog, + title: "Not implemented", + content: "Sorry this feature is not yet implemented", + canEscape: true + } + case "OFFLINE": + return { + ...dialog, + title: "Offline is great but...", + content: "You need to be online to proceed with this action" + } + case "UNKNOWN_ERROR": + return { + title: "Oups, something went wrong!", + content: + "Something unexpected happened and oboku could not proceed with your action. Maybe you can try again?", + ...dialog + } + case "CONFIRM": + return { + ...dialog, + title: dialog.title || "Hold on a minute!", + content: + dialog.content || + "Are you sure you want to proceed with this action?", + cancellable: + dialog.cancellable !== undefined ? dialog.cancellable : true + } + default: + return dialog + } +} + +const InnerDialog = () => { + const dialogs = useSignalValue(dialogSignal) + + const currentDialog = enrichDialogWithPreset(dialogs[0]) + + const handleClose = useCallback(() => { + currentDialog?.onClose && currentDialog.onClose() + if (currentDialog) { + removeDialog(currentDialog.id) + } + }, [currentDialog]) + + const onCancel = useCallback(() => { + currentDialog?.onCancel && currentDialog.onCancel() + handleClose() + }, [handleClose, currentDialog]) + + const actions = currentDialog?.actions || [ + { + title: currentDialog?.confirmTitle || "Ok", + onConfirm: currentDialog?.onConfirm, + type: "confirm" + } + ] + + return ( + + {currentDialog?.title} + + {currentDialog?.content} + + + {currentDialog?.cancellable === true && ( + + )} + {actions.map((action, id) => ( + + ))} + + + ) +} + +export const DialogProvider = ({ children }: { children: ReactNode }) => { + return ( + <> + {children} + + + ) +} diff --git a/packages/web/src/common/dialogs/createDialog.ts b/packages/web/src/common/dialogs/createDialog.ts new file mode 100644 index 00000000..361d5f2e --- /dev/null +++ b/packages/web/src/common/dialogs/createDialog.ts @@ -0,0 +1,49 @@ +import { Observable, noop, share } from "rxjs" +import { CancelError } from "../errors/errors" +import { DialogType, dialogSignal } from "./state" + +let generatedId = 0 + +export const createDialog = ( + dialog: Omit, "id"> +) => { + generatedId++ + const id = generatedId.toString() + + const $ = new Observable((observer) => { + const wrappedDialog: DialogType = { + ...dialog, + id, + onCancel: () => { + dialog.onCancel?.() + observer.error(new CancelError()) + observer.complete() + }, + onConfirm: () => { + const data = dialog.onConfirm?.() + observer.next(data) + observer.complete() + + return data as Result + }, + actions: dialog.actions?.map((action) => ({ + ...action, + onConfirm: () => { + const data = action.onConfirm?.() + + console.log("data", data) + observer.next(data) + observer.complete() + + return data as Result + } + })) + } + + dialogSignal.setValue((old) => [...old, wrappedDialog]) + }).pipe(share()) + + $.subscribe({ error: noop }) + + return { id, $ } +} diff --git a/packages/web/src/common/dialogs/removeDialog.ts b/packages/web/src/common/dialogs/removeDialog.ts new file mode 100644 index 00000000..ec501cd1 --- /dev/null +++ b/packages/web/src/common/dialogs/removeDialog.ts @@ -0,0 +1,5 @@ +import { dialogSignal } from "./state" + +export const removeDialog = (id: string) => [ + dialogSignal.setValue((old) => old.filter((dialog) => id !== dialog.id)) +] diff --git a/packages/web/src/common/dialogs/state.ts b/packages/web/src/common/dialogs/state.ts new file mode 100644 index 00000000..9b6e1e64 --- /dev/null +++ b/packages/web/src/common/dialogs/state.ts @@ -0,0 +1,22 @@ +import { signal } from "reactjrx" + +type Preset = "NOT_IMPLEMENTED" | "OFFLINE" | "CONFIRM" | "UNKNOWN_ERROR" + +export type DialogType = { + title?: string + content?: string + id: string + preset?: Preset + cancellable?: boolean + canEscape?: boolean + cancelTitle?: string + confirmTitle?: string + actions?: { title: string; type: "confirm"; onConfirm: () => T }[] + onConfirm?: () => T + onClose?: () => void + onCancel?: () => void +} + +export const dialogSignal = signal[]>({ + default: [] +}) diff --git a/packages/web/src/common/dialogs/withDialog.ts b/packages/web/src/common/dialogs/withDialog.ts new file mode 100644 index 00000000..054507e9 --- /dev/null +++ b/packages/web/src/common/dialogs/withDialog.ts @@ -0,0 +1,14 @@ +import { Observable, map, mergeMap } from "rxjs" +import { DialogType } from "./state" +import { createDialog } from "./createDialog" + +export const withDialog = ( + dialog: Omit, "id"> +) => { + return (stream: Observable) => + stream.pipe( + mergeMap((data) => + createDialog(dialog).$.pipe(map((dialogResult) => [data, dialogResult] as const)) + ) + ) +} diff --git a/packages/web/src/errors.tsx b/packages/web/src/common/errors/errors.tsx similarity index 87% rename from packages/web/src/errors.tsx rename to packages/web/src/common/errors/errors.tsx index 1ee8b4f9..6ec31bb9 100644 --- a/packages/web/src/errors.tsx +++ b/packages/web/src/common/errors/errors.tsx @@ -1,5 +1,5 @@ import { ObokuErrorCode } from "@oboku/shared" -import { HttpClientError } from "./http/httpClient" +import { HttpClientError } from "../../http/httpClient" type HttpApiError = { response: { @@ -20,9 +20,17 @@ export const createServerError = async (response: Response) => { } } -export class CancelError extends Error {} +export class CancelError extends Error { + constructor() { + super("CancelError") + } +} -export class OfflineError extends Error {} +export class OfflineError extends Error { + constructor() { + super("OfflineError") + } +} export const isCancelError = (error: unknown) => error instanceof CancelError @@ -63,3 +71,5 @@ export class ServerError extends Error { this.errors = errors } } + + diff --git a/packages/web/src/common/errors/withUnknownErrorDialog.ts b/packages/web/src/common/errors/withUnknownErrorDialog.ts new file mode 100644 index 00000000..5fb23290 --- /dev/null +++ b/packages/web/src/common/errors/withUnknownErrorDialog.ts @@ -0,0 +1,18 @@ +import { Observable, catchError } from "rxjs" +import { CancelError, OfflineError } from "./errors" +import { createDialog } from "../dialogs/createDialog" + +export function withUnknownErrorDialog() { + return function operator(stream: Observable) { + return stream.pipe( + catchError((error) => { + if (error instanceof CancelError) throw error + if (error instanceof OfflineError) throw error + + createDialog({ preset: "UNKNOWN_ERROR" }) + + throw error + }) + ) + } +} diff --git a/packages/web/src/common/fullscreen/useFullscreenOnMount.ts b/packages/web/src/common/fullscreen/useFullscreenOnMount.ts index 045b5e9f..9c1ca98f 100644 --- a/packages/web/src/common/fullscreen/useFullscreenOnMount.ts +++ b/packages/web/src/common/fullscreen/useFullscreenOnMount.ts @@ -1,7 +1,6 @@ import { useEffect } from "react" import screenfull from "screenfull" import { Report } from "../../debug/report.shared" -import { useDialogManager } from "../dialog" import { EMPTY, catchError, @@ -14,7 +13,8 @@ import { timer } from "rxjs" import { useSubscribe } from "reactjrx" -import { CancelError } from "../../errors" +import { CancelError } from "../errors/errors" +import { createDialog } from "../dialogs/createDialog" const isPermissionCheckFailedError = (error: unknown): error is TypeError => error instanceof TypeError && @@ -26,8 +26,6 @@ const isPermissionCheckFailedError = (error: unknown): error is TypeError => error.message === "Fullscreen request denied") export const useFullscreenOnMount = ({ enabled }: { enabled: boolean }) => { - const dialog = useDialogManager() - useSubscribe(() => { if (enabled && screenfull.isEnabled && !screenfull.isFullscreen) { return defer(() => { @@ -42,13 +40,13 @@ export const useFullscreenOnMount = ({ enabled }: { enabled: boolean }) => { // we avoid double dialog because of strict mode screenfull.isFullscreen ? throwError(() => error) - : dialog({ + : createDialog({ title: "Fullscreen request", content: "Your browser does not allow automatic fullscreen without an interaction", confirmTitle: "Fullscreen", cancellable: true - }).$.pipe(endWith(true)) + }).$ ) ) } @@ -72,7 +70,7 @@ export const useFullscreenOnMount = ({ enabled }: { enabled: boolean }) => { } return EMPTY - }, [enabled, dialog]) + }, [enabled]) useEffect(() => { return () => { diff --git a/packages/web/src/common/network/useWithNetwork.ts b/packages/web/src/common/network/useWithNetwork.ts index d336d377..f08c92f7 100644 --- a/packages/web/src/common/network/useWithNetwork.ts +++ b/packages/web/src/common/network/useWithNetwork.ts @@ -1,17 +1,16 @@ import { Observable, tap } from "rxjs" -import { useDialogManager } from "../dialog" import { useNetworkState } from "react-use" -import { OfflineError } from "../../errors" +import { OfflineError } from "../errors/errors" +import { createDialog } from "../dialogs/createDialog" export const useWithNetwork = () => { - const dialog = useDialogManager() const networkState = useNetworkState() return (stream: Observable) => stream.pipe( tap(() => { if (!networkState.online) { - dialog({ preset: "OFFLINE" }) + createDialog({ preset: "OFFLINE" }) throw new OfflineError() } diff --git a/packages/web/src/common/network/withOfflineErrorDialog.ts b/packages/web/src/common/network/withOfflineErrorDialog.ts new file mode 100644 index 00000000..0e902fbe --- /dev/null +++ b/packages/web/src/common/network/withOfflineErrorDialog.ts @@ -0,0 +1,17 @@ +import { Observable, catchError } from "rxjs" +import { createDialog } from "../dialogs/createDialog" +import { OfflineError } from "../errors/errors" + +export function withOfflineErrorDialog() { + return function operator(stream: Observable) { + return stream.pipe( + catchError((error) => { + if (error instanceof OfflineError) { + createDialog({ preset: "OFFLINE" }) + } + + throw error + }) + ) + } +} diff --git a/packages/web/src/dataSources/DataSourcesActionsDrawer.tsx b/packages/web/src/dataSources/DataSourcesActionsDrawer.tsx index a6954df4..f8a020a2 100644 --- a/packages/web/src/dataSources/DataSourcesActionsDrawer.tsx +++ b/packages/web/src/dataSources/DataSourcesActionsDrawer.tsx @@ -13,18 +13,19 @@ import { RadioButtonUncheckedOutlined, CheckCircleRounded } from "@mui/icons-material" -import { useSynchronizeDataSource, useRemoveDataSource } from "./helpers" +import { useSynchronizeDataSource } from "./helpers" import { useDataSource } from "./useDataSource" import { toggleDatasourceProtected } from "./triggers" import { useSignalValue } from "reactjrx" import { libraryStateSignal } from "../library/states" +import { useRemoveDataSource } from "./useRemoveDataSource" export const DataSourcesActionsDrawer: FC<{ openWith: string onClose: () => void }> = memo(({ openWith, onClose }) => { const syncDataSource = useSynchronizeDataSource() - const { mutate: remove } = useRemoveDataSource() + const { mutate: removeDataSource } = useRemoveDataSource() const { data: dataSource } = useDataSource(openWith) const library = useSignalValue(libraryStateSignal) @@ -73,7 +74,7 @@ export const DataSourcesActionsDrawer: FC<{ button onClick={() => { onClose() - remove({ id: openWith }) + removeDataSource({ id: openWith }) }} > diff --git a/packages/web/src/dataSources/dbHelpers.ts b/packages/web/src/dataSources/dbHelpers.ts new file mode 100644 index 00000000..9f7ffe89 --- /dev/null +++ b/packages/web/src/dataSources/dbHelpers.ts @@ -0,0 +1,5 @@ +import { Database } from "../rxdb" + +export const getDataSourceById = (db: Database, id: string) => { + return db.datasource.findOne(id).exec() +} diff --git a/packages/web/src/dataSources/helpers.ts b/packages/web/src/dataSources/helpers.ts index 466405ae..c7c26040 100644 --- a/packages/web/src/dataSources/helpers.ts +++ b/packages/web/src/dataSources/helpers.ts @@ -3,29 +3,28 @@ import { DataSourceDocType, ObokuErrorCode } from "@oboku/shared" import { Report } from "../debug/report.shared" import { plugins } from "../plugins/configure" import { useCallback, useMemo } from "react" -import { useDialogManager } from "../common/dialog" import { useNetworkState } from "react-use" import { useSyncReplicate } from "../rxdb/replication/useSyncReplicate" import { AtomicUpdateFunction } from "rxdb" import { catchError, EMPTY, from, switchMap, map, of, filter } from "rxjs" import { usePluginSynchronize } from "../plugins/usePluginSynchronize" -import { isDefined, useMutation } from "reactjrx" +import { isDefined } from "reactjrx" import { isPluginError } from "../plugins/plugin-front" import { getDataSourcePlugin } from "./getDataSourcePlugin" import { httpClient } from "../http/httpClient" +import { createDialog } from "../common/dialogs/createDialog" export const useSynchronizeDataSource = () => { const { db: database } = useDatabase() const { atomicUpdateDataSource } = useAtomicUpdateDataSource() const synchronizeDataSource = usePluginSynchronize() const network = useNetworkState() - const dialog = useDialogManager() const { mutateAsync: sync } = useSyncReplicate() return useCallback( async (_id: string) => { if (!network.online) { - return dialog({ preset: "OFFLINE" }) + return createDialog({ preset: "OFFLINE" }) } if (!database) return @@ -66,14 +65,7 @@ export const useSynchronizeDataSource = () => { ) .subscribe() }, - [ - atomicUpdateDataSource, - database, - dialog, - network, - sync, - synchronizeDataSource - ] + [atomicUpdateDataSource, database, network, sync, synchronizeDataSource] ) } @@ -106,15 +98,6 @@ export const useCreateDataSource = () => { } } -export const useRemoveDataSource = () => { - const { db } = useDatabase() - - return useMutation({ - mutationFn: async ({ id }: { id: string }) => - db?.datasource.findOne({ selector: { _id: id } }).remove() - }) -} - export const useAtomicUpdateDataSource = () => { const { db: database } = useDatabase() diff --git a/packages/web/src/dataSources/useRemoveDataSource.ts b/packages/web/src/dataSources/useRemoveDataSource.ts new file mode 100644 index 00000000..87a9c404 --- /dev/null +++ b/packages/web/src/dataSources/useRemoveDataSource.ts @@ -0,0 +1,70 @@ +import { useMutation } from "reactjrx" +import { getLatestDatabase } from "../rxdb/useCreateDatabase" +import { combineLatest, from, mergeMap, of } from "rxjs" +import { withDialog } from "../common/dialogs/withDialog" +import { getLinksForDataSource } from "../links/dbHelpers" +import { useRemoveBook } from "../books/helpers" +import { getDataSourceById } from "./dbHelpers" +import { withUnknownErrorDialog } from "../common/errors/withUnknownErrorDialog" + +export const useRemoveDataSource = () => { + const { mutateAsync: removeBook } = useRemoveBook() + + return useMutation({ + mutationFn: ({ id }: { id: string }) => + getLatestDatabase().pipe( + withDialog({ preset: "CONFIRM" }), + withDialog({ + title: "Remove books?", + content: + "Do you want to delete the books together with the data source?", + cancellable: true, + actions: [ + { + title: "Yes", + type: "confirm", + onConfirm: () => true + }, + { + title: "No", + type: "confirm", + onConfirm: () => false + } + ] + }), + mergeMap(([[db], deleteBooks]) => + from(getDataSourceById(db, id)).pipe( + mergeMap((dataSource) => { + if (!dataSource) throw new Error("Invalid data source") + + const dataSourceDelete$ = from(dataSource.remove()) + + if (deleteBooks) { + const links$ = from(getLinksForDataSource(db, dataSource)) + + return links$.pipe( + mergeMap((links) => { + const booksDelete$ = links.map((link) => + !link.book + ? of(null) + : from( + removeBook({ + id: link.book, + deleteFromDataSource: false + }) + ) + ) + + return combineLatest([dataSourceDelete$, ...booksDelete$]) + }) + ) + } + + return dataSourceDelete$ + }) + ) + ), + withUnknownErrorDialog() + ) + }) +} diff --git a/packages/web/src/download/useDownloadBook.ts b/packages/web/src/download/useDownloadBook.ts index 46a15504..01639972 100644 --- a/packages/web/src/download/useDownloadBook.ts +++ b/packages/web/src/download/useDownloadBook.ts @@ -7,20 +7,19 @@ import { useDatabase } from "../rxdb" import { DOWNLOAD_PREFIX } from "../constants.shared" import { BookFile } from "./types" import { getLinkStateAsync } from "../links/states" -import { useDialogManager } from "../common/dialog" import { bytesToMb } from "../common/utils" import { createCbzFromReadableStream } from "./createCbzFromReadableStream" import { useDownloadBookFromDataSource } from "../plugins/useDownloadBookFromDataSource" import { plugin } from "../plugins/local" import { isPluginError } from "../plugins/plugin-front" import { BookQueryResult } from "../books/states" +import { createDialog } from "../common/dialogs/createDialog" class NoLinkFound extends Error {} export const useDownloadBook = () => { const downloadBook = useDownloadBookFromDataSource() const { db: database } = useDatabase() - const dialog = useDialogManager() const setDownloadData = useCallback( ( @@ -63,7 +62,7 @@ export const useDownloadBook = () => { }) if (!firstLink) { - dialog({ + createDialog({ title: "No link!", content: "Your book does not have a valid link to download the file. Please add one before proceeding" @@ -119,7 +118,7 @@ export const useDownloadBook = () => { }) // @todo shorten this description and redirect to the documentation - dialog({ + createDialog({ preset: `UNKNOWN_ERROR`, title: `Unable to download`, content: ` @@ -174,7 +173,7 @@ export const useDownloadBook = () => { Report.error(e) } }, - [setDownloadData, downloadBook, dialog, database] + [setDownloadData, downloadBook, database] ) } diff --git a/packages/web/src/links/dbHelpers.ts b/packages/web/src/links/dbHelpers.ts new file mode 100644 index 00000000..a497f0a6 --- /dev/null +++ b/packages/web/src/links/dbHelpers.ts @@ -0,0 +1,15 @@ +import { DataSourceDocType } from "@oboku/shared" +import { Database } from "../rxdb" + +export const getLinksForDataSource = ( + db: Database, + dataSource: DataSourceDocType +) => { + return db.link + .find({ + selector: { + dataSourceId: dataSource._id + } + }) + .exec() +} diff --git a/packages/web/src/plugins/useCreateRequestPopupDialog.ts b/packages/web/src/plugins/useCreateRequestPopupDialog.ts index 36306e8e..b01ab32f 100644 --- a/packages/web/src/plugins/useCreateRequestPopupDialog.ts +++ b/packages/web/src/plugins/useCreateRequestPopupDialog.ts @@ -1,14 +1,12 @@ import { useCallback } from "react" -import { useDialogManager } from "../common/dialog" +import { createDialog } from "../common/dialogs/createDialog" export const useCreateRequestPopupDialog = () => { - const dialog = useDialogManager() - return useCallback( ({ name }: { name: string }) => () => - new Promise((resolve, reject) => { - dialog({ + new Promise((resolve) => { + createDialog({ preset: "CONFIRM", title: `Plugin ${name} requires some actions`, content: `To proceed, the plugin ${name} requires some action from you which involve opening a popup`, @@ -16,6 +14,6 @@ export const useCreateRequestPopupDialog = () => { onCancel: () => resolve(false) }) }), - [dialog] + [] ) } diff --git a/packages/web/src/plugins/usePluginSynchronize.ts b/packages/web/src/plugins/usePluginSynchronize.ts index 2c34364c..0f944d22 100644 --- a/packages/web/src/plugins/usePluginSynchronize.ts +++ b/packages/web/src/plugins/usePluginSynchronize.ts @@ -1,13 +1,11 @@ import { DataSourceDocType } from "@oboku/shared" import { useCallback, useRef } from "react" -import { useDialogManager } from "../common/dialog" import { plugins } from "./configure" import { useCreateRequestPopupDialog } from "./useCreateRequestPopupDialog" import { ObokuPlugin } from "./plugin-front" export const usePluginSynchronize = () => { const createRequestPopupDialog = useCreateRequestPopupDialog() - const dialog = useDialogManager() // It's important to use array for plugins and be careful of the order since // it will trigger all hooks @@ -45,5 +43,5 @@ export const usePluginSynchronize = () => { throw new Error("no datasource found for this link") } - return useCallback(execute, [getPluginFn, dialog]) + return useCallback(execute, [getPluginFn]) } diff --git a/packages/web/src/queries/queryClient.ts b/packages/web/src/queries/queryClient.ts index 1f39cffa..ad7d909f 100644 --- a/packages/web/src/queries/queryClient.ts +++ b/packages/web/src/queries/queryClient.ts @@ -1,6 +1,22 @@ -import { QueryClient } from "reactjrx" +import { MutationCache, QueryCache, QueryClient } from "reactjrx" +import { Report } from "../debug/report.shared" +import { CancelError } from "../common/errors/errors" export const queryClient = new QueryClient({ + mutationCache: new MutationCache({ + onError: (error) => { + if (error instanceof CancelError) return + + Report.error(error) + } + }), + queryCache: new QueryCache({ + onError: (error) => { + if (error instanceof CancelError) return + + Report.error(error) + } + }), defaultOptions: { mutations: { /** diff --git a/packages/web/src/reader/navigation/useShowRemoveBookOnExitDialog.ts b/packages/web/src/reader/navigation/useShowRemoveBookOnExitDialog.ts index 0ac9f0b2..9fab90e3 100644 --- a/packages/web/src/reader/navigation/useShowRemoveBookOnExitDialog.ts +++ b/packages/web/src/reader/navigation/useShowRemoveBookOnExitDialog.ts @@ -1,12 +1,12 @@ import { useEffect, useState } from "react" import { useBook } from "../../books/states" -import { useDialogManager } from "../../common/dialog" import { useRemoveDownloadFile } from "../../download/useRemoveDownloadFile" import { ReadingStateState } from "@oboku/shared" import { useMutation } from "reactjrx" import { getLatestDatabase } from "../../rxdb/useCreateDatabase" -import { Observable, mergeMap, noop, of } from "rxjs" +import { mergeMap, noop, of } from "rxjs" import { getBookById } from "../../books/helpers" +import { createDialog } from "../../common/dialogs/createDialog" export const useShowRemoveBookOnExitDialog = ({ onSettled, @@ -15,7 +15,6 @@ export const useShowRemoveBookOnExitDialog = ({ onSettled?: () => void bookId?: string }) => { - const dialog = useDialogManager() const removeDownloadFile = useRemoveDownloadFile() const { data: book } = useBook({ id: bookId }) const readingState = book?.readingStateCurrentState @@ -44,24 +43,18 @@ export const useShowRemoveBookOnExitDialog = ({ return of(null) } - return new Observable((subscriber) => { - dialog({ - title: "Free up some space!", - content: - "Congratulation on finishing your book! Would you like to remove its download to free up some space? (Don't worry the book will not be removed, only its locally downloaded files)", - cancellable: true, - confirmTitle: "Remove", - cancelTitle: "Keep", - canEscape: false, - onConfirm: () => { - removeDownloadFile(book._id).catch(noop) - }, - onClose: () => { - subscriber.next() - subscriber.complete() - } - }) - }) + return createDialog({ + title: "Free up some space!", + content: + "Congratulation on finishing your book! Would you like to remove its download to free up some space? (Don't worry the book will not be removed, only its locally downloaded files)", + cancellable: true, + confirmTitle: "Remove", + cancelTitle: "Keep", + canEscape: false, + onConfirm: () => { + removeDownloadFile(book._id).catch(noop) + }, + }).$ }) ), onSettled diff --git a/packages/web/src/reading/BackToReadingDialog.tsx b/packages/web/src/reading/BackToReadingDialog.tsx index db02ab4e..1e189206 100644 --- a/packages/web/src/reading/BackToReadingDialog.tsx +++ b/packages/web/src/reading/BackToReadingDialog.tsx @@ -1,12 +1,12 @@ import { useEffect, useRef } from "react" import { useLocation, useNavigate } from "react-router-dom" import { ROUTES } from "../constants" -import { useDialogManager } from "../common/dialog" import { hasOpenedReaderAlreadyStateSignal, bookBeingReadStateSignal } from "./states" import { SIGNAL_RESET, useSignalValue } from "reactjrx" +import { createDialog } from "../common/dialogs/createDialog" const BASE_READER_ROUTE = ROUTES.READER.replace(`/:id`, ``) @@ -16,7 +16,6 @@ export const BackToReadingDialog = () => { const hasOpenedReaderAlready = useSignalValue( hasOpenedReaderAlreadyStateSignal ) - const dialog = useDialogManager() const navigate = useNavigate() const location = useLocation() @@ -32,7 +31,7 @@ export const BackToReadingDialog = () => { isOpen.current = true - dialog({ + createDialog({ title: `Take me back to my book`, content: `It looks like you were reading a book last time you used the app. Do you want to go back to reading?`, cancellable: true, @@ -46,7 +45,7 @@ export const BackToReadingDialog = () => { isOpen.current = false } }) - }, [dialog, bookBeingRead, location, navigate, hasOpenedReaderAlready]) + }, [bookBeingRead, location, navigate, hasOpenedReaderAlready]) return null } diff --git a/packages/web/src/rxdb/collections/link.ts b/packages/web/src/rxdb/collections/link.ts index 41cced1d..3ef2f1bb 100644 --- a/packages/web/src/rxdb/collections/link.ts +++ b/packages/web/src/rxdb/collections/link.ts @@ -47,6 +47,7 @@ const linkSchema: RxJsonSchema> = { _id: { type: `string`, maxLength: 100 }, data: { type: ["string", "object", "null"] }, resourceId: { type: "string" }, + dataSourceId: { type: "string" }, type: { type: "string" }, book: { type: ["string", "null"] }, contentLength: { type: ["number", "null"] }, diff --git a/packages/web/src/rxdb/databases.ts b/packages/web/src/rxdb/databases.ts index bb4dbf47..f73794ee 100644 --- a/packages/web/src/rxdb/databases.ts +++ b/packages/web/src/rxdb/databases.ts @@ -84,7 +84,7 @@ export const createDatabase = async () => { }) const db = await createRxDatabase({ - name: "oboku-34", + name: "oboku-35", // NOTICE: Schema validation can be CPU expensive and increases your build size. // You should always use a schema validation plugin in development mode. // For most use cases, you should not use a validation plugin in production. diff --git a/packages/web/src/settings/ProfileScreen.tsx b/packages/web/src/settings/ProfileScreen.tsx index 6aa0306a..0f29ec17 100644 --- a/packages/web/src/settings/ProfileScreen.tsx +++ b/packages/web/src/settings/ProfileScreen.tsx @@ -41,7 +41,6 @@ import { import { libraryStateSignal } from "../library/states" import packageJson from "../../package.json" import { ROUTES } from "../constants" -import { useDialogManager } from "../common/dialog" import { toggleDebug } from "../debug" import { useDatabase } from "../rxdb" import { catchError, forkJoin, from, of, switchMap, takeUntil, tap } from "rxjs" @@ -52,6 +51,7 @@ import { firstTimeExperienceStateSignal } from "../firstTimeExperience/firstTime import { unlockLibraryDialogSignal } from "../auth/UnlockLibraryDialog" import { authStateSignal } from "../auth/authState" import { useRemoveAllContents } from "./useRemoveAllContents" +import { createDialog } from "../common/dialogs/createDialog" export const ProfileScreen = () => { const navigate = useNavigate() @@ -67,7 +67,6 @@ export const ProfileScreen = () => { const library = useSignalValue(libraryStateSignal) const signOut = useSignOut() const theme = useTheme() - const dialog = useDialogManager() const { mutate: updateSettings } = useUpdateSettings() const { mutate: removeAllContents } = useRemoveAllContents() @@ -225,7 +224,7 @@ export const ProfileScreen = () => { About}> - dialog({ preset: "NOT_IMPLEMENTED" })}> + createDialog({ preset: "NOT_IMPLEMENTED" })}> @@ -296,7 +295,7 @@ export const ProfileScreen = () => { secondary="Remove all contents from your account" /> - dialog({ preset: "NOT_IMPLEMENTED" })}> + createDialog({ preset: "NOT_IMPLEMENTED" })}> diff --git a/packages/web/src/settings/useRemoveAllContents.ts b/packages/web/src/settings/useRemoveAllContents.ts index bbcd069a..f0502be8 100644 --- a/packages/web/src/settings/useRemoveAllContents.ts +++ b/packages/web/src/settings/useRemoveAllContents.ts @@ -3,15 +3,14 @@ import { getLatestDatabase } from "../rxdb/useCreateDatabase" import { catchError, combineLatest, from, map, mergeMap, of, tap } from "rxjs" import { useSyncReplicate } from "../rxdb/replication/useSyncReplicate" import { useLock } from "../common/BlockingBackdrop" -import { useDialogManager } from "../common/dialog" import { withAuthorization } from "../auth/AuthorizeActionDialog" import { Report } from "../debug/report.shared" -import { CancelError } from "../errors" +import { CancelError } from "../common/errors/errors" +import { createDialog } from "../common/dialogs/createDialog" export const useRemoveAllContents = () => { const { mutateAsync: sync } = useSyncReplicate() const [lock] = useLock() - const dialog = useDialogManager() return useMutation({ mutationFn: () => @@ -33,7 +32,7 @@ export const useRemoveAllContents = () => { tagCount, dataSourceCount ]) => { - const confirmed$ = dialog({ + const confirmed$ = createDialog({ title: "Account reset", content: `This action will remove all of your content. Here is a breakdown of everything that will be removed:\n ${bookCount} books, ${collectionCount} collections, ${tagCount} tags and ${dataSourceCount} data sources. \n\nThis operation can take a long time and you NEED to be connected to internet`, @@ -83,7 +82,7 @@ export const useRemoveAllContents = () => { Report.error(e) - dialog({ + createDialog({ title: "Something went wrong!", content: "Something went wrong during the process. No need to panic since you already wanted to destroy everything anyway. If everything is gone, you should not worry too much, if you still have contents, try to do it again"