mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-06 07:36:01 +02:00
feat: add clipboard image handling for pasting and staging attachments
This commit is contained in:
@@ -35,6 +35,10 @@ public sealed class AppOrchestrator : IDisposable
|
||||
private readonly HashSet<string> _channelsLoadingMore = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<string> _stagedAttachments = [];
|
||||
|
||||
// Temp PNGs created for clipboard-image pastes; deleted once their message is sent
|
||||
// (or the staging tray is cleared) so pasted screenshots don't pile up in %TEMP%.
|
||||
private readonly HashSet<string> _tempPastedFiles = [];
|
||||
|
||||
// E2E channels whose unlock prompt the user cancelled — don't nag on every reselect.
|
||||
// Cleared on connect/reconnect; an explicit /join or a send attempt re-offers the prompt.
|
||||
private readonly HashSet<string> _declinedUnlocks = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -94,6 +98,8 @@ public sealed class AppOrchestrator : IDisposable
|
||||
_mainWindow.OnDisconnectRequested += HandleDisconnect;
|
||||
_mainWindow.OnLogoutRequested += HandleLogout;
|
||||
_mainWindow.OnMessageSubmitted += HandleMessageSubmitted;
|
||||
_mainWindow.OnFilesStaged += HandleFilesStaged;
|
||||
_mainWindow.OnImagePasted += HandleImagePasted;
|
||||
_mainWindow.OnChannelSelected += HandleChannelSelected;
|
||||
_mainWindow.OnProfileRequested += HandleProfileRequested;
|
||||
_mainWindow.OnStatusRequested += HandleStatusRequested;
|
||||
@@ -221,11 +227,89 @@ public sealed class AppOrchestrator : IDisposable
|
||||
|
||||
private Task HandleCmdClearAttachments()
|
||||
{
|
||||
var temps = _stagedAttachments.Where(_tempPastedFiles.Contains).ToList();
|
||||
_tempPastedFiles.ExceptWith(temps);
|
||||
CleanupPastedTempFiles(temps);
|
||||
|
||||
_stagedAttachments.Clear();
|
||||
InvokeUI(RefreshStagingTray);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stages a batch of local files (multi-file paste or drag-and-drop) as attachments of the
|
||||
/// next message. Runs synchronously on the UI thread — unlike routing each file through a
|
||||
/// fire-and-forget /send command, a 10-file paste can't race the staging list.
|
||||
/// </summary>
|
||||
private void HandleFilesStaged(string channel, IReadOnlyList<string> files)
|
||||
{
|
||||
if (!_conn.IsAuthenticated || !_conn.IsConnected)
|
||||
return;
|
||||
|
||||
var slotsLeft = HubConstants.MaxAttachmentsPerMessage - _stagedAttachments.Count;
|
||||
_stagedAttachments.AddRange(files.Take(Math.Max(0, slotsLeft)));
|
||||
|
||||
if (files.Count > slotsLeft)
|
||||
_mainWindow.ShowError($"You can attach at most {HubConstants.MaxAttachmentsPerMessage} files per message.");
|
||||
|
||||
RefreshStagingTray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stages an image pasted as raw clipboard data (copied from a browser, a screenshot tool,
|
||||
/// or an image editor). The PNG is written to a per-paste temp folder so it flows through
|
||||
/// the same path-based staging/encryption pipeline as regular files, and the temp file is
|
||||
/// deleted once the message is sent.
|
||||
/// </summary>
|
||||
private void HandleImagePasted(string channel, byte[] png)
|
||||
{
|
||||
if (!_conn.IsAuthenticated || !_conn.IsConnected)
|
||||
return;
|
||||
|
||||
if (_stagedAttachments.Count >= HubConstants.MaxAttachmentsPerMessage)
|
||||
{
|
||||
_mainWindow.ShowError($"You can attach at most {HubConstants.MaxAttachmentsPerMessage} files per message.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// A unique folder per paste keeps the Discord-style "image.png" display name
|
||||
// while letting several pasted images coexist in one message.
|
||||
var dir = Path.Combine(Path.GetTempPath(), "EchoHub", "pasted", Guid.NewGuid().ToString("N")[..8]);
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, "image.png");
|
||||
File.WriteAllBytes(path, png);
|
||||
|
||||
_tempPastedFiles.Add(path);
|
||||
_stagedAttachments.Add(path);
|
||||
RefreshStagingTray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Staging a pasted clipboard image failed");
|
||||
_mainWindow.ShowError($"Pasting image failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Best-effort removal of pasted-image temp files and their per-paste folders.</summary>
|
||||
private static void CleanupPastedTempFiles(IReadOnlyList<string> files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file);
|
||||
if (Path.GetDirectoryName(file) is { } dir)
|
||||
Directory.Delete(dir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Could not delete pasted-image temp file {File}", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the ASCII-art size picker (no argument) or sets it directly from "s"/"m"/"l" (or
|
||||
/// small/medium/large). The choice is a persistent preference applied to attached images.
|
||||
@@ -307,17 +391,29 @@ public sealed class AppOrchestrator : IDisposable
|
||||
_stagedAttachments.Clear();
|
||||
InvokeUI(RefreshStagingTray);
|
||||
|
||||
var hasRoomKey = _conn.RoomKeys.TryGetKey(channel, out var roomKey);
|
||||
// Pasted clipboard images live in temp files; once this send owns them they are
|
||||
// deleted whether the upload succeeds or fails (the tray is already cleared).
|
||||
var tempFiles = staged.Where(_tempPastedFiles.Contains).ToList();
|
||||
_tempPastedFiles.ExceptWith(tempFiles);
|
||||
|
||||
var outgoing = new List<OutgoingAttachment>();
|
||||
foreach (var path in staged)
|
||||
outgoing.Add(await BuildOutgoingAttachmentAsync(path, hasRoomKey ? roomKey : null, size));
|
||||
try
|
||||
{
|
||||
var hasRoomKey = _conn.RoomKeys.TryGetKey(channel, out var roomKey);
|
||||
|
||||
var wireContent = hasRoomKey && !string.IsNullOrEmpty(content)
|
||||
? RoomCrypto.EncryptText(content, roomKey)
|
||||
: content;
|
||||
var outgoing = new List<OutgoingAttachment>();
|
||||
foreach (var path in staged)
|
||||
outgoing.Add(await BuildOutgoingAttachmentAsync(path, hasRoomKey ? roomKey : null, size));
|
||||
|
||||
await _conn.Api!.SendMessageWithAttachmentsAsync(channel, wireContent, outgoing, size);
|
||||
var wireContent = hasRoomKey && !string.IsNullOrEmpty(content)
|
||||
? RoomCrypto.EncryptText(content, roomKey)
|
||||
: content;
|
||||
|
||||
await _conn.Api!.SendMessageWithAttachmentsAsync(channel, wireContent, outgoing, size);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupPastedTempFiles(tempFiles);
|
||||
}
|
||||
}, "Send failed");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user