mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 06:18:45 +08:00
feat(mobile): disable swipe back gesture when there is no back in header (#8876)
close AF-1663, AF-1756 - new global `ModalConfigContext` - new logic to judge whether inside modal - render `✕` for PageHeader back if inside modal - only enable `NavigationGesture` when there is `<` in PageHeader
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
|
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
|
||||||
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
|
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
|
||||||
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||||
|
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationGesturePlugin.swift; sourceTree = "<group>"; };
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -65,7 +67,6 @@
|
|||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||||
9D6A85312CCF6D6B00DAB35F /* Recovered References */,
|
|
||||||
);
|
);
|
||||||
indentWidth = 2;
|
indentWidth = 2;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -101,6 +102,7 @@
|
|||||||
9D90BE1A2CCB9876006677DB /* plugins */ = {
|
9D90BE1A2CCB9876006677DB /* plugins */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E93B276A2CED9298001409B8 /* NavigationGesture */,
|
||||||
9D90BE192CCB9876006677DB /* Cookie */,
|
9D90BE192CCB9876006677DB /* Cookie */,
|
||||||
);
|
);
|
||||||
path = plugins;
|
path = plugins;
|
||||||
@@ -122,6 +124,14 @@
|
|||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E93B276A2CED9298001409B8 /* NavigationGesture */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E93B276B2CED92B1001409B8 /* NavigationGesturePlugin.swift */,
|
||||||
|
);
|
||||||
|
path = NavigationGesture;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -234,6 +244,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */,
|
||||||
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
|
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
|
||||||
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
|
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
|
||||||
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
|
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ class AFFiNEViewController: CAPBridgeViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
webView?.allowsBackForwardNavigationGestures = true
|
// disable by default, enable manually when there is a "back" button in page-header
|
||||||
|
webView?.allowsBackForwardNavigationGestures = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override func capacitorDidLoad() {
|
override func capacitorDidLoad() {
|
||||||
bridge?.registerPluginInstance(CookiePlugin())
|
bridge?.registerPluginInstance(CookiePlugin())
|
||||||
bridge?.registerPluginInstance(HashcashPlugin())
|
bridge?.registerPluginInstance(HashcashPlugin())
|
||||||
|
bridge?.registerPluginInstance(NavigationGesturePlugin())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@objc(NavigationGesturePlugin)
|
||||||
|
public class NavigationGesturePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||||
|
public let identifier = "NavigationGesturePlugin"
|
||||||
|
public let jsName = "NavigationGesture"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "isEnabled", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "enable", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "disable", returnType: CAPPluginReturnPromise)
|
||||||
|
]
|
||||||
|
|
||||||
|
@objc func isEnabled(_ call: CAPPluginCall) {
|
||||||
|
let enabled = self.bridge?.webView?.allowsBackForwardNavigationGestures ?? true
|
||||||
|
call.resolve(["value": enabled])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func enable(_ call: CAPPluginCall) {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
self.bridge?.webView?.allowsBackForwardNavigationGestures = true
|
||||||
|
call.resolve([:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func disable(_ call: CAPPluginCall) {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
self.bridge?.webView?.allowsBackForwardNavigationGestures = false
|
||||||
|
call.resolve([:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AffineContext } from '@affine/core/components/context';
|
import { AffineContext } from '@affine/core/components/context';
|
||||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||||
|
import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigation-gesture';
|
||||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||||
import { router } from '@affine/core/mobile/router';
|
import { router } from '@affine/core/mobile/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
@@ -32,8 +33,10 @@ import { Suspense } from 'react';
|
|||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import { configureFetchProvider } from './fetch';
|
import { configureFetchProvider } from './fetch';
|
||||||
|
import { ModalConfigProvider } from './modal-config';
|
||||||
import { Cookie } from './plugins/cookie';
|
import { Cookie } from './plugins/cookie';
|
||||||
import { Hashcash } from './plugins/hashcash';
|
import { Hashcash } from './plugins/hashcash';
|
||||||
|
import { NavigationGesture } from './plugins/navigation-gesture';
|
||||||
|
|
||||||
const future = {
|
const future = {
|
||||||
v7_startTransition: true,
|
v7_startTransition: true,
|
||||||
@@ -86,6 +89,11 @@ framework.impl(VirtualKeyboardProvider, {
|
|||||||
Keyboard.removeAllListeners();
|
Keyboard.removeAllListeners();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
framework.impl(NavigationGestureProvider, {
|
||||||
|
isEnabled: () => NavigationGesture.isEnabled(),
|
||||||
|
enable: () => NavigationGesture.enable(),
|
||||||
|
disable: () => NavigationGesture.disable(),
|
||||||
|
});
|
||||||
const frameworkProvider = framework.provider();
|
const frameworkProvider = framework.provider();
|
||||||
|
|
||||||
// setup application lifecycle events, and emit application start event
|
// setup application lifecycle events, and emit application start event
|
||||||
@@ -132,11 +140,13 @@ export function App() {
|
|||||||
<FrameworkRoot framework={frameworkProvider}>
|
<FrameworkRoot framework={frameworkProvider}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<AffineContext store={getCurrentStore()}>
|
||||||
<RouterProvider
|
<ModalConfigProvider>
|
||||||
fallbackElement={<AppFallback />}
|
<RouterProvider
|
||||||
router={router}
|
fallbackElement={<AppFallback />}
|
||||||
future={future}
|
router={router}
|
||||||
/>
|
future={future}
|
||||||
|
/>
|
||||||
|
</ModalConfigProvider>
|
||||||
</AffineContext>
|
</AffineContext>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</FrameworkRoot>
|
</FrameworkRoot>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ModalConfigContext } from '@affine/component';
|
||||||
|
import { NavigationGestureService } from '@affine/core/mobile/modules/navigation-gesture';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { type PropsWithChildren, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const ModalConfigProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const navigationGesture = useService(NavigationGestureService);
|
||||||
|
|
||||||
|
const onOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
const prev = navigationGesture.enabled$.value;
|
||||||
|
if (open && !prev) {
|
||||||
|
navigationGesture.setEnabled(false);
|
||||||
|
return () => {
|
||||||
|
navigationGesture.setEnabled(prev);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
[navigationGesture]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalConfigContext.Provider value={{ onOpenChange }}>
|
||||||
|
{children}
|
||||||
|
</ModalConfigContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface NavigationGesturePlugin {
|
||||||
|
isEnabled: () => Promise<boolean>;
|
||||||
|
enable: () => Promise<void>;
|
||||||
|
disable: () => Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerPlugin } from '@capacitor/core';
|
||||||
|
|
||||||
|
import type { NavigationGesturePlugin } from './definitions';
|
||||||
|
|
||||||
|
const NavigationGesture =
|
||||||
|
registerPlugin<NavigationGesturePlugin>('NavigationGesture');
|
||||||
|
|
||||||
|
export * from './definitions';
|
||||||
|
export { NavigationGesture };
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export interface ModalConfig {
|
||||||
|
/**
|
||||||
|
* add global callback for modal open/close
|
||||||
|
*/
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
export const ModalConfigContext = createContext<ModalConfig>({});
|
||||||
|
|
||||||
|
export const InsideModalContext = createContext<number>(0);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './confirm-modal';
|
export * from './confirm-modal';
|
||||||
|
export * from './context';
|
||||||
export * from './modal';
|
export * from './modal';
|
||||||
export * from './overlay-modal';
|
export * from './overlay-modal';
|
||||||
export * from './prompt-modal';
|
export * from './prompt-modal';
|
||||||
|
|||||||
@@ -10,12 +10,19 @@ import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
|||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { startScopedViewTransition } from '../../utils';
|
import { startScopedViewTransition } from '../../utils';
|
||||||
import type { IconButtonProps } from '../button';
|
import type { IconButtonProps } from '../button';
|
||||||
import { IconButton } from '../button';
|
import { IconButton } from '../button';
|
||||||
import { SafeArea } from '../safe-area';
|
import { SafeArea } from '../safe-area';
|
||||||
|
import { InsideModalContext, ModalConfigContext } from './context';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export interface ModalProps extends DialogProps {
|
export interface ModalProps extends DialogProps {
|
||||||
@@ -123,6 +130,7 @@ function createContainer() {
|
|||||||
|
|
||||||
export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
|
const modalConfig = useContext(ModalConfigContext);
|
||||||
const {
|
const {
|
||||||
modal,
|
modal,
|
||||||
portalOptions,
|
portalOptions,
|
||||||
@@ -164,6 +172,10 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
modalConfig.onOpenChange?.(open ?? false);
|
||||||
|
}, [modalConfig, open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const container = createContainer();
|
const container = createContainer();
|
||||||
@@ -304,10 +316,20 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
ModalInner.displayName = 'ModalInner';
|
ModalInner.displayName = 'ModalInner';
|
||||||
|
|
||||||
export const Modal = forwardRef<HTMLDivElement, ModalProps>((props, ref) => {
|
export const Modal = forwardRef<HTMLDivElement, ModalProps>((props, ref) => {
|
||||||
|
const insideModal = useContext(InsideModalContext);
|
||||||
if (!props.open) {
|
if (!props.open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return <ModalInner {...props} ref={ref} />;
|
return (
|
||||||
|
<InsideModalContext.Provider value={insideModal + 1}>
|
||||||
|
<ModalInner {...props} ref={ref} />
|
||||||
|
</InsideModalContext.Provider>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Modal.displayName = 'Modal';
|
Modal.displayName = 'Modal';
|
||||||
|
|
||||||
|
export const useIsInsideModal = () => {
|
||||||
|
const context = useContext(InsideModalContext);
|
||||||
|
return context > 0;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Modal } from '@affine/component';
|
import { Button, Modal } from '@affine/component';
|
||||||
|
import { PageHeader } from '@affine/core/mobile/components';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { PageHeader } from '../page-header';
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
interface ConfigModalProps {
|
interface ConfigModalProps {
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export * from './config-modal';
|
export * from './config-modal';
|
||||||
export * from './page-header';
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './app-tabs';
|
export * from './app-tabs';
|
||||||
export * from './doc-card';
|
export * from './doc-card';
|
||||||
|
export * from './page-header';
|
||||||
export * from './rename';
|
export * from './rename';
|
||||||
export * from './search-input';
|
export * from './search-input';
|
||||||
export * from './search-result';
|
export * from './search-result';
|
||||||
|
|||||||
+20
-3
@@ -1,13 +1,16 @@
|
|||||||
import { IconButton, SafeArea } from '@affine/component';
|
import { IconButton, SafeArea, useIsInsideModal } from '@affine/component';
|
||||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowLeftSmallIcon, CloseIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
type HtmlHTMLAttributes,
|
type HtmlHTMLAttributes,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { NavigationGestureService } from '../../modules/navigation-gesture';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export interface PageHeaderProps
|
export interface PageHeaderProps
|
||||||
@@ -60,6 +63,20 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
|
const navigationGesture = useService(NavigationGestureService);
|
||||||
|
const isInsideModal = useIsInsideModal();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInsideModal) return;
|
||||||
|
|
||||||
|
const prev = navigationGesture.enabled$.value;
|
||||||
|
navigationGesture.setEnabled(!!back);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
navigationGesture.setEnabled(prev);
|
||||||
|
};
|
||||||
|
}, [back, isInsideModal, navigationGesture]);
|
||||||
|
|
||||||
const handleRouteBack = useCallback(() => {
|
const handleRouteBack = useCallback(() => {
|
||||||
backAction ? backAction() : history.back();
|
backAction ? backAction() : history.back();
|
||||||
}, [backAction]);
|
}, [backAction]);
|
||||||
@@ -83,7 +100,7 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
|||||||
size={24}
|
size={24}
|
||||||
style={{ padding: 10 }}
|
style={{ padding: 10 }}
|
||||||
onClick={handleRouteBack}
|
onClick={handleRouteBack}
|
||||||
icon={<ArrowLeftSmallIcon />}
|
icon={isInsideModal ? <CloseIcon /> : <ArrowLeftSmallIcon />}
|
||||||
data-testid="page-header-back"
|
data-testid="page-header-back"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
Scrollable,
|
Scrollable,
|
||||||
useThemeColorMeta,
|
useThemeColorMeta,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { PageHeader } from '@affine/core/components/mobile';
|
import { PageHeader } from '@affine/core/mobile/components';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { Framework } from '@toeverything/infra';
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { configureMobileNavigationGestureModule } from './navigation-gesture';
|
||||||
import { configureMobileSearchModule } from './search';
|
import { configureMobileSearchModule } from './search';
|
||||||
import { configureMobileVirtualKeyboardModule } from './virtual-keyboard';
|
import { configureMobileVirtualKeyboardModule } from './virtual-keyboard';
|
||||||
|
|
||||||
export function configureMobileModules(framework: Framework) {
|
export function configureMobileModules(framework: Framework) {
|
||||||
configureMobileSearchModule(framework);
|
configureMobileSearchModule(framework);
|
||||||
configureMobileVirtualKeyboardModule(framework);
|
configureMobileVirtualKeyboardModule(framework);
|
||||||
|
configureMobileNavigationGestureModule(framework);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { NavigationGestureProvider } from './providers/navigation-gesture';
|
||||||
|
import { NavigationGestureService } from './services/navigation-gesture';
|
||||||
|
|
||||||
|
export { NavigationGestureProvider, NavigationGestureService };
|
||||||
|
|
||||||
|
export function configureMobileNavigationGestureModule(framework: Framework) {
|
||||||
|
framework.service(
|
||||||
|
NavigationGestureService,
|
||||||
|
f => new NavigationGestureService(f.getOptional(NavigationGestureProvider))
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { createIdentifier } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export interface NavigationGestureProvider {
|
||||||
|
isEnabled: () => Promise<boolean>;
|
||||||
|
enable: () => Promise<void>;
|
||||||
|
disable: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavigationGestureProvider =
|
||||||
|
createIdentifier<NavigationGestureProvider>('NavigationGestureProvider');
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import {
|
||||||
|
effect,
|
||||||
|
exhaustMapWithTrailing,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
Service,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { catchError, distinctUntilChanged, EMPTY, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
|
import type { NavigationGestureProvider } from '../providers/navigation-gesture';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('affine:navigation-gesture');
|
||||||
|
|
||||||
|
export class NavigationGestureService extends Service {
|
||||||
|
public enabled$ = new LiveData(false);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly navigationGestureProvider?: NavigationGestureProvider
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnabled = effect(
|
||||||
|
distinctUntilChanged<boolean>(),
|
||||||
|
exhaustMapWithTrailing((enable: boolean) => {
|
||||||
|
return fromPromise(async () => {
|
||||||
|
if (!this.navigationGestureProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (enable) {
|
||||||
|
await this.enable();
|
||||||
|
} else {
|
||||||
|
await this.disable();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}).pipe(
|
||||||
|
mergeMap(() => EMPTY),
|
||||||
|
catchError(err => {
|
||||||
|
logger.error('navigationGestureProvider error', err);
|
||||||
|
return EMPTY;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
async enable() {
|
||||||
|
this.enabled$.next(true);
|
||||||
|
logger.debug(`Enable navigation gesture`);
|
||||||
|
return this.navigationGestureProvider?.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable() {
|
||||||
|
this.enabled$.next(false);
|
||||||
|
logger.debug(`Disable navigation gesture`);
|
||||||
|
return this.navigationGestureProvider?.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-
|
|||||||
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
||||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
import { PageHeader } from '@affine/core/components/mobile';
|
|
||||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||||
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
||||||
|
import { PageHeader } from '@affine/core/mobile/components';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
@@ -202,19 +202,22 @@ const DetailPageImpl = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const skeleton = (
|
const getSkeleton = (back: boolean) => (
|
||||||
<>
|
<>
|
||||||
<PageHeader back className={styles.header} />
|
<PageHeader back={back} className={styles.header} />
|
||||||
<PageDetailSkeleton />
|
<PageDetailSkeleton />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
const getNotFound = (back: boolean) => (
|
||||||
const notFound = (
|
|
||||||
<>
|
<>
|
||||||
<PageHeader back className={styles.header} />
|
<PageHeader back={back} className={styles.header} />
|
||||||
Page Not Found (TODO)
|
Page Not Found (TODO)
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
const skeleton = getSkeleton(false);
|
||||||
|
const skeletonWithBack = getSkeleton(true);
|
||||||
|
const notFound = getNotFound(false);
|
||||||
|
const notFoundWithBack = getNotFound(true);
|
||||||
|
|
||||||
const MobileDetailPage = ({
|
const MobileDetailPage = ({
|
||||||
pageId,
|
pageId,
|
||||||
@@ -237,12 +240,12 @@ const MobileDetailPage = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<DetailPageWrapper
|
<DetailPageWrapper
|
||||||
skeleton={skeleton}
|
skeleton={date ? skeleton : skeletonWithBack}
|
||||||
notFound={notFound}
|
notFound={date ? notFound : notFoundWithBack}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
>
|
>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
back
|
back={!date}
|
||||||
className={styles.header}
|
className={styles.header}
|
||||||
suffix={
|
suffix={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IconButton, MobileMenu } from '@affine/component';
|
import { IconButton, MobileMenu } from '@affine/component';
|
||||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
||||||
import { PageHeader } from '@affine/core/components/mobile';
|
|
||||||
import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection';
|
import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection';
|
||||||
|
import { PageHeader } from '@affine/core/mobile/components';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IconButton, MobileMenu } from '@affine/component';
|
import { IconButton, MobileMenu } from '@affine/component';
|
||||||
import { PageHeader } from '@affine/core/components/mobile';
|
import { PageHeader } from '@affine/core/mobile/components';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData } from '@toeverything/infra';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
|
|||||||
Reference in New Issue
Block a user