feat: enhance profile editing with avatar support and UI adjustments

- Added AvatarPath to ProfileEditResult for user profile updates.
- Updated ProfileEditDialog to include avatar selection with a browse button.
- Increased dialog height to accommodate new avatar input fields.
- Implemented avatar file path handling in the profile edit dialog.

feat: introduce moderation features and user roles

- Added ServerRole enum to define user roles (Member, Mod, Admin, Owner).
- Extended User model to include role, mute, and ban status.
- Created ModerationController for user role assignment, kicking, banning, and muting.
- Implemented methods in IChatBroadcaster and SignalRBroadcaster for user moderation actions.
- Updated database schema with new columns for user roles and moderation states.

fix: ensure muted users cannot send messages

- Added mute status checks in ChatService to prevent message sending for muted users.
- Updated user presence and status handling to reflect role changes and moderation actions.

chore: update constants for ASCII art rendering

- Introduced AsciiArtHeightHalfBlock constant for improved ASCII art rendering.
This commit is contained in:
HueByte
2026-02-19 17:47:10 +01:00
parent 257ac34224
commit b4c3ebd254
28 changed files with 1457 additions and 73 deletions
+204
View File
@@ -76,6 +76,7 @@ public sealed class AppOrchestrator : IDisposable
_mainWindow.OnThemeSelected += HandleThemeSelected;
_mainWindow.OnSavedServersRequested += HandleSavedServersRequested;
_mainWindow.OnCreateChannelRequested += HandleCreateChannelRequested;
_mainWindow.OnDeleteChannelRequested += HandleDeleteChannelRequested;
}
// ── Command Handler Wiring ─────────────────────────────────────────────
@@ -292,6 +293,56 @@ public sealed class AppOrchestrator : IDisposable
}
};
_commandHandler.OnKickUser += async (username, reason) =>
{
if (!IsAuthenticated) return;
await _apiClient!.KickUserAsync(username, reason);
};
_commandHandler.OnBanUser += async (username, reason) =>
{
if (!IsAuthenticated) return;
await _apiClient!.BanUserAsync(username, reason);
};
_commandHandler.OnUnbanUser += async (username) =>
{
if (!IsAuthenticated) return;
await _apiClient!.UnbanUserAsync(username);
};
_commandHandler.OnMuteUser += async (username, duration) =>
{
if (!IsAuthenticated) return;
await _apiClient!.MuteUserAsync(username, duration);
};
_commandHandler.OnUnmuteUser += async (username) =>
{
if (!IsAuthenticated) return;
await _apiClient!.UnmuteUserAsync(username);
};
_commandHandler.OnAssignRole += async (username, roleStr) =>
{
if (!IsAuthenticated) return;
var role = roleStr switch
{
"admin" => ServerRole.Admin,
"mod" => ServerRole.Mod,
_ => ServerRole.Member,
};
await _apiClient!.AssignRoleAsync(username, role);
};
_commandHandler.OnNukeChannel += async () =>
{
if (!IsAuthenticated) return;
var channel = _mainWindow.CurrentChannel;
if (string.IsNullOrEmpty(channel)) return;
await _apiClient!.NukeChannelAsync(channel);
};
_commandHandler.OnQuit += () =>
{
InvokeUI(() => _app.RequestStop());
@@ -361,6 +412,7 @@ public sealed class AppOrchestrator : IDisposable
// History might not be available
}
FetchAndUpdateOnlineUsers();
SaveServerToConfig(result);
}, "Connection failed", "Connect");
}
@@ -440,6 +492,8 @@ public sealed class AppOrchestrator : IDisposable
{
// History might not be available
}
FetchAndUpdateOnlineUsers();
}, "Failed to join channel");
}
@@ -526,6 +580,45 @@ public sealed class AppOrchestrator : IDisposable
});
}
// Upload avatar if specified
if (editResult.AvatarPath is not null)
{
try
{
Stream stream;
string fileName;
if (Uri.TryCreate(editResult.AvatarPath, 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
{
stream = File.OpenRead(editResult.AvatarPath);
fileName = Path.GetFileName(editResult.AvatarPath);
}
await using (stream)
{
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}", editResult.AvatarPath);
InvokeUI(() => _mainWindow.ShowError($"Avatar upload failed: {ex.Message}"));
}
}
_config.DefaultPreset = new AccountPreset
{
DisplayName = editResult.DisplayName,
@@ -617,6 +710,47 @@ public sealed class AppOrchestrator : IDisposable
}, "Failed to create channel");
}
private void HandleDeleteChannelRequested()
{
if (!IsAuthenticated || !IsConnected)
{
_mainWindow.ShowError("Not connected to a server.");
return;
}
var channel = _mainWindow.CurrentChannel;
if (string.IsNullOrEmpty(channel))
{
_mainWindow.ShowError("No channel selected.");
return;
}
if (channel == HubConstants.DefaultChannel)
{
_mainWindow.ShowError($"The #{HubConstants.DefaultChannel} channel cannot be deleted.");
return;
}
var confirm = MessageBox.Query(_app, "Delete Channel",
$"Are you sure you want to delete #{channel}?\nThis will remove all messages permanently.", "Delete", "Cancel");
if (confirm != 0) return;
RunAsync(async () =>
{
await _apiClient!.DeleteChannelAsync(channel);
_joinedChannels.Remove(channel);
var channels = await _apiClient.GetChannelsAsync();
InvokeUI(() =>
{
_mainWindow.SetChannels(channels);
_mainWindow.SwitchToChannel(HubConstants.DefaultChannel);
_mainWindow.AddSystemMessage(HubConstants.DefaultChannel, $"Channel #{channel} has been deleted.");
});
}, "Failed to delete channel");
}
// ── Connection Event Wiring ────────────────────────────────────────────
private void WireConnectionEvents(EchoHubConnection connection)
@@ -625,10 +759,18 @@ public sealed class AppOrchestrator : IDisposable
InvokeUI(() => _mainWindow.AddMessage(message));
connection.OnUserJoined += (channelName, username) =>
{
InvokeUI(() => _mainWindow.AddSystemMessage(channelName, $"{username} joined the channel"));
if (channelName == _mainWindow.CurrentChannel)
FetchAndUpdateOnlineUsers();
};
connection.OnUserLeft += (channelName, username) =>
{
InvokeUI(() => _mainWindow.AddSystemMessage(channelName, $"{username} left the channel"));
if (channelName == _mainWindow.CurrentChannel)
FetchAndUpdateOnlineUsers();
};
connection.OnUserStatusChanged += presence =>
{
@@ -642,6 +784,49 @@ public sealed class AppOrchestrator : IDisposable
foreach (var channelName in _mainWindow.GetChannelNames())
_mainWindow.AddStatusMessage(channelName, displayName, statusText);
});
FetchAndUpdateOnlineUsers();
};
connection.OnUserKicked += (channelName, username, reason) =>
{
var reasonText = reason is not null ? $" ({reason})" : "";
InvokeUI(() =>
{
_mainWindow.AddSystemMessage(channelName, $"{username} was kicked{reasonText}");
if (username.Equals(_currentUsername, StringComparison.OrdinalIgnoreCase))
{
_mainWindow.AddSystemMessage(channelName, "You were kicked from this channel.");
}
});
};
connection.OnUserBanned += (username, reason) =>
{
InvokeUI(() =>
{
if (username.Equals(_currentUsername, StringComparison.OrdinalIgnoreCase))
{
_mainWindow.ShowError("You have been banned from this server.");
HandleDisconnect();
}
});
};
connection.OnMessageDeleted += (channelName, messageId) =>
{
InvokeUI(() =>
{
_mainWindow.RemoveMessage(channelName, messageId);
});
};
connection.OnChannelNuked += channelName =>
{
InvokeUI(() =>
{
_mainWindow.ClearChannelMessages(channelName);
_mainWindow.AddSystemMessage(channelName, "Channel history has been cleared by a moderator.");
});
};
connection.OnError += errorMessage =>
@@ -673,6 +858,25 @@ public sealed class AppOrchestrator : IDisposable
// ── Private Helpers ────────────────────────────────────────────────────
private void FetchAndUpdateOnlineUsers()
{
var channel = _mainWindow.CurrentChannel;
if (string.IsNullOrEmpty(channel) || !IsConnected) return;
Task.Run(async () =>
{
try
{
var users = await _connection!.GetOnlineUsersAsync(channel);
InvokeUI(() => _mainWindow.UpdateOnlineUsers(users));
}
catch (Exception ex)
{
Log.Debug(ex, "Failed to fetch online users for {Channel}", channel);
}
});
}
private void SaveServerToConfig(ConnectDialogResult result)
{
var savedServer = new SavedServer