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

View File

@@ -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 { SnapshotHelper } from '@affine/core/modules/comment/services/snapshot-helper';
import type { CommentAttachment } from '@affine/core/modules/comment/types';
import { PeekViewService } from '@affine/core/modules/peek-view';
import { downloadResourceWithUrl } from '@affine/core/utils/resource';
import { DebugLogger } from '@affine/debug';
import { getAttachmentFileIconRC } from '@blocksuite/affine/components/icons';
import { type RichText, selectTextModel } from '@blocksuite/affine/rich-text';
@@ -80,16 +81,6 @@ export interface CommentEditorRef {
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
const useSnapshotDoc = (
defaultSnapshotOrDoc: DocSnapshot | Store,
@@ -313,6 +304,28 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
[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 () => {
if (isUploadDisabled) return;
const files = await openFilesWith('Any');
@@ -382,8 +395,6 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
if (!attachments) return;
const att = attachments[index];
if (!att) return;
const url = att.url || att.localUrl;
if (!url) return;
if (isImageAttachment(att)) {
// translate attachment index to image index
const imageAttachments = attachments.filter(isImageAttachment);
@@ -391,13 +402,19 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
if (imageIndex >= 0) {
handleImagePreview(imageIndex);
}
} else if (att.url || att.localUrl) {
} else if (att.url) {
// todo: open attachment preview. for now, just download it
download(url, att.filename ?? att.file?.name ?? 'attachment');
notify({
title: 'Downloading attachment',
message: 'The attachment is being downloaded to your computer.',
downloadResourceWithUrl(
att.url,
att.filename ?? att.file?.name ?? 'attachment'
).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]
@@ -538,6 +555,8 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
onClick={readonly ? undefined : handleClickEditor}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
onDragOver={handleDragOver}
onDrop={handleDrop}
data-readonly={!!readonly}
className={clsx(styles.container, 'comment-editor-viewport')}
>

View File

@@ -596,7 +596,7 @@ const CommentList = ({ entity }: { entity: DocCommentEntity }) => {
const [filterState, setFilterState] = useState<CommentFilterState>({
showResolvedComments: false,
onlyMyReplies: false,
onlyCurrentMode: true,
onlyCurrentMode: false,
});
const onFilterChange = useCallback(

View File

@@ -33,7 +33,22 @@ export async function downloadBlob(blob: Blob, filename: string) {
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) {
// 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
const blob = await resourceUrlToBlob(url);
if (!blob) return;

View File

@@ -8239,7 +8239,7 @@ export function useAFFiNEI18N(): {
*/
["com.affine.comment.comments"](): string;
/**
* `No comments yet`
* `No comments yet, select content to add comment to`
*/
["com.affine.comment.no-comments"](): string;
/**

View File

@@ -2067,7 +2067,7 @@
"com.affine.migration-all-docs-notification.error": "Migration failed: {{errorMessage}}",
"com.affine.migration-all-docs-notification.button": "Migrate data",
"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.description": "All comments will also be deleted, and this action cannot be undone.",
"com.affine.comment.reply.delete.confirm.title": "Delete this reply?",