feat(mobile): mobile index page UI (#7959)

This commit is contained in:
CatsJuice
2024-08-29 06:09:46 +00:00
parent f37051dc87
commit db76780bc9
47 changed files with 1404 additions and 84 deletions

View File

@@ -1,34 +1,32 @@
import type { DocCollection } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { Suspense } from 'react';
import { type ReactNode, Suspense } from 'react';
import { useBlockSuitePagePreview } from './use-block-suite-page-preview';
import { useDocCollectionPage } from './use-block-suite-workspace-page';
interface PagePreviewInnerProps {
interface PagePreviewProps {
docCollection: DocCollection;
pageId: string;
emptyFallback?: ReactNode;
}
const PagePreviewInner = ({
docCollection: workspace,
pageId,
}: PagePreviewInnerProps) => {
emptyFallback,
}: PagePreviewProps) => {
const page = useDocCollectionPage(workspace, pageId);
const previewAtom = useBlockSuitePagePreview(page);
const preview = useAtomValue(previewAtom);
return preview ? preview : null;
const res = preview ? preview : null;
return res || emptyFallback;
};
interface PagePreviewProps {
docCollection: DocCollection;
pageId: string;
}
export const PagePreview = ({ docCollection, pageId }: PagePreviewProps) => {
export const PagePreview = (props: PagePreviewProps) => {
return (
<Suspense>
<PagePreviewInner docCollection={docCollection} pageId={pageId} />
<PagePreviewInner {...props} />
</Suspense>
);
};

View File

@@ -5,6 +5,7 @@ import { map } from 'rxjs';
import type { CollapsibleSectionName } from '../types';
const DEFAULT_COLLAPSABLE_STATE: Record<CollapsibleSectionName, boolean> = {
recent: true,
favorites: false,
organize: false,
collections: true,

View File

@@ -8,6 +8,7 @@ import { ExplorerSection } from './entities/explore-section';
import { ExplorerService } from './services/explorer';
export { ExplorerService } from './services/explorer';
export type { CollapsibleSectionName } from './types';
export { CollapsibleSection } from './views/layouts/collapsible-section';
export { ExplorerMobileContext } from './views/mobile.context';
export { ExplorerCollections } from './views/sections/collections';
export { ExplorerFavorites } from './views/sections/favorites';

View File

@@ -4,6 +4,7 @@ import { ExplorerSection } from '../entities/explore-section';
import type { CollapsibleSectionName } from '../types';
const allSectionName: Array<CollapsibleSectionName> = [
'recent', // mobile only
'favorites',
'organize',
'collections',

View File

@@ -1,4 +1,5 @@
export type CollapsibleSectionName =
| 'recent'
| 'collections'
| 'favorites'
| 'tags'

View File

@@ -77,7 +77,7 @@ export const postfix = style({
});
export const iconContainer = style({
display: 'flex',
alignContent: 'center',
justifyContent: 'center',
alignItems: 'center',
width: 20,
height: 20,

View File

@@ -1,9 +1,11 @@
export { Workbench } from './entities/workbench';
export { ViewScope } from './scopes/view';
export { WorkbenchService } from './services/workbench';
export { useBindWorkbenchToBrowserRouter } from './view/browser-adapter';
export { useIsActiveView } from './view/use-is-active-view';
export { ViewBody, ViewHeader, ViewSidebarTab } from './view/view-islands';
export { ViewIcon, ViewTitle } from './view/view-meta';
export type { WorkbenchLinkProps } from './view/workbench-link';
export { WorkbenchLink } from './view/workbench-link';
export { WorkbenchRoot } from './view/workbench-root';

View File

@@ -10,56 +10,57 @@ import { forwardRef, type MouseEvent } from 'react';
import { WorkbenchService } from '../services/workbench';
export const WorkbenchLink = forwardRef<
HTMLAnchorElement,
React.PropsWithChildren<
{
to: To;
onClick?: (e: MouseEvent) => void;
} & React.HTMLProps<HTMLAnchorElement>
>
>(function WorkbenchLink({ to, onClick, ...other }, ref) {
const { featureFlagService, workbenchService } = useServices({
FeatureFlagService,
WorkbenchService,
});
const enableMultiView = useLiveData(
featureFlagService.flags.enable_multi_view.$
);
const workbench = workbenchService.workbench;
const basename = useLiveData(workbench.basename$);
const link =
basename +
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
const handleClick = useCatchEventCallback(
async (event: React.MouseEvent<HTMLAnchorElement>) => {
onClick?.(event);
if (event.defaultPrevented) {
return;
}
const at = (() => {
if (isNewTabTrigger(event)) {
return event.altKey && enableMultiView && environment.isDesktop
? 'tail'
: 'new-tab';
}
return 'active';
})();
workbench.open(to, { at });
event.preventDefault();
},
[enableMultiView, onClick, to, workbench]
);
export type WorkbenchLinkProps = React.PropsWithChildren<
{
to: To;
onClick?: (e: MouseEvent) => void;
} & React.HTMLProps<HTMLAnchorElement>
>;
// eslint suspicious runtime error
// eslint-disable-next-line react/no-danger-with-children
return (
<a
{...other}
ref={ref}
href={link}
onClick={handleClick}
onAuxClick={handleClick}
/>
);
});
export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
function WorkbenchLink({ to, onClick, ...other }, ref) {
const { featureFlagService, workbenchService } = useServices({
FeatureFlagService,
WorkbenchService,
});
const enableMultiView = useLiveData(
featureFlagService.flags.enable_multi_view.$
);
const workbench = workbenchService.workbench;
const basename = useLiveData(workbench.basename$);
const link =
basename +
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
const handleClick = useCatchEventCallback(
async (event: React.MouseEvent<HTMLAnchorElement>) => {
onClick?.(event);
if (event.defaultPrevented) {
return;
}
const at = (() => {
if (isNewTabTrigger(event)) {
return event.altKey && enableMultiView && environment.isDesktop
? 'tail'
: 'new-tab';
}
return 'active';
})();
workbench.open(to, { at });
event.preventDefault();
},
[enableMultiView, onClick, to, workbench]
);
// eslint suspicious runtime error
// eslint-disable-next-line react/no-danger-with-children
return (
<a
{...other}
ref={ref}
href={link}
onClick={handleClick}
onAuxClick={handleClick}
/>
);
}
);

View File

@@ -28,7 +28,11 @@ export const loader: LoaderFunction = async () => {
return null;
};
export const Component = () => {
export const Component = ({
defaultIndexRoute = WorkspaceSubPath.ALL,
}: {
defaultIndexRoute?: WorkspaceSubPath;
}) => {
// navigating and creating may be slow, to avoid flickering, we show workspace fallback
const [navigating, setNavigating] = useState(true);
const [creating, setCreating] = useState(false);
@@ -59,11 +63,11 @@ export const Component = () => {
if (defaultDocId) {
jumpToPage(meta.id, defaultDocId);
} else {
openPage(meta.id, WorkspaceSubPath.ALL);
openPage(meta.id, defaultIndexRoute);
}
})
.catch(err => console.error('Failed to create cloud workspace', err));
}, [jumpToPage, openPage, workspacesService]);
}, [defaultIndexRoute, jumpToPage, openPage, workspacesService]);
useLayoutEffect(() => {
if (!navigating) {
@@ -86,7 +90,7 @@ export const Component = () => {
const openWorkspace =
list.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
list[0];
openPage(openWorkspace.id, WorkspaceSubPath.ALL);
openPage(openWorkspace.id, defaultIndexRoute);
} else {
return;
}
@@ -99,7 +103,7 @@ export const Component = () => {
const lastId = localStorage.getItem('last_workspace_id');
const openWorkspace = list.find(w => w.id === lastId) ?? list[0];
openPage(openWorkspace.id, WorkspaceSubPath.ALL);
openPage(openWorkspace.id, defaultIndexRoute);
}
}, [
createCloudWorkspace,
@@ -109,6 +113,7 @@ export const Component = () => {
listIsLoading,
loggedIn,
navigating,
defaultIndexRoute,
]);
useEffect(() => {

View File

@@ -1,5 +1,7 @@
import { WorkbenchService } from '@affine/core/modules/workbench';
import { useBindWorkbenchToBrowserRouter } from '@affine/core/modules/workbench/view/browser-adapter';
import {
useBindWorkbenchToBrowserRouter,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { ViewRoot } from '@affine/core/modules/workbench/view/view-root';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react';

View File

@@ -7,12 +7,14 @@ export enum WorkspaceSubPath {
ALL = 'all',
TRASH = 'trash',
SHARED = 'shared',
HOME = 'home',
}
export const WorkspaceSubPathName = {
[WorkspaceSubPath.ALL]: 'All Pages',
[WorkspaceSubPath.TRASH]: 'Trash',
[WorkspaceSubPath.SHARED]: 'Shared',
[WorkspaceSubPath.HOME]: 'Home',
} satisfies {
[Path in WorkspaceSubPath]: string;
};
@@ -21,6 +23,7 @@ export const pathGenerator = {
all: workspaceId => `/workspace/${workspaceId}/all`,
trash: workspaceId => `/workspace/${workspaceId}/trash`,
shared: workspaceId => `/workspace/${workspaceId}/shared`,
home: workspaceId => `/workspace/${workspaceId}/home`,
} satisfies {
[Path in WorkspaceSubPath]: (workspaceId: string) => string;
};