mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 18:16:15 +08:00
fix(core): comment mention filters (#13062)
#### PR Dependency Tree * **PR #13062** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Replies in comment threads are now collapsed when there are more than four, with an option to expand and view all replies. * Mentions within comments and replies are automatically detected and tracked. * "Show more replies" indicator is now localized for English users. * **Improvements** * Filtering for "only my replies" now includes replies where you are mentioned. * Enhanced focus behavior in the comment editor for improved usability. * Updated styling for active and collapsed reply states in comment threads. * **Bug Fixes** * Ensured consistent handling of mentions and reply associations in comment data. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -108,6 +108,15 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
||||
[doc, snapshotHelper]
|
||||
);
|
||||
|
||||
const focusEditor = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
const lastChild = editorRef.current.std.store.root?.lastChild();
|
||||
if (lastChild) {
|
||||
focusTextModel(editorRef.current.std, lastChild.id);
|
||||
}
|
||||
}
|
||||
}, [editorRef]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancel = false;
|
||||
if (autoFocus && editorRef.current && doc) {
|
||||
@@ -119,25 +128,20 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
||||
'rich-text'
|
||||
) as unknown as RichText;
|
||||
if (!richText) return;
|
||||
|
||||
// Finally focus the inline editor
|
||||
const inlineEditor = richText.inlineEditor;
|
||||
richText.focus();
|
||||
|
||||
richText.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
|
||||
// fixme: the following does not work
|
||||
inlineEditor?.focusEnd();
|
||||
// Finally focus the inline editor
|
||||
richText.focus();
|
||||
focusEditor();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
return () => {
|
||||
cancel = true;
|
||||
};
|
||||
}, [autoFocus, doc]);
|
||||
}, [autoFocus, doc, focusEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (doc) {
|
||||
@@ -179,14 +183,9 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
|
||||
const handleClickEditor = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (editorRef.current) {
|
||||
const lastChild = editorRef.current.std.store.root?.lastChild();
|
||||
if (lastChild) {
|
||||
focusTextModel(editorRef.current.std, lastChild.id);
|
||||
}
|
||||
}
|
||||
focusEditor();
|
||||
},
|
||||
[editorRef]
|
||||
[focusEditor]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,7 +12,7 @@ export const container = style({
|
||||
borderRadius: 16,
|
||||
padding: '0 8px',
|
||||
},
|
||||
'&[data-readonly="false"]:focus-within': {
|
||||
'&[data-readonly="false"]:is(:focus-within, :active)': {
|
||||
borderColor: cssVarV2('layer/insideBorder/primaryBorder'),
|
||||
boxShadow: cssVar('activeShadow'),
|
||||
},
|
||||
|
||||
@@ -293,6 +293,74 @@ const CommentItem = ({
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
// When the comment item is rendered the first time, the replies will be collapsed by default
|
||||
// The replies will be collapsed when replies length > 4, that is, the comment, first reply and the last 2 replies
|
||||
// will be shown
|
||||
// When new reply is added either by clicking the reply button or synced remotely, we will NOT collapse the replies
|
||||
const [collapsed, setCollapsed] = useState(
|
||||
(comment.replies?.length ?? 0) > 4
|
||||
);
|
||||
|
||||
const renderedReplies = useMemo(() => {
|
||||
// Sort replies ascending by createdAt
|
||||
const sortedReplies =
|
||||
comment.replies?.toSorted((a, b) => a.createdAt - b.createdAt) ?? [];
|
||||
if (sortedReplies.length === 0) return null;
|
||||
|
||||
// If not collapsed or replies are fewer than threshold, render all
|
||||
if (!collapsed || sortedReplies.length <= 4) {
|
||||
return sortedReplies.map(reply => (
|
||||
<ReadonlyCommentRenderer
|
||||
key={reply.id}
|
||||
avatarUrl={reply.user.avatarUrl}
|
||||
name={reply.user.name}
|
||||
time={reply.createdAt}
|
||||
snapshot={reply.content.snapshot}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
// Collapsed state: first reply + collapsed indicator + last two replies
|
||||
const firstReply = sortedReplies[0];
|
||||
const tailReplies = sortedReplies.slice(-2);
|
||||
|
||||
return (
|
||||
<>
|
||||
{firstReply && (
|
||||
<ReadonlyCommentRenderer
|
||||
key={firstReply.id}
|
||||
avatarUrl={firstReply.user.avatarUrl}
|
||||
name={firstReply.user.name}
|
||||
time={firstReply.createdAt}
|
||||
snapshot={firstReply.content.snapshot}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={styles.collapsedReplies}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setCollapsed(false);
|
||||
}}
|
||||
>
|
||||
<div className={styles.collapsedRepliesTitle}>
|
||||
{t['com.affine.comment.reply.show-more']({
|
||||
count: (sortedReplies.length - 4).toString(),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{tailReplies.map(reply => (
|
||||
<ReadonlyCommentRenderer
|
||||
key={reply.id}
|
||||
avatarUrl={reply.user.avatarUrl}
|
||||
name={reply.user.name}
|
||||
time={reply.createdAt}
|
||||
snapshot={reply.content.snapshot}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [collapsed, comment.replies, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClickPreview}
|
||||
@@ -353,19 +421,7 @@ const CommentItem = ({
|
||||
time={comment.createdAt}
|
||||
snapshot={comment.content.snapshot}
|
||||
/>
|
||||
|
||||
{/* unlike comment, replies are sorted by createdAt in ascending order */}
|
||||
{comment.replies
|
||||
?.toSorted((a, b) => a.createdAt - b.createdAt)
|
||||
.map(reply => (
|
||||
<ReadonlyCommentRenderer
|
||||
key={reply.id}
|
||||
avatarUrl={reply.user.avatarUrl}
|
||||
name={reply.user.name}
|
||||
time={reply.createdAt}
|
||||
snapshot={reply.content.snapshot}
|
||||
/>
|
||||
))}
|
||||
{renderedReplies}
|
||||
</div>
|
||||
|
||||
{highlighting &&
|
||||
@@ -421,13 +477,31 @@ const CommentList = ({ entity }: { entity: DocCommentEntity }) => {
|
||||
}
|
||||
|
||||
// Filter by only my replies and mentions
|
||||
if (filterState.onlyMyReplies) {
|
||||
if (filterState.onlyMyReplies && account) {
|
||||
filteredComments = filteredComments.filter(comment => {
|
||||
return (
|
||||
comment.user.id === account?.id ||
|
||||
comment.replies?.some(reply => reply.user.id === account?.id)
|
||||
comment.user.id === account.id ||
|
||||
comment.mentions.includes(account.id) ||
|
||||
comment.replies?.some(reply => {
|
||||
return (
|
||||
reply.user.id === account.id ||
|
||||
reply.mentions.includes(account.id)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
filteredComments = filteredComments.map(comment => {
|
||||
return {
|
||||
...comment,
|
||||
replies: comment.replies?.filter(reply => {
|
||||
return (
|
||||
reply.user.id === account.id ||
|
||||
reply.mentions.includes(account.id)
|
||||
);
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by only current mode
|
||||
@@ -444,8 +518,8 @@ const CommentList = ({ entity }: { entity: DocCommentEntity }) => {
|
||||
filterState.showResolvedComments,
|
||||
filterState.onlyMyReplies,
|
||||
filterState.onlyCurrentMode,
|
||||
account?.id,
|
||||
docMode,
|
||||
account,
|
||||
]);
|
||||
|
||||
const newPendingComment = useLiveData(entity.pendingComment$);
|
||||
|
||||
@@ -189,3 +189,24 @@ export const time = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontWeight: '500',
|
||||
});
|
||||
|
||||
export const collapsedReplies = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
cursor: 'pointer',
|
||||
height: '28px',
|
||||
paddingLeft: '42px',
|
||||
borderRadius: 8,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const collapsedRepliesTitle = style({
|
||||
color: cssVarV2('text/emphasis'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: '500',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user