feat: Add avatar upload functionality and refactor profile handling in AppOrchestrator

This commit is contained in:
HueByte
2026-02-19 09:11:14 +01:00
parent bed6040f7c
commit ad69420ec8
7 changed files with 370 additions and 370 deletions
+84 -19
View File
@@ -144,9 +144,54 @@ public sealed class AppOrchestrator : IDisposable
} }
}; };
_commandHandler.OnOpenProfile += () => _commandHandler.OnSetAvatar += async (target) =>
{ {
InvokeUI(HandleProfileRequested); if (!IsAuthenticated) return;
try
{
Stream stream;
string fileName;
if (Uri.TryCreate(target, UriKind.Absolute, out var uri)
&& (uri.Scheme == "http" || uri.Scheme == "https"))
{
using var http = new HttpClient();
var bytes = await http.GetByteArrayAsync(uri);
stream = new MemoryStream(bytes);
fileName = Path.GetFileName(uri.LocalPath);
if (string.IsNullOrWhiteSpace(fileName) || !fileName.Contains('.'))
fileName = "avatar.png";
}
else
{
if (!File.Exists(target))
{
InvokeUI(() => _mainWindow.ShowError($"File not found: {target}"));
return;
}
stream = File.OpenRead(target);
fileName = Path.GetFileName(target);
}
await using (stream)
{
var ascii = await _apiClient!.UploadAvatarAsync(stream, fileName);
var channel = _mainWindow.CurrentChannel;
if (!string.IsNullOrEmpty(channel))
InvokeUI(() => _mainWindow.AddSystemMessage(channel, "Avatar updated."));
}
}
catch (Exception ex)
{
Log.Error(ex, "Avatar upload failed for {Target}", target);
InvokeUI(() => _mainWindow.ShowError($"Avatar upload failed: {ex.Message}"));
}
};
_commandHandler.OnOpenProfile += (username) =>
{
InvokeUI(() => HandleViewProfile(username));
return Task.CompletedTask; return Task.CompletedTask;
}; };
@@ -400,35 +445,55 @@ public sealed class AppOrchestrator : IDisposable
private void HandleProfileRequested() private void HandleProfileRequested()
{ {
HandleViewProfile(null);
}
private void HandleViewProfile(string? username)
{
// If no username or it's our own, show the full user panel
var isOwnProfile = string.IsNullOrWhiteSpace(username)
|| username.Equals(_currentUsername, StringComparison.OrdinalIgnoreCase);
Task.Run(async () => Task.Run(async () =>
{ {
UserProfileDto? profile = null; UserProfileDto? profile = null;
try try
{ {
if (IsAuthenticated && !string.IsNullOrEmpty(_currentUsername)) if (IsAuthenticated)
profile = await _apiClient!.GetUserProfileAsync(_currentUsername); {
var target = isOwnProfile ? _currentUsername : username!;
if (!string.IsNullOrEmpty(target))
profile = await _apiClient!.GetUserProfileAsync(target);
}
} }
catch catch (Exception ex)
{ {
// Profile may not be available; continue with null InvokeUI(() => _mainWindow.ShowError($"Failed to load profile: {ex.Message}"));
return;
} }
InvokeUI(() => InvokeUI(() =>
{ {
var action = UserPanelDialog.Show(_app, if (isOwnProfile)
profile,
_config.SavedServers,
_currentStatus,
_currentStatusMessage);
switch (action)
{ {
case UserPanelAction.EditProfile: var action = ProfileViewDialog.ShowOwn(_app,
HandleEditProfile(profile); profile,
break; _currentStatus,
case UserPanelAction.SetStatus: _currentStatusMessage);
HandleStatusRequested();
break; switch (action)
{
case ProfileAction.EditProfile:
HandleEditProfile(profile);
break;
case ProfileAction.SetStatus:
HandleStatusRequested();
break;
}
}
else
{
ProfileViewDialog.Show(_app, profile);
} }
}); });
}); });
+21 -5
View File
@@ -11,12 +11,13 @@ public class CommandHandler
public event Func<string, Task>? OnSetColor; public event Func<string, Task>? OnSetColor;
public event Func<string, Task>? OnSetTheme; public event Func<string, Task>? OnSetTheme;
public event Func<string, Task>? OnSendFile; public event Func<string, Task>? OnSendFile;
public event Func<Task>? OnOpenProfile; public event Func<string?, Task>? OnOpenProfile;
public event Func<Task>? OnOpenServers; public event Func<Task>? OnOpenServers;
public event Func<string, Task>? OnJoinChannel; public event Func<string, Task>? OnJoinChannel;
public event Func<Task>? OnLeaveChannel; public event Func<Task>? OnLeaveChannel;
public event Func<string, Task>? OnSetTopic; public event Func<string, Task>? OnSetTopic;
public event Func<Task>? OnListUsers; public event Func<Task>? OnListUsers;
public event Func<string, Task>? OnSetAvatar;
public event Func<Task>? OnQuit; public event Func<Task>? OnQuit;
public event Func<Task>? OnHelp; public event Func<Task>? OnHelp;
@@ -38,7 +39,8 @@ public class CommandHandler
"color" => await HandleColor(args), "color" => await HandleColor(args),
"theme" => await HandleTheme(args), "theme" => await HandleTheme(args),
"send" => await HandleSend(args), "send" => await HandleSend(args),
"profile" => await HandleProfile(), "profile" => await HandleProfile(args),
"avatar" => await HandleAvatar(args),
"servers" => await HandleServers(), "servers" => await HandleServers(),
"join" => await HandleJoin(args), "join" => await HandleJoin(args),
"leave" => await HandleLeave(), "leave" => await HandleLeave(),
@@ -141,13 +143,26 @@ public class CommandHandler
return new CommandResult(true, $"Uploading: {Path.GetFileName(target)}..."); return new CommandResult(true, $"Uploading: {Path.GetFileName(target)}...");
} }
private async Task<CommandResult> HandleProfile() private async Task<CommandResult> HandleProfile(string args)
{ {
var username = string.IsNullOrWhiteSpace(args) ? null : args.Trim();
if (OnOpenProfile is not null) if (OnOpenProfile is not null)
await OnOpenProfile(); await OnOpenProfile(username);
return new CommandResult(true); return new CommandResult(true);
} }
private async Task<CommandResult> HandleAvatar(string args)
{
if (string.IsNullOrWhiteSpace(args))
return new CommandResult(true, "Usage: /avatar <URL or filepath>", IsError: true);
var target = args.Trim().Trim('"');
if (OnSetAvatar is not null)
await OnSetAvatar(target);
return new CommandResult(true, "Uploading avatar...");
}
private async Task<CommandResult> HandleServers() private async Task<CommandResult> HandleServers()
{ {
if (OnOpenServers is not null) if (OnOpenServers is not null)
@@ -209,7 +224,8 @@ public class CommandHandler
/color <#hex> - Set nickname color /color <#hex> - Set nickname color
/theme <name> - Switch theme /theme <name> - Switch theme
/send <filepath or URL> - Send a file or image /send <filepath or URL> - Send a file or image
/profile - Open your profile /avatar <URL or filepath> - Set your avatar
/profile [username] - View a profile (yours if no name given)
/servers - Open saved servers /servers - Open saved servers
/join <channel> - Join a channel /join <channel> - Join a channel
/leave - Leave current channel /leave - Leave current channel
+3 -3
View File
@@ -14,11 +14,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="appsettings.json"> <Content Include="appsettings.json" Condition="Exists('appsettings.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<EmbeddedResource Include="appsettings.json"> <EmbeddedResource Include="appsettings.example.json">
<LogicalName>EchoHub.Client.appsettings.json</LogicalName> <LogicalName>EchoHub.Client.appsettings.example.json</LogicalName>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
+1 -1
View File
@@ -9,7 +9,7 @@ var appSettingsPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json")
if (!File.Exists(appSettingsPath)) if (!File.Exists(appSettingsPath))
{ {
using var stream = typeof(AppOrchestrator).Assembly using var stream = typeof(AppOrchestrator).Assembly
.GetManifestResourceStream("EchoHub.Client.appsettings.json"); .GetManifestResourceStream("EchoHub.Client.appsettings.example.json");
if (stream is not null) if (stream is not null)
{ {
+15 -5
View File
@@ -27,7 +27,7 @@ public sealed class MainWindow : Runnable
// Cached Key constants — compare via .KeyCode to avoid Key.Equals (which also checks Handled) // Cached Key constants — compare via .KeyCode to avoid Key.Equals (which also checks Handled)
private static readonly Key EnterKey = Key.Enter; private static readonly Key EnterKey = Key.Enter;
private static readonly Key AltEnterKey = Key.Enter.WithAlt; private static readonly Key NewlineKey = Key.N.WithCtrl;
private static readonly Key CtrlCKey = Key.C.WithCtrl; private static readonly Key CtrlCKey = Key.C.WithCtrl;
private static readonly Key TabKey = Key.Tab; private static readonly Key TabKey = Key.Tab;
@@ -35,8 +35,8 @@ public sealed class MainWindow : Runnable
private static readonly string[] SlashCommands = private static readonly string[] SlashCommands =
[ [
"/status", "/nick", "/color", "/theme", "/send", "/status", "/nick", "/color", "/theme", "/send",
"/profile", "/servers", "/join", "/leave", "/topic", "/avatar", "/profile", "/servers", "/join", "/leave",
"/users", "/quit", "/help" "/topic", "/users", "/quit", "/help"
]; ];
private readonly List<string> _channelNames = []; private readonly List<string> _channelNames = [];
@@ -159,7 +159,7 @@ public sealed class MainWindow : Runnable
// Bottom input area // Bottom input area
var inputFrame = new FrameView var inputFrame = new FrameView
{ {
Title = "Message (Enter=send, Tab=autocomplete)", Title = "Message (Enter=send, Ctrl+N=newline, Tab=autocomplete)",
X = 25, X = 25,
Y = Pos.Bottom(_chatFrame), Y = Pos.Bottom(_chatFrame),
Width = Dim.Fill(), Width = Dim.Fill(),
@@ -194,7 +194,9 @@ public sealed class MainWindow : Runnable
ApplyColorSchemes(); ApplyColorSchemes();
// Re-wrap messages when the chat area is resized // Re-wrap messages when the chat area is resized
// Subscribe to both ListView and FrameView viewport changes for reliable resize detection
_messageList.ViewportChanged += (_, _) => OnChatViewportChanged(); _messageList.ViewportChanged += (_, _) => OnChatViewportChanged();
_chatFrame.ViewportChanged += (_, _) => OnChatViewportChanged();
// Window-level key handling for Ctrl+C (quit) // Window-level key handling for Ctrl+C (quit)
KeyDown += OnWindowKeyDown; KeyDown += OnWindowKeyDown;
@@ -316,7 +318,7 @@ public sealed class MainWindow : Runnable
TryAutocompleteCommand(); TryAutocompleteCommand();
e.Handled = true; e.Handled = true;
} }
else if (e.KeyCode == AltEnterKey.KeyCode) else if (e.KeyCode == NewlineKey.KeyCode)
{ {
_inputField.InsertText("\n"); _inputField.InsertText("\n");
e.Handled = true; e.Handled = true;
@@ -603,6 +605,14 @@ public sealed class MainWindow : Runnable
if (_channelMessages.TryGetValue(_currentChannel, out var messages)) if (_channelMessages.TryGetValue(_currentChannel, out var messages))
{ {
var width = _messageList.Viewport.Width; var width = _messageList.Viewport.Width;
// Update cached width when viewport reports a valid value;
// fall back to last known width if viewport hasn't been laid out yet.
if (width > 0)
_lastChatWidth = width;
else
width = _lastChatWidth;
var source = new ChatListSource(); var source = new ChatListSource();
if (width > 0) if (width > 0)
+246
View File
@@ -0,0 +1,246 @@
using Terminal.Gui.App;
using Terminal.Gui.Views;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Drawing;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using Attribute = Terminal.Gui.Drawing.Attribute;
namespace EchoHub.Client.UI;
/// <summary>
/// Action selected by the user in their own profile dialog.
/// </summary>
public enum ProfileAction
{
Close,
EditProfile,
SetStatus
}
/// <summary>
/// Dialog for viewing a user's server profile.
/// Shows Edit Profile / Set Status buttons when viewing own profile.
/// </summary>
public sealed class ProfileViewDialog
{
/// <summary>
/// Show a read-only profile view for another user.
/// </summary>
public static void Show(IApplication app, UserProfileDto? profile)
{
ShowInternal(app, profile, isOwnProfile: false);
}
/// <summary>
/// Show the profile view for the current user with action buttons.
/// Returns the action the user selected.
/// </summary>
public static ProfileAction ShowOwn(
IApplication app,
UserProfileDto? profile,
UserStatus currentStatus,
string? currentStatusMessage)
{
return ShowInternal(app, profile, isOwnProfile: true, currentStatus, currentStatusMessage);
}
private static ProfileAction ShowInternal(
IApplication app,
UserProfileDto? profile,
bool isOwnProfile,
UserStatus? currentStatus = null,
string? currentStatusMessage = null)
{
if (profile is null)
{
MessageBox.ErrorQuery(app, "Profile", "User not found.", "OK");
return ProfileAction.Close;
}
var action = ProfileAction.Close;
var dialog = new Dialog
{
Title = isOwnProfile ? "My Profile" : $"Profile \u2014 {profile.Username}",
Width = 50,
Height = 20
};
int row = 0;
// Username
var usernameLabel = new Label { Text = "Username:", X = 1, Y = row };
var usernameValue = new Label { Text = profile.Username, X = 14, Y = row };
usernameValue.SetScheme(new Scheme
{
Normal = new Attribute(Color.BrightYellow, Color.Blue)
});
dialog.Add(usernameLabel, usernameValue);
row++;
// Display Name
var nameLabel = new Label { Text = "Name:", X = 1, Y = row };
var nameValue = new Label { Text = profile.DisplayName ?? "-", X = 14, Y = row };
dialog.Add(nameLabel, nameValue);
row++;
// Status — use live status for own profile, stored status for others
var displayStatus = isOwnProfile && currentStatus.HasValue ? currentStatus.Value : profile.Status;
var displayStatusMsg = isOwnProfile ? currentStatusMessage : profile.StatusMessage;
var statusLabel = new Label { Text = "Status:", X = 1, Y = row };
var statusText = FormatStatus(displayStatus);
var statusValue = new Label { Text = statusText, X = 14, Y = row };
statusValue.SetScheme(new Scheme
{
Normal = new Attribute(GetStatusColor(displayStatus), Color.Blue)
});
dialog.Add(statusLabel, statusValue);
row++;
// Status Message
if (!string.IsNullOrWhiteSpace(displayStatusMsg))
{
var msgLabel = new Label { Text = "Message:", X = 1, Y = row };
var msgValue = new Label { Text = displayStatusMsg, X = 14, Y = row, Width = Dim.Fill(2) };
dialog.Add(msgLabel, msgValue);
row++;
}
// Color
var colorLabel = new Label { Text = "Color:", X = 1, Y = row };
var colorValue = new Label { Text = profile.NicknameColor ?? "-", X = 14, Y = row };
if (ColorHelper.ParseHexColor(profile.NicknameColor) is { } colorAttr)
colorValue.SetScheme(new Scheme { Normal = colorAttr });
dialog.Add(colorLabel, colorValue);
row++;
// Bio
row++;
var bioLabel = new Label { Text = "Bio:", X = 1, Y = row };
dialog.Add(bioLabel);
row++;
var bioView = new TextView
{
X = 1,
Y = row,
Width = Dim.Fill(2),
Height = 3,
Text = profile.Bio ?? "-",
ReadOnly = true,
WordWrap = true
};
bioView.SetScheme(new Scheme
{
Normal = new Attribute(Color.White, Color.DarkGray),
Focus = new Attribute(Color.White, Color.DarkGray)
});
dialog.Add(bioView);
row += 3;
// ASCII Avatar
if (!string.IsNullOrWhiteSpace(profile.AvatarAscii))
{
row++;
var avatarLines = profile.AvatarAscii.Split('\n').Length;
var avatarHeight = Math.Min(avatarLines + 2, 6);
var avatarFrame = new FrameView
{
Title = "Avatar",
X = 1,
Y = row,
Width = Dim.Fill(2),
Height = avatarHeight
};
avatarFrame.Add(new Label { Text = profile.AvatarAscii, X = 0, Y = 0 });
dialog.Add(avatarFrame);
// Grow dialog to fit avatar
dialog.Height = row + avatarHeight + 4;
}
// Buttons
if (isOwnProfile)
{
var editButton = new Button
{
Text = "Edit Profile",
X = Pos.Center() - 20,
Y = Pos.AnchorEnd(2)
};
editButton.Accepting += (s, e) =>
{
action = ProfileAction.EditProfile;
e.Handled = true;
app.RequestStop();
};
var statusButton = new Button
{
Text = "Set Status",
X = Pos.Center() - 4,
Y = Pos.AnchorEnd(2)
};
statusButton.Accepting += (s, e) =>
{
action = ProfileAction.SetStatus;
e.Handled = true;
app.RequestStop();
};
var closeButton = new Button
{
Text = "Close",
IsDefault = true,
X = Pos.Center() + 13,
Y = Pos.AnchorEnd(2)
};
closeButton.Accepting += (s, e) =>
{
action = ProfileAction.Close;
e.Handled = true;
app.RequestStop();
};
dialog.Add(editButton, statusButton, closeButton);
}
else
{
var closeButton = new Button
{
Text = "Close",
IsDefault = true,
X = Pos.Center(),
Y = Pos.AnchorEnd(2)
};
closeButton.Accepting += (s, e) =>
{
e.Handled = true;
app.RequestStop();
};
dialog.Add(closeButton);
}
app.Run(dialog);
return action;
}
private static string FormatStatus(UserStatus status) => status switch
{
UserStatus.Online => "\u25cf Online",
UserStatus.Away => "\u25cf Away",
UserStatus.DoNotDisturb => "\u25cf Do Not Disturb",
UserStatus.Invisible => "\u25cb Invisible",
_ => "\u25cf Unknown"
};
private static Color GetStatusColor(UserStatus status) => status switch
{
UserStatus.Online => Color.BrightGreen,
UserStatus.Away => Color.BrightYellow,
UserStatus.DoNotDisturb => Color.BrightRed,
UserStatus.Invisible => Color.Gray,
_ => Color.White
};
}
-337
View File
@@ -1,337 +0,0 @@
using System.Collections.ObjectModel;
using Terminal.Gui.App;
using Terminal.Gui.Views;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Drawing;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using EchoHub.Client.Config;
using Attribute = Terminal.Gui.Drawing.Attribute;
namespace EchoHub.Client.UI;
/// <summary>
/// Action selected by the user in the user panel dialog.
/// </summary>
public enum UserPanelAction
{
Close,
EditProfile,
SetStatus
}
/// <summary>
/// A Terminal.Gui dialog for viewing the user panel -- profile info, saved servers, and status.
/// </summary>
public sealed class UserPanelDialog
{
/// <summary>
/// Shows the user panel dialog and returns the action the user selected.
/// </summary>
public static UserPanelAction Show(
IApplication app,
UserProfileDto? profile,
List<SavedServer> savedServers,
UserStatus currentStatus,
string? currentStatusMessage)
{
var action = UserPanelAction.Close;
var dialog = new Dialog { Title = "User Panel", Width = 70, Height = 24 };
// -- Left side: Profile info ------------------------------------------
var profileFrame = new FrameView
{
Title = "Profile",
X = 0,
Y = 0,
Width = 35,
Height = Dim.Fill(3)
};
int row = 0;
// Username
var usernameLabel = new Label
{
Text = "Username:",
X = 1,
Y = row
};
var usernameValue = new Label
{
Text = profile?.Username ?? "N/A",
X = 12,
Y = row
};
usernameValue.SetScheme(new Scheme
{
Normal = new Attribute(Color.BrightYellow, Color.Blue)
});
profileFrame.Add(usernameLabel, usernameValue);
row += 1;
// Display Name
var displayLabel = new Label
{
Text = "Name:",
X = 1,
Y = row
};
var displayValue = new Label
{
Text = profile?.DisplayName ?? "-",
X = 12,
Y = row
};
profileFrame.Add(displayLabel, displayValue);
row += 1;
// Status
var statusLabel = new Label
{
Text = "Status:",
X = 1,
Y = row
};
var statusText = FormatStatus(currentStatus);
var statusValue = new Label
{
Text = statusText,
X = 12,
Y = row
};
statusValue.SetScheme(new Scheme
{
Normal = new Attribute(GetStatusColor(currentStatus), Color.Blue)
});
profileFrame.Add(statusLabel, statusValue);
row += 1;
// Status Message
if (!string.IsNullOrWhiteSpace(currentStatusMessage))
{
var msgLabel = new Label
{
Text = "Message:",
X = 1,
Y = row
};
var msgValue = new Label
{
Text = Truncate(currentStatusMessage, 20),
X = 12,
Y = row
};
profileFrame.Add(msgLabel, msgValue);
row += 1;
}
// Bio
row += 1;
var bioLabel = new Label
{
Text = "Bio:",
X = 1,
Y = row
};
profileFrame.Add(bioLabel);
row += 1;
var bioText = profile?.Bio ?? "-";
var bioView = new TextView()
{
X = 1,
Y = row,
Width = Dim.Fill(1),
Height = 3,
Text = bioText,
ReadOnly = true,
WordWrap = true
};
bioView.SetScheme(new Scheme
{
Normal = new Attribute(Color.White, Color.DarkGray),
Focus = new Attribute(Color.White, Color.DarkGray)
});
profileFrame.Add(bioView);
row += 3;
// Color
var colorLabel = new Label
{
Text = "Color:",
X = 1,
Y = row
};
var colorValue = new Label
{
Text = profile?.NicknameColor ?? "-",
X = 12,
Y = row
};
profileFrame.Add(colorLabel, colorValue);
row += 1;
// ASCII Avatar
if (!string.IsNullOrWhiteSpace(profile?.AvatarAscii))
{
row += 1;
var avatarFrame = new FrameView
{
Title = "Avatar",
X = 1,
Y = row,
Width = Dim.Fill(1),
Height = 4
};
var avatarLabel = new Label
{
Text = profile.AvatarAscii,
X = 0,
Y = 0
};
avatarFrame.Add(avatarLabel);
profileFrame.Add(avatarFrame);
}
dialog.Add(profileFrame);
// -- Right side: Saved Servers ----------------------------------------
var serversFrame = new FrameView
{
Title = "Saved Servers",
X = 36,
Y = 0,
Width = Dim.Fill(1),
Height = Dim.Fill(3)
};
var serverNames = savedServers.Select(s => s.Name).ToList();
var serverList = new ListView
{
Source = new ListWrapper<string>(new ObservableCollection<string>(serverNames)),
X = 0,
Y = 0,
Width = Dim.Fill(0),
Height = Dim.Fill(4)
};
var serverUrlLabel = new Label
{
Text = "URL: -",
X = 0,
Y = Pos.AnchorEnd(3),
Width = Dim.Fill(0)
};
var serverLastLabel = new Label
{
Text = "Last: -",
X = 0,
Y = Pos.AnchorEnd(2),
Width = Dim.Fill(0)
};
var serverUserLabel = new Label
{
Text = "User: -",
X = 0,
Y = Pos.AnchorEnd(1),
Width = Dim.Fill(0)
};
serverList.ValueChanged += (sender, e) =>
{
var index = e.NewValue;
if (index.HasValue && index.Value >= 0 && index.Value < savedServers.Count)
{
var server = savedServers[index.Value];
serverUrlLabel.Text = $"URL: {Truncate(server.Url, 25)}";
serverLastLabel.Text = $"Last: {server.LastConnected:yyyy-MM-dd HH:mm}";
serverUserLabel.Text = $"User: {server.Username ?? "-"}";
}
};
// Show initial details if there are servers
if (savedServers.Count > 0)
{
var first = savedServers[0];
serverUrlLabel.Text = $"URL: {Truncate(first.Url, 25)}";
serverLastLabel.Text = $"Last: {first.LastConnected:yyyy-MM-dd HH:mm}";
serverUserLabel.Text = $"User: {first.Username ?? "-"}";
}
serversFrame.Add(serverList, serverUrlLabel, serverLastLabel, serverUserLabel);
dialog.Add(serversFrame);
// -- Bottom buttons ---------------------------------------------------
var editProfileButton = new Button
{
Text = "Edit Profile",
X = Pos.Center() - 22,
Y = Pos.AnchorEnd(2)
};
var setStatusButton = new Button
{
Text = "Set Status",
X = Pos.Center() - 5,
Y = Pos.AnchorEnd(2)
};
var closeButton = new Button
{
Text = "Close",
IsDefault = true,
X = Pos.Center() + 12,
Y = Pos.AnchorEnd(2)
};
editProfileButton.Accepting += (s, e) =>
{
action = UserPanelAction.EditProfile;
e.Handled = true;
app.RequestStop();
};
setStatusButton.Accepting += (s, e) =>
{
action = UserPanelAction.SetStatus;
e.Handled = true;
app.RequestStop();
};
closeButton.Accepting += (s, e) =>
{
action = UserPanelAction.Close;
e.Handled = true;
app.RequestStop();
};
dialog.Add(editProfileButton, setStatusButton, closeButton);
app.Run(dialog);
return action;
}
private static string FormatStatus(UserStatus status) => status switch
{
UserStatus.Online => "\u25cf Online",
UserStatus.Away => "\u25cf Away",
UserStatus.DoNotDisturb => "\u25cf Do Not Disturb",
UserStatus.Invisible => "\u25cb Invisible",
_ => "\u25cf Unknown"
};
private static Color GetStatusColor(UserStatus status) => status switch
{
UserStatus.Online => Color.BrightGreen,
UserStatus.Away => Color.BrightYellow,
UserStatus.DoNotDisturb => Color.BrightRed,
UserStatus.Invisible => Color.Gray,
_ => Color.White
};
private static string Truncate(string value, int maxLength) =>
value.Length <= maxLength ? value : string.Concat(value.AsSpan(0, maxLength - 3), "...");
}