feat: Add ASCII art size selection for image attachments and enhance message context menu

This commit is contained in:
HueByte
2026-07-16 05:35:11 +02:00
parent 2d33773c24
commit 6292e82cec
9 changed files with 344 additions and 43 deletions
+141 -12
View File
@@ -40,7 +40,7 @@ public sealed partial class MainWindow : Runnable
private readonly UserListSource _usersListSource;
private bool _usersPanelVisible = true;
private const int UsersPanelWidth = 22;
private const string DefaultInputTitle = "Message │ Enter=send │ Ctrl+N=newline │ Tab=complete │ Ctrl+K=search";
private const string DefaultInputTitle = "Message │ Enter=send │ Tab=complete │ Ctrl+K=search │ F6=pick message";
private static readonly Key F2Key = Key.F2;
private bool _hasStagedAttachments;
@@ -57,12 +57,13 @@ public sealed partial class MainWindow : Runnable
private static readonly Key CtrlXKey = Key.X.WithCtrl;
private static readonly Key CtrlCKey = Key.C.WithCtrl;
private static readonly Key CtrlYKey = Key.Y.WithCtrl;
private static readonly Key F6Key = Key.F6;
// Available slash commands for Tab autocomplete
private static readonly string[] SlashCommands =
[
"/status", "/nick", "/color", "/theme", "/send",
"/avatar", "/profile", "/servers", "/join", "/passwd", "/leave", "/clear", "/downloadpath",
"/avatar", "/profile", "/servers", "/join", "/passwd", "/leave", "/clear", "/size", "/downloadpath",
"/topic", "/users", "/kick", "/ban", "/unban",
"/mute", "/unmute", "/role", "/nuke", "/test-sound", "/quit", "/help"
];
@@ -248,6 +249,7 @@ public sealed partial class MainWindow : Runnable
_messageList.Source = new ChatListSource();
_messageList.Accepting += OnMessageListAccepting;
_messageList.KeyDown += OnMessageListKeyDown;
_messageList.MouseEvent += OnMessageListMouseEvent;
_messageList.VerticalScrollBar.Scrolled += OnMessageListVerticalScrollBarScrolled;
_messageList.VerticalScrollBar.Visible = true;
@@ -330,10 +332,10 @@ public sealed partial class MainWindow : Runnable
}
/// <summary>
/// Updates the attachment staging indicator shown on the input frame's title.
/// Passing an empty list restores the default hint.
/// Updates the attachment staging indicator shown on the input frame's title, including the
/// current ASCII-art size for images. Passing an empty list restores the default hint.
/// </summary>
public void SetStagedAttachments(IReadOnlyList<string> fileNames)
public void SetStagedAttachments(IReadOnlyList<string> fileNames, string asciiSizeLabel)
{
_hasStagedAttachments = fileNames.Count > 0;
if (fileNames.Count == 0)
@@ -343,9 +345,9 @@ public sealed partial class MainWindow : Runnable
else
{
var names = string.Join(", ", fileNames);
if (names.Length > 60)
names = names[..57] + "...";
_inputFrame.Title = $"📎 {fileNames.Count} staged: {names} │ Enter=send │ /clear to drop";
if (names.Length > 45)
names = names[..42] + "...";
_inputFrame.Title = $"📎 {fileNames.Count}: {names} │ art: {asciiSizeLabel} (/size) │ Enter=send │ /clear";
}
_inputFrame.SetNeedsDraw();
}
@@ -541,6 +543,14 @@ public sealed partial class MainWindow : Runnable
private void OnMessageListKeyDown(object? sender, Key e)
{
// F6 returns focus to the input box.
if (e.KeyCode == F6Key.KeyCode)
{
_inputField.SetFocus();
e.Handled = true;
return;
}
if (e.KeyCode != Key.Delete.KeyCode && e.KeyCode != Key.Backspace.KeyCode)
return;
@@ -557,12 +567,104 @@ public sealed partial class MainWindow : Runnable
// Server enforces the real permission (own message, or Mod+ over a lower role);
// the client just confirms intent and lets the server reject if disallowed.
var confirm = MessageBox.Query(_app, "Delete Message",
"Delete this message?", "Delete", "Cancel");
ConfirmDeleteMessage(messageId);
e.Handled = true;
}
private void OnMessageListMouseEvent(object? sender, Mouse e)
{
if (!e.Flags.HasFlag(MouseFlags.RightButtonClicked))
return;
if (_messageList.Source is not ChatListSource source || source.Count == 0 || e.Position is not { } pos)
return;
var index = _messageList.TopItem + pos.Y;
if (index < 0 || index >= source.Count)
return;
// Select the right-clicked row (so the menu acts on it and it highlights), then show the menu.
_messageList.SelectedItem = index;
_messageList.SetFocus();
var line = source.GetLine(index);
if (line is null)
return;
ShowMessageContextMenu(line, e.ScreenPosition);
e.Handled = true;
}
/// <summary>
/// Builds and shows a right-click context menu for a message line: attachment actions,
/// mention/profile for the sender, copy, and delete (permission enforced server-side).
/// </summary>
private void ShowMessageContextMenu(ChatLine line, System.Drawing.Point screenPosition)
{
var items = new List<View>();
var sender = line.SenderUsername;
if (line.AttachmentKind is { } kind && line.AttachmentUrl is { } url && line.AttachmentFileName is { } name)
{
switch (kind)
{
case AttachmentKind.Image:
items.Add(new MenuItem("Save original image", "", () => OnImageSaveRequested?.Invoke(url, name), Key.Empty));
break;
case AttachmentKind.Audio:
items.Add(new MenuItem("Play audio", "", () => OnAudioPlayRequested?.Invoke(url, name), Key.Empty));
break;
default:
items.Add(new MenuItem("Download file", "", () => OnFileDownloadRequested?.Invoke(url, name), Key.Empty));
break;
}
}
if (sender is not null)
{
items.Add(new MenuItem($"Mention @{sender}", "", () => MentionUser(sender), Key.Empty));
items.Add(new MenuItem($"View {sender}'s profile", "", () => OnUserProfileRequested?.Invoke(sender), Key.Empty));
}
items.Add(new MenuItem("Copy text", "", () => CopyToClipboard(line.ToString()), Key.Empty));
if (line.MessageId is { } messageId)
{
items.Add(new Line());
items.Add(new MenuItem("Delete message", "", () => ConfirmDeleteMessage(messageId), Key.Empty));
}
if (items.Count == 0)
return;
var menu = new PopoverMenu(items);
_app.Popovers?.Register(menu);
menu.MakeVisible(screenPosition);
}
private void MentionUser(string username)
{
_inputField.InsertText($"@{username} ");
_inputField.SetFocus();
}
private void CopyToClipboard(string text)
{
try
{
_app.Clipboard?.TrySetClipboardData(text);
}
catch (Exception ex)
{
Log.Warning(ex, "Copy to clipboard failed");
}
}
private void ConfirmDeleteMessage(Guid messageId)
{
var confirm = MessageBox.Query(_app, "Delete Message", "Delete this message?", "Delete", "Cancel");
if (confirm == 0)
OnDeleteMessageRequested?.Invoke(messageId);
e.Handled = true;
}
private void OnMessageListVerticalScrollBarScrolled(object? sender, EventArgs<int> e)
@@ -619,6 +721,13 @@ public sealed partial class MainWindow : Runnable
ShowSearchDialog();
e.Handled = true;
}
else if (e.KeyCode == F6Key.KeyCode)
{
// Move focus into the message list so you can select a message (arrows) and
// delete it (Delete). F6 again returns focus here. (Esc is the app quit key.)
FocusMessageList();
e.Handled = true;
}
else if (e.KeyCode == CtrlVKey.KeyCode || e.KeyCode == CtrlYKey.KeyCode)
{
// If a file was copied in the OS file manager, the clipboard holds a file list
@@ -1049,6 +1158,26 @@ public sealed partial class MainWindow : Runnable
_inputField.SetFocus();
}
/// <summary>
/// Moves focus into the message list for selection (arrows) and deletion (Delete). Selects the
/// most recent message when nothing is selected. No-op when the channel has no messages.
/// </summary>
private void FocusMessageList()
{
if (_messageList.Source is not ChatListSource source || source.Count == 0)
return;
if (!_messageList.SelectedItem.HasValue
|| _messageList.SelectedItem < 0
|| _messageList.SelectedItem >= source.Count)
{
_messageList.SelectedItem = source.Count - 1;
}
_messageList.SetFocus();
_messageList.SetNeedsDraw();
}
private void RefreshMessages()
{
var messages = _messageManager.GetMessages(_messageManager.CurrentChannel);