From 91a2264e574efe90a124b771a43ad1a0e8e3dbf1 Mon Sep 17 00:00:00 2001 From: maxime Date: Fri, 23 Feb 2024 21:42:20 +0100 Subject: [PATCH] feat: fixed authorizing access dialog --- packages/web/src/App.tsx | 15 +-- packages/web/src/PreloadQueries.ts | 4 +- .../web/src/auth/AuthorizeActionDialog.tsx | 113 ++++++++++++++++++ .../LockActionBehindUserPasswordDialog.tsx | 101 ---------------- packages/web/src/auth/LockActionDialog.tsx | 71 ----------- packages/web/src/auth/UnlockLibraryDialog.tsx | 4 +- packages/web/src/auth/helpers.ts | 33 ----- packages/web/src/books/Cover.tsx | 4 +- .../src/books/ManageBookCollectionsDialog.tsx | 4 +- packages/web/src/books/bookList/BookList.tsx | 2 +- .../books/bookList/BookListWithControls.tsx | 2 +- .../src/books/bookList/SelectableBookList.tsx | 2 +- .../src/books/details/BookDetailsScreen.tsx | 4 +- packages/web/src/books/states.ts | 4 +- .../collections/CollectionActionsDrawer.tsx | 4 +- .../collections/CollectionDetailsScreen.tsx | 4 +- .../ManageCollectionBooksDialog.tsx | 4 +- .../src/collections/list/CollectionList.tsx | 2 +- .../list/CollectionListItemList.tsx | 4 +- .../list/SelectableCollectionList.tsx | 2 +- packages/web/src/collections/states.ts | 10 +- .../{ => common}/lists/ListActionsToolbar.tsx | 4 +- .../{ => common}/lists/ReactWindowList.tsx | 2 +- .../web/src/library/LibraryBooksScreen.tsx | 4 +- .../src/library/LibraryCollectionScreen.tsx | 4 +- .../web/src/library/LibraryTagsScreen.tsx | 10 +- packages/web/src/library/UploadBookDrawer.tsx | 4 +- .../web/src/navigation/TopBarNavigation.tsx | 6 +- packages/web/src/reader/fullScreen.ts | 4 +- packages/web/src/settings/ProfileScreen.tsx | 48 +++++--- packages/web/src/settings/SettingsScreen.tsx | 20 ++-- .../web/src/settings/StatisticsScreen.tsx | 4 +- packages/web/src/settings/helpers.ts | 84 +++++++++++-- packages/web/src/settings/states.ts | 8 +- .../src/tags/tagList/SelectableTagList.tsx | 2 +- packages/web/src/tags/tagList/TagList.tsx | 2 +- packages/web/src/theme/ThemeProvider.tsx | 15 +++ packages/web/src/{ => theme}/theme.tsx | 72 +++++------ 38 files changed, 332 insertions(+), 354 deletions(-) create mode 100644 packages/web/src/auth/AuthorizeActionDialog.tsx delete mode 100644 packages/web/src/auth/LockActionBehindUserPasswordDialog.tsx delete mode 100644 packages/web/src/auth/LockActionDialog.tsx rename packages/web/src/{ => common}/lists/ListActionsToolbar.tsx (94%) rename packages/web/src/{ => common}/lists/ReactWindowList.tsx (99%) create mode 100644 packages/web/src/theme/ThemeProvider.tsx rename packages/web/src/{ => theme}/theme.tsx (68%) diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 5254d51a..3a690317 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -1,13 +1,7 @@ import { FC, Suspense, useEffect, useState } from "react" import { AppNavigator } from "./navigation/AppNavigator" -import { - ThemeProvider, - Theme, - StyledEngineProvider, - Fade, - Box -} from "@mui/material" -import { theme } from "./theme" +import { Theme, StyledEngineProvider, Fade, Box } from "@mui/material" +import { theme } from "./theme/theme" import { BlockingBackdrop } from "./common/BlockingBackdrop" import { TourProvider } from "./app-tour/TourProvider" import { ManageBookCollectionsDialog } from "./books/ManageBookCollectionsDialog" @@ -36,6 +30,8 @@ import { import localforage from "localforage" import { signalEntriesToPersist } from "./storage" import { queryClient } from "./queries/client" +import { ThemeProvider } from "./theme/ThemeProvider" +import { AuthorizeActionDialog } from "./auth/AuthorizeActionDialog" declare module "@mui/styles/defaultTheme" { // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -70,7 +66,7 @@ export function App() { }} > - + }> {/* */} @@ -93,6 +89,7 @@ export function App() { + { const [prefetched, setPrefetched] = useState(false) - const { data, status } = useAccountSettings({ + const { data, status } = useSettings({ enabled: !prefetched }) diff --git a/packages/web/src/auth/AuthorizeActionDialog.tsx b/packages/web/src/auth/AuthorizeActionDialog.tsx new file mode 100644 index 00000000..16d52a6a --- /dev/null +++ b/packages/web/src/auth/AuthorizeActionDialog.tsx @@ -0,0 +1,113 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField +} from "@mui/material" +import { FC, useEffect } from "react" +import { useValidateAppPassword } from "../settings/helpers" +import { Controller, useForm } from "react-hook-form" +import { errorToHelperText } from "../common/forms/errorToHelperText" +import { signal, useSignalValue } from "reactjrx" + +const FORM_ID = "LockActionBehindUserPasswordDialog" + +type Inputs = { + password: string +} + +const actionSignal = signal<(() => void) | undefined>({}) + +export const authorizeAction = (action: () => void) => + actionSignal.setValue(() => action) + +export const AuthorizeActionDialog: FC<{}> = () => { + const action = useSignalValue(actionSignal) + const open = !!action + const { control, handleSubmit, setFocus, setError, reset } = useForm({ + defaultValues: { + password: "" + } + }) + const { + mutate: validatePassword, + reset: resetValidatePasswordMutation, + error + } = useValidateAppPassword({ + onSuccess: () => { + onClose() + action && action() + }, + onError: () => { + setError("password", { + message: "Invalid" + }) + } + }) + + const onClose = () => { + actionSignal.setValue(undefined) + } + + useEffect(() => { + reset() + resetValidatePasswordMutation() + + if (open) { + setTimeout(() => { + setFocus("password") + }) + } + }, [open, resetValidatePasswordMutation, reset, setFocus]) + + return ( + + Authorization required + + + This action requires explicit authorization. Please enter your app + password to continue. + +
{ + validatePassword(data.password) + })} + > + { + return ( + + ) + }} + /> + +
+ + + + +
+ ) +} diff --git a/packages/web/src/auth/LockActionBehindUserPasswordDialog.tsx b/packages/web/src/auth/LockActionBehindUserPasswordDialog.tsx deleted file mode 100644 index 63144adb..00000000 --- a/packages/web/src/auth/LockActionBehindUserPasswordDialog.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - TextField -} from "@mui/material" -import { FC, useEffect, useState } from "react" -import { useAuthorize } from "./helpers" -import { useSignalValue } from "reactjrx" -import { authStateSignal } from "./authState" - -const FORM_ID = "LockActionBehindUserPasswordDialog" - -export const LockActionBehindUserPasswordDialog: FC<{ - action?: () => void -}> = ({ action }) => { - const [open, setOpen] = useState(false) - const [success, setSuccess] = useState(false) - const [text, setText] = useState("") - const auth = useSignalValue(authStateSignal) - const authorize = useAuthorize() - - const onClose = () => { - setOpen(false) - } - - const onConfirm = () => { - authorize({ - variables: { password: text }, - onSuccess: () => { - setSuccess(true) - } - }) - } - - useEffect(() => { - if (success) { - onClose() - action && action() - } - }, [success, action]) - - useEffect(() => { - setSuccess(false) - setText("") - }, [open]) - - useEffect(() => { - if (action) { - setOpen(true) - } - }, [action]) - - return ( - - Please enter your account password to continue - - - Make sure you are online to proceed since we need to authorize you - with the server - -
e.preventDefault()}> - - setText(e.target.value)} - /> - -
- - - - -
- ) -} diff --git a/packages/web/src/auth/LockActionDialog.tsx b/packages/web/src/auth/LockActionDialog.tsx deleted file mode 100644 index 84215b89..00000000 --- a/packages/web/src/auth/LockActionDialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - TextField -} from "@mui/material" -import { crypto } from "@oboku/shared" -import { FC, useEffect, useState } from "react" -import { useAccountSettings } from "../settings/helpers" - -export const LockActionDialog: FC<{ - action?: () => void -}> = ({ action }) => { - const [open, setOpen] = useState(false) - const [text, setText] = useState("") - const { data: accountSettings } = useAccountSettings() - - const onClose = () => { - setOpen(false) - } - - const onConfirm = async () => { - const hashedPassword = await crypto.hashContentPassword(text) - if (accountSettings?.contentPassword === hashedPassword) { - onClose() - action && action() - } - } - - useEffect(() => { - setText("") - }, [open]) - - useEffect(() => { - if (action) { - setOpen(true) - } - }, [action]) - - return ( - - Please enter your content password to continue - - - This is required because the action you want to perform involve your - protected contents - - setText(e.target.value)} - /> - - - - - - - ) -} diff --git a/packages/web/src/auth/UnlockLibraryDialog.tsx b/packages/web/src/auth/UnlockLibraryDialog.tsx index dbeba124..c694e76c 100644 --- a/packages/web/src/auth/UnlockLibraryDialog.tsx +++ b/packages/web/src/auth/UnlockLibraryDialog.tsx @@ -15,7 +15,7 @@ import { PreventAutocompleteFields } from "../common/forms/PreventAutocompleteFi import { useModalNavigationControl } from "../navigation/useModalNavigationControl" import { libraryStateSignal } from "../library/states" import { signal, useSignalValue } from "reactjrx" -import { useAccountSettings } from "../settings/helpers" +import { useSettings } from "../settings/helpers" export const unlockLibraryDialogSignal = signal({ key: "unlockLibraryDialog", @@ -34,7 +34,7 @@ export const UnlockLibraryDialog: FC<{}> = () => { unlockPassword: "" } }) - const { data: accountSettings } = useAccountSettings() + const { data: accountSettings } = useSettings() const isOpened = useSignalValue(unlockLibraryDialogSignal) const contentPassword = accountSettings?.contentPassword const { closeModalWithNavigation } = useModalNavigationControl( diff --git a/packages/web/src/auth/helpers.ts b/packages/web/src/auth/helpers.ts index 321d9e38..5e5e08ce 100644 --- a/packages/web/src/auth/helpers.ts +++ b/packages/web/src/auth/helpers.ts @@ -2,7 +2,6 @@ import { useCallback, useState } from "react" import { API_URI } from "../constants" import { useLock } from "../common/BlockingBackdrop" import { useReCreateDb } from "../rxdb" -import { Report } from "../debug/report.shared" import { createServerError } from "../errors" import { authStateSignal } from "./authState" import { SIGNAL_RESET, useSignalValue } from "reactjrx" @@ -16,38 +15,6 @@ export const useSignOut = () => { }, []) } -export const useAuthorize = () => { - const [lock, unlock] = useLock() - const auth = useSignalValue(authStateSignal) - - return async ({ - variables: { password }, - onSuccess - }: { - variables: { password: string } - onSuccess: () => void - }) => { - try { - lock("authorize") - const response = await fetch(`${API_URI}/signin`, { - method: "POST", - body: JSON.stringify({ email: auth?.email, password }), - headers: { - "Content-Type": "application/json" - } - }) - if (!response.ok) { - throw await createServerError(response) - } - unlock("authorize") - onSuccess() - } catch (e) { - Report.error(e) - unlock("authorize") - } - } -} - export const useSignUp = () => { const [lock, unlock] = useLock() const reCreateDb = useReCreateDb() diff --git a/packages/web/src/books/Cover.tsx b/packages/web/src/books/Cover.tsx index 6131b154..9c3bc036 100644 --- a/packages/web/src/books/Cover.tsx +++ b/packages/web/src/books/Cover.tsx @@ -10,7 +10,7 @@ import { } from "../tags/helpers" import { useCSS } from "../common/utils" import { API_URI } from "../constants" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { normalizedBookDownloadsStateSignal } from "../download/states" import { useSignalValue } from "reactjrx" import { authStateSignal } from "../auth/authState" @@ -71,7 +71,7 @@ export const Cover: FC = memo( const [isLoading, setIsLoading] = useState(true) const classes = useStyle({ withShadow, fullWidth, rounded, isLoading }) const assetHash = book?.lastMetadataUpdatedAt?.toString() - const localSettings = useLocalSettingsState() + const localSettings = useLocalSettings() const shouldBlurCover = book?.isBlurred && blurIfNeeded && diff --git a/packages/web/src/books/ManageBookCollectionsDialog.tsx b/packages/web/src/books/ManageBookCollectionsDialog.tsx index 738b0a9d..34c4ca26 100644 --- a/packages/web/src/books/ManageBookCollectionsDialog.tsx +++ b/packages/web/src/books/ManageBookCollectionsDialog.tsx @@ -4,7 +4,7 @@ import { useAddCollectionToBook, useRemoveCollectionFromBook } from "./helpers" import { useBookState } from "./states" import { CollectionsSelectionDialog } from "../collections/CollectionsSelectionDialog" import { libraryStateSignal } from "../library/states" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useProtectedTagIds, useTagsByIds } from "../tags/helpers" import { SIGNAL_RESET, signal, useSignalValue } from "reactjrx" @@ -34,7 +34,7 @@ export const ManageBookCollectionsDialog: FC<{}> = () => { const open = !!id const collections = useCollectionIdsState({ libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) diff --git a/packages/web/src/books/bookList/BookList.tsx b/packages/web/src/books/bookList/BookList.tsx index 55d763f3..813c3a0a 100644 --- a/packages/web/src/books/bookList/BookList.tsx +++ b/packages/web/src/books/bookList/BookList.tsx @@ -5,7 +5,7 @@ import { BookListGridItem } from "./BookListGridItem" import { LibrarySorting } from "../../library/states" import { LibraryViewMode } from "../../rxdb" import { BookListListItem } from "./BookListListItem" -import { ReactWindowList } from "../../lists/ReactWindowList" +import { ReactWindowList } from "../../common/lists/ReactWindowList" export const BookList: FC<{ viewMode?: "grid" | "list" diff --git a/packages/web/src/books/bookList/BookListWithControls.tsx b/packages/web/src/books/bookList/BookListWithControls.tsx index 48fac241..a89707bd 100644 --- a/packages/web/src/books/bookList/BookListWithControls.tsx +++ b/packages/web/src/books/bookList/BookListWithControls.tsx @@ -1,5 +1,5 @@ import React, { ComponentProps, FC, useState } from "react" -import { ListActionsToolbar } from "../../lists/ListActionsToolbar" +import { ListActionsToolbar } from "../../common/lists/ListActionsToolbar" import { useBookIdsSortedBy } from "../helpers" import { BookList } from "./BookList" diff --git a/packages/web/src/books/bookList/SelectableBookList.tsx b/packages/web/src/books/bookList/SelectableBookList.tsx index aaeef7b7..a9a79bd1 100644 --- a/packages/web/src/books/bookList/SelectableBookList.tsx +++ b/packages/web/src/books/bookList/SelectableBookList.tsx @@ -3,7 +3,7 @@ import { useTheme } from "@mui/material" import { useWindowSize } from "react-use" import { SelectableBookListItem } from "./SelectableBookListItem" import { useCSS } from "../../common/utils" -import { ReactWindowList } from "../../lists/ReactWindowList" +import { ReactWindowList } from "../../common/lists/ReactWindowList" export const SelectableBookList: FC<{ style?: React.CSSProperties diff --git a/packages/web/src/books/details/BookDetailsScreen.tsx b/packages/web/src/books/details/BookDetailsScreen.tsx index 01398030..19011c4a 100644 --- a/packages/web/src/books/details/BookDetailsScreen.tsx +++ b/packages/web/src/books/details/BookDetailsScreen.tsx @@ -40,7 +40,7 @@ import { isDebugEnabled } from "../../debug/isDebugEnabled.shared" import { useRemoveDownloadFile } from "../../download/useRemoveDownloadFile" import { libraryStateSignal } from "../../library/states" import { normalizedBookDownloadsStateSignal } from "../../download/states" -import { useLocalSettingsState } from "../../settings/states" +import { useLocalSettings } from "../../settings/states" import { useProtectedTagIds, useTagsByIds } from "../../tags/helpers" import { useSignalValue } from "reactjrx" @@ -71,7 +71,7 @@ export const BookDetailsScreen = () => { const collections = useBookCollectionsState({ bookId: id, libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data, tags: useTagsByIds().data }) diff --git a/packages/web/src/books/states.ts b/packages/web/src/books/states.ts index 38709b43..f0e2af27 100644 --- a/packages/web/src/books/states.ts +++ b/packages/web/src/books/states.ts @@ -15,7 +15,7 @@ import { getCollectionState, useCollections } from "../collections/states" import { map, switchMap, tap, withLatestFrom } from "rxjs" import { plugin } from "../plugins/local" import { latestDatabase$ } from "../rxdb/useCreateDatabase" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useForeverQuery } from "reactjrx" import { keyBy } from "lodash" import { Database } from "../rxdb" @@ -322,7 +322,7 @@ export const useBookCollectionsState = ({ }: { bookId: string libraryState: ReturnType - localSettingsState: ReturnType + localSettingsState: ReturnType protectedTagIds: ReturnType["data"] tags: ReturnType["data"] }) => { diff --git a/packages/web/src/collections/CollectionActionsDrawer.tsx b/packages/web/src/collections/CollectionActionsDrawer.tsx index 4d447acb..40c8a4dc 100644 --- a/packages/web/src/collections/CollectionActionsDrawer.tsx +++ b/packages/web/src/collections/CollectionActionsDrawer.tsx @@ -25,7 +25,7 @@ import { useModalNavigationControl } from "../navigation/useModalNavigationContr import { useCallback } from "react" import { useRef } from "react" import { libraryStateSignal } from "../library/states" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useProtectedTagIds } from "../tags/helpers" import { signal, useSignalValue } from "reactjrx" @@ -185,7 +185,7 @@ const EditCollectionDialog: FC<{ const collection = useCollectionState({ id: id || "-1", libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) const { mutate: editCollection } = useUpdateCollection() diff --git a/packages/web/src/collections/CollectionDetailsScreen.tsx b/packages/web/src/collections/CollectionDetailsScreen.tsx index 50d9a8b8..e9a8bd9a 100644 --- a/packages/web/src/collections/CollectionDetailsScreen.tsx +++ b/packages/web/src/collections/CollectionDetailsScreen.tsx @@ -7,7 +7,7 @@ import CollectionBgSvg from "../assets/series-bg.svg" import { useCollectionState } from "./states" import { useCollectionActionsDrawer } from "./CollectionActionsDrawer" import { BookListWithControls } from "../books/bookList/BookListWithControls" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useProtectedTagIds } from "../tags/helpers" import { useSignalValue } from "reactjrx" import { libraryStateSignal } from "../library/states" @@ -24,7 +24,7 @@ export const CollectionDetailsScreen = () => { const collection = useCollectionState({ id: id || "-1", libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) const data = diff --git a/packages/web/src/collections/ManageCollectionBooksDialog.tsx b/packages/web/src/collections/ManageCollectionBooksDialog.tsx index 00a1208c..07ec6cec 100644 --- a/packages/web/src/collections/ManageCollectionBooksDialog.tsx +++ b/packages/web/src/collections/ManageCollectionBooksDialog.tsx @@ -9,7 +9,7 @@ import { useMemo } from "react" import { useCallback } from "react" import { BooksSelectionDialog } from "../books/BooksSelectionDialog" import { normalizedBookDownloadsStateSignal } from "../download/states" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useProtectedTagIds } from "../tags/helpers" import { useSignalValue } from "reactjrx" import { libraryStateSignal } from "../library/states" @@ -23,7 +23,7 @@ export const ManageCollectionBooksDialog: FC<{ const collection = useCollectionState({ id: collectionId || "-1", libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) const { data: books } = useBooksAsArrayState({ diff --git a/packages/web/src/collections/list/CollectionList.tsx b/packages/web/src/collections/list/CollectionList.tsx index 381ba890..6875eb8e 100644 --- a/packages/web/src/collections/list/CollectionList.tsx +++ b/packages/web/src/collections/list/CollectionList.tsx @@ -8,7 +8,7 @@ import React, { } from "react" import { List, useTheme } from "@mui/material" import { useCSS } from "../../common/utils" -import { ReactWindowList } from "../../lists/ReactWindowList" +import { ReactWindowList } from "../../common/lists/ReactWindowList" import { CollectionListItemList } from "./CollectionListItemList" import { CollectionDocType } from "@oboku/shared" diff --git a/packages/web/src/collections/list/CollectionListItemList.tsx b/packages/web/src/collections/list/CollectionListItemList.tsx index 7d40acb4..00734a92 100644 --- a/packages/web/src/collections/list/CollectionListItemList.tsx +++ b/packages/web/src/collections/list/CollectionListItemList.tsx @@ -14,7 +14,7 @@ import { useCollectionState } from "../states" import { CollectionDocType } from "@oboku/shared" import { Cover } from "../../books/Cover" import { useCollectionActionsDrawer } from "../CollectionActionsDrawer" -import { useLocalSettingsState } from "../../settings/states" +import { useLocalSettings } from "../../settings/states" import { useProtectedTagIds } from "../../tags/helpers" import { useSignalValue } from "reactjrx" import { libraryStateSignal } from "../../library/states" @@ -39,7 +39,7 @@ export const CollectionListItemList: FC<{ const item = useCollectionState({ id, libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) const { open: openActionDrawer } = useCollectionActionsDrawer(id) diff --git a/packages/web/src/collections/list/SelectableCollectionList.tsx b/packages/web/src/collections/list/SelectableCollectionList.tsx index 139ea3b5..53fed274 100644 --- a/packages/web/src/collections/list/SelectableCollectionList.tsx +++ b/packages/web/src/collections/list/SelectableCollectionList.tsx @@ -1,7 +1,7 @@ import React, { useCallback, FC, useMemo, memo } from "react" import { useTheme } from "@mui/material" import { useCSS } from "../../common/utils" -import { ReactWindowList } from "../../lists/ReactWindowList" +import { ReactWindowList } from "../../common/lists/ReactWindowList" import { SelectableCollectionListItem } from "./SelectableCollectionListItem" export const SelectableCollectionList: FC<{ diff --git a/packages/web/src/collections/states.ts b/packages/web/src/collections/states.ts index 8bb69aef..f0c3199e 100644 --- a/packages/web/src/collections/states.ts +++ b/packages/web/src/collections/states.ts @@ -1,6 +1,6 @@ import { CollectionDocType, directives } from "@oboku/shared" import { useVisibleBookIdsState } from "../books/states" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useProtectedTagIds } from "../tags/helpers" import { libraryStateSignal } from "../library/states" import { useForeverQuery } from "reactjrx" @@ -38,7 +38,7 @@ export const useCollectionsAsArrayState = ({ protectedTagIds = [] }: { libraryState: ReturnType - localSettingsState: ReturnType + localSettingsState: ReturnType protectedTagIds: ReturnType["data"] }) => { const localSettings = localSettingsState @@ -84,7 +84,7 @@ export const useCollectionIdsState = ({ protectedTagIds = [] }: { libraryState: ReturnType - localSettingsState: ReturnType + localSettingsState: ReturnType protectedTagIds: ReturnType["data"] }) => { return useCollectionsAsArrayState({ @@ -101,7 +101,7 @@ export const getCollectionState = ({ bookIds }: { id: string - localSettingsState: ReturnType + localSettingsState: ReturnType normalizedCollections: ReturnType["data"] bookIds: ReturnType }) => { @@ -130,7 +130,7 @@ export const useCollectionState = ({ }: { id: string libraryState: ReturnType - localSettingsState: ReturnType + localSettingsState: ReturnType protectedTagIds: ReturnType["data"] }) => { const { data: normalizedCollections } = useCollections() diff --git a/packages/web/src/lists/ListActionsToolbar.tsx b/packages/web/src/common/lists/ListActionsToolbar.tsx similarity index 94% rename from packages/web/src/lists/ListActionsToolbar.tsx rename to packages/web/src/common/lists/ListActionsToolbar.tsx index 9ec171b4..f8b363a4 100644 --- a/packages/web/src/lists/ListActionsToolbar.tsx +++ b/packages/web/src/common/lists/ListActionsToolbar.tsx @@ -6,9 +6,9 @@ import { LockOpenRounded, SortRounded } from "@mui/icons-material" -import { SortByDialog } from "../books/bookList/SortByDialog" +import { SortByDialog } from "../../books/bookList/SortByDialog" import { useSignalValue } from "reactjrx" -import { libraryStateSignal } from "../library/states" +import { libraryStateSignal } from "../../library/states" type Sorting = ComponentProps["value"] diff --git a/packages/web/src/lists/ReactWindowList.tsx b/packages/web/src/common/lists/ReactWindowList.tsx similarity index 99% rename from packages/web/src/lists/ReactWindowList.tsx rename to packages/web/src/common/lists/ReactWindowList.tsx index aff024f0..ca88d1f4 100644 --- a/packages/web/src/lists/ReactWindowList.tsx +++ b/packages/web/src/common/lists/ReactWindowList.tsx @@ -13,7 +13,7 @@ import { VariableSizeList } from "react-window" import AutoSizer from "react-virtualized-auto-sizer" -import { useCSS } from "../common/utils" +import { useCSS } from "../utils" import { useTheme } from "@mui/material" export const ReactWindowList: FC<{ diff --git a/packages/web/src/library/LibraryBooksScreen.tsx b/packages/web/src/library/LibraryBooksScreen.tsx index 3e2d264c..d24784fd 100644 --- a/packages/web/src/library/LibraryBooksScreen.tsx +++ b/packages/web/src/library/LibraryBooksScreen.tsx @@ -28,7 +28,7 @@ import { } from "./states" import { UploadBookDrawer } from "./UploadBookDrawer" import { SortByDialog } from "../books/bookList/SortByDialog" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useCallback } from "react" import { useTranslation } from "react-i18next" import { useBooks } from "./useBooks" @@ -44,7 +44,7 @@ export const LibraryBooksScreen = () => { isUploadBookDrawerOpenedStateSignal ) const [isSortingDialogOpened, setIsSortingDialogOpened] = useState(false) - const localSettings = useLocalSettingsState() + const localSettings = useLocalSettings() const [ isUploadBookFromDataSourceDialogOpened, setIsUploadBookFromDataSourceDialogOpened diff --git a/packages/web/src/library/LibraryCollectionScreen.tsx b/packages/web/src/library/LibraryCollectionScreen.tsx index 34543862..0e356e01 100644 --- a/packages/web/src/library/LibraryCollectionScreen.tsx +++ b/packages/web/src/library/LibraryCollectionScreen.tsx @@ -16,7 +16,7 @@ import { useCollectionIdsState } from "../collections/states" import { useCSS, useMeasureElement } from "../common/utils" import { CollectionList } from "../collections/list/CollectionList" import { useDebouncedCallback } from "use-debounce" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" import { useProtectedTagIds } from "../tags/helpers" import { signal, useSignalValue } from "reactjrx" import { libraryStateSignal } from "./states" @@ -47,7 +47,7 @@ export const LibraryCollectionScreen = () => { const libraryState = useSignalValue(libraryStateSignal) const collections = useCollectionIdsState({ libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) diff --git a/packages/web/src/library/LibraryTagsScreen.tsx b/packages/web/src/library/LibraryTagsScreen.tsx index fa19afa7..dd6d4092 100644 --- a/packages/web/src/library/LibraryTagsScreen.tsx +++ b/packages/web/src/library/LibraryTagsScreen.tsx @@ -11,7 +11,6 @@ import { } from "@mui/material" import { useCreateTag } from "../tags/helpers" import { TagActionsDrawer } from "../tags/TagActionsDrawer" -import { LockActionDialog } from "../auth/LockActionDialog" import { useCSS, useMeasureElement } from "../common/utils" import { TagList } from "../tags/tagList/TagList" import { AppTourFirstTourTagsStep2 } from "../firstTimeExperience/AppTourFirstTourTags" @@ -19,11 +18,9 @@ import { useTagIds } from "../tags/helpers" import { Controller, SubmitHandler, useForm } from "react-hook-form" import { errorToHelperText } from "../common/forms/errorToHelperText" import { isTagsTourPossibleStateSignal } from "../firstTimeExperience/firstTimeExperienceStates" +import { authorizeAction } from "../auth/AuthorizeActionDialog" export const LibraryTagsScreen = () => { - const [lockedAction, setLockedAction] = useState<(() => void) | undefined>( - undefined - ) const classes = useStyles() const [isAddTagDialogOpened, setIsAddTagDialogOpened] = useState(false) const [isTagActionsDrawerOpenedWith, setIsTagActionsDrawerOpenedWith] = @@ -104,7 +101,7 @@ export const LibraryTagsScreen = () => { onItemClick={(tag) => { const action = () => setIsTagActionsDrawerOpenedWith(tag?._id) if (tag?.isProtected) { - setLockedAction((_) => action) + authorizeAction(action) } else { action() } @@ -119,7 +116,6 @@ export const LibraryTagsScreen = () => { onClose={() => setIsAddTagDialogOpened(false)} open={isAddTagDialogOpened} /> - setIsTagActionsDrawerOpenedWith(undefined)} @@ -139,7 +135,7 @@ const AddTagDialog: FC<{ onConfirm: (name: string) => void onClose: () => void }> = ({ onClose, onConfirm, open }) => { - const { control, handleSubmit, setFocus, reset, setError } = useForm({ + const { control, handleSubmit, setFocus, reset } = useForm({ defaultValues: { name: "" } diff --git a/packages/web/src/library/UploadBookDrawer.tsx b/packages/web/src/library/UploadBookDrawer.tsx index 4ca5a3e2..c54fc940 100644 --- a/packages/web/src/library/UploadBookDrawer.tsx +++ b/packages/web/src/library/UploadBookDrawer.tsx @@ -8,13 +8,13 @@ import { } from "@mui/material" import { SdStorageRounded } from "@mui/icons-material" import { plugins as dataSourcePlugins } from "../dataSources" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" export const UploadBookDrawer: FC<{ open: boolean onClose: (type?: "device" | string | undefined) => void }> = ({ open, onClose }) => { - const { showSensitiveDataSources } = useLocalSettingsState() + const { showSensitiveDataSources } = useLocalSettings() return ( <> diff --git a/packages/web/src/navigation/TopBarNavigation.tsx b/packages/web/src/navigation/TopBarNavigation.tsx index 08b3ad93..645f195a 100644 --- a/packages/web/src/navigation/TopBarNavigation.tsx +++ b/packages/web/src/navigation/TopBarNavigation.tsx @@ -40,7 +40,11 @@ export const TopBarNavigation: FC<{ const navigate = useNavigate() return ( - + <> {showBack && ( diff --git a/packages/web/src/reader/fullScreen.ts b/packages/web/src/reader/fullScreen.ts index 94be5458..101987f1 100644 --- a/packages/web/src/reader/fullScreen.ts +++ b/packages/web/src/reader/fullScreen.ts @@ -2,10 +2,10 @@ import { useEffect } from "react" import screenfull from "screenfull" import { IS_MOBILE_DEVICE } from "../constants" import { Report } from "../debug/report.shared" -import { useLocalSettingsState } from "../settings/states" +import { useLocalSettings } from "../settings/states" export const useFullScreenSwitch = () => { - const localSettings = useLocalSettingsState() + const localSettings = useLocalSettings() useEffect(() => { if ( diff --git a/packages/web/src/settings/ProfileScreen.tsx b/packages/web/src/settings/ProfileScreen.tsx index b66aa707..8ed21030 100644 --- a/packages/web/src/settings/ProfileScreen.tsx +++ b/packages/web/src/settings/ProfileScreen.tsx @@ -26,13 +26,18 @@ import { TextField, Typography, useTheme, - FormControlLabel + FormControlLabel, + ListItemButton } from "@mui/material" import { useNavigate } from "react-router-dom" import { useStorageUse } from "./useStorageUse" -import { LockActionBehindUserPasswordDialog } from "../auth/LockActionBehindUserPasswordDialog" +import { authorizeAction } from "../auth/AuthorizeActionDialog" import { useSignOut } from "../auth/helpers" -import { useAccountSettings, useUpdateContentPassword } from "./helpers" +import { + useSettings, + useUpdateContentPassword, + useUpdateSettings +} from "./helpers" import { libraryStateSignal } from "../library/states" import packageJson from "../../package.json" import { ROUTES } from "../constants" @@ -49,24 +54,20 @@ import { authStateSignal } from "../auth/authState" export const ProfileScreen = () => { const navigate = useNavigate() - const [lockedAction, setLockedAction] = useState<(() => void) | undefined>( - undefined - ) const [ isEditContentPasswordDialogOpened, setIsEditContentPasswordDialogOpened ] = useState(false) const [isDeleteMyDataDialogOpened, setIsDeleteMyDataDialogOpened] = useState(false) - const [isLoadLibraryDebugOpened, setIsLoadLibraryDebugOpened] = - useState(false) const { quotaUsed, quotaInGb, usedInMb } = useStorageUse([]) const auth = useSignalValue(authStateSignal) - const { data: accountSettings } = useAccountSettings() + const { data: accountSettings } = useSettings() const library = useSignalValue(libraryStateSignal) const signOut = useSignOut() const theme = useTheme() const dialog = useDialogManager() + const { mutate: updateSettings } = useUpdateSettings() return (
{ button onClick={() => { if (accountSettings?.contentPassword) { - setLockedAction( - (_) => () => setIsEditContentPasswordDialogOpened(true) - ) + authorizeAction(() => setIsEditContentPasswordDialogOpened(true)) } else { setIsEditContentPasswordDialogOpened(true) } }} > + { @@ -269,6 +269,19 @@ export const ProfileScreen = () => { } style={{ backgroundColor: alpha(theme.palette.error.light, 0.2) }} > + {!!accountSettings?.contentPassword && ( + { + authorizeAction(() => { + updateSettings({ + contentPassword: null + }) + }) + }} + > + + + )} navigate(ROUTES.PROBLEMS)}> { - setIsEditContentPasswordDialogOpened(false)} @@ -417,7 +429,7 @@ const EditContentPasswordDialog: FC<{ onClose: () => void }> = ({ onClose, open }) => { const updatePassword = useUpdateContentPassword() - const { data: accountSettings } = useAccountSettings() + const { data: accountSettings } = useSettings() const [text, setText] = useState("") const contentPassword = accountSettings?.contentPassword || "" diff --git a/packages/web/src/settings/SettingsScreen.tsx b/packages/web/src/settings/SettingsScreen.tsx index e22f3628..3d0bd275 100644 --- a/packages/web/src/settings/SettingsScreen.tsx +++ b/packages/web/src/settings/SettingsScreen.tsx @@ -13,9 +13,9 @@ import { ListItemText, ListSubheader } from "@mui/material" -import { localSettingsStateSignal, useLocalSettingsState } from "./states" +import { localSettingsSignal, useLocalSettings } from "./states" -type LocalSettings = ReturnType +type LocalSettings = ReturnType const fullScreenModes: Record< LocalSettings["readingFullScreenSwitchMode"], @@ -34,7 +34,7 @@ const showCollectionWithProtectedContentLabels: Record< } export const SettingsScreen = memo(() => { - const localSettings = useLocalSettingsState() + const localSettings = useLocalSettings() const [isDrawerOpened, setIsDrawerOpened] = useState(false) const [isShowCollectionDrawerOpened, setIsShowCollectionDrawerOpened] = useState(false) @@ -51,7 +51,7 @@ export const SettingsScreen = memo(() => { { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, hideDirectivesFromCollectionName: !old.hideDirectivesFromCollectionName @@ -78,7 +78,7 @@ export const SettingsScreen = memo(() => { { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, showSensitiveDataSources: !old.showSensitiveDataSources })) @@ -114,7 +114,7 @@ export const SettingsScreen = memo(() => { { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, unBlurWhenProtectedVisible: !old.unBlurWhenProtectedVisible })) @@ -155,7 +155,7 @@ export const SettingsScreen = memo(() => { { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, useOptimizedTheme: !old.useOptimizedTheme })) @@ -190,7 +190,7 @@ export const SettingsScreen = memo(() => { button key={text} onClick={() => { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, readingFullScreenSwitchMode: text })) @@ -206,7 +206,7 @@ export const SettingsScreen = memo(() => { open={isDrawerOpened} onClose={() => setIsDrawerOpened(false)} onChoiceSelect={(value) => { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, readingFullScreenSwitchMode: value })) @@ -224,7 +224,7 @@ export const SettingsScreen = memo(() => { open={isShowCollectionDrawerOpened} onClose={() => setIsShowCollectionDrawerOpened(false)} onChoiceSelect={(value) => { - localSettingsStateSignal.setValue((old) => ({ + localSettingsSignal.setValue((old) => ({ ...old, showCollectionWithProtectedContent: value })) diff --git a/packages/web/src/settings/StatisticsScreen.tsx b/packages/web/src/settings/StatisticsScreen.tsx index c678e29e..c2a4609d 100644 --- a/packages/web/src/settings/StatisticsScreen.tsx +++ b/packages/web/src/settings/StatisticsScreen.tsx @@ -3,7 +3,7 @@ import { Box, List, ListItem, ListItemText, ListSubheader } from "@mui/material" import { useBookIdsState } from "../books/states" import { useCollectionsAsArrayState } from "../collections/states" import { libraryStateSignal } from "../library/states" -import { useLocalSettingsState } from "./states" +import { useLocalSettings } from "./states" import { useProtectedTagIds } from "../tags/helpers" import { useSignalValue } from "reactjrx" @@ -12,7 +12,7 @@ export const StatisticsScreen = () => { const libraryState = useSignalValue(libraryStateSignal) const collectionsAsArray = useCollectionsAsArrayState({ libraryState, - localSettingsState: useLocalSettingsState(), + localSettingsState: useLocalSettings(), protectedTagIds: useProtectedTagIds().data }) diff --git a/packages/web/src/settings/helpers.ts b/packages/web/src/settings/helpers.ts index 5933e28c..83064847 100644 --- a/packages/web/src/settings/helpers.ts +++ b/packages/web/src/settings/helpers.ts @@ -1,23 +1,66 @@ import { crypto } from "@oboku/shared" -import { useDatabase } from "../rxdb" -import { useForeverQuery } from "reactjrx" -import { latestDatabase$ } from "../rxdb/useCreateDatabase" -import { map, switchMap } from "rxjs" +import { Database, SettingsDocType } from "../rxdb" +import { useForeverQuery, useMutation } from "reactjrx" +import { getLatestDatabase, latestDatabase$ } from "../rxdb/useCreateDatabase" +import { from, map, mergeMap, of, switchMap } from "rxjs" + +export const getSettings = (database: Database) => { + return database.settings + .findOne({ + selector: { + _id: "settings" + } + }) + .exec() +} + +export const getSettingsOrThrow = async (database: Database) => { + const settings = await getSettings(database) + + if (!settings) throw new Error("Settings not found") + + return settings +} export const useUpdateContentPassword = () => { - const { db } = useDatabase() + const { mutate: updateSettings } = useUpdateSettings() - return async (password: string) => { - const hashed = await crypto.hashContentPassword(password) + return (password: string) => { + const hashed = crypto.hashContentPassword(password) - await db?.settings.safeUpdate( - { $set: { contentPassword: hashed } }, - (collection) => collection.findOne() - ) + updateSettings({ + contentPassword: hashed + }) } } -export const useAccountSettings = ( +export const useValidateAppPassword = (options: { + onSuccess: () => void + onError: () => void +}) => { + return useMutation({ + ...options, + mapOperator: "switch", + mutationFn: (input: string) => { + if (!input) throw new Error("Invalid password") + + return getLatestDatabase().pipe( + mergeMap((database) => from(getSettingsOrThrow(database))), + mergeMap((settings) => { + const hashedInput = crypto.hashContentPassword(input) + + if (hashedInput !== settings.contentPassword) { + throw new Error("Invalid password") + } + + return of(null) + }) + ) + } + }) +} + +export const useSettings = ( options: { enabled?: boolean } = {} @@ -35,9 +78,24 @@ export const useAccountSettings = ( * Since the query is a live stream the data are always fresh anyway. */ gcTime: Infinity, - staleTime: Infinity, ...options }) return data } + +export const useUpdateSettings = () => { + return useMutation({ + mutationFn: (data: Partial) => + getLatestDatabase().pipe( + mergeMap((database) => getSettingsOrThrow(database)), + mergeMap((settings) => + from( + settings?.update({ + $set: data + }) + ) + ) + ) + }) +} diff --git a/packages/web/src/settings/states.ts b/packages/web/src/settings/states.ts index 8d538e0a..5ab466b7 100644 --- a/packages/web/src/settings/states.ts +++ b/packages/web/src/settings/states.ts @@ -11,7 +11,7 @@ const localSettingsStateDefaultValues = { showSensitiveDataSources: false } -export const localSettingsStateSignal = signal<{ +export const localSettingsSignal = signal<{ useOptimizedTheme: boolean readingFullScreenSwitchMode: "automatic" | "always" | "never" unBlurWhenProtectedVisible: boolean @@ -23,7 +23,7 @@ export const localSettingsStateSignal = signal<{ default: localSettingsStateDefaultValues }) -export const localSettingsStatePersist = localSettingsStateSignal +export const localSettingsStatePersist = localSettingsSignal -export const useLocalSettingsState = () => - useSignalValue(localSettingsStateSignal) +export const useLocalSettings = () => + useSignalValue(localSettingsSignal) diff --git a/packages/web/src/tags/tagList/SelectableTagList.tsx b/packages/web/src/tags/tagList/SelectableTagList.tsx index ba4fe5aa..a5ef498c 100644 --- a/packages/web/src/tags/tagList/SelectableTagList.tsx +++ b/packages/web/src/tags/tagList/SelectableTagList.tsx @@ -1,7 +1,7 @@ import React, { useCallback, FC, useMemo, memo } from "react" import { useTheme } from "@mui/material" import { useCSS } from "../../common/utils" -import { ReactWindowList } from "../../lists/ReactWindowList" +import { ReactWindowList } from "../../common/lists/ReactWindowList" import { SelectableTagListItem } from "./SelectableTagListItem" export const SelectableTagList: FC<{ diff --git a/packages/web/src/tags/tagList/TagList.tsx b/packages/web/src/tags/tagList/TagList.tsx index dd74d337..6791261c 100644 --- a/packages/web/src/tags/tagList/TagList.tsx +++ b/packages/web/src/tags/tagList/TagList.tsx @@ -1,7 +1,7 @@ import React, { useCallback, FC, useMemo, memo } from "react" import { useTheme } from "@mui/material" import { useCSS } from "../../common/utils" -import { ReactWindowList } from "../../lists/ReactWindowList" +import { ReactWindowList } from "../../common/lists/ReactWindowList" import { TagListItemList } from "./TagListItemList" import { TagsDocType } from "@oboku/shared" diff --git a/packages/web/src/theme/ThemeProvider.tsx b/packages/web/src/theme/ThemeProvider.tsx new file mode 100644 index 00000000..975ae4a2 --- /dev/null +++ b/packages/web/src/theme/ThemeProvider.tsx @@ -0,0 +1,15 @@ +import { CssBaseline, ThemeProvider as MuiThemeProvider } from "@mui/material" +import { eInkTheme, theme } from "./theme" +import { ReactNode, memo } from "react" +import { useLocalSettings } from "../settings/states" + +export const ThemeProvider = memo(({ children }: { children: ReactNode }) => { + const { useOptimizedTheme } = useLocalSettings() + + return ( + + + {children} + + ) +}) diff --git a/packages/web/src/theme.tsx b/packages/web/src/theme/theme.tsx similarity index 68% rename from packages/web/src/theme.tsx rename to packages/web/src/theme/theme.tsx index bd0b4292..eaa58e96 100644 --- a/packages/web/src/theme.tsx +++ b/packages/web/src/theme/theme.tsx @@ -3,6 +3,7 @@ * @see https://material-ui.com/customization/palette/ */ import { createTheme, alpha } from "@mui/material/styles" +import { deepmerge } from "@mui/utils" declare module "@mui/material/styles" { interface Theme { @@ -20,10 +21,6 @@ declare module "@mui/material/styles" { } export const theme = createTheme({ - transitions: { - // So we have `transition: none;` everywhere - // create: () => "none" - }, palette: { mode: `light`, primary: { @@ -31,14 +28,6 @@ export const theme = createTheme({ main: "#e16432", // #e16432 dark: `#9D4623` } - // text: { - // primary: 'rgb(255, 255, 255)', - // }, - // secondary: { - // light, - // main: "rgb(225, 100, 50, 1)" - // dark, - // } }, components: { /** @@ -61,15 +50,6 @@ export const theme = createTheme({ } }, // Name of the component ⚛️ - MuiCssBaseline: { - // Name of the rule - // "@global": { - // "*, *::before, *::after": { - // transition: "none !important", - // animation: "none !important" - // } - // } - }, MuiBottomNavigationAction: { styleOverrides: { root: { @@ -91,28 +71,7 @@ export const theme = createTheme({ minWidth: 260 } } - }, - // MuiBottomNavigationAction: { - // root: { - // paddingTop: '0 !important', - // } - // } - MuiButtonBase: { - styleOverrides: { - root: { - // color: '#fff', - } - } } - // MuiButton: { - // root: { - // color: "#fff" - // }, - // outlined: { - // border: "1px solid rgba(255, 255, 255, 1)" - // // color: '#fff', - // } - // } }, custom: { maxWidthCenteredContent: 320, @@ -123,3 +82,32 @@ export const theme = createTheme({ coverAverageRatio: 9 / 14 } }) + +export const eInkTheme = createTheme( + deepmerge(theme, { + transitions: { + // So we have `transition: none;` everywhere + create: () => "none" + }, + palette: { + text: { + primary: "#000000", + secondary: "#000000" + }, + primary: { + main: "#fff", + contrastText: "#000000" + } + }, + components: { + MuiAppBar: { + styleOverrides: { + root: { + borderBottom: "1px solid black" + } + } + } + }, + custom: theme.custom + } satisfies Parameters[0]) +)