From fb48bab7b07f7ec0099c35a5869848aa2d30e288 Mon Sep 17 00:00:00 2001 From: maxime Date: Thu, 14 Mar 2024 11:54:23 +0100 Subject: [PATCH] feat: fixed fullscreen switch --- .../web/src/auth/AuthorizeActionDialog.tsx | 15 ++++ .../useRefreshCollectionMetadata.ts | 2 +- packages/web/src/dialog.tsx | 56 ++++++++----- .../src/fullscreen/useFullscreenOnMount.ts | 84 +++++++++++++++++++ packages/web/src/reader/ReaderScreen.tsx | 4 +- packages/web/src/reader/fullScreen.ts | 32 ++----- .../web/src/settings/useRemoveAllContents.ts | 33 +++----- 7 files changed, 155 insertions(+), 71 deletions(-) create mode 100644 packages/web/src/fullscreen/useFullscreenOnMount.ts diff --git a/packages/web/src/auth/AuthorizeActionDialog.tsx b/packages/web/src/auth/AuthorizeActionDialog.tsx index 23685f87..674ae1c5 100644 --- a/packages/web/src/auth/AuthorizeActionDialog.tsx +++ b/packages/web/src/auth/AuthorizeActionDialog.tsx @@ -12,6 +12,8 @@ import { useValidateAppPassword } from "../settings/helpers" 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" const FORM_ID = "LockActionBehindUserPasswordDialog" @@ -29,6 +31,18 @@ export const authorizeAction = (action: () => void, onCancel?: () => void) => onCancel }) +export function withAuthorization(stream: Observable) { + return stream.pipe( + mergeMap(() => + from( + new Promise((resolve, reject) => + authorizeAction(resolve, () => reject(new CancelError())) + ) + ) + ) + ) +} + export const AuthorizeActionDialog: FC<{}> = () => { const { action, onCancel = () => {} } = useSignalValue(actionSignal) ?? {} const open = !!action @@ -37,6 +51,7 @@ export const AuthorizeActionDialog: FC<{}> = () => { password: "" } }) + const { mutate: validatePassword, reset: resetValidatePasswordMutation, diff --git a/packages/web/src/collections/useRefreshCollectionMetadata.ts b/packages/web/src/collections/useRefreshCollectionMetadata.ts index f1c926f2..77c0991b 100644 --- a/packages/web/src/collections/useRefreshCollectionMetadata.ts +++ b/packages/web/src/collections/useRefreshCollectionMetadata.ts @@ -1,4 +1,4 @@ -import { catchError, from, map, of, switchMap, throwError } from "rxjs" +import { catchError, from, map, of, switchMap } from "rxjs" import { usePluginRefreshMetadata } from "../plugins/usePluginRefreshMetadata" import { useSyncReplicate } from "../rxdb/replication/useSyncReplicate" import { useUpdateCollection } from "./useUpdateCollection" diff --git a/packages/web/src/dialog.tsx b/packages/web/src/dialog.tsx index 6620928a..15aea9ae 100644 --- a/packages/web/src/dialog.tsx +++ b/packages/web/src/dialog.tsx @@ -15,7 +15,8 @@ import { useMemo, useState } from "react" -import { signal } from "reactjrx" +import { Subject, lastValueFrom, map, merge, of } from "rxjs" +import { CancelError } from "./errors" type Preset = "NOT_IMPLEMENTED" | "OFFLINE" | "CONFIRM" | "UNKNOWN_ERROR" @@ -34,17 +35,13 @@ type DialogType = { onCancel?: () => void } -const dialogUpdateSignal = signal<{ id: string; state: "closed" } | undefined>( - {} -) - const DialogContext = createContext([]) const ManageDialogContext = createContext({ remove: (id: string) => {}, add: (options: Omit) => ({ id: "-1" as string, - promise: Promise.resolve() + $: of({}) }) }) @@ -109,13 +106,13 @@ const InnerDialog = () => { }, [remove, currentDialog]) const onCancel = useCallback(() => { - handleClose() currentDialog?.onCancel && currentDialog.onCancel() + handleClose() }, [handleClose, currentDialog]) const onConfirm = useCallback(() => { - handleClose() currentDialog?.onConfirm && currentDialog.onConfirm() + handleClose() }, [handleClose, currentDialog]) const actions = currentDialog?.actions || [ @@ -174,27 +171,44 @@ export const DialogProvider: FC<{ children: ReactNode }> = ({ children }) => { const remove = useCallback((id: string) => { setDialogs((old) => old.filter((dialog) => id !== dialog.id)) - - dialogUpdateSignal.setValue({ id, state: "closed" }) }, []) const add = useCallback((options: Omit) => { generatedId++ - setDialogs((old) => [...old, { ...options, id: generatedId.toString() }]) + 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?.() + } + } - const id = generatedId.toString() + setDialogs((old) => [...old, newDialog]) - const promise = new Promise((resolve) => { - const sub = dialogUpdateSignal.subject.subscribe((value) => { - if (value?.id === id && value.state === "closed") { - sub.unsubscribe() - resolve() - } - }) - }) + const $ = merge( + cancel.pipe( + map(() => { + throw new CancelError() + }) + ), + confirm.pipe(map(() => ({}))) + ) - return { id: generatedId.toString(), promise } + return { id: newDialog.id, $ } }, []) const controls = useMemo( diff --git a/packages/web/src/fullscreen/useFullscreenOnMount.ts b/packages/web/src/fullscreen/useFullscreenOnMount.ts new file mode 100644 index 00000000..83c72924 --- /dev/null +++ b/packages/web/src/fullscreen/useFullscreenOnMount.ts @@ -0,0 +1,84 @@ +import { useEffect } from "react" +import screenfull from "screenfull" +import { Report } from "../debug/report.shared" +import { useDialogManager } from "../dialog" +import { + EMPTY, + catchError, + defer, + endWith, + from, + mergeMap, + retry, + throwError, + timer +} from "rxjs" +import { useSubscribe } from "reactjrx" +import { CancelError } from "../errors" + +const isPermissionCheckFailedError = (error: unknown): error is TypeError => + error instanceof TypeError && + // chrome + (error.message === "Permissions check failed" || + // safari + error.message === "Type error" || + // firefox + error.message === "Fullscreen request denied") + +export const useFullscreenOnMount = ({ enabled }: { enabled: boolean }) => { + const dialog = useDialogManager() + + useSubscribe(() => { + if (enabled && screenfull.isEnabled && !screenfull.isFullscreen) { + return defer(() => { + return from(screenfull.request(undefined, { navigationUI: "hide" })) + }).pipe( + retry({ + count: 1, + delay: (error) => { + if (isPermissionCheckFailedError(error)) { + return timer(5).pipe( + mergeMap(() => + // we avoid double dialog because of strict mode + screenfull.isFullscreen + ? throwError(() => error) + : dialog({ + title: "Fullscreen request", + content: + "Your browser does not allow automatic fullscreen without an interaction", + confirmTitle: "Fullscreen", + cancellable: true + }).$.pipe(endWith(true)) + ) + ) + } + + throw error + } + }), + catchError((error) => { + if ( + isPermissionCheckFailedError(error) || + error instanceof CancelError + ) { + return EMPTY + } + + Report.error(error) + + return EMPTY + }) + ) + } + + return EMPTY + }, [enabled, dialog]) + + useEffect(() => { + return () => { + if (screenfull.isEnabled && screenfull.isFullscreen) { + screenfull.exit().catch(Report.error) + } + } + }, []) +} diff --git a/packages/web/src/reader/ReaderScreen.tsx b/packages/web/src/reader/ReaderScreen.tsx index f8824163..023230ed 100644 --- a/packages/web/src/reader/ReaderScreen.tsx +++ b/packages/web/src/reader/ReaderScreen.tsx @@ -2,7 +2,7 @@ import { FC, useEffect } from "react" import { useParams } from "react-router-dom" import { AppTourReader } from "../firstTimeExperience/AppTourReader" import { useWakeLock } from "../common/useWakeLock" -import { useFullScreenSwitch } from "./fullScreen" +import { useFullscreenAutoSwitch } from "./fullScreen" import { Reader } from "./Reader" import { MoreDialog } from "./MoreDialog" import { useTrackBookBeingRead } from "../reading/useTrackBookBeingRead" @@ -19,7 +19,7 @@ export const ReaderScreen: FC<{}> = () => { useTrackBookBeingRead(bookId) useWakeLock() - useFullScreenSwitch() + useFullscreenAutoSwitch() useEffect(() => () => { ;[ diff --git a/packages/web/src/reader/fullScreen.ts b/packages/web/src/reader/fullScreen.ts index 101987f1..cd5c89eb 100644 --- a/packages/web/src/reader/fullScreen.ts +++ b/packages/web/src/reader/fullScreen.ts @@ -1,29 +1,13 @@ -import { useEffect } from "react" -import screenfull from "screenfull" import { IS_MOBILE_DEVICE } from "../constants" -import { Report } from "../debug/report.shared" import { useLocalSettings } from "../settings/states" +import { useFullscreenOnMount } from "../fullscreen/useFullscreenOnMount" -export const useFullScreenSwitch = () => { - const localSettings = useLocalSettings() +export const useFullscreenAutoSwitch = () => { + const { readingFullScreenSwitchMode } = useLocalSettings() - useEffect(() => { - if ( - (localSettings.readingFullScreenSwitchMode === "always" || - (localSettings.readingFullScreenSwitchMode === "automatic" && - IS_MOBILE_DEVICE)) && - screenfull.isEnabled && - !screenfull.isFullscreen - ) { - screenfull - .request(undefined, { navigationUI: "hide" }) - .catch(Report.error) - } - - return () => { - if (screenfull.isEnabled && screenfull.isFullscreen) { - screenfull.exit().catch(Report.error) - } - } - }, [localSettings]) + useFullscreenOnMount({ + enabled: + readingFullScreenSwitchMode === "always" || + (readingFullScreenSwitchMode === "automatic" && IS_MOBILE_DEVICE) + }) } diff --git a/packages/web/src/settings/useRemoveAllContents.ts b/packages/web/src/settings/useRemoveAllContents.ts index fb8b85a0..0df61dd5 100644 --- a/packages/web/src/settings/useRemoveAllContents.ts +++ b/packages/web/src/settings/useRemoveAllContents.ts @@ -4,7 +4,7 @@ import { catchError, combineLatest, from, map, mergeMap, of, tap } from "rxjs" import { useSyncReplicate } from "../rxdb/replication/useSyncReplicate" import { useLock } from "../common/BlockingBackdrop" import { useDialogManager } from "../dialog" -import { authorizeAction } from "../auth/AuthorizeActionDialog" +import { withAuthorization } from "../auth/AuthorizeActionDialog" import { Report } from "../debug/report.shared" import { CancelError } from "../errors" @@ -33,30 +33,16 @@ export const useRemoveAllContents = () => { tagCount, dataSourceCount ]) => { - const confirmed$ = from( - new Promise((resolve, reject) => { - dialog({ - 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`, - canEscape: true, - cancellable: true, - onConfirm: resolve, - onCancel() { - reject(new CancelError()) - } - }) - }) - ) + const confirmed$ = dialog({ + 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`, + canEscape: true, + cancellable: true + }).$ return confirmed$.pipe( - mergeMap(() => - from( - new Promise((resolve, reject) => - authorizeAction(resolve, () => reject(new CancelError())) - ) - ) - ), + withAuthorization, map(() => lock()), mergeMap((unlock) => from( @@ -102,6 +88,7 @@ export const useRemoveAllContents = () => { 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" }) + throw e }) )