fix(core): fix ui flashing (#7056)

This commit is contained in:
EYHN
2024-05-27 08:05:20 +00:00
parent 306cf2ae6f
commit b356ddbe6e
33 changed files with 545 additions and 404 deletions
@@ -1,6 +1,12 @@
import type { ReactElement } from 'react';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { AppSidebarFallback } from '../app-sidebar';
import type { WorkspaceRootProps } from '../workspace';
import { AppContainer as AppContainerWithoutSettings } from '../workspace';
import {
AppContainer as AppContainerWithoutSettings,
MainContainer,
} from '../workspace';
export const AppContainer = (props: WorkspaceRootProps) => {
const { appSettings } = useAppSettingHelper();
@@ -17,3 +23,12 @@ export const AppContainer = (props: WorkspaceRootProps) => {
/>
);
};
export const AppFallback = (): ReactElement => {
return (
<AppContainer>
<AppSidebarFallback />
<MainContainer />
</AppContainer>
);
};
@@ -4,7 +4,7 @@ import {
} from '@affine/component/setting-components';
import { Avatar } from '@affine/component/ui/avatar';
import { Tooltip } from '@affine/component/ui/tooltip';
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import { AuthService } from '@affine/core/modules/cloud';
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
@@ -277,7 +277,6 @@ const WorkspaceListItem = ({
UserFeatureService,
});
const information = useWorkspaceInfo(meta);
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
const currentWorkspace = workspaceService.workspace;
const isCurrent = currentWorkspace.id === meta.id;
@@ -318,9 +317,10 @@ const WorkspaceListItem = ({
onClick={onClickPreference}
data-testid="workspace-list-item"
>
<Avatar
<WorkspaceAvatar
key={meta.id}
meta={meta}
size={16}
url={avatarUrl}
name={name}
colorfulFallback
style={{
@@ -1,9 +1,8 @@
import { FlexWrapper, Input, notify, Wrapper } from '@affine/component';
import { Avatar } from '@affine/component/ui/avatar';
import { Button } from '@affine/component/ui/button';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { Upload } from '@affine/core/components/pure/file-upload';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
@@ -28,18 +27,13 @@ export const ProfilePanel = () => {
}, [permissionService]);
const workspaceIsReady = useLiveData(workspace?.engine.rootDocState$)?.ready;
const [avatarBlob, setAvatarBlob] = useState<string | null>(null);
const [name, setName] = useState('');
const avatarUrl = useWorkspaceBlobObjectUrl(workspace?.meta, avatarBlob);
useEffect(() => {
if (workspace?.docCollection) {
setAvatarBlob(workspace.docCollection.meta.avatar ?? null);
setName(workspace.docCollection.meta.name ?? UNTITLED_WORKSPACE_NAME);
const dispose = workspace.docCollection.meta.commonFieldsUpdated.on(
() => {
setAvatarBlob(workspace.docCollection.meta.avatar ?? null);
setName(workspace.docCollection.meta.name ?? UNTITLED_WORKSPACE_NAME);
}
);
@@ -47,7 +41,6 @@ export const ProfilePanel = () => {
dispose.dispose();
};
} else {
setAvatarBlob(null);
setName(UNTITLED_WORKSPACE_NAME);
}
return;
@@ -139,7 +132,7 @@ export const ProfilePanel = () => {
[setWorkspaceAvatar]
);
const canAdjustAvatar = workspaceIsReady && avatarUrl && isOwner;
const canAdjustAvatar = workspaceIsReady && isOwner;
return (
<div className={style.profileWrapper}>
@@ -149,9 +142,9 @@ export const ProfilePanel = () => {
data-testid="upload-avatar"
disabled={!isOwner}
>
<Avatar
<WorkspaceAvatar
meta={workspace.meta}
size={56}
url={avatarUrl}
name={name}
imageProps={avatarImageProps}
fallbackProps={avatarImageProps}
@@ -4,7 +4,6 @@ export const floatingMaxWidth = 768;
export const navWrapperStyle = style({
zIndex: 3,
paddingBottom: '8px',
backgroundColor: cssVar('backgroundPrimaryColor'),
'@media': {
print: {
display: 'none',
@@ -15,6 +14,9 @@ export const navWrapperStyle = style({
'&[data-has-border=true]': {
borderRight: `1px solid ${cssVar('borderColor')}`,
},
'&[data-is-floating="true"]': {
backgroundColor: cssVar('backgroundPrimaryColor'),
},
},
});
export const navHeaderButton = style({
@@ -10,7 +10,7 @@ import {
WorkspacesService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { Suspense, useCallback, useEffect } from 'react';
import { Suspense, useCallback } from 'react';
import {
authAtom,
@@ -130,11 +130,6 @@ const UserWithWorkspaceListInner = ({
const workspaceManager = useService(WorkspacesService);
const workspaces = useLiveData(workspaceManager.list.workspaces$);
// revalidate workspace list when mounted
useEffect(() => {
workspaceManager.list.revalidate();
}, [workspaceManager]);
return (
<div className={styles.workspaceListWrapper}>
{isAuthenticated ? (
@@ -3,7 +3,6 @@ import { Divider } from '@affine/component/ui/divider';
import { WorkspaceList } from '@affine/component/workspace-list';
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
import {
useWorkspaceAvatar,
useWorkspaceInfo,
useWorkspaceName,
} from '@affine/core/hooks/use-workspace-info';
@@ -76,7 +75,6 @@ const CloudWorkSpaceList = ({
onSettingClick={onClickWorkspaceSetting}
useIsWorkspaceOwner={useIsWorkspaceOwner}
useWorkspaceName={useWorkspaceName}
useWorkspaceAvatar={useWorkspaceAvatar}
/>
</div>
);
@@ -115,7 +113,6 @@ const LocalWorkspaces = ({
onEnableCloudClick={onClickEnableCloud}
useIsWorkspaceOwner={useIsWorkspaceOwner}
useWorkspaceName={useWorkspaceName}
useWorkspaceAvatar={useWorkspaceAvatar}
/>
</div>
);
@@ -186,8 +183,18 @@ export const AFFiNEWorkspaceList = ({
const onClickWorkspace = useCallback(
(workspaceMetadata: WorkspaceMetadata) => {
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
onEventEnd?.();
if (document.startViewTransition) {
document.startViewTransition(() => {
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
onEventEnd?.();
return new Promise(resolve =>
setTimeout(resolve, 150)
); /* start transition after 150ms */
});
} else {
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
onEventEnd?.();
}
},
[jumpToSubPath, onEventEnd]
);
@@ -1,9 +1,9 @@
import { notify, Tooltip } from '@affine/component';
import { Avatar, type AvatarProps } from '@affine/component/ui/avatar';
import { type AvatarProps } from '@affine/component/ui/avatar';
import { Loading } from '@affine/component/ui/loading';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status';
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
@@ -284,11 +284,6 @@ export const WorkspaceCard = forwardRef<
const information = useWorkspaceInfo(currentWorkspace.meta);
const avatarUrl = useWorkspaceBlobObjectUrl(
currentWorkspace.meta,
information?.avatar
);
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
return (
@@ -301,12 +296,13 @@ export const WorkspaceCard = forwardRef<
ref={ref}
{...props}
>
<Avatar
<WorkspaceAvatar
key={currentWorkspace.id}
meta={currentWorkspace.meta}
imageProps={avatarImageProps}
fallbackProps={avatarImageProps}
data-testid="workspace-avatar"
size={32}
url={avatarUrl}
name={name}
colorfulFallback
/>
@@ -13,7 +13,7 @@ import { useLiveData, useService } from '@toeverything/infra';
import { useAtomValue } from 'jotai';
import { nanoid } from 'nanoid';
import type { HTMLAttributes, ReactElement } from 'react';
import { forwardRef, useCallback, useEffect } from 'react';
import { forwardRef, memo, useCallback, useEffect } from 'react';
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
@@ -89,174 +89,177 @@ RouteMenuLinkItem.displayName = 'RouteMenuLinkItem';
*
* @todo(himself65): rewrite all styled component into @vanilla-extract/css
*/
export const RootAppSidebar = ({
currentWorkspace,
openPage,
createPage,
paths,
onOpenQuickSearchModal,
onOpenSettingModal,
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace.id;
const { appSettings } = useAppSettingHelper();
const docCollection = currentWorkspace.docCollection;
const t = useAFFiNEI18N();
const currentPath = useLiveData(
useService(WorkbenchService).workbench.location$.map(
location => location.pathname
)
);
export const RootAppSidebar = memo(
({
currentWorkspace,
openPage,
createPage,
paths,
onOpenQuickSearchModal,
onOpenSettingModal,
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace.id;
const { appSettings } = useAppSettingHelper();
const docCollection = currentWorkspace.docCollection;
const t = useAFFiNEI18N();
const currentPath = useLiveData(
useService(WorkbenchService).workbench.location$.map(
location => location.pathname
)
);
const allPageActive = currentPath === '/all';
const allPageActive = currentPath === '/all';
const trashActive = currentPath === '/trash';
const trashActive = currentPath === '/trash';
const onClickNewPage = useAsyncCallback(async () => {
const page = createPage();
page.load();
openPage(page.id);
mixpanel.track('DocCreated', {
page: allPageActive ? 'all' : trashActive ? 'trash' : 'other',
segment: 'navigation panel',
module: 'bottom button',
control: 'new doc button',
category: 'page',
type: 'doc',
const onClickNewPage = useAsyncCallback(async () => {
const page = createPage();
page.load();
openPage(page.id);
mixpanel.track('DocCreated', {
page: allPageActive ? 'all' : trashActive ? 'trash' : 'other',
segment: 'navigation panel',
module: 'bottom button',
control: 'new doc button',
category: 'page',
type: 'doc',
});
}, [allPageActive, createPage, openPage, trashActive]);
const { trashModal, setTrashModal, handleOnConfirm } =
useTrashModalHelper(docCollection);
const deletePageTitles = trashModal.pageTitles;
const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback(
(open: boolean) => {
setTrashModal({
...trashModal,
open,
});
},
[trashModal, setTrashModal]
);
const navigateHelper = useNavigateHelper();
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
return events?.applicationMenu.onNewPageAction(onClickNewPage);
}
return;
}, [onClickNewPage]);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop) {
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
console.error(err);
});
}
}, [sidebarOpen]);
const dropItemId = getDNDId('sidebar-trash', 'container', 'trash');
const trashDroppable = useDroppable({
id: dropItemId,
});
}, [allPageActive, createPage, openPage, trashActive]);
const { trashModal, setTrashModal, handleOnConfirm } =
useTrashModalHelper(docCollection);
const deletePageTitles = trashModal.pageTitles;
const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback(
(open: boolean) => {
setTrashModal({
...trashModal,
open,
});
},
[trashModal, setTrashModal]
);
const collection = useService(CollectionService);
const { node, open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
const handleCreateCollection = useCallback(() => {
open('')
.then(name => {
const id = nanoid();
collection.addCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(docCollection.id, id);
})
.catch(err => {
console.error(err);
});
}, [docCollection.id, collection, navigateHelper, open]);
const navigateHelper = useNavigateHelper();
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
return events?.applicationMenu.onNewPageAction(onClickNewPage);
}
return;
}, [onClickNewPage]);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop) {
apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
console.error(err);
});
}
}, [sidebarOpen]);
const dropItemId = getDNDId('sidebar-trash', 'container', 'trash');
const trashDroppable = useDroppable({
id: dropItemId,
});
const collection = useService(CollectionService);
const { node, open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
const handleCreateCollection = useCallback(() => {
open('')
.then(name => {
const id = nanoid();
collection.addCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(docCollection.id, id);
})
.catch(err => {
console.error(err);
});
}, [docCollection.id, collection, navigateHelper, open]);
return (
<AppSidebar
clientBorder={appSettings.clientBorder}
translucentUI={appSettings.enableBlurBackground}
>
<MoveToTrash.ConfirmModal
open={trashConfirmOpen}
onConfirm={handleOnConfirm}
onOpenChange={onTrashConfirmOpenChange}
titles={deletePageTitles}
/>
<SidebarContainer>
<div className={workspaceAndUserWrapper}>
<div className={workspaceWrapper}>
<WorkspaceSelector />
return (
<AppSidebar
clientBorder={appSettings.clientBorder}
translucentUI={appSettings.enableBlurBackground}
>
<MoveToTrash.ConfirmModal
open={trashConfirmOpen}
onConfirm={handleOnConfirm}
onOpenChange={onTrashConfirmOpenChange}
titles={deletePageTitles}
/>
<SidebarContainer>
<div className={workspaceAndUserWrapper}>
<div className={workspaceWrapper}>
<WorkspaceSelector />
</div>
<UserInfo />
</div>
<UserInfo />
</div>
<QuickSearchInput
data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal}
/>
<RouteMenuLinkItem
icon={<FolderIcon />}
active={allPageActive}
path={paths.all(currentWorkspaceId)}
>
<span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()}
</span>
</RouteMenuLinkItem>
<AppSidebarJournalButton
docCollection={currentWorkspace.docCollection}
/>
{runtimeConfig.enableNewSettingModal ? (
<MenuItem
data-testid="slider-bar-workspace-setting-button"
icon={<SettingsIcon />}
onClick={onOpenSettingModal}
>
<span data-testid="settings-modal-trigger">
{t['com.affine.settingSidebar.title']()}
</span>
</MenuItem>
) : null}
</SidebarContainer>
<SidebarScrollableContainer>
<FavoriteList docCollection={docCollection} />
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
<AddCollectionButton node={node} onClick={handleCreateCollection} />
</CategoryDivider>
<CollectionsList
docCollection={docCollection}
onCreate={handleCreateCollection}
/>
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
{/* fixme: remove the following spacer */}
<div style={{ height: '4px' }} />
<div style={{ padding: '0 8px' }}>
<QuickSearchInput
data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal}
/>
<RouteMenuLinkItem
ref={trashDroppable.setNodeRef}
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
active={trashActive || trashDroppable.isOver}
path={paths.trash(currentWorkspaceId)}
icon={<FolderIcon />}
active={allPageActive}
path={paths.all(currentWorkspaceId)}
>
<span data-testid="trash-page">
{t['com.affine.workspaceSubPath.trash']()}
<span data-testid="all-pages">
{t['com.affine.workspaceSubPath.all']()}
</span>
</RouteMenuLinkItem>
<ImportPage docCollection={docCollection} />
</div>
</SidebarScrollableContainer>
<SidebarContainer>
{environment.isDesktop ? <UpdaterButton /> : <AppDownloadButton />}
<div style={{ height: '4px' }} />
<AddPageButton onClick={onClickNewPage} />
</SidebarContainer>
</AppSidebar>
);
};
<AppSidebarJournalButton
docCollection={currentWorkspace.docCollection}
/>
{runtimeConfig.enableNewSettingModal ? (
<MenuItem
data-testid="slider-bar-workspace-setting-button"
icon={<SettingsIcon />}
onClick={onOpenSettingModal}
>
<span data-testid="settings-modal-trigger">
{t['com.affine.settingSidebar.title']()}
</span>
</MenuItem>
) : null}
</SidebarContainer>
<SidebarScrollableContainer>
<FavoriteList docCollection={docCollection} />
<CategoryDivider label={t['com.affine.rootAppSidebar.collections']()}>
<AddCollectionButton node={node} onClick={handleCreateCollection} />
</CategoryDivider>
<CollectionsList
docCollection={docCollection}
onCreate={handleCreateCollection}
/>
<CategoryDivider label={t['com.affine.rootAppSidebar.others']()} />
{/* fixme: remove the following spacer */}
<div style={{ height: '4px' }} />
<div style={{ padding: '0 8px' }}>
<RouteMenuLinkItem
ref={trashDroppable.setNodeRef}
icon={<AnimatedDeleteIcon closed={trashDroppable.isOver} />}
active={trashActive || trashDroppable.isOver}
path={paths.trash(currentWorkspaceId)}
>
<span data-testid="trash-page">
{t['com.affine.workspaceSubPath.trash']()}
</span>
</RouteMenuLinkItem>
<ImportPage docCollection={docCollection} />
</div>
</SidebarScrollableContainer>
<SidebarContainer>
{environment.isDesktop ? <UpdaterButton /> : <AppDownloadButton />}
<div style={{ height: '4px' }} />
<AddPageButton onClick={onClickNewPage} />
</SidebarContainer>
</AppSidebar>
);
}
);
RootAppSidebar.displayName = 'memo(RootAppSidebar)';
@@ -1,6 +1,7 @@
import { Menu } from '@affine/component';
import { useService, WorkspacesService } from '@toeverything/infra';
import { useAtom } from 'jotai';
import { Suspense, useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { openWorkspaceListModalAtom } from '../../atoms';
import { mixpanel } from '../../utils';
@@ -21,16 +22,21 @@ export const WorkspaceSelector = () => {
setOpenUserWorkspaceList(true);
}, [setOpenUserWorkspaceList]);
const workspaceManager = useService(WorkspacesService);
// revalidate workspace list when open workspace list
useEffect(() => {
if (isUserWorkspaceListOpened) {
workspaceManager.list.revalidate();
}
}, [workspaceManager, isUserWorkspaceListOpened]);
return (
<Menu
rootOptions={{
open: isUserWorkspaceListOpened,
}}
items={
<Suspense>
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
</Suspense>
}
items={<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />}
contentOptions={{
// hide trigger
sideOffset: -58,
@@ -9,7 +9,7 @@ import { useAtomValue } from 'jotai';
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { forwardRef } from 'react';
import { AppSidebarFallback, appSidebarOpenAtom } from '../app-sidebar';
import { appSidebarOpenAtom } from '../app-sidebar';
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
export type WorkspaceRootProps = PropsWithChildren<{
@@ -87,12 +87,3 @@ export const ToolContainer = (props: PropsWithChildren): ReactElement => {
</div>
);
};
export const WorkspaceFallback = (): ReactElement => {
return (
<AppContainer>
<AppSidebarFallback />
<MainContainer />
</AppContainer>
);
};