fix(core): some comment editor ux enhancements (#13126)

fix AF-2726, AF-2729

#### PR Dependency Tree


* **PR #13126** 👈

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**
* Added drag-and-drop support for file attachments in the comment
editor.
* Improved user feedback with notifications and toasts when downloading
attachments.

* **Bug Fixes**
  * Enhanced error handling and reporting for attachment downloads.

* **Improvements**
* Optimized file download process for same-origin resources to improve
performance.
* Updated default comment filter to show all comments, not just those
for the current mode.

* **Documentation**
* Updated English localization to provide clearer instructions when no
comments are present.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #13126** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
Peng Xiao
2025-07-10 11:58:00 +08:00
committed by GitHub
parent 11a9e67bc1
commit ed6fde550f
5 changed files with 55 additions and 21 deletions
@@ -1,8 +1,9 @@
import { IconButton, notify } from '@affine/component'; import { IconButton, notify, toast } from '@affine/component';
import { LitDocEditor, type PageEditor } from '@affine/core/blocksuite/editors'; import { LitDocEditor, type PageEditor } from '@affine/core/blocksuite/editors';
import { SnapshotHelper } from '@affine/core/modules/comment/services/snapshot-helper'; import { SnapshotHelper } from '@affine/core/modules/comment/services/snapshot-helper';
import type { CommentAttachment } from '@affine/core/modules/comment/types'; import type { CommentAttachment } from '@affine/core/modules/comment/types';
import { PeekViewService } from '@affine/core/modules/peek-view'; import { PeekViewService } from '@affine/core/modules/peek-view';
import { downloadResourceWithUrl } from '@affine/core/utils/resource';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { getAttachmentFileIconRC } from '@blocksuite/affine/components/icons'; import { getAttachmentFileIconRC } from '@blocksuite/affine/components/icons';
import { type RichText, selectTextModel } from '@blocksuite/affine/rich-text'; import { type RichText, selectTextModel } from '@blocksuite/affine/rich-text';
@@ -80,16 +81,6 @@ export interface CommentEditorRef {
focus: () => void; focus: () => void;
} }
const download = (url: string, name: string) => {
const element = document.createElement('a');
element.setAttribute('download', name);
element.setAttribute('href', url);
element.style.display = 'none';
document.body.append(element);
element.click();
element.remove();
};
// todo: get rid of circular data changes // todo: get rid of circular data changes
const useSnapshotDoc = ( const useSnapshotDoc = (
defaultSnapshotOrDoc: DocSnapshot | Store, defaultSnapshotOrDoc: DocSnapshot | Store,
@@ -313,6 +304,28 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
[addAttachments] [addAttachments]
); );
const handleDragOver = useCallback(
(e: React.DragEvent) => {
if (readonly) return;
// Prevent default to allow drop
e.preventDefault();
},
[readonly]
);
const handleDrop = useCallback(
(e: React.DragEvent) => {
if (readonly) return;
e.preventDefault();
e.stopPropagation();
const files = Array.from(e.dataTransfer?.files ?? []);
if (files.length) {
addAttachments(files);
}
},
[addAttachments, readonly]
);
const openFilePicker = useAsyncCallback(async () => { const openFilePicker = useAsyncCallback(async () => {
if (isUploadDisabled) return; if (isUploadDisabled) return;
const files = await openFilesWith('Any'); const files = await openFilesWith('Any');
@@ -382,8 +395,6 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
if (!attachments) return; if (!attachments) return;
const att = attachments[index]; const att = attachments[index];
if (!att) return; if (!att) return;
const url = att.url || att.localUrl;
if (!url) return;
if (isImageAttachment(att)) { if (isImageAttachment(att)) {
// translate attachment index to image index // translate attachment index to image index
const imageAttachments = attachments.filter(isImageAttachment); const imageAttachments = attachments.filter(isImageAttachment);
@@ -391,13 +402,19 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
if (imageIndex >= 0) { if (imageIndex >= 0) {
handleImagePreview(imageIndex); handleImagePreview(imageIndex);
} }
} else if (att.url || att.localUrl) { } else if (att.url) {
// todo: open attachment preview. for now, just download it // todo: open attachment preview. for now, just download it
download(url, att.filename ?? att.file?.name ?? 'attachment'); downloadResourceWithUrl(
notify({ att.url,
title: 'Downloading attachment', att.filename ?? att.file?.name ?? 'attachment'
message: 'The attachment is being downloaded to your computer.', ).catch(e => {
console.error('Failed to download attachment', e);
notify.error({
title: 'Failed to download attachment',
message: e.message,
});
}); });
toast('The attachment is being downloaded to your computer.');
} }
}, },
[attachments, handleImagePreview] [attachments, handleImagePreview]
@@ -538,6 +555,8 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
onClick={readonly ? undefined : handleClickEditor} onClick={readonly ? undefined : handleClickEditor}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste} onPaste={handlePaste}
onDragOver={handleDragOver}
onDrop={handleDrop}
data-readonly={!!readonly} data-readonly={!!readonly}
className={clsx(styles.container, 'comment-editor-viewport')} className={clsx(styles.container, 'comment-editor-viewport')}
> >
@@ -596,7 +596,7 @@ const CommentList = ({ entity }: { entity: DocCommentEntity }) => {
const [filterState, setFilterState] = useState<CommentFilterState>({ const [filterState, setFilterState] = useState<CommentFilterState>({
showResolvedComments: false, showResolvedComments: false,
onlyMyReplies: false, onlyMyReplies: false,
onlyCurrentMode: true, onlyCurrentMode: false,
}); });
const onFilterChange = useCallback( const onFilterChange = useCallback(
@@ -33,7 +33,22 @@ export async function downloadBlob(blob: Blob, filename: string) {
URL.revokeObjectURL(blobUrl); URL.revokeObjectURL(blobUrl);
} }
export function downloadFile(url: string, filename: string) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.target = '_blank';
a.click();
}
export async function downloadResourceWithUrl(url: string, filename: string) { export async function downloadResourceWithUrl(url: string, filename: string) {
// 1. if url is not same origin, fetch it to blob and download it
// 2. otherwise, download it directly
const sameOrigin = new URL(url).origin === window.location.origin;
if (sameOrigin) {
downloadFile(url, filename);
return;
}
// given input url may not have correct mime type // given input url may not have correct mime type
const blob = await resourceUrlToBlob(url); const blob = await resourceUrlToBlob(url);
if (!blob) return; if (!blob) return;
+1 -1
View File
@@ -8239,7 +8239,7 @@ export function useAFFiNEI18N(): {
*/ */
["com.affine.comment.comments"](): string; ["com.affine.comment.comments"](): string;
/** /**
* `No comments yet` * `No comments yet, select content to add comment to`
*/ */
["com.affine.comment.no-comments"](): string; ["com.affine.comment.no-comments"](): string;
/** /**
+1 -1
View File
@@ -2067,7 +2067,7 @@
"com.affine.migration-all-docs-notification.error": "Migration failed: {{errorMessage}}", "com.affine.migration-all-docs-notification.error": "Migration failed: {{errorMessage}}",
"com.affine.migration-all-docs-notification.button": "Migrate data", "com.affine.migration-all-docs-notification.button": "Migrate data",
"com.affine.comment.comments": "Comments", "com.affine.comment.comments": "Comments",
"com.affine.comment.no-comments": "No comments yet", "com.affine.comment.no-comments": "No comments yet, select content to add comment to",
"com.affine.comment.delete.confirm.title": "Delete the thread?", "com.affine.comment.delete.confirm.title": "Delete the thread?",
"com.affine.comment.delete.confirm.description": "All comments will also be deleted, and this action cannot be undone.", "com.affine.comment.delete.confirm.description": "All comments will also be deleted, and this action cannot be undone.",
"com.affine.comment.reply.delete.confirm.title": "Delete this reply?", "com.affine.comment.reply.delete.confirm.title": "Delete this reply?",