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,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])}
>
<Avatar
<WorkspaceAvatar
key={meta.id}
meta={meta}
imageProps={avatarImageProps}
fallbackProps={avatarImageProps}
size={28}
url={avatar}
name={name}
colorfulFallback
/>
@@ -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',
@@ -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<HTMLDivElement, ResizePanelProps>(
const [{ status }, toggle] = useTransition({
timeout: animationTimeout,
});
useEffect(() => {
useLayoutEffect(() => {
toggle(open);
}, [open]);
return (
@@ -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<string, { imageBitmap: ImageBitmap; key: string }>();
/**
* 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 <Avatar image={downloadedAvatar?.imageBitmap} {...otherProps} />;
};
@@ -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 (
<div className={workspaceItemStyle} data-testid="draggable-item">
@@ -55,7 +50,6 @@ const SortableWorkspaceItem = ({
openingId={openingId}
isOwner={isOwner}
name={name}
avatar={avatar}
/>
</div>
);