Skip to content

Drag and drop file attachments to Finder - #7853

Open
FellowTraveler wants to merge 6 commits into
signalapp:mainfrom
FellowTraveler:feature/drag-drop-attachments
Open

Drag and drop file attachments to Finder#7853
FellowTraveler wants to merge 6 commits into
signalapp:mainfrom
FellowTraveler:feature/drag-drop-attachments

Conversation

@FellowTraveler

@FellowTraveler FellowTraveler commented May 2, 2026

Copy link
Copy Markdown

Summary

Allows users to drag a downloaded file attachment from Signal directly into a Finder folder (or any drop target). Electron's native webContents.startDrag() API handles the OS-level drag-and-drop.

Changes

  • app/main.main.ts — adds start-attachment-drag IPC handler: decrypts the attachment to a temp file and calls event.sender.startDrag() with a generic icon
  • ts/state/ducks/conversations.preload.ts — adds dragAttachment Redux thunk and DragAttachmentActionCreatorType; decrypts attachment to OS temp dir, then sends the IPC message
  • ts/components/conversation/Message.dom.tsx — wraps the generic attachment button in a draggable div; onDragStart dispatches dragAttachment
  • ts/state/smart/TimelineItem.preload.tsx — passes dragAttachment down to SmartTimelineItem

Test plan

  • Drag a downloaded file attachment out of Signal → file appears in Finder / drop target
  • Drag an attachment that has not been downloaded yet → no drag initiated
  • Images, audio, and video attachments are unaffected

@scottnonnenberg-signal

Copy link
Copy Markdown
Contributor

We won't be merging #7855; please consider updating this PR to be a standalone PR.

@FellowTraveler
FellowTraveler force-pushed the feature/drag-drop-attachments branch from a55a8fe to 7093611 Compare May 9, 2026 00:06
@FellowTraveler

Copy link
Copy Markdown
Author

We won't be merging #7855; please consider updating this PR to be a standalone PR.

Respect your decision; submitting an updated PR for #7853.
I will be running my own custom build with #7855 however, because it's definitely a pain point.
Presumably you have security reasons for this decision, understood.

image

@FellowTraveler

Copy link
Copy Markdown
Author

FYI I'm still testing this updated PR, don't merge it yet. I'll post again here when it's ready.

@FellowTraveler

Copy link
Copy Markdown
Author

Okay it's working now; good to go.

@scottnonnenberg-signal scottnonnenberg-signal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for spending time on this, and thanks for separating from the other PR.

Can you make a couple changes?

  1. You're saving files to the OS temp directory, but Signal Desktop maintains its own temp directory, and I think that would be a better place
  2. When dragging is cancelled, or completed, we need to clean up the temp file. And really, ideally, we wouldn't decrypt at the moment of starting to drag, but when the dragging is complete. Is that possible?
  3. You're using group_default.png as the icon shown when dragging - what about something like generic-file.svg instead?

@FellowTraveler

Copy link
Copy Markdown
Author

Thanks for spending time on this, and thanks for separating from the other PR.

Can you make a couple changes?

  1. You're saving files to the OS temp directory, but Signal Desktop maintains its own temp directory, and I think that would be a better place

===> I'll replace tmpdir() with TEMP_PATH from basePaths.preload.ts (already platform-correct). Add onDragEnd to the wrapper div → dispatch cleanup → IPC to main → fsExtra.remove(filePath).

  1. When dragging is cancelled, or completed, we need to clean up the temp file. And really, ideally, we wouldn't decrypt at the moment of starting to drag, but when the dragging is complete. Is that possible?

===> Not possible with Electron's API — startDrag requires the file to exist before being called. We have to clean up after instead.

  1. You're using group_default.png as the icon shown when dragging - what about something like generic-file.svg instead?

Yes, I can change icon to generic-file.svg.

@FellowTraveler

Copy link
Copy Markdown
Author

