feat(composer): a single pasted/dropped image lands in its caption field (#129)
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
Verified first: after a paste the focus stayed in the composer and the caption needed a click. Now, when exactly one image is pasted or dropped into an empty composer, its upload card's caption input takes focus; Enter there sends the board (with any composer text) and Escape returns focus to the composer. Multi-file drops and non-empty composers are unchanged. The target card is matched by file name + mtime because the metadata strip re-wraps the File. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -283,6 +283,12 @@ type UploadCardRendererProps = {
|
||||
setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
|
||||
onRemove: (file: TUploadContent) => void;
|
||||
onComplete?: (upload: UploadSuccess) => void;
|
||||
/** [Gitea #129] Focus the caption field on mount (single image into an empty composer). */
|
||||
autoFocusCaption?: boolean;
|
||||
/** Enter in the caption field: send the board (and any composer text). */
|
||||
onCaptionSubmit?: () => void;
|
||||
/** Escape in the caption field: hand focus back to the composer. */
|
||||
onCaptionEscape?: () => void;
|
||||
};
|
||||
export function UploadCardRenderer({
|
||||
isEncrypted,
|
||||
@@ -290,6 +296,9 @@ export function UploadCardRenderer({
|
||||
setMetadata,
|
||||
onRemove,
|
||||
onComplete,
|
||||
autoFocusCaption,
|
||||
onCaptionSubmit,
|
||||
onCaptionEscape,
|
||||
}: UploadCardRendererProps) {
|
||||
const mx = useMatrixClient();
|
||||
const mediaConfig = useMediaConfig();
|
||||
@@ -379,6 +388,16 @@ export function UploadCardRenderer({
|
||||
size="300"
|
||||
radii="300"
|
||||
style={{ marginTop: config.space.S200, width: '100%' }}
|
||||
autoFocus={autoFocusCaption}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||
e.preventDefault();
|
||||
onCaptionSubmit?.();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCaptionEscape?.();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CompressionCheckbox fileItem={fileItem} metadata={metadata} setMetadata={setMetadata} />
|
||||
|
||||
@@ -158,6 +158,13 @@ const EmojiBoard = React.lazy(() =>
|
||||
);
|
||||
|
||||
/** [Gitea #37] Debounce for persisting the composer draft while typing. */
|
||||
/** [Gitea #129] Same picked file, allowing for re-wrapped File objects. */
|
||||
const isAutoFocusTarget = (file: TUploadContent, target: File | undefined): boolean =>
|
||||
!!target &&
|
||||
file instanceof File &&
|
||||
file.name === target.name &&
|
||||
file.lastModified === target.lastModified;
|
||||
|
||||
const DRAFT_PERSIST_DEBOUNCE_MS = 500;
|
||||
|
||||
interface RoomInputProps {
|
||||
@@ -253,6 +260,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
selectedFiles.map((f) => f.file),
|
||||
);
|
||||
const uploadBoardHandlers = useRef<UploadBoardImperativeHandlers | undefined>(undefined);
|
||||
// [Gitea #129] The file whose caption input should take focus once its card mounts.
|
||||
// (Matched by name + mtime: the metadata strip and safeFile may re-wrap the File.)
|
||||
const autoFocusCaptionRef = useRef<File | undefined>(undefined);
|
||||
|
||||
const imagePackRooms: Room[] = useImagePackRooms(roomId, roomToParents);
|
||||
|
||||
@@ -379,12 +389,17 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const handleFiles = useCallback(
|
||||
async (files: File[]) => {
|
||||
setUploadBoard(true);
|
||||
// [Gitea #129] One image into an empty composer: land in its caption
|
||||
// field so "here's the bug" is one motion (Enter sends, Esc returns).
|
||||
const focusCaption =
|
||||
files.length === 1 && files[0].type.startsWith('image/') && isEmptyEditor(editor);
|
||||
setSelectedFiles({
|
||||
type: 'PUT',
|
||||
item: await filesToUploadItems(room, files, stripImageMetadata),
|
||||
});
|
||||
if (focusCaption) autoFocusCaptionRef.current = files[0];
|
||||
},
|
||||
[setSelectedFiles, room, stripImageMetadata],
|
||||
[setSelectedFiles, room, stripImageMetadata, editor],
|
||||
);
|
||||
const pickFile = useFilePicker(handleFiles, true);
|
||||
const handleFilePaste = useFilePasteHandler(handleFiles);
|
||||
@@ -1037,6 +1052,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
fileItem={fileItem}
|
||||
setMetadata={handleFileMetadata}
|
||||
onRemove={handleRemoveUpload}
|
||||
autoFocusCaption={isAutoFocusTarget(
|
||||
fileItem.originalFile,
|
||||
autoFocusCaptionRef.current,
|
||||
)}
|
||||
onCaptionSubmit={submit}
|
||||
onCaptionEscape={() => ReactEditor.focus(editor)}
|
||||
/>
|
||||
))}
|
||||
</UploadBoardContent>
|
||||
|
||||
Reference in New Issue
Block a user