init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -0,0 +1,120 @@
import { Fragment, useState, useEffect, useCallback } from 'react';
import { styled, MuiClickAwayListener } from '@toeverything/components/ui';
import { services } from '@toeverything/datasource/db-service';
import { QuotedContent } from './item/QuotedContent';
import { ReplyItem } from './item/ReplyItem';
import { ReplyInput } from './item/ReplyInput';
import { CommentInfo } from './type';
export const CommentItem = (props: CommentInfo) => {
const {
id,
workspace,
attachedToBlocksIds,
quote,
replyList,
resolve,
activeCommentId,
resolveComment,
} = props;
const [isActive, setIsActive] = useState(false);
const handleSubmitComment = useCallback(
async (value: string) => {
await services.api.commentService.createReply({
workspace,
parentId: id,
content: { value: [{ text: value }] },
});
},
[id, workspace]
);
const handleToggleResolveComment = useCallback(async () => {
resolveComment(attachedToBlocksIds[0], id);
await services.api.commentService.updateComment({
workspace,
id,
attachedToBlocksIds,
resolve: !resolve,
});
}, [attachedToBlocksIds, id, resolve, resolveComment, workspace]);
useEffect(() => {
if (activeCommentId === id) {
setIsActive(true);
} else {
setIsActive(false);
}
}, [activeCommentId, id]);
return (
<MuiClickAwayListener onClickAway={() => setIsActive(false)}>
<StyledContainerForCommentItem
isActive={isActive}
onClick={() => setIsActive(true)}
>
<StyledItemContent>
<QuotedContent
content={quote.value[0].text}
onToggle={handleToggleResolveComment}
/>
{replyList?.map((reply, index) => {
if (index === replyList.length - 1) {
return <ReplyItem {...reply} key={reply.id} />;
}
return (
<Fragment key={reply.id}>
<ReplyItem {...reply} />
<StyledReplySeparator />
</Fragment>
);
})}
{isActive ? (
<ReplyInput onSubmit={handleSubmitComment} />
) : null}
</StyledItemContent>
</StyledContainerForCommentItem>
</MuiClickAwayListener>
);
};
const StyledContainerForCommentItem = styled('div', {
shouldForwardProp: (prop: string) => !['isActive'].includes(prop),
})<{ isActive?: boolean }>(({ theme, isActive }) => {
return {
position: 'relative',
width: 322,
border: `2px solid ${theme.affine.palette.menuSeparator}`,
borderRadius: theme.affine.shape.borderRadius,
marginBottom: theme.affine.spacing.smSpacing,
left: isActive ? -58 : 0,
transition: 'left 150ms ease-in-out',
backgroundColor: theme.affine.palette.white,
'&:hover': {
boxShadow: theme.affine.shadows.shadowSxDownLg,
},
};
});
const StyledItemContent = styled('div')(({ theme }) => {
return {
marginLeft: theme.affine.spacing.main,
marginRight: theme.affine.spacing.main,
marginTop: theme.affine.spacing.smSpacing,
marginBottom: theme.affine.spacing.smSpacing,
};
});
const StyledReplySeparator = styled('div')(({ theme }) => {
return {
width: 290,
height: 1,
marginTop: 6,
marginBottom: 6,
color: theme.affine.palette.menuSeparator,
backgroundColor: theme.affine.palette.menuSeparator,
};
});
@@ -0,0 +1,36 @@
import { styled } from '@toeverything/components/ui';
import { useComments } from './use-comments';
import { CommentItem } from './CommentItem';
type CommentsProps = {
activeCommentId: string;
resolveComment: (blockId: string, commentId: string) => void;
};
export const Comments = ({
activeCommentId,
resolveComment,
}: CommentsProps) => {
const { comments } = useComments();
return (
<StyledContainerForComments className="id-comments-panel">
{comments?.map(comment => {
return (
<CommentItem
{...comment}
activeCommentId={activeCommentId}
resolveComment={resolveComment}
key={comment.id}
/>
);
})}
</StyledContainerForComments>
);
};
const StyledContainerForComments = styled('div')(({ theme }) => {
return {
position: 'relative',
};
});
@@ -0,0 +1 @@
export { Comments } from './Comments';
@@ -0,0 +1,21 @@
import { styled } from '@toeverything/components/ui';
type CommentContentProps = {
content: string;
};
export const CommentContent = ({ content }: CommentContentProps) => {
return (
<StyledContainerForCommentContent>
<p>{content || ''}</p>
</StyledContainerForCommentContent>
);
};
const StyledContainerForCommentContent = styled('div')(({ theme }) => {
return {
display: 'flex',
color: theme.affine.palette.primaryText,
marginTop: theme.affine.spacing.xsSpacing,
};
});
@@ -0,0 +1,54 @@
import { useMemo } from 'react';
import { styled, MuiAvatar as Avatar } from '@toeverything/components/ui';
import { useUserAndSpaces } from '@toeverything/datasource/state';
import { getUserDisplayName } from '@toeverything/utils';
type CommentedByUserProps = {
username: string;
updateTime: number;
};
export const CommentedByUser = ({
username,
updateTime: updatedTime,
}: CommentedByUserProps) => {
const updateDatetime = useMemo(() => new Date(updatedTime), [updatedTime]);
//TODO temp
const { user } = useUserAndSpaces();
return (
<StyledContainerForCommentedByUser>
<Avatar sx={{ bgcolor: '#9176FF' }} src={user?.photo || ''}>
{/* {username ? username.slice(0, 2).toLocaleUpperCase() : ''} */}
{getUserDisplayName(user)}
</Avatar>
<StyledCommentUserInfo>
<div> {getUserDisplayName(user)}</div>
<StyledCommentTime>
{updateDatetime.toTimeString().slice(0, 5)}{' '}
{updateDatetime.toDateString().slice(4, 10)}
</StyledCommentTime>
</StyledCommentUserInfo>
</StyledContainerForCommentedByUser>
);
};
const StyledContainerForCommentedByUser = styled('div')(({ theme }) => {
return {
display: 'flex',
alignItems: 'center',
marginBottom: 6,
};
});
const StyledCommentUserInfo = styled('div')(({ theme }) => {
return {
marginLeft: theme.affine.spacing.smSpacing,
};
});
const StyledCommentTime = styled('div')(({ theme }) => {
return {
color: theme.affine.palette.icons,
};
});
@@ -0,0 +1,53 @@
import { styled } from '@toeverything/components/ui';
import { DoneIcon } from '@toeverything/components/icons';
type QuotedContentProps = {
content: string;
onToggle: () => void;
};
export const QuotedContent = ({ content, onToggle }: QuotedContentProps) => {
return (
<StyledContainerForQuotedContent>
<StyledVerticalLine />
<StyledQuotedContent>{content || ''}</StyledQuotedContent>
<StyledResolveAction onClick={onToggle} fontSize="small" />
</StyledContainerForQuotedContent>
);
};
const StyledContainerForQuotedContent = styled('div')(({ theme }) => {
return {
display: 'flex',
// marginBottom: theme.affine.spacing.xsSpacing,
marginBottom: 6,
};
});
const StyledVerticalLine = styled('div')(({ theme }) => {
return {
width: 2,
height: 18,
marginRight: theme.affine.spacing.smSpacing,
backgroundColor: '#97EEF2',
};
});
const StyledQuotedContent = styled('div')(({ theme }) => {
return {
color: theme.affine.palette.primaryText,
flex: '1 1 0',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
};
});
const StyledResolveAction = styled(DoneIcon)(({ theme }) => {
return {
marginLeft: theme.affine.spacing.xsSpacing,
color: theme.affine.palette.primary,
// fontSize: '1.2rem',
cursor: 'pointer',
};
});
@@ -0,0 +1,45 @@
import {
useState,
useCallback,
KeyboardEventHandler,
ChangeEvent,
} from 'react';
import { styled } from '@toeverything/components/ui';
export const ReplyInput = (props: any) => {
const { onSubmit } = props;
const [value, setValue] = useState('');
const onKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
if (!e.metaKey && !e.shiftKey && e.code === 'Enter' && value) {
onSubmit && onSubmit(value);
setValue('');
}
};
const handleInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value.trim());
},
[]
);
return (
<StyledContainerForReplyInput className="affine-comment-reply-input">
<input
type="text"
placeholder={'reply...'}
onKeyDown={onKeyDown}
onChange={handleInputChange}
value={value}
/>
</StyledContainerForReplyInput>
);
};
const StyledContainerForReplyInput = styled('div')(({ theme }) => {
return {
// marginTop: theme.affine.spacing.xsSpacing,
marginTop: 8,
};
});
@@ -0,0 +1,16 @@
import { styled } from '@toeverything/components/ui';
import type { CommentReply } from '@toeverything/datasource/db-service';
import { CommentContent } from './CommentContent';
import { CommentedByUser } from './CommentedByUser';
export const ReplyItem = (props: CommentReply) => {
const { creator, lastUpdated, content } = props;
return (
<>
<CommentedByUser username={creator} updateTime={lastUpdated} />
<CommentContent content={content.value[0].text} />
</>
);
};
@@ -0,0 +1,10 @@
import type {
Comment,
CommentReply,
} from '@toeverything/datasource/db-service';
export interface CommentInfo extends Comment {
replyList?: CommentReply[];
activeCommentId?: string;
resolveComment: (blockId: string, commentId: string) => void;
}
@@ -0,0 +1,129 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { services } from '@toeverything/datasource/db-service';
import type { Virgo } from '@toeverything/components/editor-core';
import {
useCurrentEditors,
useShowSettingsSidebar,
} from '@toeverything/datasource/state';
import type { CommentInfo } from './type';
export const useComments = () => {
const { workspace_id: workspaceId, page_id: pageId } = useParams();
const [comments, setComments] = useState<CommentInfo[]>([]);
const [observeIds, setObserveIds] = useState<string[]>([]);
const fetchComments = useCallback(async () => {
if (!workspaceId || !pageId) return;
const ids = [];
const pageComment = await services.api.commentService.getPageComments({
workspace: workspaceId,
pageId: pageId,
});
ids.push(pageComment.id);
let comments = await services.api.commentService.getComments({
workspace: workspaceId,
ids: pageComment?.children,
});
comments = await Promise.all(
comments.map(async comment => {
const commentInfo = comment as CommentInfo;
commentInfo.replyList =
await services.api.commentService.getReplyList({
workspace: workspaceId,
ids: comment.children,
});
ids.push(comment.id);
ids.push(...comment.children);
return commentInfo;
})
);
setComments(comments.reverse() as CommentInfo[]);
setObserveIds(ids);
}, [pageId, workspaceId]);
useEffect(() => {
fetchComments();
}, [fetchComments]);
// first simple implementation
useEffect(() => {
const unobserveList: any[] = [];
observeIds.forEach(async id => {
const unobserve = await services.api.editorBlock.observe(
{ workspace: workspaceId, id: id },
block => {
fetchComments();
}
);
unobserveList.push(unobserve);
});
return () => {
unobserveList.forEach(unobserve => unobserve?.());
};
}, [fetchComments, workspaceId, observeIds]);
return { comments };
};
export const useActiveComment = () => {
const { workspace_id: workspaceId, page_id: pageId } = useParams();
const { currentEditors } = useCurrentEditors();
const editor = useMemo(() => {
return currentEditors[pageId] as Virgo;
}, [currentEditors, pageId]);
const [activeCommentId, setActiveCommentId] = useState('');
const { setShowSettingsSidebar: setShowInfoSidebar } =
useShowSettingsSidebar();
const resolveComment = useCallback(
(blockId: string, commentId: string) => {
editor?.blockHelper.resolveComment(blockId, commentId);
},
[editor]
);
useEffect(() => {
if (!editor) return;
editor.selection.onSelectEnd(info => {
// TODO: only do the following when sidebar is open
const { type, anchorNode } = info;
if (type === 'None' || !anchorNode) return;
const currentSelectionInTextBlock =
editor.blockHelper.getCurrentSelection(anchorNode.id);
if (!currentSelectionInTextBlock) return;
// get possible commentId from selection
let maybeActiveCommentsIds = [] as string[];
if (editor.blockHelper.isSelectionCollapsed(anchorNode.id)) {
// TODO: search before/after for comment text node, improve this
maybeActiveCommentsIds =
editor.blockHelper.getCommentsIdsBySelection(anchorNode.id);
} else {
maybeActiveCommentsIds =
editor.blockHelper.getCommentsIdsBySelection(anchorNode.id);
}
// TODO: set the shortest comment as active comment instead of the first
setActiveCommentId(
maybeActiveCommentsIds.length ? maybeActiveCommentsIds[0] : ''
);
});
}, [currentEditors, editor]);
useEffect(() => {
if (activeCommentId) {
setShowInfoSidebar(true);
}
}, [activeCommentId, setShowInfoSidebar]);
return { activeCommentId, resolveComment };
};
@@ -0,0 +1,16 @@
import type {
ReturnEditorBlock,
Comment,
} from '@toeverything/datasource/db-service';
export const getCommentsFromEditorBlocks = (
editorBlocks: ReturnEditorBlock[]
) => {
return [] as Comment[];
};
export const getCommentReplyFromEditorBlock = (
editorBlock: ReturnEditorBlock
) => {
return {};
};