Thanks for the feedback! Here's what was addressed in the latest push:

  1. Signal temp directory — switched from os.tmpdir() to Signal's own TEMP_PATH (from ts/util/basePaths.preload.ts), which resolves to the platform-correct {userData}/temp path via getTempPath().

  2. Temp file cleanup — added an onDragEnd handler on the draggable element that dispatches a cleanup-drag-temp-file IPC message, which the main process handles with fsExtra.remove(). This fires whether the drag was completed or cancelled.

  3. generic-file.svg icon — we ran into a problem here: nativeImage.createFromPath() returns an empty image when given an SVG file in Electron 41, which causes startDrag to silently fail (no drag occurs at all). We confirmed this by testing with and without the SVG. We've kept group_default.png for now as a placeholder. Could you advise on the preferred approach for using an SVG as a drag icon? Options we considered: rasterizing via a build step, or embedding a pre-converted PNG — happy to go whichever direction you prefer.

  4. Re: "ideally decrypt when dragging is complete" — Electron's webContents.startDrag() requires the file to already exist on disk at the moment it's called, so decryption must happen at drag start rather than after the drop. The cleanup on dragend is the closest we can get to deferring the I/O impact.

@scottnonnenberg-signal

Copy link
Copy Markdown
Contributor

Thanks for those updates! I tested it a bit on macOS, and it doesn't seem to be cleaning up the file when the drag is completed or cancelled. That definitely needs to be fixed.

Looking at the architecture, I also see that it's only set up for one historical drag file location; we probably need to be more ready for overlapping drags, just to be safe. Could have just one variable, but if it's set when we want to add a new thing, delete the old thing first.

Lastly, I now realize that this is only for generic attachments. Not for any media attachments, which is what people really want to drag. Can you expand it to work with those kinds of attachments as well?

@FellowTraveler

Copy link
Copy Markdown
Author

Thanks for the detailed review! Here's what the latest push addresses:

1. Cleanup not working — The root cause was that onDragEnd never fires: calling event.preventDefault() in onDragStart cancels the browser drag before it starts, so the browser never dispatches dragend. Fixed by moving cleanup entirely to the main process. At the start of each new start-attachment-drag IPC call, we now delete the previous drag's temp file before storing the new path. This also removes the need for the separate cleanup-drag-temp-file IPC channel.

2. Overlapping drags — The main-process lastDragTempPath variable now handles this: if a second drag starts before the first temp file is cleaned up, the stale file is deleted first.

3. Media attachments — Added drag support to image, video, and GIF attachment containers (gates on attachments.length === 1 — multi-image grid drag is a separate question). The same draggable / onDragStart pattern is applied to the container div for each media type.

Still pending: SVG icon — We haven't heard back on the generic-file.svg question from the last round. We're keeping group_default.png for now. Happy to switch to a rasterized PNG or a build-step conversion once you advise on the preferred approach.

@FellowTraveler
FellowTraveler force-pushed the feature/drag-drop-attachments branch from 8dcd45f to 0ef6b29 Compare June 19, 2026 11:49
Allows users to drag downloaded file attachments from Signal directly
into a Finder folder. Decrypts the attachment to a temp file and uses
Electron's webContents.startDrag() for native OS drag-and-drop support.
- Move temp file cleanup to main process: delete the previous drag's
  temp file at the start of each new drag (onDragEnd doesn't fire
  because event.preventDefault() in onDragStart cancels the browser
  drag before it starts)
- Remove renderer-side cleanupDragAttachment action and cleanup-drag-
  temp-file IPC channel — no longer needed
- Add drag support to image, video, and GIF attachment containers
  (single attachment only; multi-image grids not yet supported)
@FellowTraveler
FellowTraveler force-pushed the feature/drag-drop-attachments branch from 0ef6b29 to 1576a25 Compare July 4, 2026 01:00
@FellowTraveler

Copy link
Copy Markdown
Author

Update on the pending generic-file.svg icon question: since nativeImage.createFromPath() doesn't support SVG, I rasterized the project's existing images/file.svg (the flat single-color page-with-folded-corner icon already used elsewhere in the app) into a static images/file.png, using @napi-rs/canvas — the same library scripts/generate-tray-icons.mjs already uses to generate the committed tray icon PNGs, so this follows the existing convention of committing pre-rasterized assets rather than converting at build time.

This replaces the group_default.png placeholder, so the drag now shows an actual file icon instead of a group avatar. Went with the bolder single-color file.svg rather than the near-white generic-file.svg since the drag icon needs to read clearly over arbitrary desktop/Finder backgrounds, not just light app UI.

Latest push includes this change and should be ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants