feat: handle android back button on android (#1656)
This commit is contained in:
@@ -26,6 +26,7 @@ import { isDesktopApplication } from '@/Utils'
|
||||
import { DesktopManager } from './Device/DesktopManager'
|
||||
import { ArchiveManager, AutolockService, IOService, WebAlertService, ThemeManager } from '@standardnotes/ui-services'
|
||||
import { MobileWebReceiver } from './MobileWebReceiver'
|
||||
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
||||
|
||||
type WebServices = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -45,6 +46,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
public iconsController: IconsController
|
||||
private onVisibilityChange: () => void
|
||||
private mobileWebReceiver?: MobileWebReceiver
|
||||
private androidBackHandler?: AndroidBackHandler
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
@@ -76,6 +78,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
if (this.isNativeMobileWeb()) {
|
||||
this.mobileWebReceiver = new MobileWebReceiver(this)
|
||||
this.androidBackHandler = new AndroidBackHandler()
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log = (...args) => {
|
||||
@@ -264,4 +267,17 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
await this.lock()
|
||||
}
|
||||
}
|
||||
|
||||
handleAndroidBackButtonPressed(): void {
|
||||
if (typeof this.androidBackHandler !== 'undefined') {
|
||||
this.androidBackHandler.notifyEvent()
|
||||
}
|
||||
}
|
||||
|
||||
addAndroidBackHandlerEventListener(listener: () => boolean) {
|
||||
if (typeof this.androidBackHandler !== 'undefined') {
|
||||
return this.androidBackHandler.addEventListener(listener)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ export class MobileWebReceiver {
|
||||
case ReactNativeToWebEvent.ResumingFromBackground:
|
||||
void this.application.handleMobileResumingFromBackgroundEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.AndroidBackButtonPressed:
|
||||
void this.application.handleAndroidBackButtonPressed()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -25,6 +25,7 @@ import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
|
||||
import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
||||
|
||||
type Props = {
|
||||
@@ -175,77 +176,79 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
}
|
||||
|
||||
return (
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<ResponsivePaneProvider>
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
<div id="app" className={appClass + ' app app-column-container'}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
<div id="app" className={appClass + ' app app-column-container'}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer application={application} viewControllerManager={viewControllerManager} />
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer application={application} viewControllerManager={viewControllerManager} />
|
||||
</>
|
||||
</div>
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</PremiumModalProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,20 @@ const ChallengeModal: FunctionComponent<Props> = ({
|
||||
}
|
||||
}, [hasBiometricPromptValue, hasOnlyBiometricPrompt, submit])
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = application.addAndroidBackHandlerEventListener(() => {
|
||||
if (challenge.cancelable) {
|
||||
cancelChallenge()
|
||||
}
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [application, cancelChallenge, challenge.cancelable])
|
||||
|
||||
if (!challenge.prompts) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { UuidGenerator } from '@standardnotes/snjs'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import PositionedPopoverContent from './PositionedPopoverContent'
|
||||
@@ -41,6 +42,8 @@ const Popover = ({
|
||||
}: Props) => {
|
||||
const popoverId = useRef(UuidGenerator.GenerateUuid())
|
||||
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useRegisterPopoverToParent(popoverId.current)
|
||||
|
||||
const [childPopovers, setChildPopovers] = useState<Set<string>>(new Set())
|
||||
@@ -64,6 +67,23 @@ const Popover = ({
|
||||
[registerChildPopover, unregisterChildPopover],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let removeListener: (() => void) | undefined
|
||||
|
||||
if (open) {
|
||||
removeListener = addAndroidBackHandler(() => {
|
||||
togglePopover()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, open, togglePopover])
|
||||
|
||||
return open ? (
|
||||
<PopoverContext.Provider value={contextValue}>
|
||||
<PositionedPopoverContent
|
||||
|
||||
@@ -8,31 +8,52 @@ import { isIOS } from '@/Utils'
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
|
||||
const PreferencesView: FunctionComponent<PreferencesProps> = (props) => {
|
||||
const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
||||
application,
|
||||
viewControllerManager,
|
||||
closePreferences,
|
||||
userProvider,
|
||||
mfaProvider,
|
||||
}) => {
|
||||
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
|
||||
const menu = useMemo(
|
||||
() => new PreferencesMenu(props.application, props.viewControllerManager.enableUnfinishedFeatures),
|
||||
[props.viewControllerManager.enableUnfinishedFeatures, props.application],
|
||||
() => new PreferencesMenu(application, viewControllerManager.enableUnfinishedFeatures),
|
||||
[viewControllerManager.enableUnfinishedFeatures, application],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
menu.selectPane(props.viewControllerManager.preferencesController.currentPane)
|
||||
const removeEscKeyObserver = props.application.io.addKeyObserver({
|
||||
menu.selectPane(viewControllerManager.preferencesController.currentPane)
|
||||
const removeEscKeyObserver = application.io.addKeyObserver({
|
||||
key: 'Escape',
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
props.closePreferences()
|
||||
closePreferences()
|
||||
},
|
||||
})
|
||||
return () => {
|
||||
removeEscKeyObserver()
|
||||
}
|
||||
}, [props, menu])
|
||||
}, [menu, viewControllerManager.preferencesController.currentPane, application.io, closePreferences])
|
||||
|
||||
useDisableBodyScrollOnMobile()
|
||||
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = addAndroidBackHandler(() => {
|
||||
closePreferences()
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, closePreferences])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -48,13 +69,20 @@ const PreferencesView: FunctionComponent<PreferencesProps> = (props) => {
|
||||
<h1 className="text-base font-bold md:text-lg">Your preferences for Standard Notes</h1>
|
||||
<RoundIconButton
|
||||
onClick={() => {
|
||||
props.closePreferences()
|
||||
closePreferences()
|
||||
}}
|
||||
type="normal"
|
||||
icon="close"
|
||||
/>
|
||||
</div>
|
||||
<PreferencesCanvas {...props} menu={menu} />
|
||||
<PreferencesCanvas
|
||||
menu={menu}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
closePreferences={closePreferences}
|
||||
userProvider={userProvider}
|
||||
mfaProvider={mfaProvider}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { useEffect, ReactNode, useMemo, createContext, useCallback, useContext, useState, memo } from 'react'
|
||||
import {
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
memo,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
MutableRefObject,
|
||||
} from 'react'
|
||||
import { AppPaneId } from './AppPaneMetadata'
|
||||
|
||||
type ResponsivePaneData = {
|
||||
@@ -20,16 +33,27 @@ export const useResponsiveAppPane = () => {
|
||||
return value
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type ChildrenProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const MemoizedChildren = memo(({ children }: Props) => <div>{children}</div>)
|
||||
function useStateRef<State>(state: State): MutableRefObject<State> {
|
||||
const ref = useRef<State>(state)
|
||||
|
||||
const ResponsivePaneProvider = ({ children }: Props) => {
|
||||
useLayoutEffect(() => {
|
||||
ref.current = state
|
||||
}, [state])
|
||||
|
||||
return ref
|
||||
}
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <div>{children}</div>)
|
||||
|
||||
const ResponsivePaneProvider = ({ children }: ChildrenProps) => {
|
||||
const [currentSelectedPane, setCurrentSelectedPane] = useState<AppPaneId>(
|
||||
isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor,
|
||||
)
|
||||
const currentSelectedPaneRef = useStateRef<AppPaneId>(currentSelectedPane)
|
||||
const [previousSelectedPane, setPreviousSelectedPane] = useState<AppPaneId>(
|
||||
isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor,
|
||||
)
|
||||
@@ -57,6 +81,27 @@ const ResponsivePaneProvider = ({ children }: Props) => {
|
||||
currentPaneElement?.classList.add('selected')
|
||||
}, [currentSelectedPane, previousSelectedPane])
|
||||
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = addAndroidBackHandler(() => {
|
||||
if (
|
||||
currentSelectedPaneRef.current === AppPaneId.Editor ||
|
||||
currentSelectedPaneRef.current === AppPaneId.Navigation
|
||||
) {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, currentSelectedPaneRef, toggleAppPane])
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
selectedPane: currentSelectedPane,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { FunctionComponent, useEffect } from 'react'
|
||||
import HistoryModalDialogContent from './HistoryModalDialogContent'
|
||||
import HistoryModalDialog from './HistoryModalDialog'
|
||||
import { RevisionHistoryModalProps } from './RevisionHistoryModalProps'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
|
||||
const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
application,
|
||||
@@ -11,6 +12,27 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
selectionController,
|
||||
subscriptionController,
|
||||
}) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
const isOpen = !!historyModalController.note
|
||||
|
||||
useEffect(() => {
|
||||
let removeListener: (() => void) | undefined
|
||||
|
||||
if (isOpen) {
|
||||
removeListener = addAndroidBackHandler(() => {
|
||||
historyModalController.dismissModal()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, historyModalController, isOpen])
|
||||
|
||||
if (!historyModalController.note) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FunctionComponent, ReactNode } from 'react'
|
||||
import { FunctionComponent, ReactNode, useEffect } from 'react'
|
||||
import { AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
|
||||
type Props = {
|
||||
closeDialog: () => void
|
||||
@@ -10,24 +11,40 @@ type Props = {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const ModalDialogLabel: FunctionComponent<Props> = ({ children, closeDialog, className, headerButtons }) => (
|
||||
<AlertDialogLabel
|
||||
className={classNames(
|
||||
'flex flex-shrink-0 items-center justify-between rounded-t border-b border-solid border-border bg-default px-4.5 py-3 text-text',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex-grow text-lg font-semibold text-text">{children}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{headerButtons}
|
||||
<button tabIndex={0} className="rounded p-1 font-bold hover:bg-contrast" onClick={closeDialog}>
|
||||
<Icon type="close" />
|
||||
</button>
|
||||
const ModalDialogLabel: FunctionComponent<Props> = ({ children, closeDialog, className, headerButtons }) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = addAndroidBackHandler(() => {
|
||||
closeDialog()
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, closeDialog])
|
||||
|
||||
return (
|
||||
<AlertDialogLabel
|
||||
className={classNames(
|
||||
'flex flex-shrink-0 items-center justify-between rounded-t border-b border-solid border-border bg-default px-4.5 py-3 text-text',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex-grow text-lg font-semibold text-text">{children}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{headerButtons}
|
||||
<button tabIndex={0} className="rounded p-1 font-bold hover:bg-contrast" onClick={closeDialog}>
|
||||
<Icon type="close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="h-1px no-border m-0 bg-border" />
|
||||
</AlertDialogLabel>
|
||||
)
|
||||
<hr className="h-1px no-border m-0 bg-border" />
|
||||
</AlertDialogLabel>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalDialogLabel
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
type Listener = () => boolean
|
||||
type RemoveListener = () => void
|
||||
|
||||
export class AndroidBackHandler {
|
||||
private listeners = new Set<Listener>()
|
||||
|
||||
addEventListener(listener: Listener): RemoveListener {
|
||||
this.listeners.add(listener)
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
notifyEvent() {
|
||||
for (const listener of Array.from(this.listeners).reverse()) {
|
||||
if (listener()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { createContext, memo, ReactNode, useCallback, useContext, useEffect } from 'react'
|
||||
|
||||
type BackHandlerContextData = WebApplication['addAndroidBackHandlerEventListener']
|
||||
|
||||
const BackHandlerContext = createContext<BackHandlerContextData | null>(null)
|
||||
|
||||
export const useAndroidBackHandler = () => {
|
||||
const value = useContext(BackHandlerContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error('Component must be a child of <AndroidBackHandlerProvider />')
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
type ChildrenProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
type ProviderProps = {
|
||||
application: WebApplication
|
||||
} & ChildrenProps
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <div>{children}</div>)
|
||||
|
||||
const AndroidBackHandlerProvider = ({ application, children }: ProviderProps) => {
|
||||
const addAndroidBackHandler = useCallback(
|
||||
(listener: () => boolean) => application.addAndroidBackHandlerEventListener(listener),
|
||||
[application],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = addAndroidBackHandler(() => {
|
||||
application.mobileDevice.confirmAndExit()
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, application.mobileDevice])
|
||||
|
||||
return (
|
||||
<BackHandlerContext.Provider value={addAndroidBackHandler}>
|
||||
<MemoizedChildren children={children} />
|
||||
</BackHandlerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(AndroidBackHandlerProvider)
|
||||
Reference in New Issue
Block a user