mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
Merge branch 'master' into feat/client-app
This commit is contained in:
@@ -12,19 +12,19 @@
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/datacenter": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/blocks": "0.4.0-alpha.2",
|
||||
"@blocksuite/editor": "0.4.0-alpha.2",
|
||||
"@blocksuite/blocks": "0.4.0-20230210031655-264744e",
|
||||
"@blocksuite/editor": "0.4.0-20230210031655-264744e",
|
||||
"@blocksuite/icons": "^2.0.2",
|
||||
"@blocksuite/store": "0.4.0-alpha.2",
|
||||
"@blocksuite/store": "0.4.0-20230210031655-264744e",
|
||||
"@emotion/css": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/poppins": "^4.5.10",
|
||||
"@fontsource/space-mono": "^4.5.10",
|
||||
"@mui/base": "=5.0.0-alpha.101",
|
||||
"@mui/icons-material": "=5.10.9",
|
||||
"@mui/material": "=5.8.6",
|
||||
"@mui/base": "=5.0.0-alpha.117",
|
||||
"@mui/icons-material": "=5.11.0",
|
||||
"@mui/material": "=5.11.8",
|
||||
"@toeverything/pathfinder-logger": "workspace:@affine/logger@*",
|
||||
"cmdk": "^0.1.20",
|
||||
"css-spring": "^4.1.0",
|
||||
@@ -37,7 +37,8 @@
|
||||
"quill-cursors": "^4.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"yjs": "^13.5.45"
|
||||
"yjs": "^13.5.45",
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.7.18",
|
||||
|
||||
@@ -22,15 +22,26 @@ export const Editor = ({ page, workspace, setEditor }: Props) => {
|
||||
const editorContainer = useRef<HTMLDivElement>(null);
|
||||
// const { currentWorkspace, currentPage, setEditor } = useAppState();
|
||||
useEffect(() => {
|
||||
let blockHubElement: HTMLElement | null = null;
|
||||
const ret = () => {
|
||||
const node = editorContainer.current;
|
||||
while (node?.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
|
||||
blockHubElement?.remove();
|
||||
};
|
||||
|
||||
const editor = new EditorContainer();
|
||||
editor.page = page;
|
||||
editor.createBlockHub().then(blockHub => {
|
||||
const toolWrapper = document.querySelector('#toolWrapper');
|
||||
if (!toolWrapper) {
|
||||
throw new Error('Can not find toolWrapper');
|
||||
}
|
||||
blockHubElement = blockHub;
|
||||
toolWrapper.appendChild(blockHub);
|
||||
});
|
||||
editorContainer.current?.appendChild(editor);
|
||||
if (page.isEmpty) {
|
||||
const isFirstPage = workspace?.meta.pageMetas.length === 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconButton, IconButtonProps } from '@affine/component';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CloudUnsyncedIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { IconButton } from '@affine/component';
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export const HelpIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 3.8a8.2 8.2 0 1 0 0 16.4 8.2 8.2 0 0 0 0-16.4ZM2.2 12c0-5.412 4.388-9.8 9.8-9.8 5.412 0 9.8 4.388 9.8 9.8 0 5.412-4.388 9.8-9.8 9.8-5.412 0-9.8-4.388-9.8-9.8Zm9.978-3.18a1.448 1.448 0 0 0-1.61.948.8.8 0 1 1-1.51-.531 3.048 3.048 0 0 1 5.924 1.015c0 1.174-.87 1.945-1.48 2.351a6.082 6.082 0 0 1-1.275.642l-.027.01-.008.002-.003.001h-.002l-.254-.758.253.759a.8.8 0 0 1-.507-1.518h.001l.01-.004.055-.02a4.488 4.488 0 0 0 .869-.445c.513-.342.768-.696.768-1.02v-.002a1.449 1.449 0 0 0-1.204-1.43Zm-1.017 6.68a.8.8 0 0 1 .8-.8h.01a.8.8 0 1 1 0 1.6h-.01a.8.8 0 0 1-.8-.8Z"
|
||||
clipRule="evenodd"
|
||||
d="M12.0041 3.8002C7.47536 3.8002 3.8041 7.47146 3.8041 12.0002C3.8041 13.311 4.1111 14.5479 4.65639 15.6449C4.92133 16.1779 4.91348 16.8471 4.85228 17.3998C4.7869 17.9901 4.63749 18.6238 4.47572 19.1908C4.3722 19.5537 4.26086 19.8987 4.1573 20.2002H12.0041C16.5328 20.2002 20.2041 16.5289 20.2041 12.0002C20.2041 7.47146 16.5328 3.8002 12.0041 3.8002ZM2.26631 20.6926L2.2668 20.6914L2.26948 20.685L2.28104 20.6567C2.29139 20.6312 2.30687 20.5928 2.32657 20.5429C2.36599 20.4431 2.42218 20.2979 2.48795 20.1192C2.61992 19.7607 2.78833 19.2734 2.93712 18.7519C3.08715 18.226 3.21054 17.6884 3.262 17.2236C3.31765 16.7212 3.2707 16.4518 3.22364 16.3571C2.57081 15.0438 2.2041 13.5637 2.2041 12.0002C2.2041 6.58781 6.59171 2.2002 12.0041 2.2002C17.4165 2.2002 21.8041 6.5878 21.8041 12.0002C21.8041 17.4126 17.4165 21.8002 12.0041 21.8002H3.0049C2.73745 21.8002 2.4876 21.6665 2.33922 21.444C2.19087 21.2215 2.16356 20.9395 2.26631 20.6926ZM11.9672 9.0502C11.4091 9.0502 10.9382 9.43186 10.8049 9.9496C10.6948 10.3775 10.2587 10.6351 9.83079 10.5249C9.40291 10.4148 9.14532 9.97867 9.25545 9.55079C9.56623 8.3433 10.6614 7.4502 11.9672 7.4502C13.5136 7.4502 14.7672 8.7038 14.7672 10.2502C14.7672 11.1058 14.3536 11.6751 13.8978 12.115C13.7108 12.2955 13.4978 12.4721 13.2997 12.6362C13.2705 12.6604 13.2416 12.6844 13.2132 12.7081C12.982 12.9004 12.7556 13.0932 12.5329 13.3159C12.2205 13.6283 11.7139 13.6283 11.4015 13.3159C11.0891 13.0035 11.0891 12.4969 11.4015 12.1845C11.6788 11.9072 11.9525 11.6756 12.19 11.478C12.2213 11.4519 12.2517 11.4267 12.2812 11.4022C12.4849 11.2332 12.6465 11.0991 12.7866 10.9638C13.0808 10.6799 13.1672 10.4992 13.1672 10.2502C13.1672 9.58745 12.6299 9.0502 11.9672 9.0502ZM11.9772 16.5502H11.9672C11.5254 16.5502 11.1672 16.192 11.1672 15.7502C11.1672 15.3084 11.5254 14.9502 11.9672 14.9502H11.9772C12.419 14.9502 12.7772 15.3084 12.7772 15.7502C12.7772 16.192 12.419 16.5502 11.9772 16.5502Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -2,85 +2,71 @@ import { useState } from 'react';
|
||||
import {
|
||||
StyledIsland,
|
||||
StyledIconWrapper,
|
||||
StyledIslandWrapper,
|
||||
StyledTransformIcon,
|
||||
StyledAnimateWrapper,
|
||||
StyledTriggerWrapper,
|
||||
} from './style';
|
||||
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
|
||||
import { MuiGrow } from '@affine/component';
|
||||
import { Tooltip } from '@affine/component';
|
||||
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useTheme } from '@/providers/ThemeProvider';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
import { MuiFade } from '@affine/component';
|
||||
export type IslandItemNames = 'contact' | 'shortcuts';
|
||||
export const HelpIsland = ({
|
||||
showList = ['contact', 'shortcuts'],
|
||||
}: {
|
||||
showList?: IslandItemNames[];
|
||||
}) => {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const { mode } = useTheme();
|
||||
const { mode: editorMode } = useCurrentPageMeta() || {};
|
||||
const [spread, setShowSpread] = useState(false);
|
||||
const { triggerShortcutsModal, triggerContactModal } = useModal();
|
||||
const isEdgelessDark = mode === 'dark' && editorMode === 'edgeless';
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<StyledIsland
|
||||
className=""
|
||||
onMouseEnter={() => {
|
||||
setShowContent(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowContent(false);
|
||||
}}
|
||||
>
|
||||
<MuiGrow in={showContent}>
|
||||
<StyledIslandWrapper>
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content={t('Contact Us')} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerContactModal();
|
||||
}}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip content={t('Keyboard Shortcuts')} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerShortcutsModal();
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledIslandWrapper>
|
||||
</MuiGrow>
|
||||
<StyledIsland
|
||||
spread={spread}
|
||||
onClick={() => {
|
||||
setShowSpread(!spread);
|
||||
}}
|
||||
>
|
||||
<StyledAnimateWrapper spread={spread}>
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content={t('Contact Us')} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
triggerContactModal();
|
||||
}}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip content={t('Keyboard Shortcuts')} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
triggerShortcutsModal();
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledAnimateWrapper>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<StyledIconWrapper
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
data-testid="faq-icon"
|
||||
>
|
||||
<HelpIcon />
|
||||
</StyledIconWrapper>
|
||||
<StyledTransformIcon in={showContent}>
|
||||
<CloseIcon />
|
||||
</StyledTransformIcon>
|
||||
</div>
|
||||
</StyledIsland>
|
||||
</>
|
||||
<MuiFade in={!spread} data-testid="faq-icon">
|
||||
<StyledTriggerWrapper>
|
||||
<HelpIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
<MuiFade in={spread}>
|
||||
<StyledTriggerWrapper>
|
||||
<CloseIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
</StyledIsland>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,71 +1,77 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { displayFlex, positionAbsolute, styled } from '@affine/component';
|
||||
|
||||
export const StyledIsland = styled('div')(({ theme }) => {
|
||||
export const StyledIsland = styled('div')<{
|
||||
spread: boolean;
|
||||
}>(({ theme, spread }) => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
color: theme.colors.iconColor,
|
||||
position: 'fixed',
|
||||
right: '30px',
|
||||
bottom: '30px',
|
||||
borderRadius: '50%',
|
||||
zIndex: theme.zIndex.popover,
|
||||
};
|
||||
});
|
||||
export const StyledTransformIcon = styled('div', {
|
||||
shouldForwardProp: prop => prop !== 'in',
|
||||
})<{ in: boolean }>(({ in: isIn, theme }) => ({
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
opacity: isIn ? 1 : 0,
|
||||
backgroundColor: isIn
|
||||
? theme.colors.hoverBackground
|
||||
: theme.colors.pageBackground,
|
||||
}));
|
||||
export const StyledIconWrapper = styled('div')<{ isEdgelessDark: boolean }>(
|
||||
({ theme, isEdgelessDark }) => {
|
||||
return {
|
||||
color: isEdgelessDark
|
||||
? theme.colors.popoverBackground
|
||||
: theme.colors.iconColor,
|
||||
marginBottom: '24px',
|
||||
...displayFlex('center', 'center'),
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isEdgelessDark
|
||||
? 'transparent'
|
||||
: theme.colors.pageBackground,
|
||||
borderRadius: '50%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
transition: 'background-color 0.3s',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
color: isEdgelessDark
|
||||
? theme.colors.iconColor
|
||||
: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledIslandWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
color: theme.colors.iconColor,
|
||||
transition: 'box-shadow 0.2s',
|
||||
width: '44px',
|
||||
position: 'relative',
|
||||
boxShadow: spread
|
||||
? '4px 4px 7px rgba(58, 76, 92, 0.04), -4px -4px 13px rgba(58, 76, 92, 0.02), 6px 6px 36px rgba(58, 76, 92, 0.06)'
|
||||
: 'unset',
|
||||
padding: '0 4px 44px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
':hover': {
|
||||
color: theme.colors.popoverColor,
|
||||
boxShadow:
|
||||
'4px 4px 7px rgba(58, 76, 92, 0.04), -4px -4px 13px rgba(58, 76, 92, 0.02), 6px 6px 36px rgba(58, 76, 92, 0.06)',
|
||||
},
|
||||
'::after': {
|
||||
content: '""',
|
||||
width: '36px',
|
||||
height: '1px',
|
||||
background: spread ? theme.colors.borderColor : 'transparent',
|
||||
...positionAbsolute({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: '44px',
|
||||
}),
|
||||
margin: 'auto',
|
||||
transition: 'background 0.15s',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledIconWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
...displayFlex('center', 'center'),
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
borderRadius: '5px',
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
margin: '4px auto 4px',
|
||||
transition: 'background-color 0.2s',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledAnimateWrapper = styled('div', {
|
||||
shouldForwardProp: prop => prop !== 'spread',
|
||||
})<{ spread: boolean }>(({ spread }) => ({
|
||||
height: spread ? '88px' : '0',
|
||||
transition: 'height 0.2s cubic-bezier(0, 0, 0.55, 1.6)',
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
export const StyledTriggerWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
color: theme.colors.iconColor,
|
||||
borderRadius: '5px',
|
||||
...displayFlex('center', 'center'),
|
||||
...positionAbsolute({ left: '4px', bottom: '4px' }),
|
||||
':hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Results } from './Results';
|
||||
import { Footer } from './Footer';
|
||||
import { Command } from 'cmdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
import { getUaHelper } from '@/utils';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PublishedResults } from './PublishedResults';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import HelpIsland from '@/components/help-island';
|
||||
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
|
||||
import { useRouter } from 'next/router';
|
||||
import { StyledPage, StyledWrapper } from './styles';
|
||||
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
|
||||
import { PageLoading } from '@/components/loading';
|
||||
@@ -19,7 +19,14 @@ export const WorkspaceLayout = ({ children }: PropsWithChildren) => {
|
||||
<WorkSpaceSliderBar />
|
||||
<StyledWrapper>
|
||||
{children}
|
||||
<HelpIsland showList={router.query.pageId ? undefined : ['contact']} />
|
||||
<StyledToolWrapper>
|
||||
<div id="toolWrapper" style={{ marginBottom: '12px' }}>
|
||||
{/* Slot for block hub */}
|
||||
</div>
|
||||
<HelpIsland
|
||||
showList={router.query.pageId ? undefined : ['contact']}
|
||||
/>
|
||||
</StyledToolWrapper>
|
||||
</StyledWrapper>
|
||||
</StyledPage>
|
||||
);
|
||||
|
||||
@@ -16,3 +16,12 @@ export const StyledWrapper = styled('div')(() => {
|
||||
position: 'relative',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledToolWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'fixed',
|
||||
right: '30px',
|
||||
bottom: '30px',
|
||||
zIndex: theme.zIndex.popover,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
import { styled } from '@affine/component';
|
||||
import { AffineIcon } from '../../icons/Icons';
|
||||
import {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import Link from 'next/link';
|
||||
import { MuiCollapse } from '@affine/component';
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { IconButton } from '@affine/component';
|
||||
import useLocalStorage from '@/hooks/use-local-storage';
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import { AppStateProvider } from '@/providers/app-state-provider';
|
||||
import ConfirmProvider from '@/providers/ConfirmProvider';
|
||||
import { ModalProvider } from '@/providers/GlobalModalProvider';
|
||||
import { ModalProvider } from '@/store/globalModal';
|
||||
// import AppStateProvider2 from '@/providers/app-state-provider2/provider';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -21,6 +21,8 @@ import { useAppState } from '@/providers/app-state-provider';
|
||||
import { PageLoading } from '@/components/loading';
|
||||
import Head from 'next/head';
|
||||
import '@affine/i18n';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import React from 'react';
|
||||
|
||||
const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), {
|
||||
ssr: false,
|
||||
@@ -39,6 +41,11 @@ type AppPropsWithLayout = AppProps & {
|
||||
|
||||
const App = ({ Component, pageProps }: AppPropsWithLayout) => {
|
||||
const getLayout = Component.getLayout || (page => page);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
React.useEffect(() => {
|
||||
document.documentElement.lang = i18n.language;
|
||||
}, [i18n.language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { IconButton } from '@affine/component';
|
||||
import NextLink from 'next/link';
|
||||
import { PaperIcon, SearchIcon } from '@blocksuite/icons';
|
||||
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
|
||||
const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
|
||||
ssr: false,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Breadcrumbs } from '@affine/component';
|
||||
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/GlobalModalProvider';
|
||||
import { useModal } from '@/store/globalModal';
|
||||
const All = () => {
|
||||
const { dataCenter } = useAppState();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -6,3 +6,10 @@
|
||||
.affine-default-page-block-container {
|
||||
width: 686px !important;
|
||||
}
|
||||
|
||||
affine-block-hub {
|
||||
position: unset !important;
|
||||
}
|
||||
.block-hub-menu-container {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { EditorHeader } from '@/components/header';
|
||||
import EdgelessToolbar from '@/components/edgeless-toolbar';
|
||||
import MobileModal from '@/components/mobile-modal';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import type { NextPageWithLayout } from '../..//_app';
|
||||
@@ -43,7 +42,6 @@ const Page: NextPageWithLayout = () => {
|
||||
setEditor={setEditorHandler}
|
||||
/>
|
||||
)}
|
||||
<EdgelessToolbar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import ShortcutsModal from '@/components/shortcuts-modal';
|
||||
import ContactModal from '@/components/contact-modal';
|
||||
import QuickSearch from '@/components/quick-search';
|
||||
import { ImportModal } from '@/components/import';
|
||||
import { LoginModal } from '@/components/login-modal';
|
||||
|
||||
type ModalContextValue = {
|
||||
triggerShortcutsModal: () => void;
|
||||
triggerContactModal: () => void;
|
||||
triggerQuickSearchModal: (visible?: boolean) => void;
|
||||
triggerImportModal: () => void;
|
||||
triggerLoginModal: () => void;
|
||||
};
|
||||
type ModalContextProps = PropsWithChildren<Record<string, unknown>>;
|
||||
type ModalMap = {
|
||||
contact: boolean;
|
||||
shortcuts: boolean;
|
||||
quickSearch: boolean;
|
||||
import: boolean;
|
||||
login: boolean;
|
||||
};
|
||||
|
||||
export const ModalContext = createContext<ModalContextValue>({
|
||||
triggerShortcutsModal: () => {},
|
||||
triggerContactModal: () => {},
|
||||
triggerQuickSearchModal: () => {},
|
||||
triggerImportModal: () => {},
|
||||
triggerLoginModal: () => {},
|
||||
});
|
||||
|
||||
export const useModal = () => useContext(ModalContext);
|
||||
|
||||
export const ModalProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<ModalContextProps>) => {
|
||||
const [modalMap, setModalMap] = useState<ModalMap>({
|
||||
contact: false,
|
||||
shortcuts: false,
|
||||
quickSearch: false,
|
||||
import: false,
|
||||
login: false,
|
||||
});
|
||||
|
||||
const triggerHandler = useCallback(
|
||||
(key: keyof ModalMap, visible?: boolean) => {
|
||||
setModalMap({
|
||||
...modalMap,
|
||||
[key]: visible ?? !modalMap[key],
|
||||
});
|
||||
},
|
||||
[modalMap]
|
||||
);
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.triggerHandler = () => triggerHandler('login');
|
||||
}, [triggerHandler]);
|
||||
|
||||
return (
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
triggerShortcutsModal: () => {
|
||||
triggerHandler('shortcuts');
|
||||
},
|
||||
triggerContactModal: () => {
|
||||
triggerHandler('contact');
|
||||
},
|
||||
triggerQuickSearchModal: (visible?) => {
|
||||
triggerHandler('quickSearch', visible);
|
||||
},
|
||||
triggerImportModal: () => {
|
||||
triggerHandler('import');
|
||||
},
|
||||
triggerLoginModal: () => {
|
||||
triggerHandler('login');
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ContactModal
|
||||
open={modalMap.contact}
|
||||
onClose={() => {
|
||||
triggerHandler('contact', false);
|
||||
}}
|
||||
></ContactModal>
|
||||
<ShortcutsModal
|
||||
open={modalMap.shortcuts}
|
||||
onClose={() => {
|
||||
triggerHandler('shortcuts', false);
|
||||
}}
|
||||
></ShortcutsModal>
|
||||
<QuickSearch
|
||||
open={modalMap.quickSearch}
|
||||
onClose={() => {
|
||||
triggerHandler('quickSearch', false);
|
||||
}}
|
||||
></QuickSearch>
|
||||
<ImportModal
|
||||
open={modalMap.import}
|
||||
onClose={() => {
|
||||
triggerHandler('import', false);
|
||||
}}
|
||||
></ImportModal>
|
||||
<LoginModal
|
||||
open={modalMap.login}
|
||||
onClose={() => {
|
||||
triggerHandler('login', false);
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalProvider;
|
||||
146
packages/app/src/store/globalModal/index.tsx
Normal file
146
packages/app/src/store/globalModal/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import type React from 'react';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
import { createStore, useStore } from 'zustand';
|
||||
import { combine, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { UseBoundStore } from 'zustand/react';
|
||||
import ContactModal from '@/components/contact-modal';
|
||||
import ShortcutsModal from '@/components/shortcuts-modal';
|
||||
import QuickSearch from '@/components/quick-search';
|
||||
import { LoginModal } from '@/components/login-modal';
|
||||
import ImportModal from '@/components/import';
|
||||
|
||||
export type ModalState = {
|
||||
contact: boolean;
|
||||
shortcuts: boolean;
|
||||
quickSearch: boolean;
|
||||
import: boolean;
|
||||
login: boolean;
|
||||
};
|
||||
|
||||
export type ModalActions = {
|
||||
triggerShortcutsModal: () => void;
|
||||
triggerContactModal: () => void;
|
||||
triggerQuickSearchModal: (visible?: boolean) => void;
|
||||
triggerImportModal: () => void;
|
||||
triggerLoginModal: () => void;
|
||||
};
|
||||
|
||||
const create = () =>
|
||||
createStore(
|
||||
subscribeWithSelector(
|
||||
combine<ModalState, ModalActions>(
|
||||
{
|
||||
contact: false,
|
||||
shortcuts: false,
|
||||
quickSearch: false,
|
||||
import: false,
|
||||
login: false,
|
||||
},
|
||||
set => ({
|
||||
triggerShortcutsModal: () => {
|
||||
set(({ shortcuts }) => ({
|
||||
shortcuts: !shortcuts,
|
||||
}));
|
||||
},
|
||||
triggerContactModal: () => {
|
||||
set(({ contact }) => ({
|
||||
contact: !contact,
|
||||
}));
|
||||
},
|
||||
triggerQuickSearchModal: (visible?: boolean) => {
|
||||
set(({ quickSearch }) => ({
|
||||
quickSearch: visible ?? !quickSearch,
|
||||
}));
|
||||
},
|
||||
triggerImportModal: () => {
|
||||
set(state => ({
|
||||
import: !state.import,
|
||||
}));
|
||||
},
|
||||
triggerLoginModal: () => {
|
||||
set(({ login }) => ({
|
||||
login: !login,
|
||||
}));
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
type Store = ReturnType<typeof create>;
|
||||
|
||||
const ModalContext = createContext<Store | null>(null);
|
||||
|
||||
export const useModalApi = () => {
|
||||
const api = useContext(ModalContext);
|
||||
if (!api) {
|
||||
throw new Error('cannot find modal context');
|
||||
}
|
||||
return api;
|
||||
};
|
||||
|
||||
export const useModal: UseBoundStore<Store> = ((
|
||||
selector: Parameters<UseBoundStore<Store>>[0],
|
||||
equals: Parameters<UseBoundStore<Store>>[1]
|
||||
) => {
|
||||
const api = useModalApi();
|
||||
return useStore(api, selector, equals);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as any;
|
||||
|
||||
const Modals: React.FC = function Modal() {
|
||||
const api = useModalApi();
|
||||
return (
|
||||
<>
|
||||
<ContactModal
|
||||
open={useModal(state => state.contact)}
|
||||
onClose={useCallback(() => {
|
||||
api.setState({
|
||||
contact: false,
|
||||
});
|
||||
}, [api])}
|
||||
></ContactModal>
|
||||
<ShortcutsModal
|
||||
open={useModal(state => state.shortcuts)}
|
||||
onClose={useCallback(() => {
|
||||
api.setState({
|
||||
shortcuts: false,
|
||||
});
|
||||
}, [api])}
|
||||
></ShortcutsModal>
|
||||
<QuickSearch
|
||||
open={useModal(state => state.quickSearch)}
|
||||
onClose={useCallback(() => {
|
||||
api.setState({
|
||||
quickSearch: false,
|
||||
});
|
||||
}, [api])}
|
||||
></QuickSearch>
|
||||
<ImportModal
|
||||
open={useModal(state => state.import)}
|
||||
onClose={useCallback(() => {
|
||||
api.setState({
|
||||
import: false,
|
||||
});
|
||||
}, [api])}
|
||||
></ImportModal>
|
||||
<LoginModal
|
||||
open={useModal(state => state.login)}
|
||||
onClose={useCallback(() => {
|
||||
api.setState({
|
||||
login: false,
|
||||
});
|
||||
}, [api])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalProvider: React.FC<React.PropsWithChildren> =
|
||||
function ModelProvider({ children }) {
|
||||
return (
|
||||
<ModalContext.Provider value={useMemo(() => create(), [])}>
|
||||
<Modals />
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getLightTheme, ThemeProvider } from '../src';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
@@ -7,3 +9,15 @@ export const parameters = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const lightTheme = getLightTheme('page');
|
||||
|
||||
export const decorators = [
|
||||
Story => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/editor": "0.4.0-alpha.2",
|
||||
"@blocksuite/editor": "0.4.0-20230209191848-0a912e3",
|
||||
"@blocksuite/icons": "^2.0.2",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/base": "=5.0.0-alpha.101",
|
||||
"@mui/icons-material": "=5.10.9",
|
||||
"@mui/material": "=5.8.6",
|
||||
"@mui/base": "=5.0.0-alpha.117",
|
||||
"@mui/icons-material": "=5.11.0",
|
||||
"@mui/material": "=5.11.8",
|
||||
"lit": "^2.6.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import React, { useMemo } from 'react';
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import React from 'react';
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import { Breadcrumbs, getLightTheme, ThemeProvider } from '..';
|
||||
import { Breadcrumbs } from '..';
|
||||
import { Link, Typography } from '@mui/material';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Breadcrumbs',
|
||||
component: Breadcrumbs,
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = args => (
|
||||
<ThemeProvider theme={useMemo(() => getLightTheme('page'), [])}>
|
||||
<Breadcrumbs {...args} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
const Template: Story = args => <Breadcrumbs {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
export const Primary = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
children: [<span>1</span>, <span>2</span>, <span>3</span>],
|
||||
children: [
|
||||
<Link underline="hover" color="inherit" href="/">
|
||||
AFFiNE
|
||||
</Link>,
|
||||
<Link underline="hover" color="inherit" href="/Docs/">
|
||||
Docs
|
||||
</Link>,
|
||||
<Typography color="text.primary">Introduction</Typography>,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
import React, { useMemo } from 'react';
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import React from 'react';
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import { Button, getLightTheme, ThemeProvider } from '..';
|
||||
import { Button } from '..';
|
||||
import { ButtonProps } from '../ui/button/interface';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Button',
|
||||
component: Button,
|
||||
} as Meta;
|
||||
argTypes: {
|
||||
hoverBackground: { control: 'color' },
|
||||
hoverColor: { control: 'color' },
|
||||
},
|
||||
} as Meta<ButtonProps>;
|
||||
|
||||
const Template: Story = args => (
|
||||
<ThemeProvider theme={useMemo(() => getLightTheme('page'), [])}>
|
||||
<Button {...args} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
const Template: Story<ButtonProps> = args => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
export const Primary = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
children: 'This is a button',
|
||||
children: 'This is a primary button',
|
||||
};
|
||||
|
||||
export const Default = Template.bind(undefined);
|
||||
Default.args = {
|
||||
type: 'default',
|
||||
children: 'This is a default button',
|
||||
};
|
||||
|
||||
export const Light = Template.bind(undefined);
|
||||
Light.args = {
|
||||
type: 'light',
|
||||
children: 'This is a light button',
|
||||
};
|
||||
|
||||
export const Warning = Template.bind(undefined);
|
||||
Warning.args = {
|
||||
type: 'warning',
|
||||
children: 'This is a warning button',
|
||||
};
|
||||
|
||||
export const Danger = Template.bind(undefined);
|
||||
Danger.args = {
|
||||
type: 'danger',
|
||||
children: 'This is a danger button',
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ export const Modal = (props: ModalProps) => {
|
||||
<StyledModal
|
||||
{...otherProps}
|
||||
open={open}
|
||||
components={{ Backdrop }}
|
||||
slots={{ backdrop: Backdrop }}
|
||||
alignItems={transformConfig[vertical]}
|
||||
justifyContent={transformConfig[horizontal]}
|
||||
>
|
||||
|
||||
@@ -3,5 +3,6 @@ import MuiCollapse from '@mui/material/Collapse';
|
||||
import MuiSlide from '@mui/material/Slide';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
import MuiGrow from '@mui/material/Grow';
|
||||
import MuiFade from '@mui/material/Fade';
|
||||
|
||||
export { MuiBreadcrumbs, MuiCollapse, MuiSlide, MuiAvatar, MuiGrow };
|
||||
export { MuiBreadcrumbs, MuiCollapse, MuiSlide, MuiAvatar, MuiGrow, MuiFade };
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
"yjs": "^13.5.45"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "0.4.0-alpha.2",
|
||||
"@blocksuite/store": "0.4.0-alpha.2",
|
||||
"@blocksuite/blocks": "0.4.0-20230210031655-264744e",
|
||||
"@blocksuite/store": "0.4.0-20230210031655-264744e",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"debug": "^4.3.4",
|
||||
"encoding": "^0.1.13",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Apis, AccessTokenMessage } from '../apis';
|
||||
|
||||
const user: AccessTokenMessage = {
|
||||
create_at: Date.now(),
|
||||
created_at: Date.now(),
|
||||
exp: 100000000,
|
||||
email: 'demo@demo.demo',
|
||||
id: '123',
|
||||
|
||||
@@ -5,13 +5,11 @@ import type {
|
||||
} from '../base';
|
||||
import type { User } from '../../types';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { storage } from './storage.js';
|
||||
import assert from 'assert';
|
||||
import { WebsocketProvider } from './sync.js';
|
||||
// import { IndexedDBProvider } from '../local/indexeddb';
|
||||
import { getApis, Workspace } from './apis/index.js';
|
||||
import type { Apis, WorkspaceDetail, Callback } from './apis';
|
||||
import { token } from './apis/token.js';
|
||||
import type { Apis, WorkspaceDetail } from './apis';
|
||||
import { WebsocketClient } from './channel';
|
||||
import {
|
||||
loadWorkspaceUnit,
|
||||
@@ -40,10 +38,10 @@ const {
|
||||
|
||||
export class AffineProvider extends BaseProvider {
|
||||
public id = 'affine';
|
||||
private _onTokenRefresh?: Callback = undefined;
|
||||
private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
|
||||
private _apis: Apis;
|
||||
private _channel?: WebsocketClient;
|
||||
private _refreshToken?: string;
|
||||
// private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
||||
private _workspaceLoadingQueue: Set<string> = new Set();
|
||||
|
||||
@@ -53,40 +51,25 @@ export class AffineProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
override async init() {
|
||||
this._onTokenRefresh = () => {
|
||||
if (this._apis.token.refresh) {
|
||||
storage.setItem('token', this._apis.token.refresh);
|
||||
this._apis.auth.onChange(() => {
|
||||
if (this._apis.auth.isLogin) {
|
||||
this._reconnectChannel();
|
||||
} else {
|
||||
this._destroyChannel();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this._apis.token.onChange(this._onTokenRefresh);
|
||||
|
||||
// initial login token
|
||||
if (this._apis.token.isExpired) {
|
||||
try {
|
||||
const refreshToken = storage.getItem('token');
|
||||
if (!refreshToken) return;
|
||||
await this._apis.token.refreshToken(refreshToken);
|
||||
|
||||
if (this._apis.token.refresh) {
|
||||
storage.set('token', this._apis.token.refresh);
|
||||
}
|
||||
|
||||
assert(this._apis.token.isLogin);
|
||||
} catch (_) {
|
||||
// this._logger('Authorization failed, fallback to local mode');
|
||||
}
|
||||
} else {
|
||||
storage.setItem('token', this._apis.token.refresh);
|
||||
}
|
||||
|
||||
if (token.isLogin) {
|
||||
this._connectChannel();
|
||||
if (this._apis.auth.isExpired && this._apis.auth.refresh) {
|
||||
// do we need to await the following?
|
||||
this._apis.auth.refreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
private _connectChannel() {
|
||||
if (!this._channel) {
|
||||
private _reconnectChannel() {
|
||||
if (this._refreshToken !== this._apis.auth.refresh) {
|
||||
// need to reconnect
|
||||
this._destroyChannel();
|
||||
|
||||
this._channel = new WebsocketClient(
|
||||
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||
window.location.host
|
||||
@@ -94,14 +77,25 @@ export class AffineProvider extends BaseProvider {
|
||||
this._logger,
|
||||
{
|
||||
params: {
|
||||
token: this._apis.token.refresh,
|
||||
token: this._apis.auth.refresh,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this._channel.on('message', (msg: ChannelMessage) => {
|
||||
this._handlerAffineListMessage(msg);
|
||||
});
|
||||
|
||||
this._refreshToken = this._apis.auth.refresh;
|
||||
}
|
||||
}
|
||||
|
||||
private _destroyChannel() {
|
||||
if (this._channel) {
|
||||
this._channel.disconnect();
|
||||
this._channel.destroy();
|
||||
this._channel = undefined;
|
||||
}
|
||||
this._channel.on('message', (msg: ChannelMessage) => {
|
||||
this._handlerAffineListMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
private async _handlerAffineListMessage({
|
||||
@@ -111,7 +105,7 @@ export class AffineProvider extends BaseProvider {
|
||||
this._logger('receive server message');
|
||||
const newlyCreatedWorkspaces: WorkspaceUnit[] = [];
|
||||
const currentWorkspaceIds = this._workspaces.list().map(w => w.id);
|
||||
const newlyRemovedWorkspacecIds = currentWorkspaceIds;
|
||||
const newlyRemovedWorkspaceIds = currentWorkspaceIds;
|
||||
for (const [id, detail] of Object.entries(ws_details)) {
|
||||
const { name, avatar } = metadata[id];
|
||||
|
||||
@@ -121,7 +115,7 @@ export class AffineProvider extends BaseProvider {
|
||||
const workspaceIndex = currentWorkspaceIds.indexOf(id);
|
||||
const ifWorkspaceExist = workspaceIndex !== -1;
|
||||
if (ifWorkspaceExist) {
|
||||
newlyRemovedWorkspacecIds.splice(workspaceIndex, 1);
|
||||
newlyRemovedWorkspaceIds.splice(workspaceIndex, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,7 +157,7 @@ export class AffineProvider extends BaseProvider {
|
||||
this._workspaces.add(newlyCreatedWorkspaces);
|
||||
|
||||
// sync newlyRemoveWorkspaces to context
|
||||
this._workspaces.remove(newlyRemovedWorkspacecIds);
|
||||
this._workspaces.remove(newlyRemovedWorkspaceIds);
|
||||
}
|
||||
|
||||
private _getWebsocketProvider(workspace: BlocksuiteWorkspace) {
|
||||
@@ -176,13 +170,13 @@ export class AffineProvider extends BaseProvider {
|
||||
window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
}://${window.location.host}/api/sync/`;
|
||||
ws = new WebsocketProvider(wsUrl, room, doc, {
|
||||
params: { token: this._apis.token.refresh },
|
||||
params: { token: this._apis.auth.refresh },
|
||||
// @ts-expect-error ignore the type
|
||||
awareness: workspace.awarenessStore.awareness,
|
||||
});
|
||||
workspace.awarenessStore.awareness.setLocalStateField('user', {
|
||||
name: token.user?.name ?? 'other',
|
||||
id: Number(token.user?.id ?? -1),
|
||||
name: this._apis.auth.user?.name ?? 'other',
|
||||
id: Number(this._apis.auth.user?.id ?? -1),
|
||||
color: '#ffa500',
|
||||
});
|
||||
|
||||
@@ -232,7 +226,7 @@ export class AffineProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
override async loadWorkspaces() {
|
||||
if (!this._apis.token.isLogin) {
|
||||
if (!this._apis.auth.isLogin) {
|
||||
return [];
|
||||
}
|
||||
const workspacesList = await this._apis.getWorkspaces();
|
||||
@@ -261,25 +255,24 @@ export class AffineProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
override async auth() {
|
||||
const refreshToken = await storage.getItem('token');
|
||||
if (refreshToken) {
|
||||
await this._apis.token.refreshToken(refreshToken);
|
||||
if (this._apis.token.isLogin && !this._apis.token.isExpired) {
|
||||
if (this._apis.auth.isLogin) {
|
||||
await this._apis.auth.refreshToken();
|
||||
if (this._apis.auth.isLogin && !this._apis.auth.isExpired) {
|
||||
// login success
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this._apis.signInWithGoogle?.();
|
||||
if (!this._channel?.connected) {
|
||||
this._connectChannel();
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
this._sendMessage(MessageCenter.messageCode.loginError);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: may need to update related workspace attributes on user info change?
|
||||
public override async getUserInfo(): Promise<User | undefined> {
|
||||
const user = this._apis.token.user;
|
||||
const user = this._apis.auth.user;
|
||||
return user
|
||||
? {
|
||||
id: user.id,
|
||||
@@ -389,7 +382,7 @@ export class AffineProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
public override getToken(): string {
|
||||
return this._apis.token.token;
|
||||
return this._apis.auth.token;
|
||||
}
|
||||
|
||||
public override async getUserByEmail(
|
||||
@@ -441,11 +434,11 @@ export class AffineProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
public override async logout(): Promise<void> {
|
||||
token.clear();
|
||||
this._channel?.disconnect();
|
||||
this._apis.auth.clear();
|
||||
this._destroyChannel();
|
||||
this._wsMap.forEach(ws => ws.disconnect());
|
||||
this._workspaces.clear(false);
|
||||
storage.removeItem('token');
|
||||
await this._apis.signOutFirebase();
|
||||
}
|
||||
|
||||
public override async getWorkspaceMembers(id: string) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { Token } from '../token.js';
|
||||
import { Auth } from '../auth.js';
|
||||
|
||||
test.describe('class Token', () => {
|
||||
test.describe('class Auth', () => {
|
||||
test('parse tokens', () => {
|
||||
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
||||
expect(Token.parse(tokenString)).toEqual({
|
||||
expect(Auth.parseIdToken(tokenString)).toEqual({
|
||||
avatar_url: 'https://test.com/avatar',
|
||||
created_at: 1675678920358,
|
||||
email: 'test@gmail.com',
|
||||
@@ -16,6 +16,6 @@ test.describe('class Token', () => {
|
||||
|
||||
test('parse invalid tokens', () => {
|
||||
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
||||
expect(Token.parse(tokenString)).toEqual(null);
|
||||
expect(Auth.parseIdToken(tokenString)).toEqual(null);
|
||||
});
|
||||
});
|
||||
254
packages/data-center/src/provider/affine/apis/auth.ts
Normal file
254
packages/data-center/src/provider/affine/apis/auth.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import {
|
||||
type Auth as FirebaseAuth,
|
||||
getAuth as getFirebaseAuth,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup,
|
||||
signOut,
|
||||
} from 'firebase/auth';
|
||||
import type { User } from 'firebase/auth';
|
||||
import { decode } from 'js-base64';
|
||||
|
||||
import { getLogger } from '../../../logger.js';
|
||||
import { bareClient } from './request.js';
|
||||
import { storage } from '../storage.js';
|
||||
|
||||
export interface AccessTokenMessage {
|
||||
created_at: number;
|
||||
exp: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export type Callback = (user: AccessTokenMessage | null) => void;
|
||||
|
||||
type LoginParams = {
|
||||
type: 'Google' | 'Refresh';
|
||||
token: string;
|
||||
};
|
||||
|
||||
type LoginResponse = {
|
||||
// access token, expires in a very short time
|
||||
token: string;
|
||||
// Refresh token
|
||||
refresh: string;
|
||||
};
|
||||
|
||||
// TODO: organize storage keys in a better way
|
||||
const AFFINE_LOGIN_STORAGE_KEY = 'affine:login';
|
||||
|
||||
/**
|
||||
* Use refresh token to get a new access token (JWT)
|
||||
* The returned token also contains the user info payload.
|
||||
*/
|
||||
const doLogin = (params: LoginParams): Promise<LoginResponse> =>
|
||||
bareClient.post('api/user/token', { json: params }).json();
|
||||
|
||||
export class Auth {
|
||||
private readonly _logger;
|
||||
private _accessToken = ''; // idtoken (JWT)
|
||||
private _refreshToken = '';
|
||||
|
||||
private _user: AccessTokenMessage | null = null;
|
||||
private _padding?: Promise<LoginResponse>;
|
||||
|
||||
constructor() {
|
||||
this._logger = getLogger('token');
|
||||
this._logger.enabled = true;
|
||||
|
||||
this.restoreLogin();
|
||||
}
|
||||
|
||||
setLogin(login: LoginResponse) {
|
||||
this._accessToken = login.token;
|
||||
this._refreshToken = login.refresh;
|
||||
this._user = Auth.parseIdToken(this._accessToken);
|
||||
|
||||
this.triggerChange(this._user);
|
||||
this.storeLogin();
|
||||
}
|
||||
|
||||
private storeLogin() {
|
||||
if (this.refresh) {
|
||||
const { token, refresh } = this;
|
||||
storage.setItem(
|
||||
AFFINE_LOGIN_STORAGE_KEY,
|
||||
JSON.stringify({ token, refresh })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private restoreLogin() {
|
||||
const loginStr = storage.getItem(AFFINE_LOGIN_STORAGE_KEY);
|
||||
if (!loginStr) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const login: LoginResponse = JSON.parse(loginStr);
|
||||
this.setLogin(login);
|
||||
} catch (err) {
|
||||
this._logger('Failed to parse login info', err);
|
||||
}
|
||||
}
|
||||
|
||||
async initToken(token: string) {
|
||||
const res = await doLogin({ token, type: 'Google' });
|
||||
this.setLogin(res);
|
||||
return this._user;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken?: string) {
|
||||
if (!this._padding) {
|
||||
this._padding = doLogin({
|
||||
type: 'Refresh',
|
||||
token: refreshToken || this._refreshToken,
|
||||
});
|
||||
this._padding.finally(() => {
|
||||
// clear on settled
|
||||
this._padding = undefined;
|
||||
});
|
||||
this._refreshToken = refreshToken || this._refreshToken;
|
||||
}
|
||||
const res = await this._padding;
|
||||
if (!refreshToken || refreshToken !== this._refreshToken) {
|
||||
this.setLogin(res);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get user() {
|
||||
// computed through access token
|
||||
return this._user;
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this._accessToken;
|
||||
}
|
||||
|
||||
get refresh() {
|
||||
return this._refreshToken;
|
||||
}
|
||||
|
||||
get isLogin() {
|
||||
return !!this._refreshToken;
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
if (!this._user) return true;
|
||||
// exp is in seconds
|
||||
return Date.now() > this._user.exp * 1000;
|
||||
}
|
||||
|
||||
static parseIdToken(token: string): AccessTokenMessage | null {
|
||||
try {
|
||||
return JSON.parse(decode(token.split('.')[1]));
|
||||
} catch (error) {
|
||||
// todo: log errors?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private callbacks: Callback[] = [];
|
||||
private lastState: AccessTokenMessage | null = null;
|
||||
|
||||
triggerChange(user: AccessTokenMessage | null) {
|
||||
this.lastState = user;
|
||||
this.callbacks.forEach(callback => callback(user));
|
||||
}
|
||||
|
||||
onChange(callback: Callback) {
|
||||
this.callbacks.push(callback);
|
||||
callback(this.lastState);
|
||||
}
|
||||
|
||||
offChange(callback: Callback) {
|
||||
const index = this.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._accessToken = '';
|
||||
this._refreshToken = '';
|
||||
storage.removeItem(AFFINE_LOGIN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export const auth = new Auth();
|
||||
|
||||
export const getAuthorizer = () => {
|
||||
let _firebaseAuth: FirebaseAuth | null = null;
|
||||
|
||||
// getAuth will send requests on calling thus we can lazy init it
|
||||
const getAuth = () => {
|
||||
try {
|
||||
if (!_firebaseAuth) {
|
||||
const app = initializeApp({
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId:
|
||||
process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||
});
|
||||
_firebaseAuth = getFirebaseAuth(app);
|
||||
}
|
||||
return _firebaseAuth;
|
||||
} catch (error) {
|
||||
getLogger('getAuthorizer')(error);
|
||||
console.error('getAuthorizer', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getToken = async () => {
|
||||
const currentUser = getAuth()?.currentUser;
|
||||
if (currentUser) {
|
||||
await currentUser.getIdTokenResult(true);
|
||||
if (!currentUser.isAnonymous) {
|
||||
return currentUser.getIdToken();
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
const idToken = await getToken();
|
||||
let loginUser: AccessTokenMessage | null = null;
|
||||
if (idToken) {
|
||||
loginUser = await auth.initToken(idToken);
|
||||
} else {
|
||||
const firebaseAuth = getAuth();
|
||||
if (firebaseAuth) {
|
||||
const googleAuthProvider = new GoogleAuthProvider();
|
||||
// make sure the user has a chance to select an account
|
||||
// https://developers.google.com/identity/openid-connect/openid-connect#prompt
|
||||
googleAuthProvider.setCustomParameters({
|
||||
prompt: 'select_account',
|
||||
});
|
||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||
const idToken = await user.user.getIdToken();
|
||||
loginUser = await auth.initToken(idToken);
|
||||
}
|
||||
}
|
||||
return loginUser;
|
||||
};
|
||||
|
||||
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||
getAuth()?.onAuthStateChanged(callback);
|
||||
};
|
||||
|
||||
const signOutFirebase = async () => {
|
||||
const firebaseAuth = getAuth();
|
||||
if (firebaseAuth?.currentUser) {
|
||||
await signOut(firebaseAuth);
|
||||
}
|
||||
};
|
||||
|
||||
return [signInWithGoogle, onAuthStateChanged, signOutFirebase] as const;
|
||||
};
|
||||
@@ -1,28 +1,40 @@
|
||||
// export { token } from './token.js';
|
||||
export type { Callback } from './token.js';
|
||||
export type { Callback } from './auth.js';
|
||||
|
||||
import { getAuthorizer } from './token.js';
|
||||
import { getAuthorizer } from './auth.js';
|
||||
import * as user from './user.js';
|
||||
import * as workspace from './workspace.js';
|
||||
import { token } from './token.js';
|
||||
import { auth } from './auth.js';
|
||||
|
||||
export type Apis = typeof user &
|
||||
Omit<typeof workspace, 'WorkspaceType' | 'PermissionType'> & {
|
||||
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
||||
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
||||
} & { token: typeof token };
|
||||
// See https://twitter.com/mattpocockuk/status/1622730173446557697
|
||||
// TODO: move to ts utils?
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
} & {};
|
||||
|
||||
export type Apis = Prettify<
|
||||
typeof user &
|
||||
Omit<typeof workspace, 'WorkspaceType' | 'PermissionType'> & {
|
||||
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
||||
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
||||
signOutFirebase: ReturnType<typeof getAuthorizer>[2];
|
||||
} & { auth: typeof auth }
|
||||
>;
|
||||
|
||||
export const getApis = (): Apis => {
|
||||
const [signInWithGoogle, onAuthStateChanged] = getAuthorizer();
|
||||
const [signInWithGoogle, onAuthStateChanged, signOutFirebase] =
|
||||
getAuthorizer();
|
||||
return {
|
||||
...user,
|
||||
...workspace,
|
||||
signInWithGoogle,
|
||||
signOutFirebase,
|
||||
onAuthStateChanged,
|
||||
token,
|
||||
auth,
|
||||
};
|
||||
};
|
||||
|
||||
export type { AccessTokenMessage } from './token';
|
||||
export type { AccessTokenMessage } from './auth';
|
||||
export type { Member, Workspace, WorkspaceDetail } from './workspace';
|
||||
export { WorkspaceType } from './workspace.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ky from 'ky-universal';
|
||||
import { MessageCenter } from '../../../message/index.js';
|
||||
import { token } from './token.js';
|
||||
import { auth } from './auth.js';
|
||||
|
||||
type KyInstance = typeof ky;
|
||||
|
||||
@@ -28,13 +28,23 @@ export const bareClient: KyInstance = ky.extend({
|
||||
},
|
||||
});
|
||||
|
||||
const refreshTokenIfExpired = async () => {
|
||||
if (auth.isLogin && auth.isExpired) {
|
||||
try {
|
||||
await auth.refreshToken();
|
||||
} catch (err) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const client: KyInstance = bareClient.extend({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async request => {
|
||||
if (token.isLogin) {
|
||||
if (token.isExpired) await token.refreshToken();
|
||||
request.headers.set('Authorization', token.token);
|
||||
if (auth.isLogin) {
|
||||
await refreshTokenIfExpired();
|
||||
request.headers.set('Authorization', auth.token);
|
||||
} else {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
@@ -43,8 +53,8 @@ export const client: KyInstance = bareClient.extend({
|
||||
|
||||
beforeRetry: [
|
||||
async ({ request }) => {
|
||||
await token.refreshToken();
|
||||
request.headers.set('Authorization', token.token);
|
||||
await refreshTokenIfExpired();
|
||||
request.headers.set('Authorization', auth.token);
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
||||
import type { User } from 'firebase/auth';
|
||||
import { decode } from 'js-base64';
|
||||
|
||||
import { getLogger } from '../../../logger.js';
|
||||
import { bareClient } from './request.js';
|
||||
|
||||
export interface AccessTokenMessage {
|
||||
create_at: number;
|
||||
exp: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export type Callback = (user: AccessTokenMessage | null) => void;
|
||||
|
||||
type LoginParams = {
|
||||
type: 'Google' | 'Refresh';
|
||||
token: string;
|
||||
};
|
||||
|
||||
type LoginResponse = {
|
||||
// access token, expires in a very short time
|
||||
token: string;
|
||||
// Refresh token
|
||||
refresh: string;
|
||||
};
|
||||
|
||||
const login = (params: LoginParams): Promise<LoginResponse> =>
|
||||
bareClient.post('api/user/token', { json: params }).json();
|
||||
|
||||
export class Token {
|
||||
private readonly _logger;
|
||||
private _accessToken!: string;
|
||||
private _refreshToken!: string;
|
||||
|
||||
private _user!: AccessTokenMessage | null;
|
||||
private _padding?: Promise<LoginResponse>;
|
||||
|
||||
constructor() {
|
||||
this._logger = getLogger('token');
|
||||
this._logger.enabled = true;
|
||||
|
||||
this._setToken(); // fill with default value
|
||||
}
|
||||
|
||||
get user() {
|
||||
return this._user;
|
||||
}
|
||||
|
||||
private _setToken(login?: LoginResponse) {
|
||||
this._accessToken = login?.token || '';
|
||||
this._refreshToken = login?.refresh || '';
|
||||
|
||||
this._user = Token.parse(this._accessToken);
|
||||
if (login) {
|
||||
this._logger('set login', login);
|
||||
this.triggerChange(this._user);
|
||||
} else {
|
||||
this._logger('empty login');
|
||||
}
|
||||
}
|
||||
|
||||
async initToken(token: string) {
|
||||
const tokens = await login({ token, type: 'Google' });
|
||||
this._setToken(tokens);
|
||||
return this._user;
|
||||
}
|
||||
|
||||
async refreshToken(token?: string) {
|
||||
if (!this._padding) {
|
||||
this._padding = login({
|
||||
type: 'Refresh',
|
||||
token: this._refreshToken || token!,
|
||||
});
|
||||
}
|
||||
this._setToken(await this._padding);
|
||||
this._padding = undefined;
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this._accessToken;
|
||||
}
|
||||
|
||||
get refresh() {
|
||||
return this._refreshToken;
|
||||
}
|
||||
|
||||
get isLogin() {
|
||||
return !!this._refreshToken;
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
if (!this._user) return true;
|
||||
return Date.now() - this._user.create_at > this._user.exp;
|
||||
}
|
||||
|
||||
static parse(token: string): AccessTokenMessage | null {
|
||||
try {
|
||||
return JSON.parse(decode(token.split('.')[1]));
|
||||
} catch (error) {
|
||||
// todo: log errors?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private callbacks: Callback[] = [];
|
||||
private lastState: AccessTokenMessage | null = null;
|
||||
|
||||
triggerChange(user: AccessTokenMessage | null) {
|
||||
this.lastState = user;
|
||||
this.callbacks.forEach(callback => callback(user));
|
||||
}
|
||||
|
||||
onChange(callback: Callback) {
|
||||
this.callbacks.push(callback);
|
||||
callback(this.lastState);
|
||||
}
|
||||
|
||||
offChange(callback: Callback) {
|
||||
const index = this.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._setToken();
|
||||
}
|
||||
}
|
||||
|
||||
export const token = new Token();
|
||||
|
||||
export const getAuthorizer = () => {
|
||||
const app = initializeApp({
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||
});
|
||||
try {
|
||||
const firebaseAuth = getAuth(app);
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider();
|
||||
|
||||
const getToken = async () => {
|
||||
const currentUser = firebaseAuth.currentUser;
|
||||
if (currentUser) {
|
||||
await currentUser.getIdTokenResult(true);
|
||||
if (!currentUser.isAnonymous) {
|
||||
return currentUser.getIdToken();
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
const idToken = await getToken();
|
||||
let loginUser: AccessTokenMessage | null = null;
|
||||
if (idToken) {
|
||||
loginUser = await token.initToken(idToken);
|
||||
} else {
|
||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||
const idToken = await user.user.getIdToken();
|
||||
loginUser = await token.initToken(idToken);
|
||||
}
|
||||
return loginUser;
|
||||
};
|
||||
|
||||
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||
firebaseAuth.onAuthStateChanged(callback);
|
||||
};
|
||||
|
||||
return [signInWithGoogle, onAuthStateChanged] as const;
|
||||
} catch (e) {
|
||||
getLogger('getAuthorizer')(e);
|
||||
console.error('getAuthorizer', e);
|
||||
return [] as const;
|
||||
}
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export interface Workspace {
|
||||
|
||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||
try {
|
||||
return client
|
||||
return await client
|
||||
.get('api/workspace', {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
@@ -63,8 +63,7 @@ export async function getWorkspaceDetail(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<WorkspaceDetail | null> {
|
||||
try {
|
||||
const response = client.get(`api/workspace/${params.id}`);
|
||||
return response.json();
|
||||
return await client.get(`api/workspace/${params.id}`).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getDetailFailed);
|
||||
throw new RequestError('get detail failed', error);
|
||||
@@ -100,7 +99,7 @@ export async function getWorkspaceMembers(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<Member[]> {
|
||||
try {
|
||||
return client.get(`api/workspace/${params.id}/permission`).json();
|
||||
return await client.get(`api/workspace/${params.id}/permission`).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getMembersFailed);
|
||||
throw new RequestError('get members failed', error);
|
||||
@@ -115,7 +114,7 @@ export async function createWorkspace(
|
||||
encodedYDoc: Blob
|
||||
): Promise<{ id: string }> {
|
||||
try {
|
||||
return client.post('api/workspace', { body: encodedYDoc }).json();
|
||||
return await client.post('api/workspace', { body: encodedYDoc }).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.createWorkspaceFailed);
|
||||
throw new RequestError('create workspace failed', error);
|
||||
@@ -131,7 +130,7 @@ export async function updateWorkspace(
|
||||
params: UpdateWorkspaceParams
|
||||
): Promise<{ public: boolean | null }> {
|
||||
try {
|
||||
return client
|
||||
return await client
|
||||
.post(`api/workspace/${params.id}`, {
|
||||
json: {
|
||||
public: params.public,
|
||||
@@ -151,10 +150,12 @@ export interface DeleteWorkspaceParams {
|
||||
export async function deleteWorkspace(
|
||||
params: DeleteWorkspaceParams
|
||||
): Promise<void> {
|
||||
await client.delete(`api/workspace/${params.id}`).catch(error => {
|
||||
try {
|
||||
await client.delete(`api/workspace/${params.id}`);
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.deleteWorkspaceFailed);
|
||||
throw new RequestError('delete workspace failed', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface InviteMemberParams {
|
||||
@@ -167,13 +168,11 @@ export interface InviteMemberParams {
|
||||
*/
|
||||
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
||||
try {
|
||||
return client
|
||||
.post(`api/workspace/${params.id}/permission`, {
|
||||
json: {
|
||||
email: params.email,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
await client.post(`api/workspace/${params.id}/permission`, {
|
||||
json: {
|
||||
email: params.email,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.inviteMemberFailed);
|
||||
throw new RequestError('invite member failed', error);
|
||||
@@ -185,10 +184,12 @@ export interface RemoveMemberParams {
|
||||
}
|
||||
|
||||
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
||||
await client.delete(`api/permission/${params.permissionId}`).catch(error => {
|
||||
try {
|
||||
await client.delete(`api/permission/${params.permissionId}`);
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.removeMemberFailed);
|
||||
throw new RequestError('remove member failed', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface AcceptInvitingParams {
|
||||
@@ -199,7 +200,9 @@ export async function acceptInviting(
|
||||
params: AcceptInvitingParams
|
||||
): Promise<Permission> {
|
||||
try {
|
||||
return bareClient.post(`api/invitation/${params.invitingCode}`).json();
|
||||
return await bareClient
|
||||
.post(`api/invitation/${params.invitingCode}`)
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.acceptInvitingFailed);
|
||||
throw new RequestError('accept inviting failed', error);
|
||||
@@ -214,7 +217,7 @@ export async function getBlob(params: {
|
||||
blobId: string;
|
||||
}): Promise<ArrayBuffer> {
|
||||
try {
|
||||
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||
return await client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getBlobFailed);
|
||||
throw new RequestError('get blob failed', error);
|
||||
@@ -226,13 +229,12 @@ export interface LeaveWorkspaceParams {
|
||||
}
|
||||
|
||||
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
||||
await client
|
||||
.delete(`api/workspace/${id}/permission`)
|
||||
.json()
|
||||
.catch(error => {
|
||||
sendMessage(messageCode.leaveWorkspaceFailed);
|
||||
throw new RequestError('leave workspace failed', error);
|
||||
});
|
||||
try {
|
||||
await client.delete(`api/workspace/${id}/permission`);
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.leaveWorkspaceFailed);
|
||||
throw new RequestError('leave workspace failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadWorkspace(
|
||||
@@ -241,9 +243,11 @@ export async function downloadWorkspace(
|
||||
): Promise<ArrayBuffer> {
|
||||
try {
|
||||
if (published) {
|
||||
return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer();
|
||||
return await bareClient
|
||||
.get(`api/public/doc/${workspaceId}`)
|
||||
.arrayBuffer();
|
||||
}
|
||||
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
|
||||
return await client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.downloadWorkspaceFailed);
|
||||
throw new RequestError('download workspace failed', error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as websocket from 'lib0/websocket';
|
||||
import { Logger } from 'src/types';
|
||||
import { token } from './apis/token';
|
||||
import { auth } from './apis/auth';
|
||||
import * as url from 'lib0/url';
|
||||
|
||||
const RECONNECT_INTERVAL_TIME = 500;
|
||||
@@ -40,7 +40,7 @@ export class WebsocketClient extends websocket.WebsocketClient {
|
||||
this.on('disconnect', ({ error }: { error: Error }) => {
|
||||
if (error) {
|
||||
// Try reconnect if connect error has occurred
|
||||
if (this.shouldReconnect && token.isLogin && !this.connected) {
|
||||
if (this.shouldReconnect && auth.isLogin && !this.connected) {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
if (this._retryTimes <= MAX_RECONNECT_TIMES) {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { varStorage as storage } from 'lib0/storage';
|
||||
import { varStorage } from 'lib0/storage';
|
||||
|
||||
export const storage = varStorage as Storage;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const createBlocksuiteWorkspace = (
|
||||
) => {
|
||||
return new BlocksuiteWorkspace({
|
||||
room: workspaceId,
|
||||
defaultFlags: { enable_slash_menu: true },
|
||||
...workspaceOption,
|
||||
})
|
||||
.register(builtInSchemas)
|
||||
|
||||
@@ -74,4 +74,11 @@ export class WorkspaceUnit {
|
||||
syncMode: this.syncMode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only for debug use
|
||||
*/
|
||||
exportWorkspaceYDoc(): void {
|
||||
this._blocksuiteWorkspace?.exportYDoc();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user