From b356ddbe6e3206f0a60fc32761a4d1a44d3d76ed Mon Sep 17 00:00:00 2001 From: EYHN Date: Mon, 27 May 2024 08:05:20 +0000 Subject: [PATCH] fix(core): fix ui flashing (#7056) --- .../src/modules/workspace/entities/profile.ts | 12 +- .../components/card/workspace-card/index.tsx | 10 +- .../resize-panel/resize-panel.css.ts | 3 - .../components/resize-panel/resize-panel.tsx | 11 +- .../src/components/workspace-avatar/index.tsx | 80 +++++ .../src/components/workspace-list/index.tsx | 6 - .../component/src/ui/avatar/avatar.tsx | 118 +++++-- .../src/components/affine/app-container.tsx | 17 +- .../setting-modal/setting-sidebar/index.tsx | 8 +- .../new-workspace-setting-detail/profile.tsx | 15 +- .../src/components/app-sidebar/index.css.ts | 4 +- .../user-with-workspace-list/index.tsx | 7 +- .../workspace-list/index.tsx | 17 +- .../workspace-card/index.tsx | 14 +- .../src/components/root-app-sidebar/index.tsx | 325 +++++++++--------- .../components/workspace-selector/index.tsx | 18 +- .../core/src/components/workspace/index.tsx | 11 +- .../core/src/hooks/use-workspace-blob.ts | 39 --- .../core/src/hooks/use-workspace-info.ts | 21 +- .../core/src/layouts/workspace-layout.tsx | 76 ++-- .../workbench/view/split-view/panel.tsx | 11 +- .../workbench/view/split-view/split-view.tsx | 19 +- .../src/modules/workbench/view/view-root.tsx | 4 +- .../modules/workbench/view/workbench-root.tsx | 8 +- .../modules/workspace-engine/impls/cloud.ts | 10 +- .../impls/engine/blob-cloud.ts | 14 +- .../modules/workspace-engine/impls/local.ts | 13 +- packages/frontend/core/src/pages/index.tsx | 4 +- .../core/src/pages/workspace/index.tsx | 22 +- packages/frontend/electron/renderer/app.tsx | 4 +- packages/frontend/web/src/app.tsx | 4 +- .../e2e/local-first-avatar.spec.ts | 24 +- tests/fixtures/blue.png | Bin 0 -> 138 bytes 33 files changed, 545 insertions(+), 404 deletions(-) create mode 100644 packages/frontend/component/src/components/workspace-avatar/index.tsx delete mode 100644 packages/frontend/core/src/hooks/use-workspace-blob.ts create mode 100644 tests/fixtures/blue.png diff --git a/packages/common/infra/src/modules/workspace/entities/profile.ts b/packages/common/infra/src/modules/workspace/entities/profile.ts index 7e864ff671..ab23001ff0 100644 --- a/packages/common/infra/src/modules/workspace/entities/profile.ts +++ b/packages/common/infra/src/modules/workspace/entities/profile.ts @@ -1,4 +1,5 @@ import { DebugLogger } from '@affine/debug'; +import { isEqual } from 'lodash-es'; import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs'; import { Entity } from '../../../framework'; @@ -54,7 +55,10 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> { providers.find(p => p.flavour === this.props.metadata.flavour) ?? null; } - private setCache(info: WorkspaceProfileInfo) { + private setProfile(info: WorkspaceProfileInfo) { + if (isEqual(this.profile$.value, info)) { + return; + } this.cache.setProfileCache(this.props.metadata.id, info); } @@ -69,7 +73,7 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> { ).pipe( mergeMap(info => { if (info) { - this.setCache({ ...this.profile$.value, ...info }); + this.setProfile({ ...this.profile$.value, ...info }); } return EMPTY; }), @@ -86,11 +90,11 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> { syncWithWorkspace(workspace: Workspace) { workspace.name$.subscribe(name => { const old = this.profile$.value; - this.setCache({ ...old, name: name ?? old?.name }); + this.setProfile({ ...old, name: name ?? old?.name }); }); workspace.avatar$.subscribe(avatar => { const old = this.profile$.value; - this.setCache({ ...old, avatar: avatar ?? old?.avatar }); + this.setProfile({ ...old, avatar: avatar ?? old?.avatar }); }); } } diff --git a/packages/frontend/component/src/components/card/workspace-card/index.tsx b/packages/frontend/component/src/components/card/workspace-card/index.tsx index eb419d8e4b..529b712c8e 100644 --- a/packages/frontend/component/src/components/card/workspace-card/index.tsx +++ b/packages/frontend/component/src/components/card/workspace-card/index.tsx @@ -1,3 +1,4 @@ +import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons'; @@ -5,7 +6,7 @@ import type { WorkspaceMetadata } from '@toeverything/infra'; import clsx from 'clsx'; import { type MouseEvent, useCallback } from 'react'; -import { Avatar, type AvatarProps } from '../../../ui/avatar'; +import { type AvatarProps } from '../../../ui/avatar'; import { Button } from '../../../ui/button'; import { Skeleton } from '../../../ui/skeleton'; import * as styles from './styles.css'; @@ -24,7 +25,6 @@ export interface WorkspaceCardProps { isOwner?: boolean; openingId?: string | null; enableCloudText?: string; - avatar?: string; name?: string; } @@ -57,7 +57,6 @@ export const WorkspaceCard = ({ isOwner = true, enableCloudText = 'Enable Cloud', name, - avatar, }: WorkspaceCardProps) => { const isLocal = meta.flavour === WorkspaceFlavour.LOCAL; const displayName = name ?? UNTITLED_WORKSPACE_NAME; @@ -78,11 +77,12 @@ export const WorkspaceCard = ({ onClick(meta); }, [onClick, meta])} > - diff --git a/packages/frontend/component/src/components/resize-panel/resize-panel.css.ts b/packages/frontend/component/src/components/resize-panel/resize-panel.css.ts index 23531464f2..7ce8e10359 100644 --- a/packages/frontend/component/src/components/resize-panel/resize-panel.css.ts +++ b/packages/frontend/component/src/components/resize-panel/resize-panel.css.ts @@ -33,9 +33,6 @@ export const root = style({ '&[data-enable-animation="true"]': { transition: `margin-left ${animationTimeout} .05s, margin-right ${animationTimeout} .05s, width ${animationTimeout} .05s`, }, - '&[data-is-floating="false"][data-transparent=true]': { - backgroundColor: 'transparent', - }, '&[data-transition-state="exited"]': { // avoid focus on hidden panel visibility: 'hidden', diff --git a/packages/frontend/component/src/components/resize-panel/resize-panel.tsx b/packages/frontend/component/src/components/resize-panel/resize-panel.tsx index 23622bb7b0..a24cd16700 100644 --- a/packages/frontend/component/src/components/resize-panel/resize-panel.tsx +++ b/packages/frontend/component/src/components/resize-panel/resize-panel.tsx @@ -1,7 +1,14 @@ import { assertExists } from '@blocksuite/global/utils'; import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; -import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; +import { + forwardRef, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; import { useTransition } from 'react-transition-state'; import * as styles from './resize-panel.css'; @@ -157,7 +164,7 @@ export const ResizePanel = forwardRef( const [{ status }, toggle] = useTransition({ timeout: animationTimeout, }); - useEffect(() => { + useLayoutEffect(() => { toggle(open); }, [open]); return ( diff --git a/packages/frontend/component/src/components/workspace-avatar/index.tsx b/packages/frontend/component/src/components/workspace-avatar/index.tsx new file mode 100644 index 0000000000..3e849f89c8 --- /dev/null +++ b/packages/frontend/component/src/components/workspace-avatar/index.tsx @@ -0,0 +1,80 @@ +import { + useLiveData, + useService, + type WorkspaceMetadata, + WorkspacesService, +} from '@toeverything/infra'; +import { useEffect, useLayoutEffect, useState } from 'react'; + +import { Avatar, type AvatarProps } from '../../ui/avatar'; + +const cache = new Map(); + +/** + * workspace avatar component with automatic cache, and avoid flashing + */ +export const WorkspaceAvatar = ({ + meta, + ...otherProps +}: { meta: WorkspaceMetadata } & AvatarProps) => { + const workspacesService = useService(WorkspacesService); + + const profile = workspacesService.getProfile(meta); + + useEffect(() => { + profile.revalidate(); + }, [meta, profile]); + + const avatarKey = useLiveData(profile.profile$.map(v => v?.avatar)); + + const [downloadedAvatar, setDownloadedAvatar] = useState< + { imageBitmap: ImageBitmap; key: string } | undefined + >(cache.get(meta.id)); + + useLayoutEffect(() => { + if (!avatarKey || !meta) { + setDownloadedAvatar(undefined); + return; + } + + let canceled = false; + workspacesService + .getWorkspaceBlob(meta, avatarKey) + .then(async blob => { + if (blob && !canceled) { + const image = document.createElement('img'); + const objectUrl = URL.createObjectURL(blob); + image.src = objectUrl; + await image.decode(); + // limit the size of the image data to reduce memory usage + const hRatio = 128 / image.naturalWidth; + const vRatio = 128 / image.naturalHeight; + const ratio = Math.min(hRatio, vRatio); + const imageBitmap = await createImageBitmap(image, { + resizeWidth: image.naturalWidth * ratio, + resizeHeight: image.naturalHeight * ratio, + }); + URL.revokeObjectURL(objectUrl); + setDownloadedAvatar(prev => { + if (prev?.key === avatarKey) { + return prev; + } + return { imageBitmap, key: avatarKey }; + }); + cache.set(meta.id, { + imageBitmap, + key: avatarKey, + }); + } + }) + .catch(err => { + console.error('get workspace blob error: ' + err); + }); + + return () => { + canceled = true; + }; + }, [meta, workspacesService, avatarKey]); + + return ; +}; diff --git a/packages/frontend/component/src/components/workspace-list/index.tsx b/packages/frontend/component/src/components/workspace-list/index.tsx index e84c1cb8f2..81944666e4 100644 --- a/packages/frontend/component/src/components/workspace-list/index.tsx +++ b/packages/frontend/component/src/components/workspace-list/index.tsx @@ -18,9 +18,6 @@ export interface WorkspaceListProps { useIsWorkspaceOwner: ( workspaceMetadata: WorkspaceMetadata ) => boolean | undefined; - useWorkspaceAvatar: ( - workspaceMetadata: WorkspaceMetadata - ) => string | undefined; useWorkspaceName: ( workspaceMetadata: WorkspaceMetadata ) => string | undefined; @@ -34,7 +31,6 @@ const SortableWorkspaceItem = ({ item, openingId, useIsWorkspaceOwner, - useWorkspaceAvatar, useWorkspaceName, currentWorkspaceId, onClick, @@ -42,7 +38,6 @@ const SortableWorkspaceItem = ({ onEnableCloudClick, }: SortableWorkspaceItemProps) => { const isOwner = useIsWorkspaceOwner?.(item); - const avatar = useWorkspaceAvatar?.(item); const name = useWorkspaceName?.(item); return (
@@ -55,7 +50,6 @@ const SortableWorkspaceItem = ({ openingId={openingId} isOwner={isOwner} name={name} - avatar={avatar} />
); diff --git a/packages/frontend/component/src/ui/avatar/avatar.tsx b/packages/frontend/component/src/ui/avatar/avatar.tsx index deb2d1b271..e479033538 100644 --- a/packages/frontend/component/src/ui/avatar/avatar.tsx +++ b/packages/frontend/component/src/ui/avatar/avatar.tsx @@ -17,7 +17,13 @@ import type { MouseEvent, ReactElement, } from 'react'; -import { forwardRef, useMemo, useState } from 'react'; +import { + forwardRef, + useCallback, + useLayoutEffect, + useMemo, + useState, +} from 'react'; import { IconButton } from '../button'; import type { TooltipProps } from '../tooltip'; @@ -29,6 +35,7 @@ import { blurVar, sizeVar } from './style.css'; export type AvatarProps = { size?: number; url?: string | null; + image?: ImageBitmap /* use pre-loaded image data can avoid flashing */; name?: string; className?: string; style?: CSSProperties; @@ -39,18 +46,56 @@ export type AvatarProps = { removeTooltipOptions?: Omit; fallbackProps?: AvatarFallbackProps; - imageProps?: Omit; + imageProps?: Omit< + AvatarImageProps & React.HTMLProps, + 'src' | 'ref' + >; avatarProps?: RadixAvatarProps; hoverWrapperProps?: HTMLAttributes; removeButtonProps?: HTMLAttributes; } & HTMLAttributes; +function drawImageFit( + img: ImageBitmap, + ctx: CanvasRenderingContext2D, + size: number +) { + const hRatio = size / img.width; + const vRatio = size / img.height; + const ratio = Math.max(hRatio, vRatio); + const centerShift_x = (size - img.width * ratio) / 2; + const centerShift_y = (size - img.height * ratio) / 2; + console.log(ctx.canvas); + ctx.canvas.dataset['drawed'] = 'true'; + console.log( + 'drawImageFit', + img.width, + img.height, + size, + ratio, + centerShift_x, + centerShift_y + ); + ctx.drawImage( + img, + 0, + 0, + img.width, + img.height, + centerShift_x, + centerShift_y, + img.width * ratio, + img.height * ratio + ); +} + export const Avatar = forwardRef( ( { size = 20, style: propsStyles = {}, url, + image, name, className, colorfulFallback = false, @@ -76,18 +121,35 @@ export const Avatar = forwardRef( const firstCharOfName = useMemo(() => { return name?.slice(0, 1) || 'A'; }, [name]); - const [imageDom, setImageDom] = useState(null); + const [containerDom, setContainerDom] = useState( + null + ); const [removeButtonDom, setRemoveButtonDom] = useState(null); + const [canvas, setCanvas] = useState(null); + + useLayoutEffect(() => { + if (canvas && image) { + const ctx = canvas?.getContext('2d'); + if (ctx) { + drawImageFit(image, ctx, size * window.devicePixelRatio); + } + } + return; + }, [canvas, image, size]); + + const canvasRef = useCallback((node: HTMLCanvasElement | null) => { + setCanvas(node); + }, []); return (
( }} {...props} > - + {image /* canvas mode */ ? ( + + ) : ( + + )} - - {colorfulFallback ? ( - - ) : ( - firstCharOfName.toUpperCase() - )} - + {!image /* no fallback on canvas mode */ && ( + + {colorfulFallback ? ( + + ) : ( + firstCharOfName.toUpperCase() + )} + + )} {hoverIcon ? (
{ const { appSettings } = useAppSettingHelper(); @@ -17,3 +23,12 @@ export const AppContainer = (props: WorkspaceRootProps) => { /> ); }; + +export const AppFallback = (): ReactElement => { + return ( + + + + + ); +}; diff --git a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx index db77e97711..7fa5a75651 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/setting-sidebar/index.tsx @@ -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" > - { }, [permissionService]); const workspaceIsReady = useLiveData(workspace?.engine.rootDocState$)?.ready; - const [avatarBlob, setAvatarBlob] = useState(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 (
@@ -149,9 +142,9 @@ export const ProfilePanel = () => { data-testid="upload-avatar" disabled={!isOwner} > - { - workspaceManager.list.revalidate(); - }, [workspaceManager]); - return (
{isAuthenticated ? ( diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx index 58cfb286db..e4a4d8b3f8 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx @@ -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} />
); @@ -115,7 +113,6 @@ const LocalWorkspaces = ({ onEnableCloudClick={onClickEnableCloud} useIsWorkspaceOwner={useIsWorkspaceOwner} useWorkspaceName={useWorkspaceName} - useWorkspaceAvatar={useWorkspaceAvatar} />
); @@ -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] ); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index 4ef3fad14b..736119e50d 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -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} > - diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index f0726f0fd6..589b4bafa4 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -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 ( - - - -
-
- + return ( + + + +
+
+ +
+
- -
- - } - active={allPageActive} - path={paths.all(currentWorkspaceId)} - > - - {t['com.affine.workspaceSubPath.all']()} - - - - {runtimeConfig.enableNewSettingModal ? ( - } - onClick={onOpenSettingModal} - > - - {t['com.affine.settingSidebar.title']()} - - - ) : null} - - - - - - - - - - {/* fixme: remove the following spacer */} -
-
+ } - active={trashActive || trashDroppable.isOver} - path={paths.trash(currentWorkspaceId)} + icon={} + active={allPageActive} + path={paths.all(currentWorkspaceId)} > - - {t['com.affine.workspaceSubPath.trash']()} + + {t['com.affine.workspaceSubPath.all']()} - -
- - - {environment.isDesktop ? : } -
- - - - ); -}; + + {runtimeConfig.enableNewSettingModal ? ( + } + onClick={onOpenSettingModal} + > + + {t['com.affine.settingSidebar.title']()} + + + ) : null} + + + + + + + + + {/* fixme: remove the following spacer */} +
+
+ } + active={trashActive || trashDroppable.isOver} + path={paths.trash(currentWorkspaceId)} + > + + {t['com.affine.workspaceSubPath.trash']()} + + + +
+ + + {environment.isDesktop ? : } +
+ + + + ); + } +); + +RootAppSidebar.displayName = 'memo(RootAppSidebar)'; diff --git a/packages/frontend/core/src/components/workspace-selector/index.tsx b/packages/frontend/core/src/components/workspace-selector/index.tsx index 93cce9948a..156407dfe0 100644 --- a/packages/frontend/core/src/components/workspace-selector/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/index.tsx @@ -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 ( - - - } + items={} contentOptions={{ // hide trigger sideOffset: -58, diff --git a/packages/frontend/core/src/components/workspace/index.tsx b/packages/frontend/core/src/components/workspace/index.tsx index 1d312f7b8e..f3ad0c86e6 100644 --- a/packages/frontend/core/src/components/workspace/index.tsx +++ b/packages/frontend/core/src/components/workspace/index.tsx @@ -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 => {
); }; - -export const WorkspaceFallback = (): ReactElement => { - return ( - - - - - ); -}; diff --git a/packages/frontend/core/src/hooks/use-workspace-blob.ts b/packages/frontend/core/src/hooks/use-workspace-blob.ts deleted file mode 100644 index 35b94bce0b..0000000000 --- a/packages/frontend/core/src/hooks/use-workspace-blob.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { WorkspaceMetadata } from '@toeverything/infra'; -import { useService, WorkspacesService } from '@toeverything/infra'; -import { useEffect, useState } from 'react'; - -export function useWorkspaceBlobObjectUrl( - meta?: WorkspaceMetadata, - blobKey?: string | null -) { - const workspacesService = useService(WorkspacesService); - - const [blob, setBlob] = useState(undefined); - - useEffect(() => { - setBlob(undefined); - if (!blobKey || !meta) { - return; - } - let canceled = false; - let objectUrl: string = ''; - workspacesService - .getWorkspaceBlob(meta, blobKey) - .then(blob => { - if (blob && !canceled) { - objectUrl = URL.createObjectURL(blob); - setBlob(objectUrl); - } - }) - .catch(err => { - console.error('get workspace blob error: ' + err); - }); - - return () => { - canceled = true; - URL.revokeObjectURL(objectUrl); - }; - }, [meta, blobKey, workspacesService]); - - return blob; -} diff --git a/packages/frontend/core/src/hooks/use-workspace-info.ts b/packages/frontend/core/src/hooks/use-workspace-info.ts index bf487a296e..17333ef8f1 100644 --- a/packages/frontend/core/src/hooks/use-workspace-info.ts +++ b/packages/frontend/core/src/hooks/use-workspace-info.ts @@ -4,24 +4,16 @@ import { useService, WorkspacesService, } from '@toeverything/infra'; -import { useEffect, useState } from 'react'; - -import { useWorkspaceBlobObjectUrl } from './use-workspace-blob'; +import { useEffect } from 'react'; export function useWorkspaceInfo(meta: WorkspaceMetadata) { const workspacesService = useService(WorkspacesService); - const [profile, setProfile] = useState(() => - workspacesService.getProfile(meta) - ); + const profile = workspacesService.getProfile(meta); useEffect(() => { - const profile = workspacesService.getProfile(meta); - profile.revalidate(); - - setProfile(profile); - }, [meta, workspacesService]); + }, [meta, profile]); return useLiveData(profile.profile$); } @@ -31,10 +23,3 @@ export function useWorkspaceName(meta: WorkspaceMetadata) { return information?.name; } - -export function useWorkspaceAvatar(meta: WorkspaceMetadata) { - const information = useWorkspaceInfo(meta); - const avatar = useWorkspaceBlobObjectUrl(meta, information?.avatar); - - return avatar; -} diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index 8b5162fc44..3a72c691b0 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -16,7 +16,7 @@ import { } from '@toeverything/infra'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import type { PropsWithChildren, ReactNode } from 'react'; -import { lazy, Suspense, useCallback, useEffect, useState } from 'react'; +import { lazy, useCallback, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { Map as YMap } from 'yjs'; @@ -24,14 +24,11 @@ import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms'; import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding'; import { AppContainer } from '../components/affine/app-container'; import { SyncAwareness } from '../components/affine/awareness'; -import { - AppSidebarFallback, - appSidebarResizingAtom, -} from '../components/app-sidebar'; +import { appSidebarResizingAtom } from '../components/app-sidebar'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; import type { DraggableTitleCellData } from '../components/page-list'; import { RootAppSidebar } from '../components/root-app-sidebar'; -import { MainContainer, WorkspaceFallback } from '../components/workspace'; +import { MainContainer } from '../components/workspace'; import { WorkspaceUpgrade } from '../components/workspace-upgrade'; import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper'; import { @@ -93,15 +90,11 @@ export const WorkspaceLayout = function WorkspaceLayout({ return ( {/* load all workspaces is costly, do not block the whole UI */} - - - - - }> - {children} - {/* should show after workspace loaded */} - - + + + {children} + {/* should show after workspace loaded */} + ); }; @@ -177,11 +170,18 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { const resizing = useAtomValue(appSidebarResizingAtom); const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 10, - }, - }) + useSensor( + MouseSensor, + useMemo( + /* useMemo is necessary to avoid re-render */ + () => ({ + activationConstraint: { + distance: 10, + }, + }), + [] + ) + ) ); const { handleDragEnd } = useGlobalDNDHelper(); @@ -192,27 +192,23 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => { {/* This DndContext is used for drag page from all-pages list into a folder in sidebar */} - }> - { - assertExists(currentWorkspace); - return openPage(currentWorkspace.id, pageId); - }, - [currentWorkspace, openPage] - )} - createPage={handleCreatePage} - paths={pathGenerator} - /> - + { + assertExists(currentWorkspace); + return openPage(currentWorkspace.id, pageId); + }, + [currentWorkspace, openPage] + )} + createPage={handleCreatePage} + paths={pathGenerator} + /> - - {needUpgrade || upgrading ? : children} - + {needUpgrade || upgrading ? : children} diff --git a/packages/frontend/core/src/modules/workbench/view/split-view/panel.tsx b/packages/frontend/core/src/modules/workbench/view/split-view/panel.tsx index d7ba5f1ab4..9c7f263e44 100644 --- a/packages/frontend/core/src/modules/workbench/view/split-view/panel.tsx +++ b/packages/frontend/core/src/modules/workbench/view/split-view/panel.tsx @@ -16,7 +16,14 @@ import type { PropsWithChildren, RefObject, } from 'react'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + memo, + useCallback, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; import type { View } from '../../entities/view'; import { WorkbenchService } from '../../services/workbench'; @@ -57,7 +64,7 @@ export const SplitViewPanel = memo(function SplitViewPanel({ const isDragging = dndIsDragging || indicatorPressed; const isActive = activeView === view; - useEffect(() => { + useLayoutEffect(() => { if (ref.current) { setSlots?.(slots => ({ ...slots, [view.id]: ref })); } diff --git a/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx b/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx index 375c8fb55a..65f3f49b46 100644 --- a/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx +++ b/packages/frontend/core/src/modules/workbench/view/split-view/split-view.tsx @@ -15,7 +15,7 @@ import { import { useService } from '@toeverything/infra'; import clsx from 'clsx'; import type { HTMLAttributes, RefObject } from 'react'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import type { View } from '../../entities/view'; @@ -52,11 +52,18 @@ export const SplitView = ({ const workbench = useService(WorkbenchService).workbench; const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 0, - }, - }) + useSensor( + PointerSensor, + useMemo( + /* avoid re-rendering */ + () => ({ + activationConstraint: { + distance: 0, + }, + }), + [] + ) + ) ); const onResizing = useCallback( diff --git a/packages/frontend/core/src/modules/workbench/view/view-root.tsx b/packages/frontend/core/src/modules/workbench/view/view-root.tsx index a9b0f9e8b2..55c1e2db96 100644 --- a/packages/frontend/core/src/modules/workbench/view/view-root.tsx +++ b/packages/frontend/core/src/modules/workbench/view/view-root.tsx @@ -1,5 +1,5 @@ import { FrameworkScope, useLiveData } from '@toeverything/infra'; -import { lazy as reactLazy, useEffect, useMemo } from 'react'; +import { lazy as reactLazy, useLayoutEffect, useMemo } from 'react'; import { createMemoryRouter, RouterProvider, @@ -34,7 +34,7 @@ export const ViewRoot = ({ view }: { view: View }) => { const location = useLiveData(view.location$); - useEffect(() => { + useLayoutEffect(() => { viewRouter.navigate(location).catch(err => { console.error('navigate error', err); }); diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx index d71654a204..883c5fcd7a 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-root.tsx @@ -1,5 +1,5 @@ import { useLiveData, useService } from '@toeverything/infra'; -import { useCallback, useEffect, useRef } from 'react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import type { View } from '../entities/view'; @@ -14,7 +14,7 @@ const useAdapter = environment.isDesktop ? useBindWorkbenchToDesktopRouter : useBindWorkbenchToBrowserRouter; -export const WorkbenchRoot = () => { +export const WorkbenchRoot = memo(() => { const workbench = useService(WorkbenchService).workbench; // for debugging @@ -50,7 +50,9 @@ export const WorkbenchRoot = () => { onMove={onMove} /> ); -}; +}); + +WorkbenchRoot.displayName = 'memo(WorkbenchRoot)'; const WorkbenchView = ({ view, index }: { view: View; index: number }) => { const workbench = useService(WorkbenchService).workbench; diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index 2c160e6052..df0e4d9615 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -24,6 +24,7 @@ import { type WorkspaceProfileInfo, } from '@toeverything/infra'; import { effect, globalBlockSuiteSchema, Service } from '@toeverything/infra'; +import { isEqual } from 'lodash-es'; import { nanoid } from 'nanoid'; import { EMPTY, map, mergeMap } from 'rxjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; @@ -148,11 +149,16 @@ export class CloudWorkspaceFlavourProviderService mergeMap(data => { if (data) { const { accountId, workspaces } = data; + const sorted = workspaces.sort((a, b) => { + return a.id.localeCompare(b.id); + }); this.globalState.set( CLOUD_WORKSPACES_CACHE_KEY + accountId, - workspaces + sorted ); - this.workspaces$.next(workspaces); + if (!isEqual(this.workspaces$.value, sorted)) { + this.workspaces$.next(sorted); + } } else { this.workspaces$.next([]); } diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/engine/blob-cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/engine/blob-cloud.ts index 1206d0a802..65013d6708 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/engine/blob-cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/engine/blob-cloud.ts @@ -22,13 +22,15 @@ export class CloudBlobStorage implements BlobStorage { ? key : `/api/workspaces/${this.workspaceId}/blobs/${key}`; - return fetch(getBaseUrl() + suffix).then(async res => { - if (!res.ok) { - // status not in the range 200-299 - return null; + return fetch(getBaseUrl() + suffix, { cache: 'default' }).then( + async res => { + if (!res.ok) { + // status not in the range 200-299 + return null; + } + return bufferToBlob(await res.arrayBuffer()); } - return bufferToBlob(await res.arrayBuffer()); - }); + ); } async set(key: string, value: Blob) { diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts index f90828a83d..468eac5494 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/local.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/local.ts @@ -9,6 +9,7 @@ import type { WorkspaceProfileInfo, } from '@toeverything/infra'; import { globalBlockSuiteSchema, LiveData, Service } from '@toeverything/infra'; +import { isEqual } from 'lodash-es'; import { nanoid } from 'nanoid'; import { Observable } from 'rxjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; @@ -96,12 +97,14 @@ export class LocalWorkspaceFlavourProvider } workspaces$ = LiveData.from( new Observable(subscriber => { + let last: WorkspaceMetadata[] | null = null; const emit = () => { - subscriber.next( - JSON.parse( - localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]' - ).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL })) - ); + const value = JSON.parse( + localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]' + ).map((id: string) => ({ id, flavour: WorkspaceFlavour.LOCAL })); + if (isEqual(last, value)) return; + subscriber.next(value); + last = value; }; emit(); diff --git a/packages/frontend/core/src/pages/index.tsx b/packages/frontend/core/src/pages/index.tsx index fa5a080f5c..51c063944b 100644 --- a/packages/frontend/core/src/pages/index.tsx +++ b/packages/frontend/core/src/pages/index.tsx @@ -19,8 +19,8 @@ import { buildShowcaseWorkspace, createFirstAppData, } from '../bootstrap/first-app-data'; +import { AppFallback } from '../components/affine/app-container'; import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list'; -import { WorkspaceFallback } from '../components/workspace'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { AuthService } from '../modules/cloud'; import { WorkspaceSubPath } from '../shared'; @@ -141,7 +141,7 @@ export const Component = () => { }, [jumpToPage, openPage, workspacesService]); if (navigating || creating) { - return ; + return ; } // TODO: We need a no workspace page diff --git a/packages/frontend/core/src/pages/workspace/index.tsx b/packages/frontend/core/src/pages/workspace/index.tsx index db31d55c59..feb65ef0b1 100644 --- a/packages/frontend/core/src/pages/workspace/index.tsx +++ b/packages/frontend/core/src/pages/workspace/index.tsx @@ -1,3 +1,4 @@ +import { AppFallback } from '@affine/core/components/affine/app-container'; import { useWorkspace } from '@affine/core/hooks/use-workspace'; import { ZipTransformer } from '@blocksuite/blocks'; import type { Workspace } from '@toeverything/infra'; @@ -9,11 +10,10 @@ import { WorkspacesService, } from '@toeverything/infra'; import type { ReactElement } from 'react'; -import { Suspense, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary'; -import { WorkspaceFallback } from '../../components/workspace'; import { WorkspaceLayout } from '../../layouts/workspace-layout'; import { RightSidebarContainer } from '../../modules/right-sidebar'; import { WorkbenchRoot } from '../../modules/workbench'; @@ -121,13 +121,13 @@ export const Component = (): ReactElement => { return ; } if (!workspace) { - return ; + return ; } if (!isRootDocReady) { return ( - + ); @@ -135,14 +135,12 @@ export const Component = (): ReactElement => { return ( - }> - - - - - - - + + + + + + ); }; diff --git a/packages/frontend/electron/renderer/app.tsx b/packages/frontend/electron/renderer/app.tsx index 240f354529..9f774d4b70 100644 --- a/packages/frontend/electron/renderer/app.tsx +++ b/packages/frontend/electron/renderer/app.tsx @@ -4,7 +4,7 @@ import '@affine/component/theme/theme.css'; import { NotificationCenter } from '@affine/component'; import { AffineContext } from '@affine/component/context'; import { GlobalLoading } from '@affine/component/global-loading'; -import { WorkspaceFallback } from '@affine/core/components/workspace'; +import { AppFallback } from '@affine/core/components/affine/app-container'; import { configureCommonModules, configureImpls } from '@affine/core/modules'; import { configureBrowserWorkspaceFlavours, @@ -108,7 +108,7 @@ export function App() { } + fallbackElement={} router={router} future={future} /> diff --git a/packages/frontend/web/src/app.tsx b/packages/frontend/web/src/app.tsx index 02a04fcd2f..6bf5b8efde 100644 --- a/packages/frontend/web/src/app.tsx +++ b/packages/frontend/web/src/app.tsx @@ -4,7 +4,7 @@ import '@affine/component/theme/theme.css'; import { NotificationCenter } from '@affine/component'; import { AffineContext } from '@affine/component/context'; import { GlobalLoading } from '@affine/component/global-loading'; -import { WorkspaceFallback } from '@affine/core/components/workspace'; +import { AppFallback } from '@affine/core/components/affine/app-container'; import { configureCommonModules, configureImpls } from '@affine/core/modules'; import { configureBrowserWorkspaceFlavours, @@ -97,7 +97,7 @@ export function App() { } + fallbackElement={} router={router} future={future} /> diff --git a/tests/affine-local/e2e/local-first-avatar.spec.ts b/tests/affine-local/e2e/local-first-avatar.spec.ts index a42aae065c..eecd791d8b 100644 --- a/tests/affine-local/e2e/local-first-avatar.spec.ts +++ b/tests/affine-local/e2e/local-first-avatar.spec.ts @@ -26,19 +26,29 @@ test('should create a page with a local first avatar and remove it', async ({ await page.getByTestId('current-workspace-label').click(); await page .getByTestId('upload-avatar') - .setInputFiles(resolve(rootDir, 'tests', 'fixtures', 'smile.png')); + .setInputFiles(resolve(rootDir, 'tests', 'fixtures', 'blue.png')); await page.mouse.click(0, 0); await page.getByTestId('workspace-name').click(); await page.getByTestId('workspace-card').nth(0).click(); await page.waitForTimeout(1000); await page.getByTestId('workspace-name').click(); await page.getByTestId('workspace-card').nth(1).click(); - const blobUrl = await page + const avatarCanvas = await page .getByTestId('workspace-avatar') - .locator('img') - .getAttribute('src'); - // out user uploaded avatar - expect(blobUrl).toContain('blob:'); + .locator('canvas') + .first() + .elementHandle(); + const avatarPixelData = await page.evaluate( + ({ avatarCanvas }) => { + return Array.from( + (avatarCanvas as HTMLCanvasElement) + .getContext('2d')! + .getImageData(1, 1, 1, 1).data // get pixel data of the avatar + ); + }, + { avatarCanvas } + ); + expect(avatarPixelData).toEqual([0, 0, 255, 255]); // blue color // Click remove button to remove workspace avatar await page.getByTestId('settings-modal-trigger').click(); @@ -51,7 +61,7 @@ test('should create a page with a local first avatar and remove it', async ({ await page.getByTestId('workspace-card').nth(1).click(); const removedAvatarImage = await page .getByTestId('workspace-avatar') - .locator('img') + .locator('canvas') .count(); expect(removedAvatarImage).toBe(0); diff --git a/tests/fixtures/blue.png b/tests/fixtures/blue.png new file mode 100644 index 0000000000000000000000000000000000000000..d2c52e2e092019cd039e2419e50a5e88dedfa12a GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k&H|6fVg?3oVGw3ym^DWND9BhG z