feat: support remove user & workspace avatar (#4302)

This commit is contained in:
Qi
2023-09-14 22:35:05 +08:00
committed by GitHub
parent e1a330a0a6
commit 465098cc9a
40 changed files with 504 additions and 808 deletions

View File

@@ -2,11 +2,12 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { SettingsIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import { useCallback } from 'react';
import { WorkspaceAvatar } from '../../workspace-avatar';
import {
StyledCard,
StyledSettingLink,
@@ -68,6 +69,7 @@ export const WorkspaceCard = ({
// const t = useAFFiNEI18N();
const workspace = useStaticBlockSuiteWorkspace(meta.id);
const [name] = useBlockSuiteWorkspaceName(workspace);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
return (
<StyledCard
@@ -77,8 +79,7 @@ export const WorkspaceCard = ({
}, [onClick, meta.id])}
active={workspace.id === currentWorkspaceId}
>
<WorkspaceAvatar size={28} workspace={workspace} />
<Avatar size={28} url={workspaceAvatar} name={name} colorfulFallback />
<StyledWorkspaceInfo>
<StyledWorkspaceTitleArea style={{ display: 'flex' }}>
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>

View File

@@ -23,12 +23,6 @@ export const AcceptInvitePage = ({
url={inviteInfo.user.avatarUrl || ''}
name={inviteInfo.user.name}
size={20}
// FIXME: fix it in @toeverything/components/avatar
imageProps={{
style: {
objectFit: 'cover',
},
}}
/>
<span className={styles.inviteName}>{inviteInfo.user.name}</span>
{t['invited you to join']()}
@@ -37,7 +31,7 @@ export const AcceptInvitePage = ({
name={inviteInfo.workspace.name}
size={20}
style={{ marginLeft: 4 }}
colorfulFallback={true}
colorfulFallback
/>
<span className={styles.inviteName}>{inviteInfo.workspace.name}</span>
</FlexWrapper>

View File

@@ -1,37 +0,0 @@
import * as Avatar from '@radix-ui/react-avatar';
import clsx from 'clsx';
import type { CSSProperties } from 'react';
import * as style from './style.css';
export interface UserAvatar {
size?: number;
url?: string;
name?: string;
className?: string;
style?: CSSProperties;
}
export const UserAvatar = ({
size = 20,
style: propsStyles = {},
url,
name,
className,
}: UserAvatar) => {
return (
<Avatar.Root
style={{
width: size,
height: size,
...propsStyles,
}}
className={clsx(style.avatarRoot, className)}
>
<Avatar.Image className={style.avatarImage} src={url} alt={name} />
<Avatar.Fallback className={style.avatarFallback} delayMs={600}>
{name?.slice(0, 1) || 'A'}
</Avatar.Fallback>
</Avatar.Root>
);
};

View File

@@ -1,31 +0,0 @@
import { style } from '@vanilla-extract/css';
export const avatarRoot = style({
display: 'inline-flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
verticalAlign: 'middle',
overflow: 'hidden',
userSelect: 'none',
borderRadius: '100%',
});
export const avatarImage = style({
width: '100%',
height: '100%',
objectFit: 'cover',
borderRadius: 'inherit',
});
export const avatarFallback = style({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'var(--affine-primary-color)',
color: 'var(--affine-white)',
fontSize: 'var(--affine-font-base)',
lineHeight: '1',
fontWeight: '500',
});

View File

@@ -1,69 +0,0 @@
import clsx from 'clsx';
import { useMemo, useRef, useState } from 'react';
import {
DefaultAvatarBottomItemStyle,
DefaultAvatarBottomItemWithAnimationStyle,
DefaultAvatarContainerStyle,
DefaultAvatarMiddleItemStyle,
DefaultAvatarMiddleItemWithAnimationStyle,
DefaultAvatarTopItemStyle,
} from './index.css';
const colorsSchema = [
['#FF0000', '#FF00E5', '#FFAE73'],
['#FF5C00', '#FFC700', '#FFE073'],
['#FFDA16', '#FFFBA6', '#FFBE73'],
['#8CD317', '#FCFF5C', '#67CAE9'],
['#28E19F', '#89FFC6', '#39A880'],
['#35B7E0', '#77FFCE', '#5076FF'],
['#3D39FF', '#77BEFF', '#3502FF'],
['#BD08EB', '#755FFF', '#6967E4'],
];
export const DefaultAvatar = ({ name: propsName }: { name: string }) => {
// Sometimes name is ' '
const name = propsName || 'A';
const colors = useMemo(() => {
const index = name[0].toUpperCase().charCodeAt(0);
return colorsSchema[index % colorsSchema.length];
}, [name]);
const timer = useRef<number>();
const [topColor, middleColor, bottomColor] = colors;
const [isHover, setIsHover] = useState(false);
return (
<div
className={DefaultAvatarContainerStyle}
onMouseEnter={() => {
timer.current = window.setTimeout(() => {
setIsHover(true);
}, 300);
}}
onMouseLeave={() => {
clearTimeout(timer.current);
setIsHover(false);
}}
>
<div
className={DefaultAvatarTopItemStyle}
style={{ background: bottomColor }}
></div>
<div
className={clsx(DefaultAvatarMiddleItemStyle, {
[DefaultAvatarMiddleItemWithAnimationStyle]: isHover,
})}
style={{ background: middleColor }}
></div>
<div
className={clsx(DefaultAvatarBottomItemStyle, {
[DefaultAvatarBottomItemWithAnimationStyle]: isHover,
})}
style={{ background: topColor }}
></div>
</div>
);
};
export default DefaultAvatar;

View File

@@ -1,146 +0,0 @@
import { keyframes, style } from '@vanilla-extract/css';
export const avatarStyle = style({
width: '100%',
height: '100%',
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
});
export const avatarImageStyle = style({
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
});
const bottomAnimation = keyframes({
'0%': {
top: '-44%',
left: '-11%',
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
},
'16%': {
left: '-18%',
top: '-51%',
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
},
'32%': {
left: '-7%',
top: '-40%',
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
},
'48%': {
left: '-15%',
top: '-39%',
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
},
'64%': {
left: '-7%',
top: '-40%',
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
},
'80%': {
left: '-18%',
top: '-51%',
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
},
'100%': {
top: '-44%',
left: '-11%',
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
},
});
const middleAnimation = keyframes({
'0%': {
left: '-30px',
top: '-30px',
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
},
'16%': {
left: '-37px',
top: '-37px',
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
},
'32%': {
left: '-20px',
top: '-10px',
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
},
'48%': {
left: '-27px',
top: '-2px',
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
},
'64%': {
left: '-20px',
top: '-10px',
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
},
'80%': {
left: '-37px',
top: '-37px',
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
},
'100%': {
left: '-30px',
top: '-30px',
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
},
});
export const DefaultAvatarContainerStyle = style({
width: '100%',
height: '100%',
position: 'relative',
borderRadius: '50%',
overflow: 'hidden',
});
export const DefaultAvatarMiddleItemStyle = style({
width: '83%',
height: '81%',
position: 'absolute',
left: '-30%',
top: '-30%',
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
opacity: '0.8',
filter: 'blur(12px)',
transformOrigin: 'center center',
animation: `${middleAnimation} 3s ease-in-out forwards infinite`,
animationPlayState: 'paused',
});
export const DefaultAvatarMiddleItemWithAnimationStyle = style({
animationPlayState: 'running',
});
export const DefaultAvatarBottomItemStyle = style({
width: '98%',
height: '97%',
position: 'absolute',
top: '-44%',
left: '-11%',
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
opacity: '0.8',
filter: 'blur(12px)',
transformOrigin: 'center center',
willChange: 'left, top, transform',
animation: `${bottomAnimation} 3s ease-in-out forwards infinite`,
animationPlayState: 'paused',
});
export const DefaultAvatarBottomItemWithAnimationStyle = style({
animationPlayState: 'running',
});
export const DefaultAvatarTopItemStyle = style({
width: '104%',
height: '94%',
position: 'absolute',
right: '-30%',
top: '-30%',
opacity: '0.8',
filter: 'blur(12px)',
transform: 'matrix(-0.28, -0.96, 0.93, -0.37, 0, 0)',
transformOrigin: 'center center',
});

View File

@@ -1,70 +0,0 @@
import type { Workspace } from '@blocksuite/store';
import * as RadixAvatar from '@radix-ui/react-avatar';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import { DefaultAvatar } from './default-avatar';
import { avatarImageStyle, avatarStyle } from './index.css';
export interface WorkspaceAvatarProps {
size?: number;
workspace: Workspace | null;
className?: string;
}
export interface BlockSuiteWorkspaceAvatar
extends Omit<WorkspaceAvatarProps, 'workspace'> {
workspace: Workspace;
}
export const BlockSuiteWorkspaceAvatar = ({
size,
workspace,
...props
}: BlockSuiteWorkspaceAvatar) => {
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
const [name] = useBlockSuiteWorkspaceName(workspace);
return (
<RadixAvatar.Root
{...props}
className={clsx(avatarStyle, props.className)}
style={{
height: size,
width: size,
}}
>
<RadixAvatar.Image className={avatarImageStyle} src={avatar} alt={name} />
<RadixAvatar.Fallback>
<DefaultAvatar name={name}></DefaultAvatar>
</RadixAvatar.Fallback>
</RadixAvatar.Root>
);
};
export const WorkspaceAvatar = ({
size = 20,
workspace,
...props
}: WorkspaceAvatarProps) => {
if (workspace) {
return (
<BlockSuiteWorkspaceAvatar {...props} size={size} workspace={workspace} />
);
}
return (
<RadixAvatar.Root
{...props}
className={clsx(avatarStyle, props.className)}
style={{
height: size,
width: size,
}}
>
<RadixAvatar.Fallback>
<DefaultAvatar name="A"></DefaultAvatar>
</RadixAvatar.Fallback>
</RadixAvatar.Root>
);
};