mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 09:36:17 +08:00
Merge remote-tracking branch 'origin/develop' into feat/update-get-started-template
This commit is contained in:
@@ -17,6 +17,7 @@ const cleanupWorkspace = (workspace: string) =>
|
||||
|
||||
const requestPermission = async (workspace: string) => {
|
||||
await cleanupWorkspace(workspace);
|
||||
// @ts-ignore
|
||||
const dirHandler = await window.showDirectoryPicker({
|
||||
id: 'AFFiNE_' + workspace,
|
||||
mode: 'readwrite',
|
||||
@@ -46,7 +47,7 @@ const requestPermission = async (workspace: string) => {
|
||||
};
|
||||
|
||||
export const FileSystem = (props: { onError: () => void }) => {
|
||||
const onSelected = useLocalTrigger();
|
||||
const [, onSelected] = useLocalTrigger();
|
||||
|
||||
const apiSupported = useMemo(() => {
|
||||
try {
|
||||
@@ -56,12 +57,6 @@ export const FileSystem = (props: { onError: () => void }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env['NX_E2E']) {
|
||||
onSelected();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Login() {
|
||||
<MuiSnackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={error}
|
||||
message="Login failed, please check if you have permission"
|
||||
message="Request File Permission failed, please check if you have permission"
|
||||
/>
|
||||
<MuiGrid item xs={8}>
|
||||
<Error
|
||||
@@ -32,7 +32,6 @@ export function Login() {
|
||||
</MuiGrid>
|
||||
|
||||
<MuiGrid item xs={4}>
|
||||
{' '}
|
||||
<MuiBox
|
||||
style={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -25,7 +25,6 @@ import { ReactEditor } from 'slate-react';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
||||
import AttachmentIcon from '@mui/icons-material/Attachment';
|
||||
import {
|
||||
MuiTooltip as Tooltip,
|
||||
styled,
|
||||
@@ -39,7 +38,7 @@ import {
|
||||
|
||||
import { getRandomString } from '../utils';
|
||||
import { colors } from '../../colors';
|
||||
|
||||
import { LinkIcon } from '@toeverything/components/icons';
|
||||
export type LinkElement = {
|
||||
type: 'link';
|
||||
url: string;
|
||||
@@ -124,7 +123,7 @@ const LinkStyledTooltip = styled(({ className, ...props }: MuiTooltipProps) => (
|
||||
))(({ theme }) => ({
|
||||
[`& .${muiTooltipClasses.tooltip}`]: {
|
||||
backgroundColor: '#fff',
|
||||
color: '#4C6275',
|
||||
color: '#3E6FDB',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
fontSize: '14px',
|
||||
},
|
||||
@@ -419,8 +418,8 @@ export const LinkModal = memo((props: LinkModalProps) => {
|
||||
}}
|
||||
>
|
||||
<div className={styles('linkModalContainerIcon')}>
|
||||
<AttachmentIcon
|
||||
style={{ color: colors.Gray04, fontSize: 16 }}
|
||||
<LinkIcon
|
||||
style={{ fontSize: '16px', marginTop: '2px' }}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
@@ -505,8 +504,10 @@ const LinkModalContainer = styled('div')(({ theme }) => ({
|
||||
|
||||
const styles = style9.create({
|
||||
linkModalContainerIcon: {
|
||||
display: 'flex',
|
||||
width: '16px',
|
||||
margin: '0 16px 0 4px',
|
||||
color: '#4C6275',
|
||||
},
|
||||
linkModalContainerInput: {
|
||||
flex: '1',
|
||||
@@ -517,6 +518,7 @@ const styles = style9.create({
|
||||
'::-webkit-input-placeholder': {
|
||||
color: '#98acbd',
|
||||
},
|
||||
color: '#4C6275',
|
||||
},
|
||||
linkMask: {
|
||||
position: 'fixed',
|
||||
|
||||
@@ -42,6 +42,9 @@ const TextBlockContainer = styled(Text)(({ theme }) => ({
|
||||
fontFamily: theme.affine.typography.body1.fontFamily,
|
||||
color: theme.affine.typography.body1.color,
|
||||
letterSpacing: '0.1px',
|
||||
a: {
|
||||
color: '#3e6fdb',
|
||||
},
|
||||
}));
|
||||
|
||||
const findSlice = (arr: string[], p: string, q: string) => {
|
||||
|
||||
@@ -48,7 +48,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
private _scrollDelay = 150;
|
||||
private _selectEndDelayTime = 500;
|
||||
private _hasEmitEndPending = false;
|
||||
|
||||
private _scrollTimer: number | null = null;
|
||||
/**
|
||||
*
|
||||
* the selection info before current
|
||||
@@ -681,6 +681,9 @@ export class SelectionManager implements VirgoSelection {
|
||||
*/
|
||||
public async activeNodeByNodeId(nodeId: string, position?: CursorTypes) {
|
||||
try {
|
||||
if (this._scrollTimer) {
|
||||
clearTimeout(this._scrollTimer);
|
||||
}
|
||||
const node = await this._editor.getBlockById(nodeId);
|
||||
if (node) {
|
||||
this._activatedNodeId = nodeId;
|
||||
@@ -689,7 +692,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
this.emit(nodeId, SelectEventTypes.active, this.lastPoint);
|
||||
// TODO: Optimize the related logic after implementing the scroll bar
|
||||
setTimeout(() => {
|
||||
this._scrollTimer = window.setTimeout(() => {
|
||||
this._editor.scrollManager.keepBlockInView(node);
|
||||
}, this._scrollDelay);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/* eslint-disable filename-rules/match */
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { IconButton, MuiSnackbar, styled } from '@toeverything/components/ui';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { useLocalTrigger } from '@toeverything/datasource/state';
|
||||
import { CloseIcon } from '@toeverything/components/common';
|
||||
|
||||
const cleanupWorkspace = (workspace: string) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const req = indexedDB.deleteDatabase(workspace);
|
||||
req.addEventListener('error', e => reject(e));
|
||||
req.addEventListener('blocked', e => reject(e));
|
||||
req.addEventListener('upgradeneeded', e => reject(e));
|
||||
req.addEventListener('success', e => resolve(e));
|
||||
});
|
||||
|
||||
const requestPermission = async (workspace: string) => {
|
||||
await cleanupWorkspace(workspace);
|
||||
// @ts-ignore
|
||||
const dirHandler = await window.showDirectoryPicker({
|
||||
id: 'AFFiNE_' + workspace,
|
||||
mode: 'readwrite',
|
||||
startIn: 'documents',
|
||||
});
|
||||
|
||||
const fileHandle = await dirHandler.getFileHandle('affine.db', {
|
||||
create: true,
|
||||
});
|
||||
const file = await fileHandle.getFile();
|
||||
const initialData = new Uint8Array(await file.arrayBuffer());
|
||||
|
||||
const exporter = async (contents: Uint8Array) => {
|
||||
try {
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(contents);
|
||||
await writable.close();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
await services.api.editorBlock.setupDataExporter(
|
||||
workspace,
|
||||
new Uint8Array(initialData),
|
||||
exporter
|
||||
);
|
||||
};
|
||||
|
||||
const StyledFileSystem = styled('div')<{ disabled?: boolean }>({
|
||||
padding: '10px 12px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
color: '#3E6FDB',
|
||||
textTransform: 'uppercase',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
background: '#F5F7F8',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
});
|
||||
|
||||
export const fsApiSupported = () => {
|
||||
try {
|
||||
return 'showOpenFilePicker' in window;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const FileSystem = () => {
|
||||
const [selected, onSelected] = useLocalTrigger();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
setError(true);
|
||||
setTimeout(() => setError(false), 3000);
|
||||
}, []);
|
||||
|
||||
const apiSupported = useMemo(() => fsApiSupported(), []);
|
||||
|
||||
if (apiSupported && !selected) {
|
||||
return (
|
||||
<>
|
||||
<MuiSnackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={error}
|
||||
message="Request File Permission failed, please check if you have permission"
|
||||
sx={{ marginTop: '3em' }}
|
||||
action={
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={() => setError(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<StyledFileSystem
|
||||
onClick={async () => {
|
||||
try {
|
||||
await requestPermission('AFFiNE');
|
||||
onSelected();
|
||||
} catch (e) {
|
||||
onError();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sync to Disk
|
||||
</StyledFileSystem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,19 +1,36 @@
|
||||
import { IconButton, styled, MuiButton } from '@toeverything/components/ui';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { IconButton, styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
LogoIcon,
|
||||
SideBarViewIcon,
|
||||
SearchIcon,
|
||||
SideBarViewCloseIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { useShowSettingsSidebar } from '@toeverything/datasource/state';
|
||||
import {
|
||||
useShowSettingsSidebar,
|
||||
useLocalTrigger,
|
||||
} from '@toeverything/datasource/state';
|
||||
|
||||
import { CurrentPageTitle } from './Title';
|
||||
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
|
||||
import { fsApiSupported, FileSystem } from './FileSystem';
|
||||
import { CurrentPageTitle } from './Title';
|
||||
|
||||
export const LayoutHeader = () => {
|
||||
const [isLocalWorkspace] = useLocalTrigger();
|
||||
const { toggleSettingsSidebar: toggleInfoSidebar, showSettingsSidebar } =
|
||||
useShowSettingsSidebar();
|
||||
|
||||
const warningTips = useMemo(() => {
|
||||
if (!fsApiSupported()) {
|
||||
return 'Your browser does not support the local storage feature, please upgrade to the latest version of Chrome or Edge browser';
|
||||
} else if (isLocalWorkspace) {
|
||||
return 'You are in DEMO mode. Changes will NOT be saved unless you SYNC TO DISK';
|
||||
} else {
|
||||
return 'AFFiNE is under active development and the current version is UNSTABLE. Please DO NOT store information or data';
|
||||
}
|
||||
}, [isLocalWorkspace]);
|
||||
|
||||
return (
|
||||
<StyledContainerForHeaderRoot>
|
||||
<StyledHeaderRoot>
|
||||
@@ -25,6 +42,7 @@ export const LayoutHeader = () => {
|
||||
</FlexContainer>
|
||||
<FlexContainer>
|
||||
<StyledHelper>
|
||||
<FileSystem />
|
||||
<StyledShare disabled={true}>Share</StyledShare>
|
||||
<div style={{ margin: '0px 12px' }}>
|
||||
<IconButton
|
||||
@@ -51,10 +69,7 @@ export const LayoutHeader = () => {
|
||||
</StyledContainerForEditorBoardSwitcher>
|
||||
</StyledHeaderRoot>
|
||||
<StyledUnstableTips>
|
||||
<StyledUnstableTipsText>
|
||||
AFFiNE now under active development, the version is
|
||||
UNSTABLE, please DO NOT store important data in this version
|
||||
</StyledUnstableTipsText>
|
||||
<StyledUnstableTipsText>{warningTips}</StyledUnstableTipsText>
|
||||
</StyledUnstableTips>
|
||||
</StyledContainerForHeaderRoot>
|
||||
);
|
||||
|
||||
@@ -36,19 +36,13 @@ export type DndTreeProps = {
|
||||
indentationWidth?: number;
|
||||
collapsible?: boolean;
|
||||
removable?: boolean;
|
||||
showDragIndicator?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Currently does not support drag and drop using the keyboard.
|
||||
*/
|
||||
export function DndTree(props: DndTreeProps) {
|
||||
const {
|
||||
indentationWidth = 20,
|
||||
collapsible,
|
||||
removable,
|
||||
showDragIndicator,
|
||||
} = props;
|
||||
const { indentationWidth = 20, collapsible, removable } = props;
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
|
||||
@@ -111,7 +105,6 @@ export function DndTree(props: DndTreeProps) {
|
||||
: depth
|
||||
}
|
||||
indentationWidth={indentationWidth}
|
||||
indicator={showDragIndicator}
|
||||
childCount={children.length}
|
||||
onCollapse={
|
||||
collapsible && children.length
|
||||
@@ -129,7 +122,8 @@ export function DndTree(props: DndTreeProps) {
|
||||
)}
|
||||
<DragOverlay
|
||||
dropAnimation={dropAnimation}
|
||||
style={{ marginTop: '-65px' }}
|
||||
// TODO fix drag offset when the position of the page tree changes
|
||||
style={{ marginTop: '-100px' }}
|
||||
>
|
||||
{activeId && activeItem ? (
|
||||
<DndTreeItem
|
||||
|
||||
@@ -16,8 +16,7 @@ export function DndTreeItem({ id, depth, ...props }: DndTreeItemProps) {
|
||||
isDragging,
|
||||
isSorting,
|
||||
listeners,
|
||||
setDraggableNodeRef,
|
||||
setDroppableNodeRef,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({ id });
|
||||
@@ -29,8 +28,7 @@ export function DndTreeItem({ id, depth, ...props }: DndTreeItemProps) {
|
||||
|
||||
return (
|
||||
<TreeItem
|
||||
ref={setDraggableNodeRef}
|
||||
wrapperRef={setDroppableNodeRef}
|
||||
ref={setNodeRef}
|
||||
pageId={id}
|
||||
style={style}
|
||||
depth={depth}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
TextLink,
|
||||
TreeItemContainer,
|
||||
TreeItemContent,
|
||||
Wrapper,
|
||||
} from './styles';
|
||||
|
||||
export type TreeItemProps = {
|
||||
@@ -33,13 +32,10 @@ export type TreeItemProps = {
|
||||
/** isDragging */
|
||||
ghost?: boolean;
|
||||
handleProps?: any;
|
||||
indicator?: boolean;
|
||||
indentationWidth: number;
|
||||
onCollapse?(): void;
|
||||
onRemove?(): void;
|
||||
/** The ref of the outermost container is often used as droppaHTMLAttributes<HTMLLIElement>ble-node; the ref of the inner dom is often used as draggable-node */
|
||||
wrapperRef?(node: HTMLLIElement): void;
|
||||
} & HTMLAttributes<HTMLLIElement>;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
(
|
||||
@@ -52,13 +48,10 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
ghost,
|
||||
handleProps,
|
||||
indentationWidth,
|
||||
indicator,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
onRemove,
|
||||
style,
|
||||
value,
|
||||
wrapperRef,
|
||||
pageId,
|
||||
...props
|
||||
},
|
||||
@@ -71,8 +64,8 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
ref={wrapperRef}
|
||||
<TreeItemContainer
|
||||
ref={ref}
|
||||
clone={clone}
|
||||
ghost={ghost}
|
||||
disableSelection={disableSelection}
|
||||
@@ -81,38 +74,38 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
active={pageId === page_id}
|
||||
{...props}
|
||||
>
|
||||
<TreeItemContainer ref={ref} style={style} title={value}>
|
||||
<ActionButton tabIndex={0} onClick={onCollapse}>
|
||||
{childCount !== 0 ? (
|
||||
collapsed ? (
|
||||
<ArrowRightIcon />
|
||||
) : (
|
||||
<ArrowDropDownIcon />
|
||||
)
|
||||
) : (
|
||||
<DotIcon />
|
||||
)}
|
||||
</ActionButton>
|
||||
{childCount !== 0 ? (
|
||||
collapsed ? (
|
||||
<ActionButton tabIndex={0} onClick={onCollapse}>
|
||||
<ArrowRightIcon />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton tabIndex={0} onClick={onCollapse}>
|
||||
<ArrowDropDownIcon />
|
||||
</ActionButton>
|
||||
)
|
||||
) : (
|
||||
<DotIcon />
|
||||
)}
|
||||
|
||||
<TreeItemContent {...handleProps}>
|
||||
<TextLink to={`/${workspace_id}/${pageId}`}>
|
||||
{value}
|
||||
</TextLink>
|
||||
{BooleanPageTreeItemMoreActions && (
|
||||
<MoreActions
|
||||
workspaceId={workspace_id}
|
||||
pageId={pageId}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
)}
|
||||
<TreeItemContent {...handleProps}>
|
||||
<TextLink to={`/${workspace_id}/${pageId}`}>
|
||||
{value}
|
||||
</TextLink>
|
||||
{BooleanPageTreeItemMoreActions && (
|
||||
<MoreActions
|
||||
workspaceId={workspace_id}
|
||||
pageId={pageId}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*{!clone && onRemove && <Remove onClick={onRemove} />}*/}
|
||||
{clone && childCount && childCount > 1 ? (
|
||||
<Counter>{childCount}</Counter>
|
||||
) : null}
|
||||
</TreeItemContent>
|
||||
</TreeItemContainer>
|
||||
</Wrapper>
|
||||
{/*{!clone && onRemove && <Remove onClick={onRemove} />}*/}
|
||||
{clone && childCount && childCount > 1 ? (
|
||||
<Counter>{childCount}</Counter>
|
||||
) : null}
|
||||
</TreeItemContent>
|
||||
</TreeItemContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const TreeItemContainer = styled('div')`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #4c6275;
|
||||
`;
|
||||
|
||||
export const Wrapper = styled('li')<{
|
||||
export const TreeItemContainer = styled('div')<{
|
||||
spacing: string;
|
||||
clone?: boolean;
|
||||
ghost?: boolean;
|
||||
indicator?: boolean;
|
||||
disableSelection?: boolean;
|
||||
disableInteraction?: boolean;
|
||||
active?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #4c6275;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding-left: ${({ spacing }) => spacing};
|
||||
list-style: none;
|
||||
@@ -26,21 +22,13 @@ export const Wrapper = styled('li')<{
|
||||
|
||||
${({ clone, disableSelection }) =>
|
||||
(clone || disableSelection) &&
|
||||
`width: 100%;
|
||||
.Text,
|
||||
.Count {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}`}
|
||||
|
||||
${({ indicator }) =>
|
||||
indicator &&
|
||||
`width: 100%;
|
||||
.Text,
|
||||
.Count {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}`}
|
||||
`
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
opacity: 0.7;
|
||||
background: transparent;
|
||||
cursor: grab;
|
||||
`}
|
||||
|
||||
${({ disableInteraction }) => disableInteraction && `pointer-events: none;`}
|
||||
|
||||
@@ -48,64 +36,6 @@ export const Wrapper = styled('li')<{
|
||||
background: #f5f7f8;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&.clone {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
margin-top: 5px;
|
||||
pointer-events: none;
|
||||
|
||||
${TreeItemContainer} {
|
||||
padding-right: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 15px 15px 0 rgba(34, 33, 81, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
&.indicator {
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
|
||||
${TreeItemContainer} {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
height: 8px;
|
||||
border-color: #2389ff;
|
||||
background-color: #56a1f8;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: -4px;
|
||||
display: block;
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #2389ff;
|
||||
}
|
||||
|
||||
> * {
|
||||
/* Items are hidden using height and opacity to retain focus */
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.indicator) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
${TreeItemContainer} > * {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Counter = styled('span')`
|
||||
@@ -138,7 +68,7 @@ export const ActionButton = styled('button')<{
|
||||
border: none;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
background: transparent;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
svg {
|
||||
@@ -151,8 +81,7 @@ export const ActionButton = styled('button')<{
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: ${({ background }) =>
|
||||
background ?? 'rgba(0, 0, 0, 0.05)'};
|
||||
background: ${({ background }) => background ?? 'rgba(0, 0, 0, 0.05)'};
|
||||
|
||||
svg {
|
||||
fill: ${({ fill }) => fill ?? '#788491'};
|
||||
@@ -170,9 +99,7 @@ export const TreeItemMoreActions = styled('div')`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export const TextLink = styled(Link, {
|
||||
shouldForwardProp: (prop: string) => !['active'].includes(prop),
|
||||
})<{ active?: boolean }>`
|
||||
export const TextLink = styled(Link)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -37,6 +37,11 @@ async function _getCurrentToken() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const _enabled = {
|
||||
demo: [],
|
||||
AFFiNE: ['sqlite'],
|
||||
} as any;
|
||||
|
||||
async function _getBlockDatabase(
|
||||
workspace: string,
|
||||
options?: BlockInitOptions
|
||||
@@ -45,16 +50,10 @@ async function _getBlockDatabase(
|
||||
await waitLoading(workspace);
|
||||
}
|
||||
|
||||
// if (
|
||||
// options?.userId &&
|
||||
// workspaces[workspace]?.getUserId() !== options?.userId
|
||||
// ) {
|
||||
// delete workspaces[workspace];
|
||||
// }
|
||||
|
||||
if (!workspaces[workspace]) {
|
||||
loading.add(workspace);
|
||||
workspaces[workspace] = await BlockClient.init(workspace, {
|
||||
enabled: _enabled[workspace] || ['idb'],
|
||||
...options,
|
||||
token: await _getCurrentToken(),
|
||||
});
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
snapshot,
|
||||
} from 'yjs';
|
||||
|
||||
import { IndexedDBProvider } from '@toeverything/datasource/jwt-rpc';
|
||||
|
||||
import {
|
||||
AsyncDatabaseAdapter,
|
||||
BlockListener,
|
||||
@@ -48,15 +46,17 @@ type ConnectivityListener = (
|
||||
workspace: string,
|
||||
connectivity: Connectivity
|
||||
) => void;
|
||||
|
||||
type YjsProviders = {
|
||||
awareness: Awareness;
|
||||
idb: IndexedDBProvider;
|
||||
binariesIdb: IndexedDBProvider;
|
||||
binaries: Doc;
|
||||
doc: Doc;
|
||||
gatekeeper: GateKeeper;
|
||||
connListener: { listeners?: ConnectivityListener };
|
||||
userId: string;
|
||||
remoteToken?: string; // remote storage token
|
||||
};
|
||||
|
||||
const _yjsDatabaseInstance = new Map<string, YjsProviders>();
|
||||
|
||||
const _asyncInitLoading = new Set<string>();
|
||||
@@ -90,13 +90,9 @@ async function _initYjsDatabase(
|
||||
const { userId, token } = options;
|
||||
|
||||
const doc = new Doc({ autoLoad: true, shouldLoad: true });
|
||||
const idb = await new IndexedDBProvider(workspace, doc).whenSynced;
|
||||
// const idb = await new IndexedDBProvider(workspace, doc).whenSynced;
|
||||
|
||||
const binaries = new Doc({ autoLoad: true, shouldLoad: true });
|
||||
const binariesIdb = await new IndexedDBProvider(
|
||||
`${workspace}_binaries`,
|
||||
binaries
|
||||
).whenSynced;
|
||||
|
||||
const awareness = new Awareness(doc);
|
||||
|
||||
@@ -114,16 +110,24 @@ async function _initYjsDatabase(
|
||||
const emitState = (c: Connectivity) =>
|
||||
connListener.listeners?.(workspace, c);
|
||||
await Promise.all(
|
||||
Object.entries(options.provider).map(async ([, p]) =>
|
||||
p({ awareness, doc, token, workspace, emitState })
|
||||
)
|
||||
Object.entries(options.provider).flatMap(async ([, p]) => [
|
||||
p({ awareness, doc, token, workspace, emitState }),
|
||||
p({
|
||||
awareness,
|
||||
doc: binaries,
|
||||
token,
|
||||
workspace: `${workspace}_binaries`,
|
||||
emitState,
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
const newInstance = {
|
||||
awareness,
|
||||
idb,
|
||||
binariesIdb,
|
||||
binaries,
|
||||
doc,
|
||||
gatekeeper,
|
||||
|
||||
connListener,
|
||||
userId,
|
||||
remoteToken: token,
|
||||
@@ -183,7 +187,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
|
||||
private constructor(providers: YjsProviders) {
|
||||
this._provider = providers;
|
||||
this._doc = providers.idb.doc;
|
||||
this._doc = providers.doc;
|
||||
this._awareness = providers.awareness;
|
||||
this._gatekeeper = providers.gatekeeper;
|
||||
this._reload = () => {
|
||||
@@ -201,7 +205,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
});
|
||||
// @ts-ignore
|
||||
this._binaries = new YjsRemoteBinaries(
|
||||
providers.binariesIdb.doc.getMap(),
|
||||
providers.binaries.getMap(),
|
||||
providers.remoteToken
|
||||
);
|
||||
// @ts-ignore
|
||||
@@ -336,7 +340,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
});
|
||||
const [file] = (await fromEvent(handles)) as File[];
|
||||
const binary = await file.arrayBuffer();
|
||||
await this._provider.idb.clearData();
|
||||
// await this._provider.idb.clearData();
|
||||
const doc = new Doc({ autoLoad: true, shouldLoad: true });
|
||||
let updated = 0;
|
||||
let isUpdated = false;
|
||||
@@ -357,8 +361,8 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
};
|
||||
check();
|
||||
});
|
||||
await new IndexedDBProvider(this._provider.idb.name, doc)
|
||||
.whenSynced;
|
||||
// await new IndexedDBProvider(this._provider.idb.name, doc)
|
||||
// .whenSynced;
|
||||
applyUpdate(doc, new Uint8Array(binary));
|
||||
await update_check;
|
||||
console.log('load success');
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Doc } from 'yjs';
|
||||
import { Awareness } from 'y-protocols/awareness.js';
|
||||
|
||||
import {
|
||||
IndexedDBProvider,
|
||||
SQLiteProvider,
|
||||
WebsocketProvider,
|
||||
} from '@toeverything/datasource/jwt-rpc';
|
||||
@@ -19,7 +20,10 @@ type YjsDefaultInstances = {
|
||||
|
||||
export type YjsProvider = (instances: YjsDefaultInstances) => Promise<void>;
|
||||
|
||||
type ProviderType = 'idb' | 'sqlite' | 'ws';
|
||||
|
||||
export type YjsProviderOptions = {
|
||||
enabled: ProviderType[];
|
||||
backend: typeof BucketBackend[keyof typeof BucketBackend];
|
||||
params?: Record<string, string>;
|
||||
importData?: () => Promise<Uint8Array> | Uint8Array | undefined;
|
||||
@@ -30,53 +34,64 @@ export type YjsProviderOptions = {
|
||||
export const getYjsProviders = (
|
||||
options: YjsProviderOptions
|
||||
): Record<string, YjsProvider> => {
|
||||
console.log('getYjsProviders', options);
|
||||
return {
|
||||
indexeddb: async (instances: YjsDefaultInstances) => {
|
||||
if (options.enabled.includes('idb')) {
|
||||
await new IndexedDBProvider(instances.workspace, instances.doc)
|
||||
.whenSynced;
|
||||
}
|
||||
},
|
||||
sqlite: async (instances: YjsDefaultInstances) => {
|
||||
const fsHandle = setInterval(async () => {
|
||||
if (options.hasExporter?.()) {
|
||||
clearInterval(fsHandle);
|
||||
const fs = new SQLiteProvider(
|
||||
instances.workspace,
|
||||
instances.doc,
|
||||
await options.importData?.()
|
||||
);
|
||||
if (options.exportData) {
|
||||
fs.registerExporter(options.exportData);
|
||||
if (options.enabled.includes('sqlite')) {
|
||||
const fsHandle = setInterval(async () => {
|
||||
if (options.hasExporter?.()) {
|
||||
clearInterval(fsHandle);
|
||||
const fs = new SQLiteProvider(
|
||||
instances.workspace,
|
||||
instances.doc,
|
||||
await options.importData?.()
|
||||
);
|
||||
if (options.exportData) {
|
||||
fs.registerExporter(options.exportData);
|
||||
}
|
||||
await fs.whenSynced;
|
||||
}
|
||||
await fs.whenSynced;
|
||||
}
|
||||
}, 500);
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
ws: async (instances: YjsDefaultInstances) => {
|
||||
if (instances.token) {
|
||||
const ws = new WebsocketProvider(
|
||||
instances.token,
|
||||
options.backend,
|
||||
instances.workspace,
|
||||
instances.doc,
|
||||
{
|
||||
awareness: instances.awareness,
|
||||
params: options.params,
|
||||
}
|
||||
) as any; // TODO: type is erased after cascading references
|
||||
if (options.enabled.includes('ws')) {
|
||||
if (instances.token) {
|
||||
const ws = new WebsocketProvider(
|
||||
instances.token,
|
||||
options.backend,
|
||||
instances.workspace,
|
||||
instances.doc,
|
||||
{
|
||||
awareness: instances.awareness,
|
||||
params: options.params,
|
||||
}
|
||||
) as any; // TODO: type is erased after cascading references
|
||||
|
||||
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
ws.once('synced', () => resolve());
|
||||
ws.once('lost-connection', () => resolve());
|
||||
ws.once('connection-error', () => reject());
|
||||
ws.on('synced', () => instances.emitState('connected'));
|
||||
ws.on('lost-connection', () =>
|
||||
instances.emitState('retry')
|
||||
);
|
||||
ws.on('connection-error', () =>
|
||||
instances.emitState('retry')
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
ws.once('synced', () => resolve());
|
||||
ws.once('lost-connection', () => resolve());
|
||||
ws.once('connection-error', () => reject());
|
||||
ws.on('synced', () => instances.emitState('connected'));
|
||||
ws.on('lost-connection', () =>
|
||||
instances.emitState('retry')
|
||||
);
|
||||
ws.on('connection-error', () =>
|
||||
instances.emitState('retry')
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -618,6 +618,7 @@ export class BlockClient<
|
||||
|
||||
const instance = await YjsAdapter.init(workspace, {
|
||||
provider: getYjsProviders({
|
||||
enabled: [],
|
||||
backend: BucketBackend.YjsWebSocketAffine,
|
||||
importData,
|
||||
exportData,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
getAuth,
|
||||
onAuthStateChanged,
|
||||
@@ -62,11 +63,17 @@ const BRAND_ID = 'AFFiNE';
|
||||
|
||||
const _localTrigger = atom<boolean>(false);
|
||||
const _useUserAndSpacesForFreeLogin = () => {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useAtom(_userAtom);
|
||||
const [loading, setLoading] = useAtom(_loadingAtom);
|
||||
const [localTrigger] = useAtom(_localTrigger);
|
||||
|
||||
useEffect(() => setLoading(false), []);
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
navigate('/demo');
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (localTrigger) {
|
||||
@@ -77,6 +84,14 @@ const _useUserAndSpacesForFreeLogin = () => {
|
||||
nickname: BRAND_ID,
|
||||
email: '',
|
||||
});
|
||||
} else {
|
||||
setUser({
|
||||
photo: '',
|
||||
id: 'demo',
|
||||
username: 'demo',
|
||||
nickname: 'demo',
|
||||
email: '',
|
||||
});
|
||||
}
|
||||
}, [localTrigger, setLoading, setUser]);
|
||||
|
||||
@@ -86,8 +101,8 @@ const _useUserAndSpacesForFreeLogin = () => {
|
||||
};
|
||||
|
||||
export const useLocalTrigger = () => {
|
||||
const [, setTrigger] = useAtom(_localTrigger);
|
||||
return () => setTrigger(true);
|
||||
const [trigger, setTrigger] = useAtom(_localTrigger);
|
||||
return [trigger, () => setTrigger(true)] as [boolean, () => void];
|
||||
};
|
||||
|
||||
export const useUserAndSpaces = process.env['NX_LOCAL']
|
||||
|
||||
Reference in New Issue
Block a user