mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-02 14:49:44 +08:00
feat: support remove user & workspace avatar (#4302)
This commit is contained in:
@@ -2,7 +2,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
marginTop: '44px',
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { FlexWrapper, Input, Wrapper } from '@affine/component';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CameraIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
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 { useSetAtom } from 'jotai';
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
type MouseEvent,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useState,
|
||||
@@ -29,7 +28,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
@@ -38,8 +37,6 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
);
|
||||
|
||||
const [input, setInput] = useState<string>(name);
|
||||
const [tooltipContainer, setTooltipContainer] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
|
||||
const handleUpdateWorkspaceName = useCallback(
|
||||
(name: string) => {
|
||||
@@ -71,35 +68,38 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
handleUpdateWorkspaceName(input);
|
||||
}, [handleUpdateWorkspaceName, input]);
|
||||
|
||||
const handleRemoveUserAvatar = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
await update(null);
|
||||
},
|
||||
[update]
|
||||
);
|
||||
return (
|
||||
<div className={style.profileWrapper}>
|
||||
<Tooltip
|
||||
content={t['Click to replace photo']()}
|
||||
portalOptions={{
|
||||
container: tooltipContainer,
|
||||
}}
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
data-testid="upload-avatar"
|
||||
disabled={!isOwner}
|
||||
>
|
||||
<div
|
||||
className={clsx(style.avatarWrapper, { disable: !isOwner })}
|
||||
ref={setTooltipContainer}
|
||||
>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
data-testid="upload-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<WorkspaceAvatar
|
||||
size={56}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
</Upload>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Avatar
|
||||
size={56}
|
||||
url={workspaceAvatar}
|
||||
name={name}
|
||||
colorfulFallback
|
||||
hoverIcon={isOwner ? <CameraIcon /> : undefined}
|
||||
onRemove={
|
||||
workspaceAvatar && isOwner ? handleRemoveUserAvatar : undefined
|
||||
}
|
||||
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
|
||||
removeTooltipOptions={{ content: t['Remove photo']() }}
|
||||
data-testid="workspace-setting-avatar"
|
||||
removeButtonProps={{
|
||||
['data-testid' as string]: 'workspace-setting-remove-avatar-button',
|
||||
}}
|
||||
/>
|
||||
</Upload>
|
||||
|
||||
<Wrapper marginLeft={20}>
|
||||
<div className={style.label}>{t['Workspace Name']()}</div>
|
||||
|
||||
@@ -4,15 +4,24 @@ import {
|
||||
SettingRow,
|
||||
StorageProgress,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { allBlobSizesQuery, uploadAvatarMutation } from '@affine/graphql';
|
||||
import {
|
||||
allBlobSizesQuery,
|
||||
removeAvatarMutation,
|
||||
uploadAvatarMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type FC, Suspense, useCallback, useState } from 'react';
|
||||
import {
|
||||
type FC,
|
||||
type MouseEvent,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
@@ -21,24 +30,16 @@ import { signOutCloud } from '../../../../utils/cloud-utils';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AvatarAndName = () => {
|
||||
export const UserAvatar = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
|
||||
const [tooltipContainer, setTooltipContainer] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
const [input, setInput] = useState<string>(user.name);
|
||||
|
||||
const { trigger: avatarTrigger } = useMutation({
|
||||
mutation: uploadAvatarMutation,
|
||||
});
|
||||
|
||||
const handleUpdateUserName = useCallback(
|
||||
(newName: string) => {
|
||||
user.update({ name: newName }).catch(console.error);
|
||||
},
|
||||
[user]
|
||||
);
|
||||
const { trigger: removeAvatarTrigger } = useMutation({
|
||||
mutation: removeAvatarMutation,
|
||||
});
|
||||
|
||||
const handleUpdateUserAvatar = useCallback(
|
||||
async (file: File) => {
|
||||
@@ -51,6 +52,50 @@ export const AvatarAndName = () => {
|
||||
},
|
||||
[avatarTrigger, user]
|
||||
);
|
||||
const handleRemoveUserAvatar = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
await removeAvatarTrigger();
|
||||
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
|
||||
user.update({ name: user.name }).catch(console.error);
|
||||
},
|
||||
[removeAvatarTrigger, user]
|
||||
);
|
||||
return (
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={handleUpdateUserAvatar}
|
||||
data-testid="upload-user-avatar"
|
||||
>
|
||||
<Avatar
|
||||
size={56}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
hoverIcon={<CameraIcon />}
|
||||
onRemove={user.image ? handleRemoveUserAvatar : undefined}
|
||||
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
|
||||
removeTooltipOptions={{ content: t['Remove photo']() }}
|
||||
data-testid="user-setting-avatar"
|
||||
removeButtonProps={{
|
||||
['data-testid' as string]: 'user-setting-remove-avatar-button',
|
||||
}}
|
||||
/>
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
export const AvatarAndName = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const [input, setInput] = useState<string>(user.name);
|
||||
|
||||
const handleUpdateUserName = useCallback(
|
||||
(newName: string) => {
|
||||
user.update({ name: newName }).catch(console.error);
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
@@ -59,32 +104,9 @@ export const AvatarAndName = () => {
|
||||
spreadCol={false}
|
||||
>
|
||||
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
|
||||
<Tooltip
|
||||
content={t['Click to replace photo']()}
|
||||
portalOptions={{
|
||||
container: tooltipContainer,
|
||||
}}
|
||||
>
|
||||
<div className={style.avatarWrapper} ref={setTooltipContainer}>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={handleUpdateUserAvatar}
|
||||
data-testid="upload-user-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<UserAvatar
|
||||
size={56}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
</>
|
||||
</Upload>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Suspense>
|
||||
<UserAvatar />
|
||||
</Suspense>
|
||||
|
||||
<div className={style.profileInputWrapper}>
|
||||
<label>{t['com.affine.settings.profile.name']()}</label>
|
||||
|
||||
@@ -2,14 +2,14 @@ import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
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 clsx from 'clsx';
|
||||
@@ -55,11 +55,13 @@ export const UserInfo = ({
|
||||
className={accountButton}
|
||||
onClick={onAccountSettingClick}
|
||||
>
|
||||
<UserAvatar
|
||||
<Avatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
style={{
|
||||
marginRight: '10px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
@@ -221,6 +223,7 @@ const WorkspaceListItem = ({
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||
const ref = useRef(null);
|
||||
|
||||
@@ -232,7 +235,15 @@ const WorkspaceListItem = ({
|
||||
data-testid="workspace-list-item"
|
||||
ref={ref}
|
||||
>
|
||||
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
|
||||
<Avatar
|
||||
size={14}
|
||||
url={workspaceAvatar}
|
||||
name={workspaceName}
|
||||
colorfulFallback
|
||||
style={{
|
||||
marginRight: '10px',
|
||||
}}
|
||||
/>
|
||||
<span className="setting-name">{workspaceName}</span>
|
||||
{isCurrent ? (
|
||||
<Tooltip
|
||||
|
||||
@@ -62,12 +62,6 @@ export const sidebarSelectItem = style({
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${settingSlideBar} .icon`, {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
marginRight: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
globalStyle(`${settingSlideBar} .setting-name`, {
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
@@ -108,13 +102,6 @@ export const accountButton = style({
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${accountButton} .avatar`, {
|
||||
border: '1px solid',
|
||||
borderColor: 'var(--affine-white)',
|
||||
marginRight: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
globalStyle(`${accountButton} .avatar.not-sign`, {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
|
||||
Reference in New Issue
Block a user