feat: add image handling actions and improve IRC message formatting

This commit is contained in:
HueByte
2026-07-17 16:52:15 +02:00
parent b9d099dd73
commit 953e081123
13 changed files with 249 additions and 209 deletions
+22
View File
@@ -7,6 +7,16 @@ using Attribute = Terminal.Gui.Drawing.Attribute;
namespace EchoHub.Client.UI.Chat;
/// <summary>An action a click on an attachment line can trigger.</summary>
public enum AttachmentAction
{
OpenImage,
SaveImage,
}
/// <summary>Inclusive column range on a chat line that triggers an attachment action when clicked.</summary>
public readonly record struct AttachmentActionSpan(int StartCol, int EndCol, AttachmentAction Action);
/// <summary>
/// A single line in the chat, composed of colored segments.
/// </summary>
@@ -20,6 +30,14 @@ public partial class ChatLine
public string? AttachmentFileName { get; set; }
public AttachmentKind? AttachmentKind { get; set; }
public string? SenderUsername { get; set; }
/// <summary>
/// Clickable sub-line targets (e.g. the "[open]" and "[save original]" brackets under an
/// image). Columns are relative to the unwrapped line, so only the first wrapped line
/// keeps them. Null means the whole line uses the kind's default action.
/// </summary>
public List<AttachmentActionSpan>? ActionSpans { get; set; }
/// <summary>Number of spaces to prepend on continuation lines when this line is word-wrapped.</summary>
public int ContinuationIndent { get; set; }
@@ -160,6 +178,10 @@ public partial class ChatLine
wrapped.IsMention = IsMention;
}
// Span columns only line up with the first wrapped line; later lines fall
// back to the kind's default action.
results[0].ActionSpans = ActionSpans;
return results;
}
@@ -443,9 +443,7 @@ public sealed class ChatMessageManager
lines.Add(new ChatLine(segments));
}
}
lines.Add(AttachmentActionLine(
$"[↓ save original] {attachment.FileName} [{FormatFileSize(attachment.FileSize)}]",
ChatColors.FileAttr, attachment));
lines.Add(ImageActionLine(attachment));
break;
case Core.Models.AttachmentKind.Audio:
@@ -489,6 +487,41 @@ public sealed class ChatMessageManager
return lines;
}
/// <summary>
/// Builds the action line below an image preview: "[open] [↓ save original] name [size]".
/// Each bracket is an <see cref="AttachmentActionSpan"/> so a mouse click can target it;
/// keyboard activation (Enter) uses the default action, open.
/// </summary>
private static ChatLine ImageActionLine(AttachmentDto attachment)
{
var segments = RailPrefix();
var col = segments.Sum(s => s.Text.GetColumns());
var spans = new List<AttachmentActionSpan>();
void AddAction(string text, AttachmentAction action)
{
var width = text.GetColumns();
spans.Add(new AttachmentActionSpan(col, col + width - 1, action));
segments.Add(new(text, ChatColors.FileAttr));
col += width;
}
AddAction("[open]", AttachmentAction.OpenImage);
segments.Add(new(" ", null));
col += 1;
AddAction("[↓ save original]", AttachmentAction.SaveImage);
segments.Add(new($" {attachment.FileName} [{FormatFileSize(attachment.FileSize)}]", ChatColors.FileAttr));
return new ChatLine(segments)
{
AttachmentUrl = attachment.Url,
AttachmentFileName = attachment.FileName,
AttachmentKind = attachment.Kind,
ActionSpans = spans,
ContinuationPrefixSegments = RailPrefix(),
};
}
/// <summary>
/// Builds a clickable attachment line carrying the metadata the message list uses to
/// route activation (play audio, download file, save original image).