diff --git a/packages/app/src/components/workspace-setting/MembersPage.tsx b/packages/app/src/components/workspace-setting/MembersPage.tsx deleted file mode 100644 index e2a9beeae0..0000000000 --- a/packages/app/src/components/workspace-setting/MembersPage.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { - StyledMemberAvatar, - StyledMemberButtonContainer, - StyledMemberEmail, - StyledMemberInfo, - StyledMemberListContainer, - StyledMemberListItem, - StyledMemberName, - StyledMemberNameContainer, - StyledMemberRoleContainer, - StyledMemberTitleContainer, - StyledMoreVerticalButton, - StyledPublishExplanation, -} from './style'; -import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons'; -import { useEffect, useState } from 'react'; -import { Button, IconButton } from '@/ui/button'; -import { InviteMembers } from '../invite-members/index'; -import { Menu, MenuItem } from '@/ui/menu'; -import { Empty } from '@/ui/empty'; -// import { -// deleteMember, -// getMembers, -// User, -// Workspace, -// } from '@/hooks/mock-data/mock'; -import { WorkspaceUnit } from '@affine/datacenter'; -import { useTemporaryHelper } from '@/providers/temporary-helper-provider'; -import { StyledMemberWarp } from './general/style'; -import { useConfirm } from '@/providers/ConfirmProvider'; -// import { useAppState } from '@/providers/app-state-provider'; - -// import { useAppState } from '@/providers/app-state-provider'; -export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => { - const [isInviteModalShow, setIsInviteModalShow] = useState(false); - - const [members, setMembers] = useState<[{ name: string; email: string }?]>( - [] - ); - - // const getMembers = async () =>{ - // const members = await dataCenter. - // } - - console.log('setMembers: ', setMembers); - const { user, login, updateWorkspaceMeta } = useTemporaryHelper(); - const { confirm } = useConfirm(); - // const refreshMembers = useCallback(() => { - // getDataCenter() - // .then(dc => - // dc.apis.getWorkspaceMembers({ - // id: workspace.id, - // }) - // ) - // .then(data => { - // setMembers(data); - // }) - // .catch(err => { - // console.log(err); - // }); - // }, [workspace.id]); - const setMembersList = () => { - // setMembers([]); - // const members = getMembers(workspace.id); - // members && setMembers(members); - }; - useEffect(() => { - setMembersList(); - // refreshMembers(); - }); - - return ( -
- {workspace.provider === 'affine' ? ( - <> - - - Users({members.length}) - - Access level - - - {members.length ? ( - members.map((member, index) => { - return ( - - - - - - - - {member?.name} - - {member?.email} - - - - {/* {member.accepted - ? member.type !== 99 - ? 'Member' - : 'Workspace Owner' - : 'Pending'} */} - Pending - - - - { - // deleteMember(workspace.id, 0); - setMembersList(); - // confirm({ - // title: 'Delete Member?', - // content: `will delete member`, - // confirmText: 'Delete', - // confirmType: 'danger', - // }).then(confirm => { - // getDataCenter() - // .then(dc => - // dc.apis.removeMember({ - // permissionId: member.id, - // }) - // ) - // .then(() => { - // // console.log('data: ', data); - // toast('Moved to Trash'); - // // refreshMembers(); - // }); - // }); - }} - icon={} - > - Delete - - - } - placement="bottom-end" - disablePortal={true} - > - - - - - - - ); - }) - ) : ( - - )} - - - - { - setIsInviteModalShow(false); - }} - onInviteSuccess={() => { - setMembersList(); - setIsInviteModalShow(false); - // refreshMembers(); - }} - workspaceId={workspace.id} - open={isInviteModalShow} - > - - - ) : ( - - <>Collaborating with other members requires AFFiNE Cloud service. - - - - - )} -
- ); -}; diff --git a/packages/app/src/components/workspace-setting/general/style.ts b/packages/app/src/components/workspace-setting/general/style.ts index 4a82017e96..739ec24aec 100644 --- a/packages/app/src/components/workspace-setting/general/style.ts +++ b/packages/app/src/components/workspace-setting/general/style.ts @@ -26,11 +26,3 @@ export const StyledSettingAvatarContent = styled('div')(() => { export const StyledSettingAvatar = styled(MuiAvatar)(() => { return { height: '72px', width: '72px', marginRight: '24px' }; }); - -export const StyledMemberWarp = styled('div')(() => { - return { - display: 'flex', - flexDirection: 'column', - padding: '40px 0', - }; -}); diff --git a/packages/app/src/components/workspace-setting/index.ts b/packages/app/src/components/workspace-setting/index.ts index c0eea1cfb0..0468074d2f 100644 --- a/packages/app/src/components/workspace-setting/index.ts +++ b/packages/app/src/components/workspace-setting/index.ts @@ -1,5 +1,5 @@ export * from './general'; export * from './ExportPage'; -export * from './MembersPage'; +export * from './member'; export * from './SyncPage'; export * from './PublishPage'; diff --git a/packages/app/src/components/invite-members/index.tsx b/packages/app/src/components/workspace-setting/member/InviteMemberModal.tsx similarity index 77% rename from packages/app/src/components/invite-members/index.tsx rename to packages/app/src/components/workspace-setting/member/InviteMemberModal.tsx index 6fb020fc52..c066908ed0 100644 --- a/packages/app/src/components/invite-members/index.tsx +++ b/packages/app/src/components/workspace-setting/member/InviteMemberModal.tsx @@ -4,9 +4,9 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal'; import { Button } from '@/ui/button'; import Input from '@/ui/input'; import { useState } from 'react'; -// import { getDataCenter } from '@affine/datacenter'; import { Avatar } from '@mui/material'; -import { setMember } from '@/hooks/mock-data/mock'; +import useMembers from '@/hooks/use-members'; +import { User } from '@affine/datacenter'; interface LoginModalProps { open: boolean; onClose: () => void; @@ -43,46 +43,27 @@ export const debounce = any>( }; const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/; -export const InviteMembers = ({ +export const InviteMemberModal = ({ open, onClose, - workspaceId, onInviteSuccess, }: LoginModalProps) => { const [email, setEmail] = useState(''); const [showMember, setShowMember] = useState(false); const [showTip, setShowTip] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [userData, setUserData] = useState({}); + const [userData, setUserData] = useState(null); + const { inviteMember, getUserByEmail } = useMembers(); const inputChange = (value: string) => { setShowMember(true); if (gmailReg.test(value)) { setEmail(value); setShowTip(false); - setUserData({ - name: 'wxl', - avatar: 'https://avatars.githubusercontent.com/u/20501502?v=4', - email: value, + getUserByEmail(value).then(data => { + if (data?.name) { + setUserData(data); + setShowTip(false); + } }); - // debounce( - // () => { - // getDataCenter() - // .then(dc => - // dc.apis.getUserByEmail({ - // email: value, - // workspace_id: workspaceId, - // }) - // ) - // .then(data => { - // if (data?.name) { - // setUserData(data); - // setShowTip(false); - // } - // }); - // }, - // 300, - // true - // )(); } else { setShowTip(true); } @@ -118,8 +99,8 @@ export const InviteMembers = ({ Non-Gmail is not supported ) : ( - {userData?.avatar_url ? ( - + {userData?.avatar ? ( + ) : ( @@ -139,19 +120,9 @@ export const InviteMembers = ({ + { + setIsInviteModalShow(false); + }} + onInviteSuccess={() => { + setIsInviteModalShow(false); + // refreshMembers(); + }} + workspaceId={workspace.id} + open={isInviteModalShow} + > + + + ); + } + + return ( + + <>Collaborating with other members requires AFFiNE Cloud service. + + + + + ); +}; diff --git a/packages/app/src/components/workspace-setting/member/index.ts b/packages/app/src/components/workspace-setting/member/index.ts new file mode 100644 index 0000000000..aced79efe5 --- /dev/null +++ b/packages/app/src/components/workspace-setting/member/index.ts @@ -0,0 +1 @@ +export * from './MembersPage'; diff --git a/packages/app/src/components/workspace-setting/member/style.ts b/packages/app/src/components/workspace-setting/member/style.ts new file mode 100644 index 0000000000..a5bf612f9a --- /dev/null +++ b/packages/app/src/components/workspace-setting/member/style.ts @@ -0,0 +1,107 @@ +import { styled } from '@/styles'; +import MuiAvatar from '@mui/material/Avatar'; +import { Button } from '@/ui/button'; + +export const StyledMemberTitleContainer = styled('div')(() => { + return { + display: 'flex', + marginTop: '60px', + fontWeight: '500', + flex: 1, + }; +}); + +export const StyledMemberAvatar = styled(MuiAvatar)(() => { + return { height: '40px', width: '40px' }; +}); + +export const StyledMemberNameContainer = styled('div')(() => { + return { + display: 'flex', + alignItems: 'center', + width: '402px', + }; +}); + +export const StyledMemberRoleContainer = styled('div')(() => { + return { + display: 'flex', + alignItems: 'center', + width: '222px', + }; +}); + +export const StyledMemberListContainer = styled('ul')(() => { + return { + marginTop: '15px', + overflowY: 'scroll', + }; +}); + +export const StyledMemberListItem = styled('li')(() => { + return { + display: 'flex', + alignItems: 'center', + height: '72px', + }; +}); + +export const StyledMemberInfo = styled('div')(() => { + return { + paddingLeft: '12px', + }; +}); + +export const StyledMemberName = styled('div')(({ theme }) => { + return { + fontWeight: '400', + fontSize: '18px', + lineHeight: '16px', + color: theme.colors.textColor, + }; +}); + +export const StyledMemberEmail = styled('div')(({ theme }) => { + return { + fontWeight: '400', + fontSize: '16px', + lineHeight: '22px', + color: theme.colors.iconColor, + }; +}); + +export const StyledMemberButtonContainer = styled('div')(() => { + return { + marginTop: '14px', + }; +}); + +export const StyledMoreVerticalButton = styled('button')(() => { + return { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: '24px', + height: '24px', + cursor: 'pointer', + }; +}); + +export const StyledPublishExplanation = styled('div')(() => { + return { + paddingRight: '48px', + fontWeight: '500', + fontSize: '18px', + lineHeight: '26px', + flex: 1, + marginTop: '60px', + }; +}); + +export const StyledMemberWarp = styled('div')(() => { + return { + display: 'flex', + flexDirection: 'column', + padding: '40px 0', + }; +}); diff --git a/packages/app/src/components/workspace-setting/style.ts b/packages/app/src/components/workspace-setting/style.ts index f750ef8598..1809ed4280 100644 --- a/packages/app/src/components/workspace-setting/style.ts +++ b/packages/app/src/components/workspace-setting/style.ts @@ -96,99 +96,6 @@ export const StyledSettingH2 = styled('h2')<{ marginTop?: number }>( } ); -export const StyledAvatarUploadBtn = styled(Button)(({ theme }) => { - return { - backgroundColor: theme.colors.hoverBackground, - color: theme.colors.primaryColor, - margin: '0 12px 0 24px', - }; -}); - -export const StyledMemberTitleContainer = styled('div')(() => { - return { - display: 'flex', - marginTop: '60px', - fontWeight: '500', - flex: 1, - }; -}); - -export const StyledMemberAvatar = styled(MuiAvatar)(() => { - return { height: '40px', width: '40px' }; -}); - -export const StyledMemberNameContainer = styled('div')(() => { - return { - display: 'flex', - alignItems: 'center', - width: '402px', - }; -}); - -export const StyledMemberRoleContainer = styled('div')(() => { - return { - display: 'flex', - alignItems: 'center', - width: '222px', - }; -}); - -export const StyledMemberListContainer = styled('ul')(() => { - return { - marginTop: '15px', - overflowY: 'scroll', - }; -}); - -export const StyledMemberListItem = styled('li')(() => { - return { - display: 'flex', - alignItems: 'center', - height: '72px', - }; -}); - -export const StyledMemberInfo = styled('div')(() => { - return { - paddingLeft: '12px', - }; -}); - -export const StyledMemberName = styled('div')(({ theme }) => { - return { - fontWeight: '400', - fontSize: '18px', - lineHeight: '16px', - color: theme.colors.textColor, - }; -}); - -export const StyledMemberEmail = styled('div')(({ theme }) => { - return { - fontWeight: '400', - fontSize: '16px', - lineHeight: '22px', - color: theme.colors.iconColor, - }; -}); - -export const StyledMemberButtonContainer = styled('div')(() => { - return { - marginTop: '14px', - }; -}); - -export const StyledMoreVerticalButton = styled('button')(() => { - return { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: '24px', - height: '24px', - cursor: 'pointer', - }; -}); - export const StyledPublishExplanation = styled('div')(() => { return { paddingRight: '48px', diff --git a/packages/app/src/hooks/use-members.ts b/packages/app/src/hooks/use-members.ts new file mode 100644 index 0000000000..3011c94f83 --- /dev/null +++ b/packages/app/src/hooks/use-members.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Member } from '@affine/datacenter'; +import { useAppState } from '@/providers/app-state-provider'; +export const useMembers = () => { + const { dataCenter, currentWorkspace } = useAppState(); + const [members, setMembers] = useState([]); + const [loaded, setLoaded] = useState(false); + const refreshMembers = useCallback(async () => { + if (!currentWorkspace || !dataCenter) return; + const members = await dataCenter.getMembers(currentWorkspace.id); + setMembers(members); + }, [dataCenter, currentWorkspace]); + + useEffect(() => { + const init = async () => { + await refreshMembers(); + setLoaded(true); + }; + init(); + }, [refreshMembers]); + + const inviteMember = async (email: string) => { + currentWorkspace && + dataCenter && + (await dataCenter.inviteMember(currentWorkspace?.id, email)); + }; + + const removeMember = async (permissionId: number) => { + if (!currentWorkspace || !dataCenter) { + return; + } + setLoaded(false); + await dataCenter.removeMember(currentWorkspace.id, permissionId); + await refreshMembers(); + setLoaded(true); + }; + + const getUserByEmail = async (email: string) => { + if (!currentWorkspace) return null; + return dataCenter?.getUserByEmail(currentWorkspace.id, email); + }; + return { + members, + removeMember, + inviteMember, + getUserByEmail, + loaded, + }; +}; + +export default useMembers; diff --git a/packages/app/src/hooks/use-workspace-helper.ts b/packages/app/src/hooks/use-workspace-helper.ts index f67a3b152f..397371e52f 100644 --- a/packages/app/src/hooks/use-workspace-helper.ts +++ b/packages/app/src/hooks/use-workspace-helper.ts @@ -56,16 +56,10 @@ export const useWorkspaceHelper = () => { }); }; - const inviteMember = async (email: string) => { - currentWorkspace && - (await dataCenter.inviteMember(currentWorkspace?.id, email)); - }; - return { createWorkspace, publishWorkspace, updateWorkspace, enableWorkspace, - inviteMember, }; }; diff --git a/packages/app/src/providers/app-state-provider/Provider.tsx b/packages/app/src/providers/app-state-provider/Provider.tsx index 710bf81853..ef6c594b0b 100644 --- a/packages/app/src/providers/app-state-provider/Provider.tsx +++ b/packages/app/src/providers/app-state-provider/Provider.tsx @@ -20,11 +20,9 @@ export const AppStateProvider = ({ children, }: PropsWithChildren) => { const [appState, setAppState] = useState({} as AppStateValue); - useEffect(() => { const initState = async () => { const dataCenter = await getDataCenter(); - // Ensure datacenter has at least one workspace if (dataCenter.workspaces.length === 0) { await createDefaultWorkspace(dataCenter); diff --git a/packages/data-center/src/index.ts b/packages/data-center/src/index.ts index fd199935fc..591a83b14a 100644 --- a/packages/data-center/src/index.ts +++ b/packages/data-center/src/index.ts @@ -26,7 +26,7 @@ const _initializeDataCenter = () => { export const getDataCenter = _initializeDataCenter(); export type { DataCenter }; -export type { AccessTokenMessage } from './provider/affine/apis'; +export * from './provider/affine/apis'; export { WorkspaceUnit } from './workspace-unit'; export { getLogger } from './logger'; export * from './message'; diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index a98386104b..bf4fd4b262 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -11,12 +11,13 @@ import { storage } from './storage.js'; import assert from 'assert'; import { WebsocketProvider } from './sync.js'; // import { IndexedDBProvider } from '../local/indexeddb'; -import { getApis } from './apis/index.js'; +import { getApis, Workspace } from './apis/index.js'; import type { Apis, WorkspaceDetail, Callback } from './apis'; import { setDefaultAvatar } from '../utils.js'; import { MessageCode } from '../../message'; import { token } from './apis/token.js'; import { WebsocketClient } from './channel'; +import { SyncMode } from '../../workspace-unit'; export interface AffineProviderConstructorParams extends ProviderConstructorParams { @@ -33,21 +34,12 @@ export class AffineProvider extends BaseProvider { private _onTokenRefresh?: Callback = undefined; private _wsMap: Map = new Map(); private _apis: Apis; - private _channel: WebsocketClient; + private _channel?: WebsocketClient; // private _idbMap: Map = new Map(); constructor({ apis, ...params }: AffineProviderConstructorParams) { super(params); this._apis = apis || getApis(); - this._channel = new WebsocketClient( - `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${ - window.location.host - }/global/sync/`, - this._logger - ); - if (token.isLogin) { - this._connectChannel(); - } } override async init() { @@ -76,14 +68,59 @@ export class AffineProvider extends BaseProvider { } else { storage.setItem('token', this._apis.token.refresh); } + + if (token.isLogin) { + this._connectChannel(); + } } private _connectChannel() { - this._channel.connect(); + if (!this._channel) { + this._channel = new WebsocketClient( + `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${ + window.location.host + }/api/global/sync/`, + this._logger, + { + params: { + token: this._apis.token.refresh, + }, + } + ); + } + this._channel.on('message', this.handlerAffineListMessage); + } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this._channel.on('message', (message: any) => { - console.log('message', message); + private handlerAffineListMessage({ + ws_details, + metadata, + }: { + ws_list: Workspace[]; + ws_details: Record; + metadata: Record; + }) { + Object.entries(ws_details).forEach(([id, detail]) => { + const { name, avatar } = metadata[id]; + assert(name); + const workspace = { + name: name, + avatar, + owner: { + name: detail.owner.name, + id: detail.owner.id, + email: detail.owner.email, + avatar: detail.owner.avatar_url, + }, + published: detail.public, + memberCount: detail.member_count, + provider: 'affine', + syncMode: 'core' as SyncMode, + }; + if (this._workspaces.get(id)) { + this._workspaces.update(id, workspace); + } else { + this._workspaces.add({ id, ...workspace }); + } }); } @@ -238,7 +275,7 @@ export class AffineProvider extends BaseProvider { } } const user = await this._apis.signInWithGoogle?.(); - if (!this._channel.connected) { + if (!this._channel?.connected) { this._connectChannel(); } if (!user) { @@ -368,13 +405,13 @@ export class AffineProvider extends BaseProvider { workspace_id: string, email: string ): Promise { - const user = await this._apis.getUserByEmail({ workspace_id, email }); - return user + const users = await this._apis.getUserByEmail({ workspace_id, email }); + return users?.length ? { - id: user.id, - name: user.name, - avatar: user.avatar_url, - email: user.email, + id: users[0].id, + name: users[0].name, + avatar: users[0].avatar_url, + email: users[0].email, } : null; } @@ -399,7 +436,7 @@ export class AffineProvider extends BaseProvider { public override async logout(): Promise { token.clear(); - this._channel.disconnect(); + this._channel?.disconnect(); this._wsMap.forEach(ws => ws.disconnect()); storage.removeItem('token'); } diff --git a/packages/data-center/src/provider/affine/apis/user.ts b/packages/data-center/src/provider/affine/apis/user.ts index 2848a9b673..58ede20fc3 100644 --- a/packages/data-center/src/provider/affine/apis/user.ts +++ b/packages/data-center/src/provider/affine/apis/user.ts @@ -15,7 +15,7 @@ export interface User { export async function getUserByEmail( params: GetUserByEmailParams -): Promise { +): Promise { const searchParams = new URLSearchParams({ ...params }); - return client.get('api/user', { searchParams }).json(); + return client.get('api/user', { searchParams }).json(); } diff --git a/packages/data-center/src/provider/affine/channel.ts b/packages/data-center/src/provider/affine/channel.ts index 4dc95004c5..1503a8b450 100644 --- a/packages/data-center/src/provider/affine/channel.ts +++ b/packages/data-center/src/provider/affine/channel.ts @@ -1,6 +1,7 @@ import * as websocket from 'lib0/websocket'; import { Logger } from 'src/types'; import { token } from './apis/token'; +import * as url from 'lib0/url'; const RECONNECT_INTERVAL_TIME = 5000; const MAX_RECONNECT_TIMES = 50; @@ -10,11 +11,21 @@ export class WebsocketClient extends websocket.WebsocketClient { private _reconnectInterval: number | null = null; private _logger: Logger; constructor( - url: string, + serverUrl: string, logger: Logger, - options?: { binaryType: 'arraybuffer' | 'blob' | null } + options?: ConstructorParameters[1] & { + params: Record; + } ) { - super(url, options); + const params = options?.params || {}; + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === '/') { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + const encodedParams = url.encodeQueryParams(params); + const newUrl = + serverUrl + '/' + (encodedParams.length === 0 ? '' : '?' + encodedParams); + super(newUrl, options); this._logger = logger; this._setupChannel(); }